Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# Imports 

2 

3from django.urls import path, re_path 

4from django.core.exceptions import ImproperlyConfigured 

5from superdjango.exceptions import IMustBeMissingSomething, NoViewForYou 

6from superdjango.patterns import PATTERNS 

7from .crud import CreateView, DeleteView, DetailView, ListView, UpdateView 

8 

9# Exports 

10 

11__all__ = ( 

12 "ModelViewSet", 

13 "ViewSet", 

14) 

15 

16# BaseView Sets 

17 

18 

19class ViewSet(object): 

20 """A generic viewset, the main purpose of which is to collect views together to consistently generate their URLs.""" 

21 

22 views = None 

23 

24 def __init__(self, app_name=None, namespace=None): 

25 """Initialize a generic view set. 

26 

27 :param app_name: The app name. 

28 :type app_name: str 

29 

30 :param namespace: The namespace in which the app will operate. 

31 :type namespace: str 

32 

33 """ 

34 self.app_name = app_name 

35 self.namespace = namespace 

36 

37 @classmethod 

38 def get_pattern_name(cls, view_class): 

39 """Get the name of a view. 

40 

41 :param view_class: The class-based view. 

42 :type view_class: class 

43 

44 :rtype: str 

45 

46 :raise: ImproperlyConfigured 

47 

48 .. note:: 

49 The view may override the default behavior by implementing a ``pattern_name`` property or the 

50 ``get_pattern_name()`` method. This method must return a ``str``. 

51 

52 """ 

53 try: 

54 return view_class.get_pattern_name() 

55 except AttributeError: 

56 pass 

57 

58 try: 

59 return view_class.pattern_name 

60 except AttributeError: 

61 pass 

62 

63 e = "'%s' must define 'pattern_name' attribute or implement the 'get_pattern_name()' method." 

64 raise ImproperlyConfigured(e % view_class.__class__.__name__) 

65 

66 @classmethod 

67 def get_pattern_value(cls, view_class): 

68 """Get the value (path) of a view. 

69 

70 :param view_class: The class-based view. 

71 :type view_class: class 

72 

73 :rtype: str 

74 

75 :raise: ImproperlyConfigured 

76 

77 """ 

78 try: 

79 return view_class.get_pattern_value() 

80 except AttributeError: 

81 pass 

82 

83 try: 

84 return view_class.pattern_value 

85 except AttributeError: 

86 pass 

87 

88 e = "'%s' must define 'pattern_value' attribute or implement the 'get_pattern_value()' method." 

89 raise ImproperlyConfigured(e % view_class.__class__.__name__) 

90 

91 @classmethod 

92 def get_pattern_regex(cls, view_class): 

93 """Get the regular expression of a view. 

94 

95 :param view_class: The class-based view. 

96 :type view_class: class 

97 

98 :rtype: str | None 

99 

100 .. note:: 

101 This method does *not* raise an improperly configured, allowing the other methods to fall back to 

102 ``get_pattern_value()``. 

103 

104 """ 

105 try: 

106 return view_class.get_pattern_regex() 

107 except AttributeError: 

108 pass 

109 

110 try: 

111 return view_class.pattern_regex 

112 except AttributeError: 

113 pass 

114 

115 return None 

116 

117 def get_url(self, view_class, prefix=None): 

118 """Get the url of a view. 

119 

120 :param view_class: The class-based view. 

121 :type view_class: class 

122 

123 :param prefix: A prefix added to the regex. Note that the URL will fail to resolve if the ``^`` appears in the 

124 regex for the view. 

125 

126 :rtype: URLPattern 

127 

128 .. note:: 

129 The view may override the default behavior by implementing a ``get_url()`` method. This method must return 

130 an instance of ``URLPattern`` using ``re_path()`` or ``path()``. 

131 

132 """ 

133 # This allows the view to completely override the URL generation process. 

134 try: 

135 return view_class.get_url() 

136 except AttributeError: 

137 pass 

138 

139 name = self.get_pattern_name(view_class) 

140 

141 # Handle regular expressions. 

