class YearMixin(ChildViewMixin, dates.YearMixin): """ Mixin for views manipulating year-based data. """ year_format = delegate_to_parent('year_format', '%Y') year = delegate_to_parent('year')
class WeekMixin(ChildViewMixin, dates.WeekMixin): """ Mixin for views manipulating week-based data. """ week_format = delegate_to_parent('week_format', '%U') week = delegate_to_parent('week')
class DateMixin(ChildViewMixin, dates.DateMixin): """ Mixin class for views manipulating date-based data. """ date_field = delegate_to_parent('date_field') allow_future = delegate_to_parent('allow_future', False)
class DayMixin(ChildViewMixin, dates.DayMixin): """ Mixin for views manipulating day-based data. """ day_format = delegate_to_parent('day_format', '%d') day = delegate_to_parent('day')
class MonthMixin(ChildViewMixin, dates.MonthMixin): """ Mixin for views manipulating month-based data. """ month_format = delegate_to_parent('month_format', '%b') month = delegate_to_parent('month')
class CanCreateMixin: """ Adds the can_create() method which is called before get() and post() to check if the request is valid or not. """ check_permissions = delegate_to_parent('check_permissions', False) raise_404_on_permission_error = delegate_to_parent('check_permissions', True) def get(self, request, *args, **kwargs): return (_check_permission_then_go(self, 'create') or super().get(request, args, kwargs)) def post(self, request, *args, **kwargs): return (_check_permission_then_go(self, 'create') or super().post(request, args, kwargs)) def can_create(self): """ Return True if the current user can create `self.object` and False otherwise. This method tries to execute the parent's can_edit method. If it does not exist, it uses :func:`viewpack.permissions.can_edit`. """ if not self.check_permissions: return True elif hasattr(self.parent, 'can_create'): return self.parent.can_create(self.object) else: return permissions.can_create(self.object, self.request.user)
class TemplateResponseMixin(ChildViewMixin, ParentTemplateNamesMixin, TemplateResponseEndpointMixin, base.TemplateResponseMixin): """ A mixin that can be used to render a template. """ # template_name = None template_engine = delegate_to_parent('template_engine') response_class = delegate_to_parent('response_class', TemplateResponse) template_extension = delegate_to_parent('template_extension', '.html') content_type = delegate_to_parent('content_type')
class DetailView(CanViewMixin, SingleObjectTemplateResponseMixin, SingleObjectMixin, detail.DetailView): """ Render a "detail" view of an object. By default this is a model instance looked up from `self.queryset`, but the view will support display of *any* object by overriding `self.get_object()`. It looks up all attributes related to the detail view in the parent object, if not defined in the view class. """ check_permissions = delegate_to_parent('check_permissions', False) raise_404_on_permission_error = delegate_to_parent('check_permissions', True)
class YearArchiveView(MultipleObjectTemplateResponseMixin, dates.YearArchiveView): """ List of objects published in a given year. """ make_object_list = delegate_to_parent('make_object_list', False)
class ArchiveIndexView(MultipleObjectTemplateResponseMixin, dates.ArchiveIndexView): """ Top-level archive of date-based items. """ allow_empty = delegate_to_parent('allow_empty', False)
class CreateView(SingleObjectTemplateResponseMixin, ModelFormMixin, edit.CreateView): """ View for creating a new object instance, with a response rendered by template. """ template_name_suffix = delegate_to_parent('template_name_suffix', '_form')
class TemplateResponsePackMixin(TemplateResponseMixin): """ Add <template_basename><sub-view name>.<template_extension> to the list of templates. <sub-view name> is the name of the child view that is going to process the current request. This method only work if the child views inherit from the classes in :mod:`viewpack.views`. Theses classes inherit from Django built-in classes, and have the same API, but they insert hooks to search for some default values in the parent classes. """ template_basename = delegate_to_parent('template_basename') template_extension = delegate_to_parent('template_extension', '.html') @lazy def template_extension_normalized(self): """ Normalized template extension. An empty extension remains empty. Other extensions are normalized to start with a dot. """ template_extension = self.template_extension if template_extension: template_extension = '.' + template_extension.lstrip('.') return template_extension def get_template_names(self, view_name): # Try default implementation that relies on the variable template_name # being set. try: names = super().get_template_names() except ImproperlyConfigured: if self.template_basename is None: raise names = [] # Now we construct a template name from the given view_name and template # extension. if self.template_basename: names.append('%s%s%s' % (self.template_basename, view_name, self.template_extension_normalized)) return names
class MultipleObjectMixin(ContextMixin, list_.MultipleObjectMixin): """ A mixin for views manipulating multiple objects. """ allow_empty = delegate_to_parent('allow_empty', True) queryset = delegate_to_parent('queryset') model = delegate_to_parent('model') paginate_by = delegate_to_parent('paginate_by') paginate_orphans = delegate_to_parent('paginate_orphans', 0) context_object_name = delegate_to_parent('context_object_name') paginator_class = delegate_to_parent('paginator_class', Paginator) page_kwarg = delegate_to_parent('page_kwarg', 'page') ordering = delegate_to_parent('ordering')
class SingleObjectMixin(ContextMixin, detail.SingleObjectMixin): """ Provides the ability to retrieve a single object for further manipulation. """ model = delegate_to_parent('model') queryset = delegate_to_parent('queryset') slug_field = delegate_to_parent('slug', 'slug') context_object_name = delegate_to_parent('context_object_name') slug_url_kwarg = delegate_to_parent('slug_url_kwarg', 'slug') pk_url_kwarg = delegate_to_parent('pk_url_kwarg', 'pk') query_pk_and_slug = delegate_to_parent('query_pk_and_slug', False) def get_object(self, queryset=None): if 'object' in self.__dict__: return self.object try: return self.parent.get_object(queryset) except (AttributeError, NotImplementedError): return super().get_object(queryset) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) if self.object: obj = self.object for parent in self.parents: name = getattr(parent, 'object_context_name', None) if name is None: name = parent.model.__name__.lower() context.setdefault(name, obj) return context
class SingleObjectTemplateResponseMixin( TemplateResponseMixin, detail.SingleObjectTemplateResponseMixin): # template_name_suffix = '_detail' template_name_field = delegate_to_parent('template_name_field') def get_template_names(self): # We mostly copy Django's implementation but insert the given template # extension instead of the hard-coded .html try: names = super().get_template_names() except ImproperlyConfigured: names = [] extension = self.template_extension extension = '.' + extension.lstrip('.') if extension else extension if self.object and self.template_name_field: name = getattr(self.object, self.template_name_field, None) if name: names.insert(0, name) if isinstance(self.object, models.Model): object_meta = self.object._meta if self.object._deferred: object_meta = self.object._meta.proxy_for_model._meta names.append("%s/%s%s%s" % ( object_meta.app_label, object_meta.model_name, self.template_name_suffix, extension, )) elif hasattr(self, 'model') and self.model is not None and issubclass( self.model, models.Model): names.append( "%s/%s%s%s" % (self.model._meta.app_label, self.model._meta.model_name, self.template_name_suffix, extension)) if not names: raise names.extend(self.get_parent_template_names()) return names
class ViewPack(ChildViewMixin, View, metaclass=ViewPackMeta): """ A parent view that dispatches to one child by asking each child if it accepts the given request. """ #: Name of the child view that it should dispatch to dispatch_to = delegate_to_parent('dispatch_to') #: initkwargs that should be passed to class-based child views initkwargs = delegate_to_parent('initkwargs') #: Instance of the child view object responsible to respond to the request. #: This value is method-based, this attribute is set to None. child_view_object = None def __init__(self, dispatch_to=None, **kwargs): self.dispatch_to = dispatch_to super().__init__(**kwargs) @classmethod def as_view(self, dispatch_to=None, initkwargs=None, **initkwargs_): """ Return a child view as a view function. Since group views comprises several different sub-views, it necessary to tell which view should be chosen. It is probably more convenient to register the ViewPack urls using the :func:`ViewPack.as_include` class method. It accepts two function signatures:: ViewPack.as_view(view_name, **kwargs) ViewPack.as_view(view_name, initkwargs, **kwargs) In the first, the kwargs are passed to the ViewPack and in the second they are passed to the sub-view, it is a class based view. """ if dispatch_to is None: raise RuntimeError( 'ViewGroups views require a second parameter with the ' 'associated child view.\n' 'Since they catch multiple urls, it is more convenient to ' 'register the urlpattenrs using ViewPack.as_include():\n') view = super().as_view(dispatch_to=dispatch_to, initkwargs=initkwargs, **initkwargs_) return view @classmethod def as_include(cls, initkwargs=None, *, namespace=None): """ Register views as an include urlpatterns Example:: urlpatterns = [ url(r'^foo/', FooViewGroup.as_include(name='foo')), ] Args: initkwargs: A dictionary with the initial keyword args passed to the view constructor. Keyword args: namespace: Base name of the view. Each child view is registered by joining name with the child view own name. """ app_name = get_app_name(cls) patterns = cls.as_include_patterns(initkwargs, app_name=app_name) includes = include(URLModule(patterns, app_name), namespace=namespace) return includes @classmethod def as_include_patterns(cls, initkwargs=None, *, prefix=None, app_name=None): """ Return a list of url patterns for view. """ if app_name is None: app_name = get_app_name(cls) basename = prefix patterns = [] for view, pattern in cls._meta.view_patterns.items(): if basename: prefix = '%s-%s' % (basename, cls._meta.view_url_names[view]) else: prefix = cls._meta.view_url_names[view] view_func = cls.as_view(view, **(initkwargs or {})) pattern = url(pattern, view_func, name=prefix) patterns.append(pattern) return patterns def init(self, request, *args, **kwargs): """ This method is called to initialize the class-based view with arguments passed to the view function before dispatching to the child view. The default implementation simply saves each value in the corresponding `request`, `args` and `kwargs` attributes. """ self.request = request self.args = args self.kwargs = kwargs def init_child(self, view): """ This method is executed every time a new sub-view view instance is created. The default implementation simply saves itself in the `parent` attribute and saves copy the `request`, `args`, and `kwargs` attributes. """ view.parent = self view.request = self.request view.args = self.args view.kwargs = self.kwargs def dispatch(self, request, *args, **kwargs): """ Instantiate each children class and return the first that accepts the request. """ self.init(request, *args, **kwargs) if self.dispatch_to is not None: return self.dispatch_to_child(self.dispatch_to, request) raise ImproperlyConfigured( 'Class must be initialized with the dispatch_to attribute set.') def dispatch_to_child(self, view_name, request): """ Dispatch url to the given child view. """ try: attr = self._meta.view_attributes[view_name] except KeyError: raise ValueError('view %r does not exist' % view_name) method = getattr(self, attr) self.child_view_name = view_name return method(request, *self.args, **self.kwargs)
class ModelFormMixin(FormMixin, SingleObjectMixin, edit.ModelFormMixin): """ A mixin that provides a way to show and handle a modelform in a request. """ fields = delegate_to_parent('fields')
class CRUDViewPack(SingleObjectPackMixin, TemplateResponsePackMixin, ViewPack): """ A view group that defines a CRUD interface to a model. It handles the following urls:: / --> list view new/ --> creates a new object <pk>/ --> detail view <pk>/edit/ --> edit object <pk>/delete/ --> delete object Each one of these entry points is controlled by a specific View inner class: * :class:`viewgoups.CRUDViewPack.ListView`: index listings * :class:`viewgoups.CRUDViewPack.CreateView`: create new objects * :class:`viewgoups.CRUDViewPack.DetailView`: show object's detail * :class:`viewgoups.CRUDViewPack.EditView`: edit object. * :class:`viewgoups.CRUDViewPack.DeleteView`: delete an object. It is possible to disable any view by setting the corresponding attribute to None in a subclass. One can completely replace theses view classes by their own views or, more conveniently, implement mixin classes that are automatically used during class creation:: class MyCRUD(CRUDViewPack): model = MyModel # Disable list views ListView = None # Mixin class that is mixed with the default CreateView class class CreateViewMixin: pattern = r'^create/$' """ CRUD_VIEWS = {'create', 'detail', 'update', 'delete', 'list'} #: List of fields that should be excluded from the model forms automatically #: generated in the child views. Can be used as an alternative to the # `fields` attribute in order to create a blacklist. exclude_fields = delegate_to_parent('exclude_fields', None) @lazy def fields(self): """Define a list of fields that are used to automatically create forms in the update and create views.""" if self.exclude_fields is None: return forms.fields_for_model(self.model) else: exclude = self.exclude_fields return forms.fields_for_model(self.model, exclude=exclude) #: If True, the generic crud templates are not included in the list of #: template for child views. disable_crud_templates = delegate_to_parent('disable_crud_templates', False) #: If True (default), if will use the functions in # :mod:`viewpack.permissions` to check if the user has permissions to view, # edit or create new objects. check_permissions = delegate_to_parent('check_permissions', False) def get_template_names(self, view_name): assert isinstance(view_name, str), 'invalid view name: %r' % view_name try: names = super().get_template_names(view_name) except ImproperlyConfigured: if ((not self.disable_crud_templates) or (view_name not in self.CRUD_VIEWS)): raise names = [] # We add the default views to the search list of valid views if not self.disable_crud_templates and view_name in self.CRUD_VIEWS: names.append('viewpack/crud/%s%s' % (view_name, self.template_extension_normalized)) names.append('viewpack/crud/%s-base%s' % (view_name, self.template_extension_normalized)) return names class CreateView(VerboseNamesContextMixin, HasUploadMixin, CreateView): """Create new objects.""" pattern = r'^new/$' class DetailView(DetailObjectContextMixin, VerboseNamesContextMixin, DetailView): """Detail view for object.""" pattern = r'(?P<pk>\d+)/$' class UpdateView(DetailObjectContextMixin, VerboseNamesContextMixin, UpdateView): """Edit object.""" pattern = r'^(?P<pk>\d+)/edit/$' success_url = '../' class DeleteView(DetailObjectContextMixin, VerboseNamesContextMixin, DeleteView): """Delete object.""" pattern = r'^(?P<pk>\d+)/delete/$' class ListView(VerboseNamesContextMixin, ListView): """List instances of the given model.""" pattern = r'^$'
class DetailWithResponseView(FormMixin, DetailView): """ A detail view that creates a form to fill up a response object that represents the user interaction with that object in the detail view. One example is the user response to a quiz in the page that shows the quiz details. """ response_form_class = delegate_to_parent('response_form_class') response_form_model = delegate_to_parent('response_form_model') response_fields = delegate_to_parent('response_fields') def post(self, request, *args, **kwargs): """ Executed when response form is submitted. """ form = self.get_form() if form.is_valid(): self.response = self.get_response(form) return self.form_valid(form) else: return self.form_invalid(form) def get_form_class(self): """ Return the form class for the response object to use in this view. """ if self.response_model is not None and self.response_form_class: raise ImproperlyConfigured( "Specifying both 'response_model' and 'response_form_class' " "is not permitted.") if self.response_form_class: return self.response_form_class else: if not (self.response_model and self.response_fields): raise ImproperlyConfigured( "Using DetailWithResponseView without the " "'response_fields' and 'response_model' attributes is " "prohibited.") model = self.response_model fields = self.response_fields return modelform_factory(model, fields=fields) def get_form_kwargs(self): kwargs = super().get_form_kwargs() if hasattr(self, 'response'): kwargs.update({'instance': self.response}) return kwargs def get_context_data(self, **kwargs): if 'response' not in kwargs: kwargs['response'] = getattr(self, 'response', None) return super().get_context_data(**kwargs) def get_response(self, form): """ Create a response object from the given model form. This method is called after the form is validated. The default implementation simply calls the save() method of the ModelForm. It can be overridden in order to save or additional fields. """ return form.save()
class HasUploadMixin: """Adds support for upload an serialized version of object from the create view. Respond to multi-part POST requests and import the uploaded file into a new object. Context attributes: upload_enable: Enable upload functionality. upload_form: A form instance for the upload form. upload_ask: True if needs to ask for upload, False otherwise (for displaying success/failure messages). upload_error: A message with the upload error, if it exists. """ #: Enable the upload functionality (default True) upload_enable = delegate_to_parent('upload_enable', True) #: Default upload form class upload_form_class = UploadForm #: The exception class raised on import errors import_object_exception = delegate_to_parent('import_object_exception', SyntaxError) #: The url to redirect upon success. It accepts the format syntax in which #: is called with a dictionary with {'object': imported_object} upload_success_url = delegate_to_parent('upload_success_url') def get_context_data(self, **kwargs): if self.upload_enable: return super().get_context_data( upload_form=self.get_upload_form(), upload_ask=getattr(self, 'upload_ask', True), upload_error=getattr(self, 'upload_error', None), upload_enable=True, **kwargs) else: return super().get_context_data(upload_enable=False, **kwargs) def get_upload_form(self, *args, **kwargs): """Return a Form instance representing an upload form.""" cls = self.get_upload_form_class() return cls(*args, **kwargs) def get_upload_form_class(self): """Return the Form subclass used from upload forms.""" return self.upload_form_class def post(self, request, *args, **kwargs): if self.upload_enable and request.FILES: form = self.get_upload_form(request.POST, request.FILES) if form.is_valid(): try: self.object = self.get_object_from_files(request.FILES) except self.import_object_exception as ex: self.upload_error = str(ex) or 'import error' return self.upload_failure(request, *args, **kwargs) else: return self.upload_success(request, *args, **kwargs) return super().post(request, *args, **kwargs) def upload_success(self, request, *args, **kwargs): """Called when import is successful.""" if self.upload_success_url is None: if hasattr(self.object, 'get_absolute_url'): url = self.object.get_absolute_url() else: raise ImproperlyConfigured( 'You must either override the upload_success() method or ' 'define a `upload_success_url` attribute.') else: url = self.upload_success_url.format(object=self.object) return redirect(url) def upload_failure(self, request, *args, **kwargs): """Called when import failed.""" self.upload_ask = False return self.get(request, *args, **kwargs) def get_object_from_files(self, files): """Return object from the dictionary of files uploaded by the user. By default it expects a dictionary with a single 'file' key. This function reads this file and calls the `get_object_from_data()` method. """ data = files['file'].read() obj = self.get_object_from_data(data) set_owner = getattr(self.parent, 'set_owner', lambda x, u: None) set_owner(obj, self.request.user) return obj def get_object_from_data(self, data): """Returns a new instance from data sent by the user. Object is always saved on the database.""" obj = self.model.from_data(data) if obj.pk is None: obj.save() return obj
class InheritanceCRUDViewPack(CRUDViewPack, metaclass=InheritanceCRUDViewPackMeta): """Similar to ViewPack, but it dispatch to different sub-cruds depending on the type of the requested object. Each sub-crud class might define a subclass_view_name attribute that is used to map each subclass with the corresponding name in the ``/new/<subclass_view_name>`` urls. """ #: Type used for dispatch. subclass_view_type = delegate_to_parent('subclass_view_type') #: A dictionary mapping each model to their respective view pack #: class. registry = None DetailViewMixin = DispatchViewMixin UpdateViewMixin = DispatchViewMixin DeleteViewMixin = DispatchViewMixin class CreateView(VerboseNamesContextMixin, TemplateView): """ Creates a new instances. This view require a second parameter that is matched to the `subclass_view_type` attribute of a registered child view. The default implementation uses urls such as:: new/<subclass_view_name>/ in order to delegate to the 'create' subview of the CRUDViewPack associated with the given <subclass_view_name> value. """ pattern = r'^new/(?P<subclass_view_name>\w*)/?$' def get_context_data(self, **kwargs): def get_name(x): try: return getattr(x, 'subclass_view_name') except: return x.model.__name__.lower() L = self.parent.get_subpack_list() L = [(get_name(x), x) for x in L] return super().get_context_data(name_view_list=L, **kwargs) def dispatch(self, request, *args, subclass_view_name=None, **kwargs): if not subclass_view_name: self.view_name = 'create-list' return super().dispatch(request, *args, **kwargs) view = self.get_subclass_view(subclass_view_name) return view.dispatch(request, *args, **kwargs) def get_subclass_view(self, subclass_view_name): """Return the child CRUDViewPack instance associated with the given child_view_name.""" return self.parent.get_pack(subclass_view_name) # class ExtraActionView(SingleObjectMixin, View): # """Captures any other sub-url and try to dispatch to some registered # class.""" # # pattern = r'^(?P<pk>\d+)/(?P<url>.*)$' # # def match_url(self, pack, url): # """Searches all subviews for a matching url.""" # # def dispatch(self, request, pk, url): # self.object = self.get_object() # pack = self.get_pack(type(self.object)) # subview = self.match_url(pack, url) # return self.process_subview(subvview) def init_subclass_view(self, cls): """Initialize an instance of a child view pack class.""" return cls(dispatch_to=self.dispatch_to, initkwargs=self.initkwargs, parent=self, request=self.request, args=self.args, kwargs=self.kwargs, **(self.initkwargs or {})) def get_pack(self, value): """Return the child CRUDViewPack instance associated with the given name or type.""" # This function accepts either a string with the pack name or a type if isinstance(value, str): for child_cls in self.registry.values(): name = getattr(child_cls, 'subclass_view_name', None) if isinstance(name, str): if name == value: return self.init_subclass_view(child_cls) continue child = self.init_subclass_view(child_cls) # If subclass_view_name is not defined, we compute it from the # lowercase version of the model name. if name is None: name = child.model.__name__.lower() if name == value: return child raise ValueError('invalid subclass_view_name: %r' % value) # Search by type #for child_cls in self. def get_subpack_list(self): """ Return a list with all registered subclass views objects. Each view is initialized as if it would have been if executed with the dispatch method. """ L = [] for child_cls in self.registry.values(): child = self.init_subclass_view(child_cls) L.append(child) return L def get_subpack_models(self): """ Return a list with all (subclass_view_name, model) items. """ return [(pack.subclass_view_name, model) for (model, pack) in self.registry.items()] @classmethod def register(cls, viewpack=None, *, model=None, as_fallback=False, force=False): """ Register the given viewgroup for objects of the given type. Can be used as a decorator. Args: viewpack: Registered class model: Optional model type associated to the group. If not givem it uses viewgroup.model. as_fallback: If true, register viewgroup as the fallback view for objects of non-registered types. force: Forces registration of an existing type. """ # Decorator form if viewpack is None: return lambda viewpack: cls.register( viewpack, model=model, as_fallback=as_fallback, force=force) # Retrieves model from view, if necessary if as_fallback: model = None elif model is None: model = viewpack.model # Register if force and model in cls.registry: if model is None: raise ImproperlyConfigured( 'Fallback view is already registered') raise ImproperlyConfigured( 'A view is already registered for model: %s' % model.__name__) cls.registry[model] = viewpack return viewpack def get_queryset(self): """ Returns the queryset. If manager has a select_subclasses() method (as in Django model util's InheritanceManager), it uses this method. """ queryset = super().get_queryset() if hasattr(queryset, 'select_subclasses'): return queryset.select_subclasses() return queryset def dispatch_to_object(self, object, request, *args, **kwargs): """ Dispatch to the sub-view that is responsible for dealing with input object. """ cls = type(object) try: view_cls = self.registry[cls] except KeyError: if None in self.registry: view_cls = self.registry[None] raise http.Http404('No view registered to %s objects' % cls.__name__) # We now create a view object and dispatch to it self.subclass_view_type = cls self.child_view_object = view_cls(parent=self, object=object, dispatch_to=self.child_view_name) return self.child_view_object.dispatch(request, *args, **kwargs)