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.core.exceptions import ImproperlyConfigured 

4from django.core.paginator import Paginator, InvalidPage 

5from django.forms import models as model_forms 

6from django.http import HttpResponseRedirect 

7from django.shortcuts import get_object_or_404 

8from django.urls import reverse, NoReverseMatch 

9from django.utils.translation import ugettext_lazy as _ 

10import os 

11from superdjango.exceptions import IMustBeMissingSomething 

12from .base import BaseView 

13from .forms import FormMixin 

14 

15# Exports 

16 

17__all__ = ( 

18 "CreateView", 

19 "DeleteView", 

20 "DetailView", 

21 "ListView", 

22 "ModelView", 

23 "UpdateView", 

24) 

25 

26# Base 

27 

28 

29class ModelView(FormMixin, BaseView): 

30 """Base class for model interactions. 

31 

32 To avoid the excessive use of mixins, an engineering compromise was struck when developing the ``ModelView``: 

33 

34 1. The view extends the base view which includes a number of commonly useful features and context variables. 

35 2. The view also extends :py:class:`FormMixin`, which doesn't make sense for detail and list views, but provides the 

36 basis for sensible defaults with create and update views. 

37 3. The delete view may be customized to include a form if one is desired. For example, to add a checkbox for an 

38 additional confirmation step. 

39 4. Other than ``get_object_list()`` and use of ``get_queryset()``, ``ModelView`` does *not* implement list logic. 

40 :py:class:`ListView` provides this instead. 

41 5. No authentication or authorization logic is provided by default. It is up to the developer to utilize 

42 ``superdjango.views.access`` or their own custom logic if these features are desired. 

43 

44 """ 

45 

46 context_object_name = None 

47 """The name of the model object or objects when used in a template.""" 

48 

49 field_names = None 

50 """A list of field names to be included in the view. For form views, these are the names included in the form. For 

51 detail and list views, these may be used to display record data. 

52 """ 

53 

54 lookup_field = "pk" 

55 """The field used to uniquely identify the model.""" 

56 

57 lookup_key = None 

58 """The key in GET used to uniquely identify the model. Defaults to the ``lookup_field``.""" 

59 

60 model = None 

61 """The model (class) to which the view pertains. Required.""" 

62 

63 object = None 

64 """The record (instance) of a model for create, detail, delete, and update views. This is not used by list views.""" 

65 

66 object_list = None 

67 """The record instances of a model for list views.""" 

68 

69 prefetch_related = None 

70 """A list of foreign key or many to many fields that should be called using Django's 

71 `prefetch_related <https://docs.djangoproject.com/en/stable/ref/models/querysets/#prefetch-related>`_. 

72 """ 

73 

74 queryset = None 

75 """The queryset used to acquire the object or object list. See ``get_queryset()``.""" 

76 

77 select_related = None 

78 """A list of foreign key fields that should be called using Django's 

79 `select_related <https://docs.djangoproject.com/en/stable/ref/models/querysets/#select-related>`_. 

80 """ 

81 

82 template_name_suffix = None 

83 """The suffix added to the end of template names when automatically generated. This should be overridden by child 

84 classes. 

85 """ 

86 

87 @classmethod 

88 def get_app_label(cls): 

89 """Get the app label (name) for the model represented by the view. 

90 

91 :rtype: str 

92 

93 """ 

94 # noinspection PyProtectedMember 

95 return cls.get_model()._meta.app_label 

96 

97 def get_cancel_url(self): 

98 """Return the value of the class attribute, if defined. Otherwise attempt to return the list view for the 

99 current model. 

100 

101 :rtype: str | None 

102 

103 """ 

104 if self.cancel_url is not None: 

105 return self.cancel_url 

106 

107 view_name = "%s_%s_list" % (self.get_app_label(), self.get_model_name()) 

108 

109 try: 

110 return reverse(view_name) 

111 except NoReverseMatch: 

112 return None 

113 

114 def get_context_data(self, **kwargs): 

