Browse Source

Admin interface for users ranks.

Rafał Pitoń 11 years ago
parent
commit
a628a133b2

+ 76 - 10
misago/admin/views/generic.py

@@ -52,16 +52,21 @@ class AdminView(View):
         matched_url = request.resolver_match.url_name
         matched_url = request.resolver_match.url_name
         return '%s:%s' % (request.resolver_match.namespace, matched_url)
         return '%s:%s' % (request.resolver_match.namespace, matched_url)
 
 
+    def process_context(self, request, context):
+        return context
+
     def render(self, request, context=None):
     def render(self, request, context=None):
         context = context or {}
         context = context or {}
 
 
         context['root_link'] = self.root_link
         context['root_link'] = self.root_link
         context['current_link'] = self.current_link(request)
         context['current_link'] = self.current_link(request)
 
 
+        self.process_context(request, context)
+
         return render(request, self.final_template(), context)
         return render(request, self.final_template(), context)
 
 
 
 
-class ItemsList(AdminView):
+class ListView(AdminView):
     """
     """
     Admin items list view
     Admin items list view
 
 
@@ -162,20 +167,81 @@ class ItemsList(AdminView):
         return self.render(request, context)
         return self.render(request, context)
 
 
 
 
-class ItemView(AdminView):
-    pass
+class TargetedView(AdminView):
+    def check_permissions(self, request, target=None):
+        pass
 
 
+    def get_target(self, kwargs):
+        if len(kwargs):
+            return self.get_model().objects.get(pk=kwargs[kwargs.keys()[0]])
+        else:
+            return self.get_model()()
 
 
-class FormView(ItemView):
-    template = 'form.html'
+    def get_target_or_none(self, request, kwargs):
+        try:
+            return self.get_target(kwargs)
+        except self.get_model().DoesNotExist:
+            return None
 
 
     def dispatch(self, request, *args, **kwargs):
     def dispatch(self, request, *args, **kwargs):
-        pass
+        target = self.get_target_or_none(request, kwargs)
+        if not target:
+            messages.error(request, self.message_404)
+            return redirect(self.root_link)
+
+        error = self.check_permissions(request, target)
+        if error:
+            messages.error(request, error)
+            return redirect(self.root_link)
 
 
+        return self.real_dispatch(request, target)
 
 
-class ButtonView(ItemView):
-    def get(self, request, *args, **kwargs):
+    def real_dispatch(self, request, target=None):
         pass
         pass
 
 
-    def post(self, request, *args, **kwargs):
-        pass
+
+class FormView(TargetedView):
+    form = None
+    template = 'form.html'
+    message_submit = None
+
+    def create_form(self, request, target=None):
+        return self.form
+
+    def initialize_form(self, FormType, request, target=None):
+        if request.method == 'POST':
+            return self.form(request.POST, request.FILES, instance=target)
+        else:
+            return self.form(instance=target)
+
+    def handle_form(self, form, request):
+        form.instance.save()
+        if self.message_submit:
+            message = self.message_submit % unicode(form.instance)
+            messages.success(request, message)
+
+    def real_dispatch(self, request, target=None):
+        FormType = self.create_form(request, target)
+        form = self.initialize_form(FormType, request, target)
+
+        if form.is_valid():
+            self.handle_form(form, request)
+
+            if 'stay' in request.POST:
+                return redirect(request.path)
+            else:
+                return redirect(self.root_link)
+
+        return self.render(request, {'form': form, 'target': target})
+
+
+class ButtonView(TargetedView):
+    def real_dispatch(self, request, target=None):
+        if request.method == 'POST':
+            new_response = self.button_action(request, target)
+            if new_response:
+                return new_response
+        return redirect(self.root_link)
+
+    def button_action(self, request, target=None):
+        raise NotImplementedError("You have to define custom button_action.")

+ 18 - 0
misago/static/misago/admin/css/misago/forms.less

@@ -162,3 +162,21 @@
     }
     }
   }
   }
 }
 }
