Browse Source

Admin views api cleanup & docs

Rafał Pitoń 11 years ago
parent
commit
32c5df06ba
3 changed files with 201 additions and 26 deletions
  1. 142 1
      docs/developers/admin_actions.rst
  2. 53 19
      misago/admin/views/generic.py
  3. 6 6
      misago/users/views/rankadmin.py

+ 142 - 1
docs/developers/admin_actions.rst

@@ -11,10 +11,151 @@ Misago brings its own admin site just like Django `does <https://docs.djangoproj
 If you intend to be sole user of your app, Django admin will propably be faster to get going. However if you plan for your app to be available to wider audience, its good for your admin interface to be part of Misago admin site. This will require you to write more code than intended, but will give your users more consistent experience and, in case for some languages, save them of quirkyness that comes with django admin automatic messages.
 If you intend to be sole user of your app, Django admin will propably be faster to get going. However if you plan for your app to be available to wider audience, its good for your admin interface to be part of Misago admin site. This will require you to write more code than intended, but will give your users more consistent experience and, in case for some languages, save them of quirkyness that comes with django admin automatic messages.
 
 
 
 
-Writing Admin Views
+Creating Admin Views
 ===================
 ===================
 
 
 
 
+Writing views
+=============
+
+Unlike Django, Misago admin is not "automagical". This means you will not get complete admin from nowhere by just creating one file and writing 3 lines of code in it. However Misago provides set of basic classes defined in  in :py:mod:`misago.admin.views.generic` module that can offload most of burden of writing views handling items lists and forms from you.
+
+Workflow with those classes is fast and easy to master. First, you define your own mixin (propably extending ``AdminBaseMixin``). This mixin will define common properties and behaviour of all admin views, like which Model are admin views focused on, how to fetch its instances from database as well as where to seek templates and which message should be used when model could not be found.
+
+Next you define your own views inheriting from your mixin and base views. Misago provides basic views for each of most common scenarios in admin:
+
+* **ListView** - For items lists. Supports pagination, sorting, filtering and mass actions.
+* **FormView** and ``ModelFormView`` - For displaying and handling forms submissions.
+* **ButtonView** - For handling state-changing button presses like "delete item" actions.
+
+
+AdminBaseMixin
+--------------
+
+:py:class:`misago.admin.views.generic.AdminBaseMixin`
+
+
+Base class for admin mixins that contain properties and behaviours shared between admin views. While you are allowed to set any properties and function on your own mixins to dry your admin views more, bare minimum expected from you is following:
+
+* **Model** property or **get_model(self)** function used to get model type.
+* **root_link** property that is string with link name for "index" view for admin actions (usually link to items list).
+* **templates_dir** property being string with name of directory with admin templates used by mixin views.
+
+Optionally if you don't plan to set up action-specific item not found messages, you may set ``message_404`` property on mixin to make all your views use same message when requested model could not be found.
+
+
+ListView
+--------
+
+:py:class:`misago.admin.views.generic.ListView`
+
+Base class for lists if items. Supports following properties:
+
+* **template** - name of template file located in ``templates_dir`` used to render this view. Defaults to ``list.html``
+* **items_per_page** - integer controlling number of items displayed on single page. Defaults to 0 which means no pagination
+* **ordering** - list of supported sorting methods. List of tuples. Each tuple should countain two items: name of ordering method (eg. "Usernames, descending") and ``order_by`` argument ("-username"). Defaults to none which means queryset will not be ordered. If contains only one element, queryset is ordered, but option for changing ordering method is not displayed.
+
+In addition to this, ListView defines following methods that you may be interested in overloading:
+
+
+.. function:: get_queryset(self)
+
+This function is expected to return queryset of items that will be displayed. If filters, sorting or pagination is defined, this queryset will be sliced and filtered.
+
+
+FormView
+--------
+
+:py:class:`misago.admin.views.generic.FormView`
+
+Base class for forms views.
+
+* **template** - name of template file located in ``templates_dir`` used to render this view. Defaults to ``form.html``
+* **Form** property or **create_form_type** method - ``create_form`` method is called with ``request`` as its argument and is expected to return form type that will be used by view. If you need to build form type dynamically, instead of defining ``Form`` property, define your own ``create_form``.
+
+
+.. function:: create_form_type(self, request)
+
+Returns form type that will be used to create form instance. By default returns value of ``Form`` property.
+
+
+.. function:: initialize_form(self, FormType, request):
+
+Initializes either bound or unbound form using request and ``FormType`` provided.
+
+
+.. function:: handle_form(self, form, request):
+
+If form validated successfully, this method is called to perform action. Here you should place code that will read data from form, perform actions on models and set result message. Optionally you may return ``HttpResponse`` from this function. If nothing is returned, view returns redirect to ``root_link``.
+
+Optionally your form template may have button with ``name="stay"`` attribute defined, pressing which will cause view to redirect you to clean form instead.
+
+
+ModelFormView
+-------------
+
+:py:class:`misago.admin.views.generic.ModelFormView`
+
+Base class for targetted forms views. Its API is largery identic to ``FormView``, except it's tailored at handling ``ModelForm`` and modifying model states. All methos documented for ``FormView`` are present in ``ModelformView``, but they accept one more argument named "target", containing model instance to which model form will be tied.
+
+In addition, this view comes with basic definition for form handler that calls ``save()`` on model instance and (if defined) sets success message using value of objects ``message_submit`` parameter.
+
+
+ButtonView
+----------
+
+:py:class:`misago.admin.views.generic.ButtonView`
+
+Base class for handling non-form based POST requests.
+
+Do control this view behaviour, define your own ``button_action`` method:
+
+
+.. function:: button_action(self, request, target)
+
+This function is expected to perform requested action on target provided and set result message on ``request``.
+
+It may return nothing or ``HttpResponse``. If nothing is returned, view returns redirect to ``root_link`` instead.
+
+
+Targeted views
+--------------
+
+Both ``ModelFormView`` and ``ButtonView`` are called "targeted views", because they are expected to manipulate model instances. They both inherit from ``TargetedView`` view, implements simple API that is used for associating request with corresponding model instance:
+
+
+.. function:: get_target_or_none(self, request, kwargs)
+
+Function expected return valid model instance or None. If None is returned, this makes view set error message using ``message_404`` attribute and returns redirect to ``root_link``.
+
+
+.. function:: get_target(self, kwargs)
+
+Called by ``get_target_or_none`` function documented above.
+
+If ``kwargs`` len is 1, its assumed to be value of searched models pk value. This makes function call model manager ``get()`` method to fetch model instance from database. Otherwhise "empty" instance is created and returned instead. Eventual ``DoesNotExist`` errors are handled by ``get_target_or_none``.
+
+
+.. function:: check_permissions(self, request, target)
+
+Once model instance is obtained either from database or empty instance is created, this function is called to see intended action is allowed for this request and target. This function is expected to return ``None`` if no issues are found or string containing error message. If string is returned, its set as error messages, and view interrupts its execution by returning redirect to ``root_link``.
+
+
+.. note::
+   While target argument value is always present, you don't have to do anything with it if its not making any sense for your view.
+
+
+Adding extra values to context
+------------------------------
+
+Each view calls its ``process_context`` method before rendering template to response. This method accepts two arguments:
+
+* **request** - HttpRequest instance received by view.
+* **context** - Dict that is going to be used to render template.
+
+It's required to return dict that will be then used as one of arguments to call ``render()``.
+
+
 Registering in Misago Admin
 Registering in Misago Admin
 ===========================
 ===========================
 
 

+ 53 - 19
misago/admin/views/generic.py

@@ -174,11 +174,11 @@ class ListView(AdminView):
 
 
 
 
 class TargetedView(AdminView):
 class TargetedView(AdminView):
-    def check_permissions(self, request, target=None):
+    def check_permissions(self, request, target):
         pass
         pass
 
 
     def get_target(self, kwargs):
     def get_target(self, kwargs):
-        if len(kwargs):
+        if len(kwargs) == 1:
             return self.get_model().objects.get(pk=kwargs[kwargs.keys()[0]])
             return self.get_model().objects.get(pk=kwargs[kwargs.keys()[0]])
         else:
         else:
             return self.get_model()()
             return self.get_model()()
@@ -202,38 +202,72 @@ class TargetedView(AdminView):
 
 
         return self.real_dispatch(request, target)
         return self.real_dispatch(request, target)
 
 
-    def real_dispatch(self, request, target=None):
+    def real_dispatch(self, request, target):
         pass
         pass
 
 
 
 
 class FormView(TargetedView):
 class FormView(TargetedView):
-    form = None
+    Form = None
     template = 'form.html'
     template = 'form.html'
-    message_submit = None
 
 
-    def create_form(self, request, target=None):
-        return self.form
+    def create_form_type(self, request):
+        return self.Form
 
 
-    def initialize_form(self, FormType, request, target=None):
+    def initialize_form(self, FormType, request):
         if request.method == 'POST':
         if request.method == 'POST':
-            return self.form(request.POST, request.FILES, instance=target)
+            return FormType(request.POST, request.FILES)
         else:
         else:
-            return self.form(instance=target)
+            return FormType()
 
 
     def handle_form(self, form, request):
     def handle_form(self, form, request):
+        raise NotImplementedError(
+            "You have to define your own handle_form method to handle "
+            "form submissions.")
+
+    def real_dispatch(self, request, target):
+        FormType = self.create_form_type(request)
+        form = self.initialize_form(FormType, request)
+
+        if request.method == 'POST' and form.is_valid():
+            response = self.handle_form(form, request)
+
+            if response:
+                return response
+            elif 'stay' in request.POST:
+                return redirect(request.path)
+            else:
+                return redirect(self.root_link)
+
+        return self.render(request, {'form': form})
+
+
+class ModelFormView(FormView):
+    message_submit = None
+
+    def create_form_type(self, request, target):
+        return self.Form
+
+    def initialize_form(self, FormType, request, target):
+        if request.method == 'POST':
+            return FormType(request.POST, request.FILES, instance=target)
+        else:
+            return FormType(instance=target)
+
+    def handle_form(self, form, request, target):
         form.instance.save()
         form.instance.save()
         if self.message_submit:
         if self.message_submit:
-            message = self.message_submit % unicode(form.instance)
-            messages.success(request, message)
+            messages.success(request, message_submit)
 
 
-    def real_dispatch(self, request, target=None):
-        FormType = self.create_form(request, target)
+    def real_dispatch(self, request, target):
+        FormType = self.create_form_type(request, target)
         form = self.initialize_form(FormType, request, target)
         form = self.initialize_form(FormType, request, target)
 
 
-        if form.is_valid():
-            self.handle_form(form, request)
+        if request.method == 'POST' and form.is_valid():
+            response = self.handle_form(form, request, target)
 
 
-            if 'stay' in request.POST:
+            if response:
+                return response
+            elif 'stay' in request.POST:
                 return redirect(request.path)
                 return redirect(request.path)
             else:
             else:
                 return redirect(self.root_link)
                 return redirect(self.root_link)
@@ -242,12 +276,12 @@ class FormView(TargetedView):
 
 
 
 
 class ButtonView(TargetedView):
 class ButtonView(TargetedView):
-    def real_dispatch(self, request, target=None):
+    def real_dispatch(self, request, target):
         if request.method == 'POST':
         if request.method == 'POST':
             new_response = self.button_action(request, target)
             new_response = self.button_action(request, target)
             if new_response:
             if new_response:
                 return new_response
                 return new_response
         return redirect(self.root_link)
         return redirect(self.root_link)
 
 
-    def button_action(self, request, target=None):
+    def button_action(self, request, target):
         raise NotImplementedError("You have to define custom button_action.")
         raise NotImplementedError("You have to define custom button_action.")

+ 6 - 6
misago/users/views/rankadmin.py

@@ -17,11 +17,11 @@ class RanksList(RankAdmin, generic.ListView):
     ordering = (('order', None),)
     ordering = (('order', None),)
 
 
 
 
-class NewRank(RankAdmin, generic.FormView):
+class NewRank(RankAdmin, generic.ModelFormView):
     message_submit = _('New rank "%s" has been saved.')
     message_submit = _('New rank "%s" has been saved.')
 
 
 
 
-class EditRank(RankAdmin, generic.FormView):
+class EditRank(RankAdmin, generic.ModelFormView):
     message_submit = _('Rank "%s" has been edited.')
     message_submit = _('Rank "%s" has been edited.')
 
 
 
 
@@ -32,14 +32,14 @@ class DeleteRank(RankAdmin, generic.ButtonView):
                         'can\'t be deleted.')
                         'can\'t be deleted.')
             return message % unicode(target.name)
             return message % unicode(target.name)
 
 
-    def button_action(self, request, target=None):
+    def button_action(self, request, target):
         target.delete()
         target.delete()
         message = _('Rank "%s" has been deleted.') % unicode(target.name)
         message = _('Rank "%s" has been deleted.') % unicode(target.name)
         messages.success(request, message)
         messages.success(request, message)
 
 
 
 
 class MoveUpRank(RankAdmin, generic.ButtonView):
 class MoveUpRank(RankAdmin, generic.ButtonView):
-    def button_action(self, request, target=None):
+    def button_action(self, request, target):
         other_target = target.prev()
         other_target = target.prev()
         if other_target:
         if other_target:
             other_target.order, target.order = target.order, other_target.order
             other_target.order, target.order = target.order, other_target.order
@@ -50,7 +50,7 @@ class MoveUpRank(RankAdmin, generic.ButtonView):
 
 
 
 
 class MoveDownRank(RankAdmin, generic.ButtonView):
 class MoveDownRank(RankAdmin, generic.ButtonView):
-    def button_action(self, request, target=None):
+    def button_action(self, request, target):
         other_target = target.next()
         other_target = target.next()
         if other_target:
         if other_target:
             other_target.order, target.order = target.order, other_target.order
             other_target.order, target.order = target.order, other_target.order
@@ -65,7 +65,7 @@ class DefaultRank(RankAdmin, generic.ButtonView):
         if target.is_default:
         if target.is_default:
             return _('Rank "%s" is already default.') % unicode(target.name)
             return _('Rank "%s" is already default.') % unicode(target.name)
 
 
-    def button_action(self, request, target=None):
+    def button_action(self, request, target):
         Rank.objects.make_rank_default(target)
         Rank.objects.make_rank_default(target)
         message = _('Rank "%s" has been made default.')
         message = _('Rank "%s" has been made default.')
         messages.success(request, message % unicode(target.name))
         messages.success(request, message % unicode(target.name))