説明なし

viewsets.py 39KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958
  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. 'id': 'id_edit_button',
  496. 'url': viewset.get_view_url(UPDATE_VIEW_TYPE, self, self.request.GET, self.get_cached_object()),
  497. 'text': edit_button_text,
  498. 'button_class': viewset.get_edit_button_class()
  499. }
  500. )
  501. if viewset.is_view_enabled[DELETE_VIEW_TYPE]:
  502. delete_button_text = viewset.get_delete_button_text()
  503. if delete_button_text is not None:
  504. action_group.append(
  505. {
  506. 'type': 'button',
  507. 'id': 'id_delete_button',
  508. 'url': viewset.get_view_url(DELETE_VIEW_TYPE, self, self.request.GET, self.get_cached_object()),
  509. 'text': delete_button_text,
  510. 'button_class': viewset.get_delete_button_class()
  511. }
  512. )
  513. if action_group:
  514. context['actions'].append(action_group)
  515. context['nested_list_views'] = []
  516. if self.viewset.nested_list_views:
  517. active_index = self.request.GET.get(active_nested_list_view_query_param, '')
  518. have_active = False
  519. for nested_viewset in self.viewset.nested_list_views:
  520. index = str(nested_viewset.nested_list_view_index)
  521. active = index == active_index
  522. have_active = have_active or active
  523. GET = self.request.GET.copy()
  524. GET[active_nested_list_view_query_param] = index
  525. nested_context = {
  526. 'active': active,
  527. 'id': 't' + index,
  528. 'caption': nested_viewset.get_list_view_title(self),
  529. 'actions': []
  530. }
  531. nested_viewset.add_list_view_context_data(self, GET, nested_context)
  532. context['nested_list_views'].append(nested_context)
  533. if not have_active:
  534. context['nested_list_views'][0]['active'] = True
  535. return context
  536. self.views[DETAIL_VIEW_TYPE] = self.get_decorated_view(DetailView)
  537. def generate_update_view(self):
  538. if self.is_view_generated(UPDATE_VIEW_TYPE):
  539. return
  540. class UpdateView(SaveErrorHandlerMixin, self.get_base_view_mixin(), generic.UpdateView):
  541. viewset = self
  542. view_type = UPDATE_VIEW_TYPE
  543. form_class = self.get_update_view_form_class()
  544. template_name = self.update_view_template_name
  545. final_breadcrumb = None if self.instance_breadcrumb_view_type == UPDATE_VIEW_TYPE else 'Update'
  546. def dispatch(self, request, *args, **kwargs):
  547. self.populate_object_cache(self.viewset.get_object(self))
  548. return super(UpdateView, self).dispatch(request, *args, **kwargs)
  549. def get_view_title(self):
  550. return self.viewset.get_update_view_title(self)
  551. def get_context_data(self, *args, **kwargs):
  552. context = super(UpdateView, self).get_context_data(*args, **kwargs)
  553. viewset = self.viewset
  554. if viewset.is_view_enabled[DELETE_VIEW_TYPE]:
  555. delete_button_text = viewset.get_delete_button_text()
  556. if delete_button_text is not None:
  557. context['actions'].append(
  558. [
  559. {
  560. 'type': 'button',
  561. 'url': viewset.get_view_url(DELETE_VIEW_TYPE, self, self.request.GET, self.get_cached_object()),
  562. 'text': delete_button_text,
  563. 'button_class': viewset.get_delete_button_class()
  564. }
  565. ]
  566. )
  567. context['form_exclude'] = self.viewset.get_update_view_form_exclude(self)
  568. return context
  569. def form_valid(self, form):
  570. self.viewset.update_view_form_valid(self, form)
  571. return super(UpdateView, self).form_valid(form)
  572. def get_initial(self):
  573. return self.viewset.get_update_view_form_initial(self)
  574. def get_form(self, form_class=None):
  575. form = super(UpdateView, self).get_form(form_class)
  576. form.view = self
  577. customise = getattr(form, 'customise_for_view', None)
  578. if customise is not None:
  579. customise(self)
  580. return form
  581. def get_success_url(self):
  582. return self.viewset.get_update_view_success_url(self)
  583. self.views[UPDATE_VIEW_TYPE] = self.get_decorated_view(UpdateView)
  584. def generate_delete_view(self):
  585. if self.is_view_generated(DELETE_VIEW_TYPE):
  586. return
  587. class DeleteView(self.get_base_view_mixin(), generic.DeleteView):
  588. viewset = self
  589. view_type = DELETE_VIEW_TYPE
  590. template_name = self.delete_view_template_name
  591. final_breadcrumb = 'Delete'
  592. def dispatch(self, request, *args, **kwargs):
  593. self.populate_object_cache(self.viewset.get_object(self))
  594. return super(DeleteView, self).dispatch(request, *args, **kwargs)
  595. def get_view_title(self):
  596. return self.viewset.get_delete_view_title(self)
  597. def get_success_url(self):
  598. return self.viewset.get_delete_view_success_url(self)
  599. if getattr(self, 'delete_object', None) is not None:
  600. def delete(self, request, *args, **kwargs):
  601. return self.viewset.delete_object(self, request, *args, **kwargs)
  602. DeleteView.delete = delete
  603. self.views[DELETE_VIEW_TYPE] = self.get_decorated_view(DeleteView)
  604. @classmethod
  605. def urls(cls):
  606. viewset = cls()
  607. lookup_regex = r'(?P<' + viewset.lookup_url_kwarg + '>' + viewset.lookup_url_kwarg_pattern + ')/' if viewset.lookup_regex is None else viewset.lookup_regex
  608. patterns = []
  609. def append_pattern(pattern, view_type):
  610. view = viewset.views[view_type]
  611. if view is not None:
  612. patterns.append(url(pattern, view, name=view_type))
  613. append_pattern(r'^$', LIST_VIEW_TYPE)
  614. append_pattern(r'^create/$', CREATE_VIEW_TYPE)
  615. append_pattern(lookup_regex + '$', DETAIL_VIEW_TYPE)
  616. append_pattern(lookup_regex + 'update/$', UPDATE_VIEW_TYPE)
  617. append_pattern(lookup_regex + 'delete/$', DELETE_VIEW_TYPE)
  618. if not patterns:
  619. # If a viewset functions only as a nested list view then it will
  620. # not define any views of its own. We need at least one view URL
  621. # to be defined in order to discover the viewset via link_viewsets,
  622. # however, so we simply define a dummy one.
  623. def not_found(request, *args, **kwargs):
  624. raise Http404('Not found')
  625. not_found.viewset = viewset
  626. patterns.append(url(r'^$', not_found, name=NOT_FOUND_VIEW_TYPE))
  627. return patterns
  628. @staticmethod
  629. def link_viewsets(views):
  630. viewsets = {}
  631. for view_function, url_pattern, view_name in views:
  632. viewset = getattr(view_function, 'viewset', None)
  633. if viewset is not None:
  634. viewset_class = viewset.__class__
  635. if viewset_class in viewsets:
  636. # TODO: we can remove this restriction by passing a name/ID
  637. # to the CrudViewset.urls() function to use in the viewset
  638. # constructor, and then specifying that name/ID instead of
  639. # the class for master or nested viewsets.
  640. # OR we could just create derived viewset classes for each
  641. # case.
  642. if viewset != viewsets[viewset_class]:
  643. raise ImproperlyConfigured("%s.urls() is called multiple times (see TODO in code)" % viewset_class.__name__)
  644. else:
  645. viewset.url_name_prefix = CrudViewset.get_parent_url_name(view_name) + ':'
  646. viewsets[viewset_class] = viewset
  647. for viewset_class, viewset in viewsets.items():
  648. if viewset.master_viewset_class is not None:
  649. # Sanity check.
  650. if viewset.master_viewset is not None:
  651. raise RuntimeError("%s already has a master viewset" % viewset_class.__name__)
  652. viewset.master_viewset = viewsets.get(viewset.master_viewset_class, None)
  653. if viewset.master_viewset is None:
  654. raise ImproperlyConfigured("%s does not have an associated URL" % viewset.master_viewset_class.__name__)
  655. if viewset.nested_list_view_index is not None:
  656. for i, nested_viewset in enumerate(viewset.master_viewset.nested_list_views):
  657. if viewset.nested_list_view_index < nested_viewset.nested_list_view_index:
  658. viewset.master_viewset.nested_list_views.insert(i, viewset)
  659. break
  660. elif viewset.nested_list_view_index == nested_viewset.nested_list_view_index:
  661. raise ImproperlyConfigured("%s and %s have the same value for nested_list_view_index", viewset.__class__.__name__, nested_viewset.__class__.__name__)
  662. else:
  663. viewset.master_viewset.nested_list_views.append(viewset)
  664. for viewset in viewsets.values():
  665. # Prevents unnecessary processing in
  666. # remove_nested_list_view_query_params() if there are no nested list
  667. # views.
  668. if not viewset.nested_list_views:
  669. viewset.nested_list_view_query_param_prefix = ''
  670. class Tables2ListViewMixin(object):
  671. list_view_base_class = generic.TemplateView
  672. list_view_template_name = 'crud/tables2_list_view.html'
  673. detail_view_template_name = 'crud/tables2_detail_view.html'
  674. tables2_class = None
  675. tables2_context_variable_name = 'table'
  676. tables2_paginator_query_string_variable_name = 'paginator_query_string'
  677. tables2_template = 'django_tables2/bootstrap.html'
  678. tables2_attributes = {}
  679. tables2_meta_attributes = {}
  680. tables2_page_field = 'p'
  681. tables2_per_page_field = 'c'
  682. tables2_order_by_field = 's'
  683. tables2_clickable_rows = False
  684. tables2_clickable_row_class = 'clickable-row'
  685. tables2_clickable_row_view_type = DETAIL_VIEW_TYPE
  686. def __init__(self, *args, **kwargs):
  687. super(Tables2ListViewMixin, self).__init__(*args, **kwargs)
  688. # For convenience.
  689. if not self.is_view_enabled[DETAIL_VIEW_TYPE] and (self.tables2_clickable_row_view_type == DETAIL_VIEW_TYPE):
  690. self.tables2_clickable_row_view_type = UPDATE_VIEW_TYPE
  691. if self.tables2_class is None:
  692. meta = type('Meta', (object,), self.tables2_meta_attributes)
  693. attrs = copy.copy(self.tables2_attributes)
  694. attrs['Meta'] = meta
  695. self.tables2_class = type('Table', (django_tables2.Table,), attrs)
  696. try:
  697. my_attrs = copy.copy(self.tables2_class.Meta.attrs)
  698. except AttributeError:
  699. my_attrs = {}
  700. my_attrs['class'] = my_attrs.get('class', 'table table-bordered table-striped table-hover')
  701. class DerivedTable(self.tables2_class):
  702. class Meta(self.tables2_class.Meta):
  703. model = self.model
  704. template = self.tables2_template
  705. page_field = self.list_view_query_param_prefix + self.tables2_page_field
  706. per_page_field = self.list_view_query_param_prefix + self.tables2_per_page_field
  707. order_by_field = self.list_view_query_param_prefix + self.tables2_order_by_field
  708. attrs = my_attrs
  709. # This will create an instance variable containing the class and
  710. # leave the original class variable unchanged.
  711. self.tables2_class = DerivedTable
  712. def add_list_view_context_data(self, view, GET, context):
  713. super(Tables2ListViewMixin, self).add_list_view_context_data(view, GET, context)
  714. # Sanity check: get_queryset() will not have been called yet as we
  715. # are using a TemplateView instead of a ListView.
  716. if 'object_list' in context:
  717. raise RuntimeError("Context contains an 'object_list' key")
  718. if self.filter_class is None:
  719. queryset = self.get_queryset(view)
  720. else:
  721. context['filter'] = self.filter_class(GET, self.get_queryset(view), prefix=self.filter_query_param_prefix)
  722. queryset = context['filter'].qs
  723. table_kwargs = {}
  724. if self.tables2_clickable_rows:
  725. table_kwargs['row_attrs'] = {
  726. 'class': self.tables2_clickable_row_class,
  727. 'data-href': lambda record: self.get_view_url(self.tables2_clickable_row_view_type, view, GET, record)
  728. }
  729. table = self.tables2_class(queryset, **table_kwargs)
  730. django_tables2.RequestConfig(view.request).configure(table)
  731. table.viewset = self
  732. table.view = view
  733. table.GET = GET
  734. paginator_query_params = GET.copy()
  735. paginator_query_params.pop(table.page_field, None)
  736. context[self.tables2_context_variable_name] = table
  737. context[self.tables2_paginator_query_string_variable_name] = paginator_query_params.urlencode()
  738. if self.is_view_enabled[CREATE_VIEW_TYPE]:
  739. create_button_text = self.get_create_button_text()
  740. if create_button_text is not None:
  741. context['actions'].append(
  742. [
  743. {
  744. 'type': 'button',
  745. 'id': self.list_view_query_param_prefix + 'create_button',
  746. 'url': self.get_view_url(CREATE_VIEW_TYPE, view, GET),
  747. 'text': create_button_text,
  748. 'button_class': self.get_create_button_class()
  749. }
  750. ]
  751. )