115 """Get context used to render the response. 

116 

117 The following variables are added to the context: 

118 

119 - ``verbose_name``: The verbose name of the model. 

120 - ``verbose_name_plural``: The plural name of the model. 

121 

122 For single-object views, the ``object`` variable is also added along with a specific name if 

123 ``context_object_name`` is defined. 

124 

125 For multi-object views, the ``object_list`` variable is added along with a specific name if 

126 ``context_object_name`` is defined. 

127 

128 """ 

129 context = super().get_context_data(**kwargs) 

130 

131 context_object_name = self.get_context_object_name() 

132 

133 if self.object is not None: 

134 context['object'] = self.object 

135 

136 if context_object_name is not None: 

137 context[context_object_name] = self.object 

138 elif self.object_list is not None: 

139 context['object_list'] = self.object_list 

140 

141 if context_object_name is not None: 

142 context[context_object_name] = self.object_list 

143 else: 

144 pass 

145 

146 context['verbose_name'] = self.get_verbose_name() 

147 context['verbose_name_plural'] = self.get_verbose_name_plural() 

148 

149 return context 

150 

151 def get_context_object_name(self): 

152 """Get the name of the model (or models) used in templates. 

153 

154 :rtype: str | None 

155 

156 """ 

157 if self.context_object_name is not None: 

158 return self.context_object_name 

159 

160 return None 

161 

162 def get_field_names(self): 

163 """Get the field names to be included in the model form. 

164 

165 :rtype: list[str] 

166 

167 .. tip:: 

168 If the ``field_names`` class attribute is not defined, *all* of the model's fields are included, which is 

169 probably *not* what you want. 

170 

171 """ 

172 if self.field_names is not None: 

173 return self.field_names 

174 

175 field_names = list() 

176 

177 model = self.get_model() 

178 

179 # noinspection PyProtectedMember 

180 for field in model._meta.get_fields(): 

181 field_names.append(field.name) 

182 

183 return field_names 

184 

185 def get_form_class(self): 

186 """Get the form class to use for instantiating a model form. If ``form_class`` is not defined, the 

187 :py:func:`django.forms.models.modelform_factory()` is used in conjunction with ``get_field_names()``. 

188 

189 :raise: AttributeError 

190 

191 """ 

192 if self.form_class is not None: 

193 return self.form_class 

194 

195 field_names = self.get_field_names() 

196 

197 model = self.get_model() 

198 

199 return model_forms.modelform_factory(model, fields=field_names) 

200 

201 @classmethod 

202 def get_lookup_field(cls): 

203 """Get the name of the field used to uniquely identify a model instance. 

204 

205 :rtype: str 

206 :raises: IMustBeMissingSomething 

207 

208 """ 

209 if cls.lookup_field is not None: 

210 return cls.lookup_field 

211 

212 raise IMustBeMissingSomething(cls.__name__, "lookup_field", "get_lookup_field") 

213 

214 @classmethod 

215 def get_lookup_key(cls): 

216 """Get the key used in GET to uniquely identify a model instance. 

217 

218 :rtype: str 

219 

220 """ 

221 if cls.lookup_key is not None: 

222 return cls.lookup_key 

223 

224 return cls.get_lookup_field() 

225 

226 @classmethod 

227 def get_model(cls): 

228 """Get the model class used by the view. 

229 

230 :raise: IMustBeMissingSomething 

231 

232 .. note:: 

233 This saves checking for the model in the various class methods rather than accessing ``cls.model`` and 

234 handling the exception every time the model is referenced. 

235 

236 """ 

237 if cls.model is not None: 

238 return cls.model 

239 

240 raise IMustBeMissingSomething(cls.__name__, "model", "get_model") 

241 

242 @classmethod 

243 def get_model_name(cls): 

244 """Get the model name in lower case. 

245 

246 :rtype: str 

247 :raise: IMustBeMissingSomething 

248 :raises: See ``get_model()``. 

249 

250 """ 

251 model = cls.get_model() 

252 return model.__name__.lower() 

253 

254 def get_object(self): 

255 """Get the object (model instance) that may be used in :py:class:`CreateView`, :py:class:`DetailView`, and 

256 :py:class:`DeleteView`. 

257 

258 """ 

259 lookup_field = self.get_lookup_field() 

