Brak opisu

viewsets.py 41KB

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