142 regex = self.get_pattern_regex(view_class) 

143 if regex is not None: 

144 if prefix is not None: 

145 regex = prefix + regex 

146 

147 return re_path(regex, view_class.as_view(), name=name) 

148 

149 # Handle paths. 

150 value = self.get_pattern_value(view_class) 

151 

152 if prefix is not None: 

153 value = prefix + value 

154 

155 return path(value, view_class.as_view(), name=name) 

156 

157 def get_urls(self, prefix=None): 

158 """Generate the url objects. 

159 

160 :param prefix: A prefix added to the URL. Note that the URL will fail to resolve if ``^`` appears in the 

161 regex for the view. 

162 :type prefix: str 

163 

164 :rtype: list 

165 

166 """ 

167 views = self.get_views() 

168 

169 urls = list() 

170 for view_class in views: 

171 urls.append(self.get_url(view_class, prefix=prefix)) 

172 

173 return urls 

174 

175 def get_views(self): 

176 """Get the views included in the view set. 

177 

178 :rtype: list 

179 :raise: ImproperlyConfigured 

180 

181 """ 

182 if self.views is not None: 

183 return self.views 

184 

185 e = "'%s' must either define 'views' or override 'get_views()'" 

186 raise ImproperlyConfigured(e % self.__class__.__name__) 

187 

188 

189class ModelViewSet(ViewSet): 

190 """Extends :py:class:`ViewSet` to add support for models. 

191 

192 **BaseView Name** 

193 

194 Unless the ``get_pattern_name()`` method is defined on the view, the name will be automatically determined based on 

195 the name of the model plus a suffix for the purpose of the view class: 

196 

197 - ``_create`` 

198 - ``_delete`` 

199 - ``_detail`` 

200 - ``_list`` 

201 - ``_update`` 

202 

203 The name of the view (used to reverse the URL) is established in the following manner: 

204 

205 1. Use the ``pattern_name`` property if it has been set on the view class. 

206 2. Use the result of ``get_pattern_name()`` if the method is defined on the view class. 

207 3. If the view is an extension of any of the model views, the name will be automatically set based on the name of 

208 the model plus the purpose of the view class: ``_create``, ``_delete``, ``_detail``, ``_list``, and 

209 ``_update``. 

210 4. If none of the other methods have produced a result, the name of the view class will be used. This is rarely 

211 ideal, but does provide a default and prevents throwing an error. 

212 

213 **Pattern Value** 

214 

215 Unless the ``get_pattern_value()`` method is defined, the base pattern will be automatically determined based on the 

216 purpose of the view class: 

217 

218 - ``create`` 

219 - ``delete`` 

220 - ``detail`` 

221 - ``update`` 

222 

223 List views are assumed to be the index of the app's URLs. Views that require an identifier will use the 

224 ``lookup_field`` on the view class. 

225 

226 **Example** 

227 

228 .. code-block:: py 

229 

230 # views.py 

231 from superdjango.views.models import CreateView, DeleteView, DetailView, ListView, UpdateView 

232 from superdjango.views.viewsets import ModelViewSet 

233 from .models import Task 

234 

235 class CreateTask(CreateView): 

236 model = Task 

237 # ... 

238 

239 class DeleteTask(DeleteView): 

240 model = Task 

241 # ... 

242 

243 class ListTasks(ListView) 

244 model = Task 

245 # ... 

246 

247 class TaskDetail(DetailView): 

248 model = Task 

249 # ... 

250 

251 class UpdateTask(UpdateView): 

252 model = Task 

253 # ... 

254 

255 class TaskViewSet(ViewSet): 

256 views = [ 

257 CreateTask, 

258 DeleteTask, 

259 ListTasks, 

260 TaskDetail, 

261 UpdateTask, 

262 ] 

263 

264 .. code-block:: py 

265 

266 # urls.py 

267 from views import TaskViewSet 

268 

269 urlpatterns = TaskViewSet().get_urls() 

270 

271 """ 

272 

273 @classmethod 

274 def get_pattern_name(cls, view_class): 