260 lookup_key = self.get_lookup_key() 

261 

262 queryset = self.get_queryset() 

263 

264 try: 

265 # noinspection PyUnresolvedReferences 

266 criteria = {lookup_field: self.kwargs[lookup_key]} 

267 except KeyError: 

268 e = 'The "%s" lookup key for the "%s" lookup field was not found in kwargs for the "%s" view.' 

269 raise ImproperlyConfigured(e % (lookup_key, lookup_field, self.__class__.__name__)) 

270 

271 return get_object_or_404(queryset, **criteria) 

272 

273 def get_object_list(self): 

274 """Get the objects to be displayed by list views. 

275 

276 :rtype: django.db.models.QuerySet 

277 

278 """ 

279 return self.get_queryset() 

280 

281 def get_queryset(self): 

282 """Get the queryset used by the view. This will either be a list or individual instance. 

283 

284 :rtype: django.db.models.QuerySet 

285 

286 """ 

287 if self.queryset is not None: 

288 # noinspection PyProtectedMember 

289 return self.queryset._clone() 

290 

291 model = self.get_model() 

292 

293 # noinspection PyProtectedMember 

294 queryset = model._default_manager.all() 

295 

296 if isinstance(self.select_related, (list, tuple)): 

297 queryset = queryset.select_related(*self.select_related) 

298 

299 if isinstance(self.prefetch_related, (list, tuple)): 

300 queryset = queryset.prefetch_related(*self.prefetch_related) 

301 

302 return queryset 

303 

304 def get_success_url(self): 

305 """Return the value of the class attribute, if defined. Otherwise attempt to return the list view for the 

306 current model. Alternatively, the absolute URL will be returned if the model defines ``get_absolute_url()``. 

307 

308 :raise: IMustBeMissingSomething 

309 

310 """ 

311 if self.success_url is not None: 

312 return self.success_url 

313 

314 view_name = "%s_%s_list" % (self.get_app_label(), self.get_model_name()) 

315 

316 try: 

317 return reverse(view_name) 

318 except NoReverseMatch: 

319 pass 

320 

321 try: 

322 return self.object.get_absolute_url() 

323 except AttributeError: 

324 pass 

325 

326 raise IMustBeMissingSomething(self.__class__.__name__, "get_success_url") 

327 

328 def get_template_name_suffix(self): 

329 """Get the suffix for the current view template. 

330 

331 :rtype: str 

332 

333 .. tip:: 

334 Extending classes should define the ``template_name_suffix``. The suffix should include an underscore for 

335 separation. For example, the suffix for a view for creating a new record would be ``_add``. 

336 

337 The default behavior here is to return an empty string if ``template_name_suffix`` is not defined. 

338 

339 """ 

340 if self.template_name_suffix is not None: 

341 return self.template_name_suffix 

342 

343 return "" 

344 

345 def get_template_names(self): 

346 """Get the template names that may be used for rendering the response. 

347 

348 :rtype: list[str] 

349 

350 The possible names are generated like so: 

351 

352 1. If the child class defines a ``template_name``, this is always returned as the first element of the list. 

353 2. This method first defines templates that may be defined by the local project in the form of 

354 ``{app_label}/{model_name_lower}{template_name_suffix}.html``. 

355 

356 """ 

357 templates = list() 

358 

359 # This will be the first template that is checked, which means if takes priority over any other possible 

360 # template names. 

361 if self.template_name is not None: 

362 templates.append(self.template_name) 

363 

364 # The default template name is based on the suffix, which may supplied by the extending class. 

365 template_name_suffix = self.get_template_name_suffix() 

366 if template_name_suffix is not None: 

367 file_name = "%s%s.html" % (self.get_model_name(), template_name_suffix) 

368 

369 # noinspection PyProtectedMember 

370 templates.append(os.path.join(self.model._meta.app_label, file_name)) 

371 

372 return templates 

373 

374 def get_verbose_name(self): 

375 """Get the verbose name for the model. 

376 

377 :rtype: str 

378 

379 """ 

380 # noinspection PyProtectedMember 

381 return self.get_model()._meta.verbose_name 