+
+
+//== Form fields
+//
+//**
+textarea {
+  resize: vertical;
+}
+
+.checkbox {
+  label {
+    font-weight: bold;
+
+    .help-block {
+      font-weight: normal;
+    }
+  }
+}

+ 1 - 1
misago/static/misago/admin/css/misago/tables.less

@@ -87,7 +87,7 @@
         &.row-action {
         &.row-action {
           width: 1%;
           width: 1%;
 
 
-          &>.btn, .dropdown-toggle {
+          .btn, &>form .btn, .dropdown-toggle {
             padding: 0px;
             padding: 0px;
             width: 30px;
             width: 30px;
             height: 30px;
             height: 30px;

+ 13 - 2
misago/static/misago/admin/css/style.css

@@ -6225,7 +6225,8 @@ body {
 .table-panel table.table tr td.row-action {
 .table-panel table.table tr td.row-action {
   width: 1%;
   width: 1%;
 }
 }
-.table-panel table.table tr td.row-action > .btn,
+.table-panel table.table tr td.row-action .btn,
+.table-panel table.table tr td.row-action > form .btn,
 .table-panel table.table tr td.row-action .dropdown-toggle {
 .table-panel table.table tr td.row-action .dropdown-toggle {
   padding: 0px;
   padding: 0px;
   width: 30px;
   width: 30px;
@@ -6233,7 +6234,8 @@ body {
   font-size: 18px;
   font-size: 18px;
   text-align: center;
   text-align: center;
 }
 }
-.table-panel table.table tr td.row-action > .btn > span,
+.table-panel table.table tr td.row-action .btn > span,
+.table-panel table.table tr td.row-action > form .btn > span,
 .table-panel table.table tr td.row-action .dropdown-toggle > span {
 .table-panel table.table tr td.row-action .dropdown-toggle > span {
   position: relative;
   position: relative;
   bottom: 1px;
   bottom: 1px;
@@ -6650,6 +6652,15 @@ body {
     margin-left: 12px;
     margin-left: 12px;
   }
   }
 }
 }
+textarea {
+  resize: vertical;
+}
+.checkbox label {
+  font-weight: bold;
+}
+.checkbox label .help-block {
+  font-weight: normal;
+}
 .login-form {
 .login-form {
   padding: 20px;
   padding: 20px;
 }
 }

+ 1 - 1
misago/templates/misago/admin/base.html

@@ -25,5 +25,5 @@
   var lang_no = "{% trans "No" %}";
   var lang_no = "{% trans "No" %}";
 </script>
 </script>
 {% compressed_js 'misago_admin' %}
 {% compressed_js 'misago_admin' %}
-{% block extra_scripts %}{% endblock %}
+{% block javascripts %}{% endblock %}
 {% endblock %}
 {% endblock %}

+ 9 - 2
misago/templates/misago/admin/generic/base.html

@@ -6,9 +6,16 @@
 <div class="page-header">
 <div class="page-header">
   <div class="container">
   <div class="container">
     <h1>
     <h1>
-      <span class="{{ active_link.icon }}">
-      {{ active_link.name }}
+      {% block page-header %}
+      <div class="main">
+        <a href="{{ active_link.link }}">
+          <span class="{{ active_link.icon }}">
+          {{ active_link.name }}
+        </a>
+      </div>
+      {% endblock page-header %}
     </h1>
     </h1>
+    {% block page-actions %}{% endblock %}
   </div>
   </div>
 </div>
 </div>
 
 

+ 45 - 0
misago/templates/misago/admin/generic/form.html

@@ -1 +1,46 @@
 {% extends "misago/admin/generic/base.html" %}
 {% extends "misago/admin/generic/base.html" %}
+{% load i18n %}
+
+
+{% block page-header %}
+{{ block.super }}
+<div class="sub">
+  <span class="fa fa-chevron-right"></span>
+  {% block page-target %}{% endblock page-target %}
+</div>
+{% endblock page-header %}
+
+
+{% block view %}
+<div class="row">
+  <div class="col-xs-12 col-md-8 col-md-offset-2">
+
+    <div class="form-panel">
+      <form role="form" method="post">
+        {% csrf_token %}
+
+        <div class="form-header">
+          {% block form-header %}{% endblock %}
+        </div>
+
+        {% block form-body %}{% endblock %}
+
+        <div class="form-footer">
+          {% block form-footer %}
+          {% if target and target.pk %}
+          <button class="btn btn-primary">{% trans "Save changes" %}</button>
+          <button class="btn btn-success" name="stay" value="1">{% trans "Save and keep editing" %}</button>
+          {% else %}
+          <button class="btn btn-primary">{% trans "Save" %}</button>
+          <button class="btn btn-success" name="stay" value="1">{% trans "Save and add another" %}</button>
+          {% endif %}
+          {% endblock %}
+          <a href="{% url root_link %}" class="btn btn-default">{% trans "Cancel" %}</a>
+        </div>
+
+      </form>
+    </div><!-- /.form-panel -->
+
+  </div>
+</div>
+{% endblock view %}

+ 3 - 5
misago/templates/misago/admin/generic/list.html

@@ -8,6 +8,7 @@
 
 
 
 
 {% block view %}
 {% block view %}
+{% if paginator or order_by %}
 <div class="table-actions">
 <div class="table-actions">
 
 
   {% if paginator %}
   {% if paginator %}
@@ -17,7 +18,7 @@
   {% if order_by %}
   {% if order_by %}
   <div class="btn-group pull-left">
   <div class="btn-group pull-left">
     <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
     <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
-      {% trans "Sorting:"%} <span class="fa fa-sort-numeric-{{ order.type }}"></span> <strong>{{ order.name }}</strong>
+      {% trans "Sorting:" %} <span class="fa fa-sort-numeric-{{ order.type }}"></span> <strong>{{ order.name }}</strong>
     </button>
     </button>
     <ul class="dropdown-menu" role="menu">
     <ul class="dropdown-menu" role="menu">
       <li class="dropdown-title">
       <li class="dropdown-title">
@@ -39,15 +40,12 @@
   {% endif %}
   {% endif %}
 
 
 </div><!-- /.table-actions -->
 </div><!-- /.table-actions -->
+{% endif %}
 
 
 <div class="table-panel">
 <div class="table-panel">
   <table class="table">
   <table class="table">
     <tr>
     <tr>
       {% block table-header %}
       {% block table-header %}
-      <th>Lorem</th>
-      <th>Ipsum</th>
-      <th style="width: 136px;">Dolor</th>
-      <th colspan="4">&nbsp;</th>
       {% endblock table-header %}
       {% endblock table-header %}
     </tr>
     </tr>
     {% for item in items %}
     {% for item in items %}

+ 53 - 0
misago/templates/misago/admin/ranks/form.html

@@ -0,0 +1,53 @@
+{% extends "misago/admin/generic/form.html" %}
+{% load crispy_forms_filters i18n %}
+
+
+{% block title %}
+{% if target.pk %}
+{% trans target.name %}
+{% else %}
+{% trans "New rank" %}
+{% endif %} | {{ active_link.name }} | {{ block.super }}
+{% endblock title %}
+
+
+{% block page-target %}
+{% if target.pk %}
+{% trans target.name %}
+{% else %}
+{% trans "New rank" %}
+{% endif %}
+{% endblock page-target %}
+
+
+{% block form-header %}
+<h1>
+  {% if target.pk %}
+  {% trans target.name %}
+  {% else %}
+  {% trans "New rank" %}
+  {% endif %}
+</h1>
+{% endblock %}
+
+
+{% block form-body %}
+<div class="form-body">
+  <fieldset>
+    <legend>{% trans "Name and description" %}</legend>
+
+    {{ form.name|as_crispy_field }}
+    {{ form.title|as_crispy_field }}
+    {{ form.description|as_crispy_field }}
+
+  </fieldset>
+  <fieldset>
+    <legend>{% trans "Display and visibility" %}</legend>
+
+    {{ form.style|as_crispy_field }}
+    {{ form.is_tab|as_crispy_field }}
+    {{ form.is_on_index|as_crispy_field }}
+
+  </fieldset>
+</div>
+{% endblock form-body %}

+ 77 - 0
misago/templates/misago/admin/ranks/list.html

@@ -2,7 +2,22 @@
 {% load i18n %}
 {% load i18n %}
 
 
 
 
+{% block page-actions %}
+<div class="page-actions">
+  <a href="{% url 'misago:admin:users:ranks:new' %}" class="btn btn-success">
+    <span class="fa fa-plus-circle"></span>
+    {% trans "New rank" %}
+  </a>
+</div>
+{% endblock %}
+
+
 {% block table-header %}
 {% block table-header %}
+<th style="width: 30%;">{% trans "Rank" %}</th>
+<th>{% trans "Title" %}</th>
+<th style="width: 1%;">&nbsp;</th>
+<th style="width: 1%;">&nbsp;</th>
+<th style="width: 1%;">&nbsp;</th>
 <th style="width: 1%;">&nbsp;</th>
 <th style="width: 1%;">&nbsp;</th>
 {% endblock table-header %}
 {% endblock table-header %}
 
 
@@ -10,6 +25,56 @@
 {% block table-row %}
 {% block table-row %}
 <td class="lead">
 <td class="lead">
   {{ item.name }}
   {{ item.name }}
+  {% if item.is_tab %}
+  <div class="fa fa-list text-primary pull-right tooltip-top" title="{% trans "Has page on users list." %}"></div>
+  {% endif %}
+  {% if item.is_on_index %}
+  <div class="fa fa-bookmark text-success pull-right tooltip-top" title="{% trans "Users online displayed on index." %}"></div>
+  {% endif %}
+</td>
+<td>
+  {% if item.title %}
+  {% trans item.title %}
+  {% else %}
+  <i class="text-muted">{% trans "No title set" %}</i>
+  {% endif %}
+</td>
+<td class="row-action">
+  {% if not forloop.last %}
+  <form action="{% url 'misago:admin:users:ranks:down' rank_id=item.id %}" method="post">
+    <button class="btn btn-default tooltip-top" title="{% trans "Move down" %}">
+      {% csrf_token %}
+      <span class="fa fa-chevron-down"></span>
+    </button>
+  </form>
+  {% else %}
+  &nbsp;
+  {% endif %}
+</td>
+<td class="row-action">
+  {% if not forloop.first %}
+  <form action="{% url 'misago:admin:users:ranks:up' rank_id=item.id %}" method="post">
+    <button class="btn btn-default tooltip-top" title="{% trans "Move up" %}">
+      {% csrf_token %}
+      <span class="fa fa-chevron-up"></span>
+    </button>
+  </form>
+  {% else %}
+  &nbsp;
+  {% endif %}
+</td>
+<td class="row-action">
+  <a href="{% url 'misago:admin:users:ranks:edit' rank_id=item.id %}" class="btn btn-primary tooltip-top" title="{% trans "Edit" %}">
+    <span class="fa fa-pencil"></span>
+  </a>
+</td>
+<td class="row-action">
+  <form action="{% url 'misago:admin:users:ranks:delete' rank_id=item.id %}" method="post" class="delete-prompt">
+    <button class="btn btn-danger tooltip-top" title="{% trans "Delete" %}">
+      {% csrf_token %}
+      <span class="fa fa-times"></span>
+    </button>
+  </form>
 </td>
 </td>
 {% endblock %}
 {% endblock %}
 
 
@@ -19,3 +84,15 @@
   <p>{% trans "No ranks are currently defined." %}</p>
   <p>{% trans "No ranks are currently defined." %}</p>
 </td>
 </td>
 {% endblock emptylist %}
 {% endblock emptylist %}
+
+
+{% block javascripts %}
+<script type="text/javascript">
+  $(function() {
+    $('.delete-prompt').submit(function() {
+      var decision = confirm("{% trans "Are you sure you want to delete this rank?" %}");
+      return decision;
+    });
+  });
+</script>
+{% endblock %}

+ 46 - 0
misago/users/forms/admin.py

@@ -0,0 +1,46 @@
+from django.utils.translation import ugettext_lazy as _
+from misago.core import forms
+from misago.core.validators import validate_sluggable
+from misago.users.models import Rank
+
+
+class RankForm(forms.ModelForm):
+    name = forms.CharField(
+        label=_("Name"),
+        validators=[validate_sluggable()],
+        help_text=_('Short and descriptive name of all users with this rank. '
+                    '"The Team" or "Game Masters" are good examples.'))
+    title = forms.CharField(
+        label=_("User title"), required=False,
+        help_text=_('Optional, singular version of rank name displayed by '
+                    'user names. For example "GM" or "Dev".'))
+    description = forms.CharField(
+        label=_("Description"), max_length=1024, required=False,
+        widget=forms.Textarea(attrs={'rows': 3}),
+        help_text=_("Optional description explaining function or status of "
+                    "members distincted with this rank."))
+    style = forms.CharField(
+        label=_("CSS Class"), required=False,
+        help_text=_("Optional css class added to content belonging to this "
+                    "rank owner."))
+    is_tab = forms.BooleanField(
+        label=_("Give rank dedicated tab on users list"), required=False,
+        help_text=_("Selecting this option will make users with this rank "
+                    "easily discoverable by others trough dedicated page on "
+                    "forum users list."))
+    is_on_index = forms.BooleanField(
+        label=_("Show users online on forum index"), required=False,
+        help_text=_("Selecting this option will make forum inform other "
+                    "users of their availability by displaying them on forum "
+                    "index page."))
+
+    class Meta:
+        model = Rank
+        fields = [
+            'name', 'description', 'style', 'title', 'is_tab', 'is_on_index'
+        ]
+
+    def clean_name(self):
+        data = self.cleaned_data['name']
+        self.instance.set_name(data)
+        return data

+ 24 - 0
misago/users/models/rankmodel.py

@@ -19,14 +19,38 @@ class Rank(models.Model):
 
 
     class Meta:
     class Meta:
         app_label = 'users'
         app_label = 'users'
+        get_latest_by = 'order'
 
 
     def __unicode__(self):
     def __unicode__(self):
         return unicode(_(self.name))
         return unicode(_(self.name))
 
 
+    def save(self, *args, **kwargs):
+        if not self.pk:
+            self.set_order()
+        return super(Rank, self).save(*args, **kwargs)
+
     def set_name(self, name):
     def set_name(self, name):
         self.name = name
         self.name = name
         self.slug = slugify(name)
         self.slug = slugify(name)
 
 
+    def set_order(self):
+        try:
+            self.order = Rank.objects.latest('order').order + 1
+        except Rank.DoesNotExist:
+            self.order = 0
+
+    def next(self):
+        try:
+            return Rank.objects.filter(order__gt=self.order).earliest('order')
+        except Rank.DoesNotExist:
+            return None
+
+    def prev(self):
+        try:
+            return Rank.objects.filter(order__lt=self.order).latest('order')
+        except Rank.DoesNotExist:
+            return None
+
 
 
 """register model in misago admin"""
 """register model in misago admin"""
 site.add_node(
 site.add_node(

+ 7 - 1
misago/users/urls/admin.py

@@ -1,7 +1,8 @@
 from django.conf.urls import url
 from django.conf.urls import url
 from misago.admin import urlpatterns
 from misago.admin import urlpatterns
 from misago.users.views.useradmin import UsersList
 from misago.users.views.useradmin import UsersList
-from misago.users.views.rankadmin import RanksList
+from misago.users.views.rankadmin import (RanksList, NewRank, EditRank,
+                                          DeleteRank, MoveUpRank, MoveDownRank)
 
 
 
 
 # Users section
 # Users section
@@ -20,4 +21,9 @@ urlpatterns.patterns('users:accounts',
 urlpatterns.namespace(r'^ranks/', 'ranks', 'users')
 urlpatterns.namespace(r'^ranks/', 'ranks', 'users')
 urlpatterns.patterns('users:ranks',
 urlpatterns.patterns('users:ranks',
     url(r'^$', RanksList.as_view(), name='index'),
     url(r'^$', RanksList.as_view(), name='index'),
+    url(r'^new/$', NewRank.as_view(), name='new'),
+    url(r'^edit/(?P<rank_id>\d+)/$', EditRank.as_view(), name='edit'),
+    url(r'^move/up/(?P<rank_id>\d+)/$', MoveUpRank.as_view(), name='up'),
+    url(r'^move/down/(?P<rank_id>\d+)/$', MoveDownRank.as_view(), name='down'),
+    url(r'^delete/(?P<rank_id>\d+)/$', DeleteRank.as_view(), name='delete'),
 )
 )

+ 42 - 1
misago/users/views/rankadmin.py

@@ -1,15 +1,56 @@
+from django.contrib import messages
 from django.utils.translation import ugettext_lazy as _
 from django.utils.translation import ugettext_lazy as _
 from misago.admin.views import generic
 from misago.admin.views import generic
 from misago.users.models import Rank
 from misago.users.models import Rank
+from misago.users.forms.admin import RankForm
 
 
 
 
 class RankAdmin(generic.AdminBaseMixin):
 class RankAdmin(generic.AdminBaseMixin):
     root_link = 'misago:admin:users:ranks:index'
     root_link = 'misago:admin:users:ranks:index'
     template_dir = 'misago/admin/ranks'
     template_dir = 'misago/admin/ranks'
+    message_404 = _("Requested rank does not exist.")
+    form = RankForm
 
 
     def get_model(self):
     def get_model(self):
         return Rank
         return Rank
 
 
 
 
-class RanksList(RankAdmin, generic.ItemsList):
+class RanksList(RankAdmin, generic.ListView):
     ordering = ((None, 'order'),)
     ordering = ((None, 'order'),)
+
+
+class NewRank(RankAdmin, generic.FormView):
+    message_submit = _('New rank "%s" has been saved.')
+
+
+class EditRank(RankAdmin, generic.FormView):
+    message_submit = _('Rank "%s" has been edited.')
+
+
+class DeleteRank(RankAdmin, generic.ButtonView):
+    def button_action(self, request, target=None):
+        target.delete()
+        message = _('Rank "%s" has been deleted.') % unicode(target.name)
+        messages.success(request, message)
+
+
+class MoveUpRank(RankAdmin, generic.ButtonView):
+    def button_action(self, request, target=None):
+        other_target = target.prev()
+        if other_target:
+            other_target.order, target.order = target.order, other_target.order
+            other_target.save(update_fields=['order'])
+            target.save(update_fields=['order'])
+            message = _('Rank "%s" has been moved up.') % unicode(target.name)
+            messages.success(request, message)
+
+
+class MoveDownRank(RankAdmin, generic.ButtonView):
+    def button_action(self, request, target=None):
+        other_target = target.next()
+        if other_target:
+            other_target.order, target.order = target.order, other_target.order
+            other_target.save(update_fields=['order'])
+            target.save(update_fields=['order'])
+            message = _('Rank "%s" has been moved down.') % unicode(target.name)
+            messages.success(request, message)

+ 1 - 1
misago/users/views/useradmin.py

@@ -11,7 +11,7 @@ class UserAdmin(generic.AdminBaseMixin):
         return get_user_model()
         return get_user_model()
 
 
 
 
-class UsersList(UserAdmin, generic.ItemsList):
+class UsersList(UserAdmin, generic.ListView):
     items_per_page = 20
     items_per_page = 20
     ordering = (
     ordering = (
         (_("From newest"), '-id'),
         (_("From newest"), '-id'),