from django.conf.urls import url from django.contrib.auth.decorators import login_required from django.core.exceptions import ImproperlyConfigured, ValidationError from django.db.models.query import QuerySet from django.http import Http404 from django.urls import reverse from django.shortcuts import get_object_or_404 from django.views import generic import django_tables2 import copy import threading #import logging #logger = logging.getLogger("console") LIST_VIEW_TYPE = 'list' CREATE_VIEW_TYPE = 'create' DETAIL_VIEW_TYPE = 'detail' UPDATE_VIEW_TYPE = 'update' DELETE_VIEW_TYPE = 'delete' # Used in the case where a viewset functions only as a nested list view, and # therefore doesn't require any URLs of its own. NOT_FOUND_VIEW_TYPE = 'not_found' INSTANCE_VIEW_TYPES = [DETAIL_VIEW_TYPE, UPDATE_VIEW_TYPE, DELETE_VIEW_TYPE] class SaveErrorHandlerMixin(object): def form_valid(self, form): try: return super(SaveErrorHandlerMixin, self).form_valid(form) except ValidationError as e: for field, errors in e: form.add_error(field, errors) return self.form_invalid(form) class CrudViewset(object): model = None queryset = None root_breadcrumbs = [] master_viewset_class = None nested_list_view_index = None # Setting this to any of the instance view types will cause the master # viewset to use its instance_breadcrumb_view_type setting, so the only # other value that makes sense is LIST_VIEW_TYPE. master_viewset_breadcrumb_view_type = DETAIL_VIEW_TYPE # instance_breadcrumb_text.format(instance) instance_breadcrumb_text = u"{0}" instance_breadcrumb_view_type = DETAIL_VIEW_TYPE lookup_field = 'pk' lookup_url_kwarg = 'pk' lookup_url_kwarg_pattern = r'\d+' lookup_regex = None view_url_kwarg_names = [] active_nested_list_view_query_param = 't' view_decorator = staticmethod(login_required) exclude_views = [] create_button_text = 'Create' create_button_class = 'btn-primary' edit_button_text = 'Edit' edit_button_class = 'btn-primary' delete_button_text = 'Delete' delete_button_class = 'btn-danger' back_button_text = 'Back' back_button_class = 'btn-default' base_template_shim = None list_view_template_name = None create_view_template_name = 'crud/create_view.html' detail_view_template_name = 'crud/detail_view.html' update_view_template_name = 'crud/update_view.html' delete_view_template_name = 'crud/delete_view.html' # list_view_title.format(model.verbose_name_plural) if model can be # determined. The value is set to None here in case model cannot be # determined, in which case a specific value must be provided. list_view_title = None # create_view_title.format(model.verbose_name) if model can be determined. # The value is set to None here in case model cannot be determined, in # which case a specific value must be provided. create_view_title = None # *_view_title.format(view.object) detail_view_title = u"{0}" update_view_title = u"Edit {0}" delete_view_title = u"Delete {0}" success_url = None create_view_success_url = None update_view_success_url = None delete_view_success_url = None filter_class = None form_class = None create_view_form_class = None update_view_form_class = None form_exclude = [] create_view_form_exclude = None update_view_form_exclude = None list_view_base_class = generic.ListView create_view_base_class = generic.CreateView def __init__(self): self.master_viewset = None self.nested_list_views = [] self.is_list_view_nested = (self.master_viewset_class is not None) and (self.nested_list_view_index is not None) self.list_view_query_param_prefix = self.get_list_view_query_param_prefix() # This will be set to an empty string later if no nested list views are # linked to this view. We need the value in the detail view before then, # however. self.nested_list_view_query_param_prefix = self.get_nested_list_view_query_param_prefix() # Remove the trailing '-' as django_filter adds it in again. self.filter_query_param_prefix = self.list_view_query_param_prefix[:-1] # Cache the get_filter_class result in an instance attribute, which will # hide the class attribute of the same name. self.filter_class = self.get_filter_class() self.base_view_mixin = None self.views = {} self.exclude_views = set(self.exclude_views) if self.is_list_view_nested: self.exclude_views.add(LIST_VIEW_TYPE) if self.model is None: queryset = self.get_queryset(None) if isinstance(queryset, QuerySet): self.model = queryset.model elif self.queryset is None: # It's the user's problem if they define 'model' and override # 'get_queryset' at the same time. self.queryset = self.model._default_manager.all() else: raise ImproperlyConfigured("%s contains definitions for both 'model' and 'queryset', but only one or the other is allowed" % self.__class__.__name__) # If model is None then list_view_title and create_view_title will be # left with their default values (which are None by default and will # cause the corresponding getters to raise an error). if self.model is not None: if self.list_view_title is None: self.list_view_title = u"{0}" self.list_view_title = self.list_view_title.format(self.model._meta.verbose_name_plural.title()) if self.create_view_title is None: self.create_view_title = u"Create {0}" self.create_view_title = self.create_view_title.format(self.model._meta.verbose_name.title()) self.generate_list_view() self.generate_create_view() self.generate_detail_view() self.generate_update_view() self.generate_delete_view() self.is_view_enabled = {view_type: view is not None for view_type, view in self.views.items()} # For convenience. if not self.is_view_enabled[DETAIL_VIEW_TYPE] and (self.instance_breadcrumb_view_type == DETAIL_VIEW_TYPE): self.instance_breadcrumb_view_type = UPDATE_VIEW_TYPE @staticmethod def reverse_with_query_params(viewname, *args, **kwargs): query_params = kwargs.pop('query_params', None) url = reverse(viewname, *args, **kwargs) if query_params: url += '?' + query_params.urlencode() return url @staticmethod def get_parent_url_name(url_name, remove_count=1): return ':'.join(url_name.split(':')[:-remove_count]) @staticmethod def derive_view_url_kwarg_names(master_viewset_class): return master_viewset_class.view_url_kwarg_names + [master_viewset_class.lookup_url_kwarg] @classmethod def depth(cls): return 0 if cls.master_viewset_class is None else cls.master_viewset_class.depth() + 1 def get_master_object(self, view, instance): raise ImproperlyConfigured("%s requires an implementation of 'get_master_object'" % self.__class__.__name__) def get_list_view_query_param_prefix_core(self, relative_depth): # The prefix must include a trailing '-' to match the way django_filter # handles prefixes. prefix = 'd%d-' % (self.depth() + relative_depth) if self.is_list_view_nested and (relative_depth == 0): return '%s%d-' % (prefix, self.nested_list_view_index) else: return prefix def get_list_view_query_param_prefix(self): return self.get_list_view_query_param_prefix_core(0) def get_nested_list_view_query_param_prefix(self): return self.get_list_view_query_param_prefix_core(1) def remove_list_view_query_params_core(self, GET, prefix): if prefix != '': filtered = GET.copy() for key in filtered.keys(): if key.startswith(prefix): filtered.pop(key, None) return filtered return GET def remove_list_view_query_params(self, GET): return self.remove_list_view_query_params_core(GET, self.list_view_query_param_prefix) def remove_nested_list_view_query_params(self, GET): return self.remove_list_view_query_params_core(GET, self.nested_list_view_query_param_prefix) def get_view_url_kwargs(self, view_type, view, instance=None): kwargs = {name: view.kwargs[name] for name in self.view_url_kwarg_names} if view_type in INSTANCE_VIEW_TYPES: if isinstance(instance, dict): kwargs[self.lookup_url_kwarg] = instance.get(self.lookup_field) else: kwargs[self.lookup_url_kwarg] = getattr(instance, self.lookup_field) return kwargs def get_view_url(self, view_type, view, GET, instance=None): if self.is_list_view_nested and (view_type == LIST_VIEW_TYPE): return self.master_viewset.get_view_url(self.master_viewset.instance_breadcrumb_view_type, view, GET, view.cached_objects[self.master_viewset]) else: 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) def get_root_breadcrumbs(self, view): return self.root_breadcrumbs def get_list_view_breadcrumb(self, view, GET, instance=None): return { 'text': self.get_list_view_title(view), 'url': self.get_view_url(LIST_VIEW_TYPE, view, GET) } def get_instance_breadcrumb_text(self, view, instance): return self.instance_breadcrumb_text.format(instance) def get_breadcrumbs(self, view_type, view, GET): breadcrumbs = [] if self.master_viewset is None: breadcrumbs += self.get_root_breadcrumbs(view) else: 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)) instance = view.cached_objects[self] # The list view of nested viewsets is part of the detail view of the # master viewset, and the breadcrumb will already have been added above. if not self.is_list_view_nested and self.is_view_enabled[LIST_VIEW_TYPE]: breadcrumbs.append(self.get_list_view_breadcrumb(view, GET, instance)) if view_type in INSTANCE_VIEW_TYPES: breadcrumbs += [{ 'text': self.get_instance_breadcrumb_text(view, instance), 'url': self.get_view_url(self.instance_breadcrumb_view_type, view, GET, instance) }] return breadcrumbs def get_list_view_title(self, view): if self.list_view_title is None: raise ImproperlyConfigured("%s requires either a definition of 'list_view_title' or an implementation of 'get_list_view_title'" % self.__class__.__name__) return self.list_view_title def get_create_view_title(self, view): if self.create_view_title is None: raise ImproperlyConfigured("%s requires either a definition of 'create_view_title' or an implementation of 'get_create_view_title'" % self.__class__.__name__) return self.create_view_title def get_detail_view_title(self, view): return self.detail_view_title.format(view.object) def get_update_view_title(self, view): return self.update_view_title.format(view.object) def get_delete_view_title(self, view): return self.delete_view_title.format(view.object) """ If get_queryset is overridden to return an iterable rather than a QuerySet you must provide an implementation of: get_object(self, view, queryset=None) in order for instance views to work correctly. You can also provide an implementation of: delete_object(self, view, request, *args, **kwargs) to implement custom deletion logic. """ def get_queryset(self, view): if self.queryset is None: raise ImproperlyConfigured("%s requires either a definition of 'model' or 'queryset', or an implementation of 'get_queryset'" % self.__class__.__name__) return self.queryset def get_object(self, view, queryset=None): if queryset is None: queryset = self.get_queryset(view) queryset = queryset.filter(**{self.lookup_field: view.kwargs[self.lookup_url_kwarg]}) return get_object_or_404(queryset) def get_create_view_object(self, view): return None def add_context_data(self, view, context): pass def add_list_view_context_data(self, view, GET, context): pass def create_view_form_valid(self, view, form): pass def update_view_form_valid(self, view, form): pass def get_filter_class(self): return self.filter_class def get_form_class(self): if self.form_class is None: 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__) return self.form_class def get_create_view_form_class(self): if self.create_view_form_class is None: return self.get_form_class() return self.create_view_form_class def get_update_view_form_class(self): if self.update_view_form_class is None: return self.get_form_class() return self.update_view_form_class def get_form_exclude(self, view): return self.form_exclude def get_create_view_form_exclude(self, view): if self.create_view_form_exclude is None: return self.get_form_exclude(view) return self.create_view_form_exclude def get_update_view_form_exclude(self, view): if self.update_view_form_exclude is None: return self.get_form_exclude(view) return self.update_view_form_exclude def get_create_view_form_initial(self, view): return {} def get_update_view_form_initial(self, view): return {} def get_create_button_text(self): return self.create_button_text def get_create_button_class(self): return self.create_button_class def get_edit_button_text(self): return self.edit_button_text def get_edit_button_class(self): return self.edit_button_class def get_delete_button_text(self): return self.delete_button_text def get_delete_button_class(self): return self.delete_button_class def get_back_button_text(self): return self.back_button_text def get_back_button_class(self): return self.back_button_class def get_relative_view_url(self, url_name_suffix, remove_url_name_components=0, kwargs=None, query_params=None): if remove_url_name_components > 0: # url_name_prefix has a trailing colon so we always need to remove # an extra component. url_name_prefix = self.get_parent_url_name(self.url_name_prefix, remove_url_name_components + 1) + ':' else: url_name_prefix = self.url_name_prefix return self.reverse_with_query_params(url_name_prefix + url_name_suffix, kwargs=kwargs, query_params=query_params) def get_success_url(self, view): if self.success_url is None: return self.get_view_url(LIST_VIEW_TYPE, view, view.request.GET) return self.success_url def get_create_view_success_url(self, view): if self.create_view_success_url is None: return self.get_success_url(view) return self.create_view_success_url def get_update_view_success_url(self, view): if self.update_view_success_url is None: return self.get_success_url(view) return self.update_view_success_url def get_delete_view_success_url(self, view): if self.delete_view_success_url is None: return self.get_success_url(view) return self.delete_view_success_url def get_base_view_mixin(self): if self.base_view_mixin is None: class BaseViewMixin(object): viewset = self view_type = None final_breadcrumb = None pk_url_kwarg = viewset.lookup_url_kwarg def get_queryset(self): return self.viewset.get_queryset(self) def get_view_title(self): raise NotImplementedError def get_cached_object(self, viewset=None): return self.cached_objects[self.viewset if viewset is None else viewset] def get_object(self, *args, **kwargs): return self.cached_objects[self.viewset] def populate_object_cache(self, instance): # Populate a cache of objects for all viewsets in the # master-detail chain so all lookup logic is in one place # and we don't have to worry about accidentally doing # the same lookup twice in other functions. viewset = self.viewset viewset_object = instance self.cached_objects = {viewset: viewset_object} while viewset.master_viewset is not None: if viewset_object is None: viewset_object = viewset.master_viewset.get_object(self) else: viewset_object = viewset.get_master_object(self, viewset_object) viewset = viewset.master_viewset self.cached_objects[viewset] = viewset_object def get_context_data(self, *args, **kwargs): context = super(BaseViewMixin, self).get_context_data(*args, **kwargs) viewset = self.viewset breadcrumbs = viewset.get_breadcrumbs(self.view_type, self, self.request.GET) if self.final_breadcrumb is not None: breadcrumbs.append( { 'text': self.final_breadcrumb, # Includes the query string. 'url': self.request.get_full_path() } ) context['base_template_shim'] = viewset.base_template_shim context['breadcrumbs'] = breadcrumbs context['query_string'] = self.request.GET.urlencode() context['view_title'] = self.get_view_title() context['is_view_enabled'] = viewset.is_view_enabled context['actions'] = [] if (self.view_type != LIST_VIEW_TYPE) and (viewset.is_list_view_nested or viewset.is_view_enabled[LIST_VIEW_TYPE]): context['actions'].append( [ { 'type': 'button', 'text': viewset.get_back_button_text(), 'url': viewset.get_view_url(LIST_VIEW_TYPE, self, self.request.GET), 'button_class': viewset.get_back_button_class() } ] ) viewset.add_context_data(self, context) return context self.base_view_mixin = BaseViewMixin return self.base_view_mixin def get_decorated_view(self, view_class): view = view_class.as_view() if self.view_decorator is not None: view = self.view_decorator(view) view.viewset = self return view def is_view_generated(self, view_type): if view_type in self.views: return True if view_type in self.exclude_views: self.views[view_type] = None return True return False def generate_list_view(self): if self.is_view_generated(LIST_VIEW_TYPE): return class ListView(self.get_base_view_mixin(), self.list_view_base_class): viewset = self view_type = LIST_VIEW_TYPE template_name = self.list_view_template_name filter = None def dispatch(self, request, *args, **kwargs): self.populate_object_cache(None) return super(ListView, self).dispatch(request, *args, **kwargs) def get_view_title(self): return self.viewset.get_list_view_title(self) def get_context_data(self, *args, **kwargs): context = super(ListView, self).get_context_data(*args, **kwargs) if self.filter is not None: context['filter'] = self.filter self.viewset.add_list_view_context_data(self, self.request.GET, context) return context if self.filter_class is not None: filter_class = self.filter_class prefix = self.filter_query_param_prefix # NOTE: This will only be called if base_list_view_class is a # ListView, not a TemplateView. def get_queryset(self): # Sanity check: verify that get_queryset() is called only once # per request. if self.filter is not None: raise RuntimeError("ListView.get_queryset() was called more than once") self.filter = filter_class(self.request.GET, super(ListView, self).get_queryset(), prefix=prefix) return self.filter.qs ListView.get_queryset = get_queryset self.views[LIST_VIEW_TYPE] = self.get_decorated_view(ListView) def generate_create_view(self): if self.is_view_generated(CREATE_VIEW_TYPE): return class CreateView(SaveErrorHandlerMixin, self.get_base_view_mixin(), self.create_view_base_class): viewset = self view_type = CREATE_VIEW_TYPE form_class = self.get_create_view_form_class() template_name = self.create_view_template_name final_breadcrumb = 'Create' def dispatch(self, request, *args, **kwargs): self.populate_object_cache(self.viewset.get_create_view_object(self)) return super(CreateView, self).dispatch(request, *args, **kwargs) def get_view_title(self): return self.viewset.get_create_view_title(self) def get_context_data(self, *args, **kwargs): context = super(CreateView, self).get_context_data(*args, **kwargs) context['form_exclude'] = self.viewset.get_create_view_form_exclude(self) return context def form_valid(self, form): self.viewset.create_view_form_valid(self, form) return super(CreateView, self).form_valid(form) def get_initial(self): return self.viewset.get_create_view_form_initial(self) def get_form(self, form_class=None): form = super(CreateView, self).get_form(form_class) form.view = self customise = getattr(form, 'customise_for_view', None) if customise is not None: customise(self) return form def get_success_url(self): return self.viewset.get_create_view_success_url(self) self.views[CREATE_VIEW_TYPE] = self.get_decorated_view(CreateView) def generate_detail_view(self): if self.is_view_generated(DETAIL_VIEW_TYPE): return active_nested_list_view_query_param = self.nested_list_view_query_param_prefix + self.active_nested_list_view_query_param class DetailView(self.get_base_view_mixin(), generic.DetailView): viewset = self view_type = DETAIL_VIEW_TYPE template_name = self.detail_view_template_name def dispatch(self, request, *args, **kwargs): self.populate_object_cache(self.viewset.get_object(self)) return super(DetailView, self).dispatch(request, *args, **kwargs) def get_view_title(self): return self.viewset.get_detail_view_title(self) def get_context_data(self, *args, **kwargs): context = super(DetailView, self).get_context_data(*args, **kwargs) viewset = self.viewset action_group = [] if viewset.is_view_enabled[UPDATE_VIEW_TYPE]: edit_button_text = viewset.get_edit_button_text() if edit_button_text is not None: action_group.append( { 'type': 'button', 'url': viewset.get_view_url(UPDATE_VIEW_TYPE, self, self.request.GET, self.get_cached_object()), 'text': edit_button_text, 'button_class': viewset.get_edit_button_class() } ) if viewset.is_view_enabled[DELETE_VIEW_TYPE]: delete_button_text = viewset.get_delete_button_text() if delete_button_text is not None: action_group.append( { 'type': 'button', 'url': viewset.get_view_url(DELETE_VIEW_TYPE, self, self.request.GET, self.get_cached_object()), 'text': delete_button_text, 'button_class': viewset.get_delete_button_class() } ) if action_group: context['actions'].append(action_group) context['nested_list_views'] = [] if self.viewset.nested_list_views: active_index = self.request.GET.get(active_nested_list_view_query_param, '') have_active = False for nested_viewset in self.viewset.nested_list_views: index = str(nested_viewset.nested_list_view_index) active = index == active_index have_active = have_active or active GET = self.request.GET.copy() GET[active_nested_list_view_query_param] = index nested_context = { 'active': active, 'id': 't' + index, 'caption': nested_viewset.get_list_view_title(self), 'actions': [] } nested_viewset.add_list_view_context_data(self, GET, nested_context) context['nested_list_views'].append(nested_context) if not have_active: context['nested_list_views'][0]['active'] = True return context self.views[DETAIL_VIEW_TYPE] = self.get_decorated_view(DetailView) def generate_update_view(self): if self.is_view_generated(UPDATE_VIEW_TYPE): return class UpdateView(SaveErrorHandlerMixin, self.get_base_view_mixin(), generic.UpdateView): viewset = self view_type = UPDATE_VIEW_TYPE form_class = self.get_update_view_form_class() template_name = self.update_view_template_name final_breadcrumb = None if self.instance_breadcrumb_view_type == UPDATE_VIEW_TYPE else 'Update' def dispatch(self, request, *args, **kwargs): self.populate_object_cache(self.viewset.get_object(self)) return super(UpdateView, self).dispatch(request, *args, **kwargs) def get_view_title(self): return self.viewset.get_update_view_title(self) def get_context_data(self, *args, **kwargs): context = super(UpdateView, self).get_context_data(*args, **kwargs) viewset = self.viewset if viewset.is_view_enabled[DELETE_VIEW_TYPE]: delete_button_text = viewset.get_delete_button_text() if delete_button_text is not None: context['actions'].append( [ { 'type': 'button', 'url': viewset.get_view_url(DELETE_VIEW_TYPE, self, self.request.GET, self.get_cached_object()), 'text': delete_button_text, 'button_class': viewset.get_delete_button_class() } ] ) context['form_exclude'] = self.viewset.get_update_view_form_exclude(self) return context def form_valid(self, form): self.viewset.update_view_form_valid(self, form) return super(UpdateView, self).form_valid(form) def get_initial(self): return self.viewset.get_update_view_form_initial(self) def get_form(self, form_class=None): form = super(UpdateView, self).get_form(form_class) form.view = self customise = getattr(form, 'customise_for_view', None) if customise is not None: customise(self) return form def get_success_url(self): return self.viewset.get_update_view_success_url(self) self.views[UPDATE_VIEW_TYPE] = self.get_decorated_view(UpdateView) def generate_delete_view(self): if self.is_view_generated(DELETE_VIEW_TYPE): return class DeleteView(self.get_base_view_mixin(), generic.DeleteView): viewset = self view_type = DELETE_VIEW_TYPE template_name = self.delete_view_template_name final_breadcrumb = 'Delete' def dispatch(self, request, *args, **kwargs): self.populate_object_cache(self.viewset.get_object(self)) return super(DeleteView, self).dispatch(request, *args, **kwargs) def get_view_title(self): return self.viewset.get_delete_view_title(self) def get_success_url(self): return self.viewset.get_delete_view_success_url(self) if getattr(self, 'delete_object', None) is not None: def delete(self, request, *args, **kwargs): return self.viewset.delete_object(self, request, *args, **kwargs) DeleteView.delete = delete self.views[DELETE_VIEW_TYPE] = self.get_decorated_view(DeleteView) @classmethod def urls(cls): viewset = cls() lookup_regex = r'(?P<' + viewset.lookup_url_kwarg + '>' + viewset.lookup_url_kwarg_pattern + ')/' if viewset.lookup_regex is None else viewset.lookup_regex patterns = [] def append_pattern(pattern, view_type): view = viewset.views[view_type] if view is not None: patterns.append(url(pattern, view, name=view_type)) append_pattern(r'^$', LIST_VIEW_TYPE) append_pattern(r'^create/$', CREATE_VIEW_TYPE) append_pattern(lookup_regex + '$', DETAIL_VIEW_TYPE) append_pattern(lookup_regex + 'update/$', UPDATE_VIEW_TYPE) append_pattern(lookup_regex + 'delete/$', DELETE_VIEW_TYPE) if not patterns: # If a viewset functions only as a nested list view then it will # not define any views of its own. We need at least one view URL # to be defined in order to discover the viewset via link_viewsets, # however, so we simply define a dummy one. def not_found(request, *args, **kwargs): raise Http404('Not found') not_found.viewset = viewset patterns.append(url(r'^$', not_found, name=NOT_FOUND_VIEW_TYPE)) return patterns @staticmethod def link_viewsets(views): viewsets = {} for view_function, url_pattern, view_name in views: viewset = getattr(view_function, 'viewset', None) if viewset is not None: viewset_class = viewset.__class__ if viewset_class in viewsets: # TODO: we can remove this restriction by passing a name/ID # to the CrudViewset.urls() function to use in the viewset # constructor, and then specifying that name/ID instead of # the class for master or nested viewsets. # OR we could just create derived viewset classes for each # case. if viewset != viewsets[viewset_class]: raise ImproperlyConfigured("%s.urls() is called multiple times (see TODO in code)" % viewset_class.__name__) else: viewset.url_name_prefix = CrudViewset.get_parent_url_name(view_name) + ':' viewsets[viewset_class] = viewset for viewset_class, viewset in viewsets.items(): if viewset.master_viewset_class is not None: # Sanity check. if viewset.master_viewset is not None: raise RuntimeError("%s already has a master viewset" % viewset_class.__name__) viewset.master_viewset = viewsets.get(viewset.master_viewset_class, None) if viewset.master_viewset is None: raise ImproperlyConfigured("%s does not have an associated URL" % viewset.master_viewset_class.__name__) if viewset.nested_list_view_index is not None: for i, nested_viewset in enumerate(viewset.master_viewset.nested_list_views): if viewset.nested_list_view_index < nested_viewset.nested_list_view_index: viewset.master_viewset.nested_list_views.insert(i, viewset) break elif viewset.nested_list_view_index == nested_viewset.nested_list_view_index: raise ImproperlyConfigured("%s and %s have the same value for nested_list_view_index", viewset.__class__.__name__, nested_viewset.__class__.__name__) else: viewset.master_viewset.nested_list_views.append(viewset) for viewset in viewsets.values(): # Prevents unnecessary processing in # remove_nested_list_view_query_params() if there are no nested list # views. if not viewset.nested_list_views: viewset.nested_list_view_query_param_prefix = '' class Tables2ListViewMixin(object): list_view_base_class = generic.TemplateView list_view_template_name = 'crud/tables2_list_view.html' detail_view_template_name = 'crud/tables2_detail_view.html' tables2_class = None tables2_context_variable_name = 'table' tables2_paginator_query_string_variable_name = 'paginator_query_string' tables2_template = 'django_tables2/bootstrap.html' tables2_attributes = {} tables2_meta_attributes = {} tables2_page_field = 'p' tables2_per_page_field = 'c' tables2_order_by_field = 's' tables2_clickable_rows = False tables2_clickable_row_class = 'clickable-row' tables2_clickable_row_view_type = DETAIL_VIEW_TYPE def __init__(self, *args, **kwargs): super(Tables2ListViewMixin, self).__init__(*args, **kwargs) # For convenience. if not self.is_view_enabled[DETAIL_VIEW_TYPE] and (self.tables2_clickable_row_view_type == DETAIL_VIEW_TYPE): self.tables2_clickable_row_view_type = UPDATE_VIEW_TYPE if self.tables2_class is None: meta = type('Meta', (object,), self.tables2_meta_attributes) attrs = copy.copy(self.tables2_attributes) attrs['Meta'] = meta self.tables2_class = type('Table', (django_tables2.Table,), attrs) try: my_attrs = copy.copy(self.tables2_class.Meta.attrs) except AttributeError: my_attrs = {} my_attrs['class'] = my_attrs.get('class', 'table table-bordered table-striped table-hover') class DerivedTable(self.tables2_class): class Meta(self.tables2_class.Meta): model = self.model template = self.tables2_template page_field = self.list_view_query_param_prefix + self.tables2_page_field per_page_field = self.list_view_query_param_prefix + self.tables2_per_page_field order_by_field = self.list_view_query_param_prefix + self.tables2_order_by_field attrs = my_attrs # This will create an instance variable containing the class and # leave the original class variable unchanged. self.tables2_class = DerivedTable def add_list_view_context_data(self, view, GET, context): super(Tables2ListViewMixin, self).add_list_view_context_data(view, GET, context) # Sanity check: get_queryset() will not have been called yet as we # are using a TemplateView instead of a ListView. if 'object_list' in context: raise RuntimeError("Context contains an 'object_list' key") if self.filter_class is None: queryset = self.get_queryset(view) else: context['filter'] = self.filter_class(GET, self.get_queryset(view), prefix=self.filter_query_param_prefix) queryset = context['filter'].qs table_kwargs = {} if self.tables2_clickable_rows: table_kwargs['row_attrs'] = { 'class': self.tables2_clickable_row_class, 'data-href': lambda record: self.get_view_url(self.tables2_clickable_row_view_type, view, GET, record) } table = self.tables2_class(queryset, **table_kwargs) django_tables2.RequestConfig(view.request).configure(table) table.viewset = self table.view = view table.GET = GET paginator_query_params = GET.copy() paginator_query_params.pop(table.page_field, None) context[self.tables2_context_variable_name] = table context[self.tables2_paginator_query_string_variable_name] = paginator_query_params.urlencode() if self.is_view_enabled[CREATE_VIEW_TYPE]: create_button_text = self.get_create_button_text() if create_button_text is not None: context['actions'].append( [ { 'type': 'button', 'url': self.get_view_url(CREATE_VIEW_TYPE, view, GET), 'text': create_button_text, 'button_class': self.get_create_button_class() } ] )