382 

383 def get_verbose_name_plural(self): 

384 """Get the plural verbose name for the model. 

385 

386 :rtype: str 

387 

388 """ 

389 # noinspection PyProtectedMember 

390 return self.get_model()._meta.verbose_name_plural 

391 

392# Views 

393 

394 

395class CreateView(ModelView): 

396 """Present, validate, and submit a form for a new model record (instance). 

397 

398 .. note:: 

399 Because :py:class:`ModelView`` extends :py:class:`FormMixin`, this class does *not* need to implement ``get()``, 

400 ``get_form()``, ``form_invalid()``, or ``post()``. 

401 

402 See also ``ModelView.get_success_url()``. 

403 

404 """ 

405 

406 template_name_suffix = "_form" 

407 

408 def form_valid(self, form): 

409 """Override to save the new record.""" 

410 self.object = form.save() 

411 

412 return super().form_valid(form) 

413 

414 

415class DeleteView(ModelView): 

416 """Delete and existing model record (instance).""" 

417 field_names = [] 

418 template_name_suffix = "_confirm_delete" 

419 

420 def get(self, request, *args, **kwargs): 

421 """Load the object and render the template. No form is used.""" 

422 self.object = self.get_object() 

423 context = self.get_context_data() 

424 return self.render_to_response(context) 

425 

426 def post(self, request, *args, **kwargs): 

427 """Delete the object and redirect using the success URL.""" 

428 self.object = self.get_object() 

429 self.object.delete() 

430 

431 success_message = self.get_success_message() 

432 if success_message is not None: 

433 self.messages.success(success_message) 

434 

435 return HttpResponseRedirect(self.get_success_url()) 

436 

437 

438class DetailView(ModelView): 

439 """Display detail for a model record (instance).""" 

440 

441 template_name_suffix = "_detail" 

442 

443 # noinspection PyUnusedLocal 

444 def get(self, request, *args, **kwargs): 

445 """Get the model record (instance) to be displayed.""" 

446 self.object = self.get_object() 

447 

448 context = self.get_context_data() 

449 return self.render_to_response(context) 

450 

451 

452class ListView(ModelView): 

453 """List model records.""" 

454 

455 allow_empty = True 

456 """Indicates whether an empty queryset is allowed. When ``False`` an empty queryset will raise an ``Http404``.""" 

457 

458 empty_message = None 

459 """The message to display when there are no results.""" 

460 

461 limit = None 

462 """The total records to be displayed on a page. Setting this value will invoke pagination.""" 

463 

464 page_keyword = "page" 

465 """The GET key used to indicate the pagination page number. Defaults to ``page``.""" 

466 

467 pagination_style = "previous-next" 

468 """The style for navigation through paginated results, ``numbers`` or ``previous-next``.""" 

469 

470 template_name_suffix = "_list" 

471 

472 # noinspection PyUnusedLocal 

473 def get(self, request, *args, **kwargs): 

474 """Get the queryset and optionally apply pagination. 

475 

476 The context includes ``no_results_message`` when ``allow_empty`` is ``True`` and no results are found. 

477 

478 ``is_paginated`` is also included and is either ``True`` or ``False``. 

479 

480 If pagination is enabled, additional variables are also added to the context: 

481 

482 - ``current_page``: The same as ``page_object``. 

483 - ``page_keyword``: The GET keyword used to indicate the current page number. 

484 - ``page_obj``: The current page object from ``get_paginated_queryset()``. 

485 - ``pagination_style``: The preferred output of pagination links. 

486 - ``paginator``: The paginator instance from ``get_paginator()``. 

487 

488 """ 

489 queryset = self.get_object_list() 

490 

491 # Handle no results. 

492 if not queryset.exists(): 

493 if not self.allow_empty: 

494 self.dispatch_not_found(self.get_empty_message()) 

495 

496 context = self.get_context_data(no_results_message=self.get_empty_message()) 

497 return self.render_to_response(context) 

498 

499 # Handle pagination. 

500 limit = self.get_limit() 

501 if limit is not None: 

502 page = self.get_paginated_queryset(queryset, limit) 

