Aucune description

viewsets.py 39KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955
  1. from django.conf.urls import url
  2. from django.contrib.auth.decorators import login_required
  3. from django.core.exceptions import ImproperlyConfigured, ValidationError
  4. from django.db.models.query import QuerySet
  5. from django.http import Http404
  6. from django.urls import reverse
  7. from django.shortcuts import get_object_or_404
  8. from django.views import generic
  9. import django_tables2
  10. import copy
  11. import threading
  12. #import logging
  13. #logger = logging.getLogger("console")
  14. LIST_VIEW_TYPE = 'list'
  15. CREATE_VIEW_TYPE = 'create'
  16. DETAIL_VIEW_TYPE = 'detail'
  17. UPDATE_VIEW_TYPE = 'update'
  18. DELETE_VIEW_TYPE = 'delete'
  19. # Used in the case where a viewset functions only as a nested list view, and
  20. # therefore doesn't require any URLs of its own.
  21. NOT_FOUND_VIEW_TYPE = 'not_found'
  22. INSTANCE_VIEW_TYPES = [DETAIL_VIEW_TYPE, UPDATE_VIEW_TYPE, DELETE_VIEW_TYPE]
  23. class SaveErrorHandlerMixin(object):
  24. def form_valid(self, form):
  25. try:
  26. return super(SaveErrorHandlerMixin, self).form_valid(form)
  27. except ValidationError as e:
  28. for field, errors in e:
  29. form.add_error(field, errors)
  30. return self.form_invalid(form)
  31. class CrudViewset(object):
  32. model = None
  33. queryset = None
  34. root_breadcrumbs = []
  35. master_viewset_class = None
  36. nested_list_view_index = None
  37. # Setting this to any of the instance view types will cause the master
  38. # viewset to use its instance_breadcrumb_view_type setting, so the only
  39. # other value that makes sense is LIST_VIEW_TYPE.
  40. master_viewset_breadcrumb_view_type = DETAIL_VIEW_TYPE
  41. # instance_breadcrumb_text.format(instance)
  42. instance_breadcrumb_text = u"{0}"
  43. instance_breadcrumb_view_type = DETAIL_VIEW_TYPE
  44. lookup_field = 'pk'
  45. lookup_url_kwarg = 'pk'
  46. lookup_url_kwarg_pattern = r'\d+'
  47. lookup_regex = None
  48. view_url_kwarg_names = []
  49. active_nested_list_view_query_param = 't'
  50. view_decorator = staticmethod(login_required)
  51. exclude_views = []
  52. create_button_text = 'Create'
  53. create_button_class = 'btn-primary'
  54. edit_button_text = 'Edit'
  55. edit_button_class = 'btn-primary'
  56. delete_button_text = 'Delete'
  57. delete_button_class = 'btn-danger'
  58. back_button_text = 'Back'
  59. back_button_class = 'btn-default'
  60. base_template_shim = None
  61. list_view_template_name = None
  62. create_view_template_name = 'crud/create_view.html'
  63. detail_view_template_name = 'crud/detail_view.html'
  64. update_view_template_name = 'crud/update_view.html'
  65. delete_view_template_name = 'crud/delete_view.html'
  66. # list_view_title.format(model.verbose_name_plural) if model can be
  67. # determined. The value is set to None here in case model cannot be
  68. # determined, in which case a specific value must be provided.
  69. list_view_title = None
  70. # create_view_title.format(model.verbose_name) if model can be determined.
  71. # The value is set to None here in case model cannot be determined, in
  72. # which case a specific value must be provided.
  73. create_view_title = None
  74. # *_view_title.format(view.object)
  75. detail_view_title = u"{0}"
  76. update_view_title = u"Edit {0}"
  77. delete_view_title = u"Delete {0}"
  78. success_url = None
  79. create_view_success_url = None
  80. update_view_success_url = None
  81. delete_view_success_url = None
  82. filter_class = None
  83. form_class = None
  84. create_view_form_class = None
  85. update_view_form_class = None
  86. form_exclude = []
  87. create_view_form_exclude = None
  88. update_view_form_exclude = None
  89. list_view_base_class = generic.ListView
  90. create_view_base_class = generic.CreateView
  91. def __init__(self):
  92. self.master_viewset = None
  93. self.nested_list_views = []
  94. self.is_list_view_nested = (self.master_viewset_class is not None) and (self.nested_list_view_index is not None)
  95. self.list_view_query_param_prefix = self.get_list_view_query_param_prefix()
  96. # This will be set to an empty string later if no nested list views are
  97. # linked to this view. We need the value in the detail view before then,
  98. # however.
  99. self.nested_list_view_query_param_prefix = self.get_nested_list_view_query_param_prefix()
  100. # Remove the trailing '-' as django_filter adds it in again.
  101. self.filter_query_param_prefix = self.list_view_query_param_prefix[:-1]
  102. # Cache the get_filter_class result in an instance attribute, which will
  103. # hide the class attribute of the same name.
  104. self.filter_class = self.get_filter_class()
  105. self.base_view_mixin = None
  106. self.views = {}
  107. self.exclude_views = set(self.exclude_views)
  108. if self.is_list_view_nested:
  109. self.exclude_views.add(LIST_VIEW_TYPE)
  110. if self.model is None:
  111. queryset = self.get_queryset(None)
  112. if isinstance(queryset, QuerySet):
  113. self.model = queryset.model
  114. elif self.queryset is None:
  115. # It's the user's problem if they define 'model' and override
  116. # 'get_queryset' at the same time.
  117. self.queryset = self.model._default_manager.all()
  118. else:
  119. raise ImproperlyConfigured("%s contains definitions for both 'model' and 'queryset', but only one or the other is allowed" % self.__class__.__name__)
  120. # If model is None then list_view_title and create_view_title will be
  121. # left with their default values (which are None by default and will
  122. # cause the corresponding getters to raise an error).
  123. if self.model is not None:
  124. if self.list_view_title is None:
  125. self.list_view_title = u"{0}"
  126. self.list_view_title = self.list_view_title.format(self.model._meta.verbose_name_plural.title())
  127. if self.create_view_title is None:
  128. self.create_view_title = u"Create {0}"
  129. self.create_view_title = self.create_view_title.format(self.model._meta.verbose_name.title())
  130. self.generate_list_view()
  131. self.generate_create_view()
  132. self.generate_detail_view()
  133. self.generate_update_view()
  134. self.generate_delete_view()
  135. self.is_view_enabled = {view_type: view is not None for view_type, view in self.views.items()}
  136. # For convenience.
  137. if not self.is_view_enabled[DETAIL_VIEW_TYPE] and (self.instance_breadcrumb_view_type == DETAIL_VIEW_TYPE):
  138. self.instance_breadcrumb_view_type = UPDATE_VIEW_TYPE
  139. @staticmethod
  140. def reverse_with_query_params(viewname, *args, **kwargs):
  141. query_params = kwargs.pop('query_params', None)
  142. url = reverse(viewname, *args, **kwargs)
  143. if query_params:
  144. url += '?' + query_params.urlencode()
  145. return url
  146. @staticmethod
  147. def get_parent_url_name(url_name, remove_count=1):
  148. return ':'.join(url_name.split(':')[:-remove_count])
  149. @staticmethod
  150. def derive_view_url_kwarg_names(master_viewset_class):
  151. return master_viewset_class.view_url_kwarg_names + [master_viewset_class.lookup_url_kwarg]
  152. @classmethod
  153. def depth(cls):
  154. return 0 if cls.master_viewset_class is None else cls.master_viewset_class.depth() + 1
  155. def get_master_object(self, view, instance):
  156. raise ImproperlyConfigured("%s requires an implementation of 'get_master_object'" % self.__class__.__name__)
  157. def get_list_view_query_param_prefix_core(self, relative_depth):
  158. # The prefix must include a trailing '-' to match the way django_filter
  159. # handles prefixes.
  160. prefix = 'd%d-' % (self.depth() + relative_depth)
  161. if self.is_list_view_nested and (relative_depth == 0):
  162. return '%s%d-' % (prefix, self.nested_list_view_index)
  163. else:
  164. return prefix
  165. def get_list_view_query_param_prefix(self):
  166. return self.get_list_view_query_param_prefix_core(0)
  167. def get_nested_list_view_query_param_prefix(self):
  168. return self.get_list_view_query_param_prefix_core(1)
  169. def remove_list_view_query_params_core(self, GET, prefix):
  170. if prefix != '':
  171. filtered = GET.copy()
  172. for key in filtered.keys():
  173. if key.startswith(prefix):
  174. filtered.pop(key, None)
  175. return filtered
  176. return GET
  177. def remove_list_view_query_params(self, GET):
  178. return self.remove_list_view_query_params_core(GET, self.list_view_query_param_prefix)
  179. def remove_nested_list_view_query_params(self, GET):
  180. return self.remove_list_view_query_params_core(GET, self.nested_list_view_query_param_prefix)
  181. def get_view_url_kwargs(self, view_type, view, instance=None):
  182. kwargs = {name: view.kwargs[name] for name in self.view_url_kwarg_names}
  183. if view_type in INSTANCE_VIEW_TYPES:
  184. if isinstance(instance, dict):
  185. kwargs[self.lookup_url_kwarg] = instance.get(self.lookup_field)
  186. else:
  187. kwargs[self.lookup_url_kwarg] = getattr(instance, self.lookup_field)
  188. return kwargs
  189. def get_view_url(self, view_type, view, GET, instance=None):
  190. if self.is_list_view_nested and (view_type == LIST_VIEW_TYPE):
  191. return self.master_viewset.get_view_url(self.master_viewset.instance_breadcrumb_view_type, view, GET, view.cached_objects[self.master_viewset])
  192. else:
  193. return self.reverse_with_query_params(self.url_name_prefix + view_type, kwargs=self.get_view_url_kwargs(view_type, view, instance), query_params=self.remove_nested_list_view_query_params(GET) if view_type == LIST_VIEW_TYPE else GET)
  194. def get_root_breadcrumbs(self, view):
  195. return self.root_breadcrumbs
  196. def get_list_view_breadcrumb(self, view, GET, instance=None):
  197. return {
  198. 'text': self.get_list_view_title(view),
  199. 'url': self.get_view_url(LIST_VIEW_TYPE, view, GET)
  200. }
  201. def get_instance_breadcrumb_text(self, view, instance):
  202. return self.instance_breadcrumb_text.format(instance)
  203. def get_breadcrumbs(self, view_type, view, GET):
  204. breadcrumbs = []
  205. if self.master_viewset is None:
  206. breadcrumbs += self.get_root_breadcrumbs(view)
  207. else:
  208. breadcrumbs += self.master_viewset.get_breadcrumbs(self.master_viewset_breadcrumb_view_type, view, GET if self.is_list_view_nested else self.remove_list_view_query_params(GET))
  209. instance = view.cached_objects[self]
  210. # The list view of nested viewsets is part of the detail view of the
  211. # master viewset, and the breadcrumb will already have been added above.
  212. if not self.is_list_view_nested and self.is_view_enabled[LIST_VIEW_TYPE]:
  213. breadcrumbs.append(self.get_list_view_breadcrumb(view, GET, instance))
  214. if view_type in INSTANCE_VIEW_TYPES:
  215. breadcrumbs += [{
  216. 'text': self.get_instance_breadcrumb_text(view, instance),
  217. 'url': self.get_view_url(self.instance_breadcrumb_view_type, view, GET, instance)
  218. }]
  219. return breadcrumbs
  220. def get_list_view_title(self, view):
  221. if self.list_view_title is None:
  222. raise ImproperlyConfigured("%s requires either a definition of 'list_view_title' or an implementation of 'get_list_view_title'" % self.__class__.__name__)
  223. return self.list_view_title
  224. def get_create_view_title(self, view):
  225. if self.create_view_title is None:
  226. raise ImproperlyConfigured("%s requires either a definition of 'create_view_title' or an implementation of 'get_create_view_title'" % self.__class__.__name__)
  227. return self.create_view_title
  228. def get_detail_view_title(self, view):
  229. return self.detail_view_title.format(view.object)
  230. def get_update_view_title(self, view):
  231. return self.update_view_title.format(view.object)
  232. def get_delete_view_title(self, view):
  233. return self.delete_view_title.format(view.object)
  234. """
  235. If get_queryset is overridden to return an iterable rather than a QuerySet
  236. you must provide an implementation of:
  237. get_object(self, view, queryset=None)
  238. in order for instance views to work correctly. You can also provide an
  239. implementation of:
  240. delete_object(self, view, request, *args, **kwargs)
  241. to implement custom deletion logic.
  242. """
  243. def get_queryset(self, view):
  244. if self.queryset is None:
  245. raise ImproperlyConfigured("%s requires either a definition of 'model' or 'queryset', or an implementation of 'get_queryset'" % self.__class__.__name__)
  246. return self.queryset
  247. def get_object(self, view, queryset=None):
  248. if queryset is None:
  249. queryset = self.get_queryset(view)
  250. queryset = queryset.filter(**{self.lookup_field: view.kwargs[self.lookup_url_kwarg]})
  251. return get_object_or_404(queryset)
  252. def get_create_view_object(self, view):
  253. return None
  254. def add_context_data(self, view, context):
  255. pass
  256. def add_list_view_context_data(self, view, GET, context):
  257. pass
  258. def create_view_form_valid(self, view, form):
  259. pass
  260. def update_view_form_valid(self, view, form):
  261. pass
  262. def get_filter_class(self):
  263. return self.filter_class
  264. def get_form_class(self):
  265. if self.form_class is None:
  266. raise ImproperlyConfigured("%s requires a definition of either 'form_class' or the appropriate combination of 'create_view_form_class' and 'update_view_form_class'" % self.__class__.__name__)
  267. return self.form_class
  268. def get_create_view_form_class(self):
  269. if self.create_view_form_class is None:
  270. return self.get_form_class()
  271. return self.create_view_form_class
  272. def get_update_view_form_class(self):
  273. if self.update_view_form_class is None:
  274. return self.get_form_class()
  275. return self.update_view_form_class
  276. def get_form_exclude(self, view):
  277. return self.form_exclude
  278. def get_create_view_form_exclude(self, view):
  279. if self.create_view_form_exclude is None:
  280. return self.get_form_exclude(view)
  281. return self.create_view_form_exclude
  282. def get_update_view_form_exclude(self, view):
  283. if self.update_view_form_exclude is None:
  284. return self.get_form_exclude(view)
  285. return self.update_view_form_exclude
  286. def get_create_view_form_initial(self, view):
  287. return {}
  288. def get_update_view_form_initial(self, view):
  289. return {}
  290. def get_create_button_text(self):
  291. return self.create_button_text
  292. def get_create_button_class(self):
  293. return self.create_button_class
  294. def get_edit_button_text(self):
  295. return self.edit_button_text
  296. def get_edit_button_class(self):
  297. return self.edit_button_class
  298. def get_delete_button_text(self):
  299. return self.delete_button_text
  300. def get_delete_button_class(self):
  301. return self.delete_button_class
  302. def get_back_button_text(self):
  303. return self.back_button_text
  304. def get_back_button_class(self):
  305. return self.back_button_class
  306. def get_relative_view_url(self, url_name_suffix, remove_url_name_components=0, kwargs=None, query_params=None):
  307. if remove_url_name_components > 0:
  308. # url_name_prefix has a trailing colon so we always need to remove
  309. # an extra component.
  310. url_name_prefix = self.get_parent_url_name(self.url_name_prefix, remove_url_name_components + 1) + ':'
  311. else:
  312. url_name_prefix = self.url_name_prefix
  313. return self.reverse_with_query_params(url_name_prefix + url_name_suffix, kwargs=kwargs, query_params=query_params)
  314. def get_success_url(self, view):
  315. if self.success_url is None:
  316. return self.get_view_url(LIST_VIEW_TYPE, view, view.request.GET)
  317. return self.success_url
  318. def get_create_view_success_url(self, view):
  319. if self.create_view_success_url is None:
  320. return self.get_success_url(view)
  321. return self.create_view_success_url
  322. def get_update_view_success_url(self, view):
  323. if self.update_view_success_url is None:
  324. return self.get_success_url(view)
  325. return self.update_view_success_url
  326. def get_delete_view_success_url(self, view):
  327. if self.delete_view_success_url is None:
  328. return self.get_success_url(view)
  329. return self.delete_view_success_url
  330. def get_base_view_mixin(self):
  331. if self.base_view_mixin is None:
  332. class BaseViewMixin(object):
  333. viewset = self
  334. view_type = None
  335. final_breadcrumb = None
  336. pk_url_kwarg = viewset.lookup_url_kwarg
  337. def get_queryset(self):
  338. return self.viewset.get_queryset(self)
  339. def get_view_title(self):
  340. raise NotImplementedError
  341. def get_cached_object(self, viewset=None):
  342. return self.cached_objects[self.viewset if viewset is None else viewset]
  343. def get_object(self, *args, **kwargs):
  344. return self.cached_objects[self.viewset]
  345. def populate_object_cache(self, instance):
  346. # Populate a cache of objects for all viewsets in the
  347. # master-detail chain so all lookup logic is in one place
  348. # and we don't have to worry about accidentally doing
  349. # the same lookup twice in other functions.
  350. viewset = self.viewset
  351. viewset_object = instance
  352. self.cached_objects = {viewset: viewset_object}
  353. while viewset.master_viewset is not None:
  354. if viewset_object is None:
  355. viewset_object = viewset.master_viewset.get_object(self)
  356. else:
  357. viewset_object = viewset.get_master_object(self, viewset_object)
  358. viewset = viewset.master_viewset
  359. self.cached_objects[viewset] = viewset_object
  360. def get_context_data(self, *args, **kwargs):
  361. context = super(BaseViewMixin, self).get_context_data(*args, **kwargs)
  362. viewset = self.viewset
  363. breadcrumbs = viewset.get_breadcrumbs(self.view_type, self, self.request.GET)
  364. if self.final_breadcrumb is not None:
  365. breadcrumbs.append(
  366. {
  367. 'text': self.final_breadcrumb,
  368. # Includes the query string.
  369. 'url': self.request.get_full_path()
  370. }
  371. )
  372. context['base_template_shim'] = viewset.base_template_shim
  373. context['breadcrumbs'] = breadcrumbs
  374. context['query_string'] = self.request.GET.urlencode()
  375. context['view_title'] = self.get_view_title()
  376. context['is_view_enabled'] = viewset.is_view_enabled
  377. context['actions'] = []
  378. if (self.view_type != LIST_VIEW_TYPE) and (viewset.is_list_view_nested or viewset.is_view_enabled[LIST_VIEW_TYPE]):
  379. context['actions'].append(
  380. [
  381. {
  382. 'type': 'button',
  383. 'text': viewset.get_back_button_text(),
  384. 'url': viewset.get_view_url(LIST_VIEW_TYPE, self, self.request.GET),
  385. 'button_class': viewset.get_back_button_class()
  386. }
  387. ]
  388. )
  389. viewset.add_context_data(self, context)
  390. return context
  391. self.base_view_mixin = BaseViewMixin
  392. return self.base_view_mixin
  393. def get_decorated_view(self, view_class):
  394. view = view_class.as_view()
  395. if self.view_decorator is not None:
  396. view = self.view_decorator(view)
  397. view.viewset = self
  398. return view
  399. def is_view_generated(self, view_type):
  400. if view_type in self.views:
  401. return True
  402. if view_type in self.exclude_views:
  403. self.views[view_type] = None
  404. return True
  405. return False
  406. def generate_list_view(self):
  407. if self.is_view_generated(LIST_VIEW_TYPE):
  408. return
  409. class ListView(self.get_base_view_mixin(), self.list_view_base_class):
  410. viewset = self
  411. view_type = LIST_VIEW_TYPE
  412. template_name = self.list_view_template_name
  413. filter = None
  414. def dispatch(self, request, *args, **kwargs):
  415. self.populate_object_cache(None)
  416. return super(ListView, self).dispatch(request, *args, **kwargs)
  417. def get_view_title(self):
  418. return self.viewset.get_list_view_title(self)
  419. def get_context_data(self, *args, **kwargs):
  420. context = super(ListView, self).get_context_data(*args, **kwargs)
  421. if self.filter is not None:
  422. context['filter'] = self.filter
  423. self.viewset.add_list_view_context_data(self, self.request.GET, context)
  424. return context
  425. if self.filter_class is not None:
  426. filter_class = self.filter_class
  427. prefix = self.filter_query_param_prefix
  428. # NOTE: This will only be called if base_list_view_class is a
  429. # ListView, not a TemplateView.
  430. def get_queryset(self):
  431. # Sanity check: verify that get_queryset() is called only once
  432. # per request.
  433. if self.filter is not None:
  434. raise RuntimeError("ListView.get_queryset() was called more than once")
  435. self.filter = filter_class(self.request.GET, super(ListView, self).get_queryset(), prefix=prefix)
  436. return self.filter.qs
  437. ListView.get_queryset = get_queryset
  438. self.views[LIST_VIEW_TYPE] = self.get_decorated_view(ListView)
  439. def generate_create_view(self):
  440. if self.is_view_generated(CREATE_VIEW_TYPE):
  441. return
  442. class CreateView(SaveErrorHandlerMixin, self.get_base_view_mixin(), self.create_view_base_class):
  443. viewset = self
  444. view_type = CREATE_VIEW_TYPE
  445. form_class = self.get_create_view_form_class()
  446. template_name = self.create_view_template_name
  447. final_breadcrumb = 'Create'
  448. def dispatch(self, request, *args, **kwargs):
  449. self.populate_object_cache(self.viewset.get_create_view_object(self))
  450. return super(CreateView, self).dispatch(request, *args, **kwargs)
  451. def get_view_title(self):
  452. return self.viewset.get_create_view_title(self)
  453. def get_context_data(self, *args, **kwargs):
  454. context = super(CreateView, self).get_context_data(*args, **kwargs)
  455. context['form_exclude'] = self.viewset.get_create_view_form_exclude(self)
  456. return context
  457. def form_valid(self, form):
  458. self.viewset.create_view_form_valid(self, form)
  459. return super(CreateView, self).form_valid(form)
  460. def get_initial(self):
  461. return self.viewset.get_create_view_form_initial(self)
  462. def get_form(self, form_class=None):
  463. form = super(CreateView, self).get_form(form_class)
  464. form.view = self
  465. customise = getattr(form, 'customise_for_view', None)
  466. if customise is not None:
  467. customise(self)
  468. return form
  469. def get_success_url(self):
  470. return self.viewset.get_create_view_success_url(self)
  471. self.views[CREATE_VIEW_TYPE] = self.get_decorated_view(CreateView)
  472. def generate_detail_view(self):
  473. if self.is_view_generated(DETAIL_VIEW_TYPE):
  474. return
  475. active_nested_list_view_query_param = self.nested_list_view_query_param_prefix + self.active_nested_list_view_query_param
  476. class DetailView(self.get_base_view_mixin(), generic.DetailView):
  477. viewset = self
  478. view_type = DETAIL_VIEW_TYPE
  479. template_name = self.detail_view_template_name
  480. def dispatch(self, request, *args, **kwargs):
  481. self.populate_object_cache(self.viewset.get_object(self))
  482. return super(DetailView, self).dispatch(request, *args, **kwargs)
  483. def get_view_title(self):
  484. return self.viewset.get_detail_view_title(self)
  485. def get_context_data(self, *args, **kwargs):
  486. context = super(DetailView, self).get_context_data(*args, **kwargs)
  487. viewset = self.viewset
  488. action_group = []
  489. if viewset.is_view_enabled[UPDATE_VIEW_TYPE]:
  490. edit_button_text = viewset.get_edit_button_text()
  491. if edit_button_text is not None:
  492. action_group.append(
  493. {
  494. 'type': 'button',
  495. 'url': viewset.get_view_url(UPDATE_VIEW_TYPE, self, self.request.GET, self.get_cached_object()),
  496. 'text': edit_button_text,
  497. 'button_class': viewset.get_edit_button_class()
  498. }
  499. )
  500. if viewset.is_view_enabled[DELETE_VIEW_TYPE]:
  501. delete_button_text = viewset.get_delete_button_text()
  502. if delete_button_text is not None:
  503. action_group.append(
  504. {
  505. 'type': 'button',
  506. 'url': viewset.get_view_url(DELETE_VIEW_TYPE, self, self.request.GET, self.get_cached_object()),
  507. 'text': delete_button_text,
  508. 'button_class': viewset.get_delete_button_class()
  509. }
  510. )
  511. if action_group:
  512. context['actions'].append(action_group)
  513. context['nested_list_views'] = []
  514. if self.viewset.nested_list_views:
  515. active_index = self.request.GET.get(active_nested_list_view_query_param, '')
  516. have_active = False
  517. for nested_viewset in self.viewset.nested_list_views:
  518. index = str(nested_viewset.nested_list_view_index)
  519. active = index == active_index
  520. have_active = have_active or active
  521. GET = self.request.GET.copy()
  522. GET[active_nested_list_view_query_param] = index
  523. nested_context = {
  524. 'active': active,
  525. 'id': 't' + index,
  526. 'caption': nested_viewset.get_list_view_title(self),
  527. 'actions': []
  528. }
  529. nested_viewset.add_list_view_context_data(self, GET, nested_context)
  530. context['nested_list_views'].append(nested_context)
  531. if not have_active:
  532. context['nested_list_views'][0]['active'] = True
  533. return context
  534. self.views[DETAIL_VIEW_TYPE] = self.get_decorated_view(DetailView)
  535. def generate_update_view(self):
  536. if self.is_view_generated(UPDATE_VIEW_TYPE):
  537. return
  538. class UpdateView(SaveErrorHandlerMixin, self.get_base_view_mixin(), generic.UpdateView):
  539. viewset = self
  540. view_type = UPDATE_VIEW_TYPE
  541. form_class = self.get_update_view_form_class()
  542. template_name = self.update_view_template_name
  543. final_breadcrumb = None if self.instance_breadcrumb_view_type == UPDATE_VIEW_TYPE else 'Update'
  544. def dispatch(self, request, *args, **kwargs):
  545. self.populate_object_cache(self.viewset.get_object(self))
  546. return super(UpdateView, self).dispatch(request, *args, **kwargs)
  547. def get_view_title(self):
  548. return self.viewset.get_update_view_title(self)
  549. def get_context_data(self, *args, **kwargs):
  550. context = super(UpdateView, self).get_context_data(*args, **kwargs)
  551. viewset = self.viewset
  552. if viewset.is_view_enabled[DELETE_VIEW_TYPE]:
  553. delete_button_text = viewset.get_delete_button_text()
  554. if delete_button_text is not None:
  555. context['actions'].append(
  556. [
  557. {
  558. 'type': 'button',
  559. 'url': viewset.get_view_url(DELETE_VIEW_TYPE, self, self.request.GET, self.get_cached_object()),
  560. 'text': delete_button_text,
  561. 'button_class': viewset.get_delete_button_class()
  562. }
  563. ]
  564. )
  565. context['form_exclude'] = self.viewset.get_update_view_form_exclude(self)
  566. return context
  567. def form_valid(self, form):
  568. self.viewset.update_view_form_valid(self, form)
  569. return super(UpdateView, self).form_valid(form)
  570. def get_initial(self):
  571. return self.viewset.get_update_view_form_initial(self)
  572. def get_form(self, form_class=None):
  573. form = super(UpdateView, self).get_form(form_class)
  574. form.view = self
  575. customise = getattr(form, 'customise_for_view', None)
  576. if customise is not None:
  577. customise(self)
  578. return form
  579. def get_success_url(self):
  580. return self.viewset.get_update_view_success_url(self)
  581. self.views[UPDATE_VIEW_TYPE] = self.get_decorated_view(UpdateView)
  582. def generate_delete_view(self):
  583. if self.is_view_generated(DELETE_VIEW_TYPE):
  584. return
  585. class DeleteView(self.get_base_view_mixin(), generic.DeleteView):
  586. viewset = self
  587. view_type = DELETE_VIEW_TYPE
  588. template_name = self.delete_view_template_name
  589. final_breadcrumb = 'Delete'
  590. def dispatch(self, request, *args, **kwargs):
  591. self.populate_object_cache(self.viewset.get_object(self))
  592. return super(DeleteView, self).dispatch(request, *args, **kwargs)
  593. def get_view_title(self):
  594. return self.viewset.get_delete_view_title(self)
  595. def get_success_url(self):
  596. return self.viewset.get_delete_view_success_url(self)
  597. if getattr(self, 'delete_object', None) is not None:
  598. def delete(self, request, *args, **kwargs):
  599. return self.viewset.delete_object(self, request, *args, **kwargs)
  600. DeleteView.delete = delete
  601. self.views[DELETE_VIEW_TYPE] = self.get_decorated_view(DeleteView)
  602. @classmethod
  603. def urls(cls):
  604. viewset = cls()
  605. lookup_regex = r'(?P<' + viewset.lookup_url_kwarg + '>' + viewset.lookup_url_kwarg_pattern + ')/' if viewset.lookup_regex is None else viewset.lookup_regex
  606. patterns = []
  607. def append_pattern(pattern, view_type):
  608. view = viewset.views[view_type]
  609. if view is not None:
  610. patterns.append(url(pattern, view, name=view_type))
  611. append_pattern(r'^$', LIST_VIEW_TYPE)
  612. append_pattern(r'^create/$', CREATE_VIEW_TYPE)
  613. append_pattern(lookup_regex + '$', DETAIL_VIEW_TYPE)
  614. append_pattern(lookup_regex + 'update/$', UPDATE_VIEW_TYPE)
  615. append_pattern(lookup_regex + 'delete/$', DELETE_VIEW_TYPE)
  616. if not patterns:
  617. # If a viewset functions only as a nested list view then it will
  618. # not define any views of its own. We need at least one view URL
  619. # to be defined in order to discover the viewset via link_viewsets,
  620. # however, so we simply define a dummy one.
  621. def not_found(request, *args, **kwargs):
  622. raise Http404('Not found')
  623. not_found.viewset = viewset
  624. patterns.append(url(r'^$', not_found, name=NOT_FOUND_VIEW_TYPE))
  625. return patterns
  626. @staticmethod
  627. def link_viewsets(views):
  628. viewsets = {}
  629. for view_function, url_pattern, view_name in views:
  630. viewset = getattr(view_function, 'viewset', None)
  631. if viewset is not None:
  632. viewset_class = viewset.__class__
  633. if viewset_class in viewsets:
  634. # TODO: we can remove this restriction by passing a name/ID
  635. # to the CrudViewset.urls() function to use in the viewset
  636. # constructor, and then specifying that name/ID instead of
  637. # the class for master or nested viewsets.
  638. # OR we could just create derived viewset classes for each
  639. # case.
  640. if viewset != viewsets[viewset_class]:
  641. raise ImproperlyConfigured("%s.urls() is called multiple times (see TODO in code)" % viewset_class.__name__)
  642. else:
  643. viewset.url_name_prefix = CrudViewset.get_parent_url_name(view_name) + ':'
  644. viewsets[viewset_class] = viewset
  645. for viewset_class, viewset in viewsets.items():
  646. if viewset.master_viewset_class is not None:
  647. # Sanity check.
  648. if viewset.master_viewset is not None:
  649. raise RuntimeError("%s already has a master viewset" % viewset_class.__name__)
  650. viewset.master_viewset = viewsets.get(viewset.master_viewset_class, None)
  651. if viewset.master_viewset is None:
  652. raise ImproperlyConfigured("%s does not have an associated URL" % viewset.master_viewset_class.__name__)
  653. if viewset.nested_list_view_index is not None:
  654. for i, nested_viewset in enumerate(viewset.master_viewset.nested_list_views):
  655. if viewset.nested_list_view_index < nested_viewset.nested_list_view_index:
  656. viewset.master_viewset.nested_list_views.insert(i, viewset)
  657. break
  658. elif viewset.nested_list_view_index == nested_viewset.nested_list_view_index:
  659. raise ImproperlyConfigured("%s and %s have the same value for nested_list_view_index", viewset.__class__.__name__, nested_viewset.__class__.__name__)
  660. else:
  661. viewset.master_viewset.nested_list_views.append(viewset)
  662. for viewset in viewsets.values():
  663. # Prevents unnecessary processing in
  664. # remove_nested_list_view_query_params() if there are no nested list
  665. # views.
  666. if not viewset.nested_list_views:
  667. viewset.nested_list_view_query_param_prefix = ''
  668. class Tables2ListViewMixin(object):
  669. list_view_base_class = generic.TemplateView
  670. list_view_template_name = 'crud/tables2_list_view.html'
  671. detail_view_template_name = 'crud/tables2_detail_view.html'
  672. tables2_class = None
  673. tables2_context_variable_name = 'table'
  674. tables2_paginator_query_string_variable_name = 'paginator_query_string'
  675. tables2_template = 'django_tables2/bootstrap.html'
  676. tables2_attributes = {}
  677. tables2_meta_attributes = {}
  678. tables2_page_field = 'p'
  679. tables2_per_page_field = 'c'
  680. tables2_order_by_field = 's'
  681. tables2_clickable_rows = False
  682. tables2_clickable_row_class = 'clickable-row'
  683. tables2_clickable_row_view_type = DETAIL_VIEW_TYPE
  684. def __init__(self, *args, **kwargs):
  685. super(Tables2ListViewMixin, self).__init__(*args, **kwargs)
  686. # For convenience.
  687. if not self.is_view_enabled[DETAIL_VIEW_TYPE] and (self.tables2_clickable_row_view_type == DETAIL_VIEW_TYPE):
  688. self.tables2_clickable_row_view_type = UPDATE_VIEW_TYPE
  689. if self.tables2_class is None:
  690. meta = type('Meta', (object,), self.tables2_meta_attributes)
  691. attrs = copy.copy(self.tables2_attributes)
  692. attrs['Meta'] = meta
  693. self.tables2_class = type('Table', (django_tables2.Table,), attrs)
  694. try:
  695. my_attrs = copy.copy(self.tables2_class.Meta.attrs)
  696. except AttributeError:
  697. my_attrs = {}
  698. my_attrs['class'] = my_attrs.get('class', 'table table-bordered table-striped table-hover')
  699. class DerivedTable(self.tables2_class):
  700. class Meta(self.tables2_class.Meta):
  701. model = self.model
  702. template = self.tables2_template
  703. page_field = self.list_view_query_param_prefix + self.tables2_page_field
  704. per_page_field = self.list_view_query_param_prefix + self.tables2_per_page_field
  705. order_by_field = self.list_view_query_param_prefix + self.tables2_order_by_field
  706. attrs = my_attrs
  707. # This will create an instance variable containing the class and
  708. # leave the original class variable unchanged.
  709. self.tables2_class = DerivedTable
  710. def add_list_view_context_data(self, view, GET, context):
  711. super(Tables2ListViewMixin, self).add_list_view_context_data(view, GET, context)
  712. # Sanity check: get_queryset() will not have been called yet as we
  713. # are using a TemplateView instead of a ListView.
  714. if 'object_list' in context:
  715. raise RuntimeError("Context contains an 'object_list' key")
  716. if self.filter_class is None:
  717. queryset = self.get_queryset(view)
  718. else:
  719. context['filter'] = self.filter_class(GET, self.get_queryset(view), prefix=self.filter_query_param_prefix)
  720. queryset = context['filter'].qs
  721. table_kwargs = {}
  722. if self.tables2_clickable_rows:
  723. table_kwargs['row_attrs'] = {
  724. 'class': self.tables2_clickable_row_class,
  725. 'data-href': lambda record: self.get_view_url(self.tables2_clickable_row_view_type, view, GET, record)
  726. }
  727. table = self.tables2_class(queryset, **table_kwargs)
  728. django_tables2.RequestConfig(view.request).configure(table)
  729. table.viewset = self
  730. table.view = view
  731. table.GET = GET
  732. paginator_query_params = GET.copy()
  733. paginator_query_params.pop(table.page_field, None)
  734. context[self.tables2_context_variable_name] = table
  735. context[self.tables2_paginator_query_string_variable_name] = paginator_query_params.urlencode()
  736. if self.is_view_enabled[CREATE_VIEW_TYPE]:
  737. create_button_text = self.get_create_button_text()
  738. if create_button_text is not None:
  739. context['actions'].append(
  740. [
  741. {
  742. 'type': 'button',
  743. 'url': self.get_view_url(CREATE_VIEW_TYPE, view, GET),
  744. 'text': create_button_text,
  745. 'button_class': self.get_create_button_class()
  746. }
  747. ]
  748. )