275 """Get the name of a view based on the implemented model view. 

276 

277 :param view_class: The class-based view. 

278 :type view_class: class 

279 

280 :rtype: str 

281 

282 :raise: NoViewForYou 

283 

284 """ 

285 

286 # This allows the view class to override the automatic behavior below. This is helpful when multiple model views 

287 # are defined in the same view set. 

288 try: 

289 return ViewSet.get_pattern_name(view_class) 

290 except ImproperlyConfigured: 

291 pass 

292 

293 # The model attribute must be defined. 

294 try: 

295 model = view_class.get_model() 

296 except (AttributeError, IMustBeMissingSomething) as e: 

297 raise NoViewForYou(str(e)) 

298 

299 # App name and model name are both required for a unique view name. 

300 # noinspection PyProtectedMember 

301 app_label = model._meta.app_label 

302 # noinspection PyProtectedMember 

303 model_name = model._meta.model_name 

304 

305 # Get the action/verb used in the name. 

306 verb = cls.get_pattern_verb(view_class) 

307 if verb is None: 

308 message = "A verb for the %s view could not be determined; add a class method get_pattern_verb() or " \ 

309 "class attribute or pattern_verb to help identify this view." 

310 raise NoViewForYou(message % view_class.__name__) 

311 

312 # We have a winner. 

313 return "%s_%s_%s" % (app_label, model_name, verb) 

314 

315 @classmethod 

316 def get_pattern_value(cls, view_class): 

317 """Get the path of the model view based on the CBV it extends. 

318 

319 :param view_class: The class-based view. 

320 :type view_class: class 

321 

322 :rtype: str 

323 

324 :raise: NoViewForYou 

325 

326 """ 

327 # This allows the view class to override the automatic behavior below. 

328 try: 

329 return ViewSet.get_pattern_value(view_class) 

330 except ImproperlyConfigured: 

331 pass 

332 

333 # The model attribute must be defined. 

334 try: 

335 model = view_class.get_model() 

336 except (AttributeError, IMustBeMissingSomething) as e: 

337 raise NoViewForYou(str(e)) 

338 

339 # noinspection PyProtectedMember 

340 model_name = model._meta.model_name 

341 

342 # Get the action/verb that the view represents. 

343 verb = cls.get_pattern_verb(view_class) 

344 

345 if verb is None: 

346 message = "A verb for the %s view could not be determined; add a class method get_pattern_verb() or " \ 

347 "class attribute or pattern_verb to help identify this view." 

348 raise NoViewForYou(message % view_class.__name__) 

349 

350 # Add and list verbs are not included in the URL and do not utilize a pattern. 

351 if verb == "create": 

352 return "%s/create/" % model_name 

353 elif verb == "list": 

354 return "%s/" % model_name 

355 else: 

356 try: 

357 pattern = PATTERNS[view_class.lookup_field] 

358 return "%s/%s/%s/" % (model_name, verb, pattern) 

359 except KeyError: 

360 message = "A pattern for the %s verb could not be determined because the lookup_field of %s view is " \ 

361 "either undefined or is not a recognized pattern identifier." 

362 raise NoViewForYou(message % (verb, view_class.__name__)) 

363 

364 @classmethod 

365 def get_pattern_verb(cls, view_class): 

366 """Get the action (verb) represented by the view class. 

367 

368 :rtype: str | None 

369 

370 """ 

371 # This allows the view class to override the automatic behavior below. This is helpful for custom model views 

372 # that define their own verb. 

373 try: 

374 return view_class.get_pattern_verb() 

375 except AttributeError: 

376 pass 

377 

378 try: 

379 return view_class.pattern_verb 

380 except AttributeError: 

381 pass 

382 

383 if issubclass(view_class, CreateView): 

384 return "create" 

385 elif issubclass(view_class, DeleteView): 

386 return "delete" 

387 elif issubclass(view_class, DetailView): 

388 return "detail" 

389 elif issubclass(view_class, ListView): 

390 return "list" 

391 elif issubclass(view_class, UpdateView): 

392 return "update" 

393 else: 

394 return None