503 self.object_list = page 

504 context = self.get_context_data( 

505 current_page=page, # superdjango 

506 page_keyword=self.get_page_keyword(), 

507 page_obj=page, # django 

508 pagination_style=self.pagination_style, 

509 is_paginated=True, 

510 paginator=page.paginator 

511 ) 

512 else: 

513 self.object_list = queryset 

514 context = self.get_context_data( 

515 current_page=None, # superdjango 

516 page_keyword=self.get_page_keyword(), 

517 page_obj=None, # django 

518 pagination_style=self.pagination_style, 

519 is_paginated=False, 

520 paginator=None 

521 ) 

522 

523 return self.render_to_response(context) 

524 

525 def get_empty_message(self): 

526 """Get the message to display when there are no results. 

527 

528 :rtype: str 

529 

530 """ 

531 if self.empty_message is not None: 

532 return self.empty_message 

533 

534 return _("There are currently no %s." % self.get_verbose_name_plural()) 

535 

536 def get_limit(self): 

537 """Get the number of records to display per page. 

538 

539 :rtype: int | None 

540 

541 """ 

542 return self.limit 

543 

544 def get_page_keyword(self): 

545 """Get the keyword used to identify the page number in GET. 

546 

547 :rtype: str 

548 

549 """ 

550 return self.page_keyword 

551 

552 def get_paginated_queryset(self, queryset, limit): 

553 """Paginate the given queryset. 

554 

555 :param queryset: The query set to be paginated. 

556 :type queryset: django.db.Queryset 

557 

558 :param limit: The number of objects per page. 

559 :type limit: int 

560 

561 :rtype: Page 

562 

563 """ 

564 # Paginator is needed for num_pages below. 

565 paginator = self.get_paginator(queryset, limit) 

566 

567 # The page keyword determines what how the page number is identified in the URL. 

568 page_keyword = self.get_page_keyword() 

569 

570 # Get the current page number. 

571 page_query_param = self.request.GET.get(page_keyword) 

572 page_number = page_query_param or 1 

573 

574 try: 

575 page_number = int(page_number) 

576 except ValueError: 

577 if page_number == 'last': 

578 page_number = paginator.num_pages 

579 else: 

580 message = _("The page number could not be determined.") 

581 self.dispatch_not_found(message) 

582 

583 # Return the page instance or die trying. 

584 try: 

585 return paginator.page(page_number) 

586 except InvalidPage as e: 

587 message = _("Invalid page (%s): %s" % (page_number, e)) 

588 self.dispatch_not_found(message) 

589 

590 # noinspection PyMethodMayBeStatic 

591 def get_paginator(self, queryset, per_page): 

592 """Get a paginator instance. 

593 

594 :rtype: Paginator 

595 

596 """ 

597 return Paginator(queryset, per_page) 

598 

599 

600class UpdateView(ModelView): 

601 """Update and existing model record (instance). 

602 

603 .. note:: 

604 Because :py:class:`ModelView`` extends :py:class:`FormMixin`, this class does *not* need to implement 

605 ``get_form()`` or ``form_invalid()``. ``get()`` and ``post()`` must be implemented so that the current model 

606 instance is bound to the form. 

607 

608 See also ``ModelView.get_success_url()``. 

609 

610 """ 

611 

612 template_name_suffix = "_form" 

613 

614 def form_valid(self, form): 

615 """Save the record.""" 

616 self.object = form.save() 

617 

618 return super().form_valid(form) 

619 

620 def get(self, request, *args, **kwargs): 

621 """Get the object and add ``form`` to the context.""" 

622 self.object = self.get_object() 

623 

624 form = self.get_form(instance=self.object) 

625 

626 context = self.get_context_data(form=form) 

627 

628 return self.render_to_response(context) 

629 

630 def post(self, request, *args, **kwargs): 

631 """Get the object and form and check whether the form is valid.""" 

632 self.object = self.get_object() 

633 

634 form = self.get_form(data=request.POST, files=request.FILES, instance=self.object) 

635 if form.is_valid(): 

636 return self.form_valid(form) 

637 

638 return self.form_invalid(form)