Browse Source

Add/edit user stubs.

Rafał Pitoń 11 years ago
parent
commit
06cb7aff56

+ 0 - 6
misago/admin/views/generic.py

@@ -258,12 +258,6 @@ class ModelFormView(FormView):
         else:
         else:
             return FormType(instance=target)
             return FormType(instance=target)
 
 
-    def transaction_pre_save(self, form, request, target):
-        pass
-
-    def transaction_after_save(self, form, request, target):
-        pass
-
     def handle_form(self, form, request, target):
     def handle_form(self, form, request, target):
         form.instance.save()
         form.instance.save()
         if self.message_submit:
         if self.message_submit:

+ 1 - 0
misago/core/forms.py

@@ -1,6 +1,7 @@
 from django.utils.translation import ugettext_lazy as _
 from django.utils.translation import ugettext_lazy as _
 from django.forms import *  # noqa
 from django.forms import *  # noqa
 from django.forms import Form as BaseForm, ModelForm as BaseModelForm
 from django.forms import Form as BaseForm, ModelForm as BaseModelForm
+from crispy_forms.helper import FormHelper
 
 
 
 
 TEXT_BASED_FIELDS = (
 TEXT_BASED_FIELDS = (

+ 1 - 1
misago/templates/misago/admin/generic/form.html

@@ -16,7 +16,7 @@
   <div class="col-xs-12 col-md-8 col-md-offset-2">
   <div class="col-xs-12 col-md-8 col-md-offset-2">
 
 
     <div class="form-panel">
     <div class="form-panel">
-      <form role="form" method="post">
+      <form role="form" method="post" {% block form-extra %}{% endblock form-extra%}>
         {% csrf_token %}
         {% csrf_token %}
 
 
         <div class="form-header">
         <div class="form-header">

+ 56 - 0
misago/templates/misago/admin/users/edit.html

@@ -0,0 +1,56 @@
+{% extends "misago/admin/generic/form.html" %}
+{% load crispy_forms_tags i18n %}
+
+
+{% block title %}
+{{ target.username }} | {{ active_link.name }} | {{ block.super }}
+{% endblock title %}
+
+
+{% block page-target %}
+{{ target.username }}
+{% endblock page-target %}
+
+
+{% block form-header %}
+<h1>
+  {{ target.username }}
+</h1>
+{% endblock %}
+
+
+{% block form-extra %}
+class="form-horizontal"
+{% endblock form-extra%}
+
+{% block form-body %}
+<div class="form-body">
+  <fieldset>
+    <legend>{% trans "Basic account settings" %}</legend>
+
+    {{ form.username|as_crispy_field }}
+
+    {% if 'rank' in form.fields %}
+    {{ form.rank|as_crispy_field }}
+    {% endif %}
+
+    {{ form.title|as_crispy_field }}
+
+    {% if 'roles' in form.fields %}
+    {{ form.roles|as_crispy_field }}
+    {% endif %}
+
+    {% if 'staff_level' in form.fields %}
+    {{ form.staff_level|as_crispy_field }}
+    {% endif %}
+
+  </fieldset>
+  <fieldset>
+    <legend>{% trans "Sign-in credentials" %}</legend>
+
+    {{ form.email|as_crispy_field }}
+    {{ form.new_password|as_crispy_field }}
+
+  </fieldset>
+</div>
+{% endblock form-body %}

+ 14 - 16
misago/templates/misago/admin/users/list.html

@@ -2,6 +2,16 @@
 {% load i18n misago_avatars %}
 {% load i18n misago_avatars %}
 
 
 
 
+{% block page-actions %}
+<div class="page-actions">
+  <a href="{% url 'misago:admin:users:accounts:new' %}" class="btn btn-success">
+    <span class="fa fa-plus-circle"></span>
+    {% trans "New user" %}
+  </a>
+</div>
+{% endblock %}
+
+
 {% block table-header %}
 {% block table-header %}
 <th style="width: 1%;">&nbsp;</th>
 <th style="width: 1%;">&nbsp;</th>
 <th>{% trans "User" %}</th>
 <th>{% trans "User" %}</th>
@@ -27,26 +37,14 @@
 </td>
 </td>
 <td class="row-action">
 <td class="row-action">
   <div class="btn-group pull-right">
   <div class="btn-group pull-right">
-    <button type="button" class="btn btn-default dropdown-toggle tooltip-top" data-toggle="dropdown" title="Item options">
+    <button type="button" class="btn btn-default dropdown-toggle tooltip-top" data-toggle="dropdown" title="{% trans "User options" %}">
       <span class="fa fa-gear"></span>
       <span class="fa fa-gear"></span>
     </button>
     </button>
     <ul class="dropdown-menu" role="menu">
     <ul class="dropdown-menu" role="menu">
       <li>
       <li>
-        <a href="#">
-          <span class="fa fa-sort-numeric-desc"></span>
-          Newest
-        </a>
-        <a href="#">
-          <span class="fa fa-sort-numeric-asc"></span>
-          Oldest
-        </a>
-        <a href="#">
-          <span class="fa fa-sort-numeric-desc"></span>
-          Most posts
-        </a>
-        <a href="#">
-          <span class="fa fa-sort-numeric-asc"></span>
-          Least posts
+        <a href="{% url 'misago:admin:users:accounts:edit' user_id=item.pk %}">
+          <span class="fa fa-pencil"></span>
+          {% trans "Edit user" %}
         </a>
         </a>
       </li>
       </li>
     </ul>
     </ul>

+ 61 - 0
misago/templates/misago/admin/users/new.html

@@ -0,0 +1,61 @@
+{% extends "misago/admin/generic/form.html" %}
+{% load crispy_forms_tags i18n %}
+
+
+{% block title %}
+{% trans "New user" %} | {{ active_link.name }} | {{ block.super }}
+{% endblock title %}
+
+
+{% block page-target %}
+{% trans "New user" %}
+{% endblock page-target %}
+
+
+{% block form-header %}
+<h1>
+  {% trans "New user" %}
+</h1>
+{% endblock %}
+
+
+{% block form-extra %}
+class="form-horizontal"
+{% endblock form-extra%}
+
+{% block form-body %}
+<div class="form-body">
+  <fieldset>
+    <legend>{% trans "Basic account settings" %}</legend>
+
+    {{ form.username|as_crispy_field }}
+
+    {% if 'rank' in form.fields %}
+    {{ form.rank|as_crispy_field }}
+    {% endif %}
+
+    {{ form.title|as_crispy_field }}
+
+    {% if 'roles' in form.fields %}
+    {{ form.roles|as_crispy_field }}
+    {% endif %}
+
+    {% if 'staff_level' in form.fields %}
+    {{ form.staff_level|as_crispy_field }}
+    {% endif %}
+
+  </fieldset>
+  <fieldset>
+    <legend>{% trans "Sign-in credentials" %}</legend>
+
+    {{ form.email|as_crispy_field }}
+    {{ form.new_password|as_crispy_field }}
+
+  </fieldset>
+</div>
+{% endblock form-body %}
+
+
+{% block form-footer %}
+<button class="btn btn-primary">{% trans "Save user" %}</button>
+{% endblock form-footer %}

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

@@ -1,8 +1,111 @@
+from django.contrib.auth import get_user_model
 from django.utils.translation import ugettext_lazy as _
 from django.utils.translation import ugettext_lazy as _
 from misago.core import forms
 from misago.core import forms
 from misago.core.validators import validate_sluggable
 from misago.core.validators import validate_sluggable
 from misago.acl.models import Role
 from misago.acl.models import Role
 from misago.users.models import Rank
 from misago.users.models import Rank
+from misago.users.validators import (validate_username, validate_email,
+                                     validate_password)
+
+
+class UserBaseForm(forms.ModelForm):
+    username = forms.CharField(
+        label=_("Username"))
+    title = forms.CharField(
+        label=_("Custom title"),
+        required=False)
+    email = forms.EmailField(
+        label=_("E-mail address"))
+
+    def clean_username(self):
+        data = self.cleaned_data['username']
+        validate_username(data)
+        return data
+
+    def clean_email(self):
+        data = self.cleaned_data['email']
+        validate_email(data)
+        return data
+
+    def clean_new_password(self):
+        data = self.cleaned_data['new_password']
+        validate_password(data)
+        return data
+
+
+class NewUserForm(UserBaseForm):
+    new_password = forms.CharField(
+        label=_("Password"),
+        widget=forms.PasswordInput)
+
+    class Meta:
+        model = get_user_model()
+        fields = ['username', 'email']
+
+
+class EditUserForm(forms.ModelForm):
+    new_password = forms.CharField(
+        label=_("Change password to"),
+        widget=forms.PasswordInput,
+        required=False)
+
+    class Meta:
+        model = get_user_model()
+
+
+def UserFormFactory(FormType, instance):
+    extra_fields = {}
+
+
+    ranks = Rank.objects.order_by('name')
+    if ranks.exists():
+        extra_fields['rank'] = forms.ModelChoiceField(
+            label=_("Rank"),
+            help_text=_("Ranks are used to group and distinguish users. "
+                        "They are also used to add permissions to groups of "
+                        "users."),
+            queryset=ranks,
+            initial=instance.rank,
+            required=False,
+            empty_label=_("No rank"))
+
+    roles = Role.objects.order_by('name')
+    if roles.exists():
+        extra_fields['roles'] = forms.ModelMultipleChoiceField(
+            label=_("Roles"),
+            help_text=_("Individual roles of this user."),
+            queryset=roles,
+            initial=instance.roles.all() if instance.pk else None,
+            required=False,
+            widget=forms.CheckboxSelectMultiple)
+
+    return type('UserFormFinal', (FormType,), extra_fields)
+
+
+def StaffFlagUserFormFactory(FormType, instance, add_staff_field):
+    FormType = UserFormFactory(FormType, instance)
+
+    if add_staff_field:
+        staff_levels = (
+            (0, _("No access")),
+            (1, _("Administrator")),
+            (2, _("Superadmin")),
+        )
+
+        staff_fields = {
+            'staff_level': forms.TypedChoiceField(
+                label=_("Admin level"),
+                help_text=_('Only administrators can access admin sites. '
+                            'In addition to admin site access, superadmins '
+                            'can also change other members admin levels.'),
+                coerce=int,
+                choices=staff_levels,
+                initial=instance.staff_level),
+        }
+
+        return type('StaffUserForm', (FormType,), staff_fields)
+    else:
+        return FormType
 
 
 
 
 class RankForm(forms.ModelForm):
 class RankForm(forms.ModelForm):

+ 105 - 0
misago/users/migrations/0006_auto__add_field_user_title__add_field_user_acl_key.py

@@ -0,0 +1,105 @@
+# -*- coding: utf-8 -*-
+from south.utils import datetime_utils as datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        # Adding field 'User.title'
+        db.add_column(u'users_user', 'title',
+                      self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True),
+                      keep_default=False)
+
+        # Adding field 'User.acl_key'
+        db.add_column(u'users_user', 'acl_key',
+                      self.gf('django.db.models.fields.CharField')(max_length=12, null=True, blank=True),
+                      keep_default=False)
+
+        # Adding M2M table for field roles on 'User'
+        m2m_table_name = db.shorten_name(u'users_user_roles')
+        db.create_table(m2m_table_name, (
+            ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+            ('user', models.ForeignKey(orm['users.user'], null=False)),
+            ('role', models.ForeignKey(orm[u'acl.role'], null=False))
+        ))
+        db.create_unique(m2m_table_name, ['user_id', 'role_id'])
+
+
+    def backwards(self, orm):
+        # Deleting field 'User.title'
+        db.delete_column(u'users_user', 'title')
+
+        # Deleting field 'User.acl_key'
+        db.delete_column(u'users_user', 'acl_key')
+
+        # Removing M2M table for field roles on 'User'
+        db.delete_table(db.shorten_name(u'users_user_roles'))
+
+
+    models = {
+        u'acl.role': {
+            'Meta': {'object_name': 'Role'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'pickled_permissions': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'special_role': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'})
+        },
+        u'auth.group': {
+            'Meta': {'object_name': 'Group'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+        },
+        u'auth.permission': {
+            'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        u'contenttypes.contenttype': {
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        },
+        'users.rank': {
+            'Meta': {'object_name': 'Rank'},
+            'css_class': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_default': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'is_on_index': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'is_tab': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'roles': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['acl.Role']", 'null': 'True', 'blank': 'True'}),
+            'slug': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'})
+        },
+        'users.user': {
+            'Meta': {'object_name': 'User'},
+            'acl_key': ('django.db.models.fields.CharField', [], {'max_length': '12', 'null': 'True', 'blank': 'True'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '255', 'db_index': 'True'}),
+            'email_hash': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'joined_on': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['users.Rank']", 'on_delete': 'models.PROTECT'}),
+            'roles': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['acl.Role']", 'symmetrical': 'False'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
+            'username': ('django.db.models.fields.CharField', [], {'max_length': '30'}),
+            'username_slug': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+        }
+    }
+
+    complete_apps = ['users']

+ 87 - 0
misago/users/migrations/0007_auto__chg_field_user_rank.py

@@ -0,0 +1,87 @@
+# -*- coding: utf-8 -*-
+from south.utils import datetime_utils as datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+
+        # Changing field 'User.rank'
+        db.alter_column(u'users_user', 'rank_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['users.Rank'], null=True, on_delete=models.PROTECT))
+
+    def backwards(self, orm):
+
+        # User chose to not deal with backwards NULL issues for 'User.rank'
+        raise RuntimeError("Cannot reverse this migration. 'User.rank' and its values cannot be restored.")
+        
+        # The following code is provided here to aid in writing a correct migration
+        # Changing field 'User.rank'
+        db.alter_column(u'users_user', 'rank_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['users.Rank'], on_delete=models.PROTECT))
+
+    models = {
+        u'acl.role': {
+            'Meta': {'object_name': 'Role'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'pickled_permissions': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'special_role': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'})
+        },
+        u'auth.group': {
+            'Meta': {'object_name': 'Group'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+        },
+        u'auth.permission': {
+            'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        u'contenttypes.contenttype': {
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        },
+        'users.rank': {
+            'Meta': {'object_name': 'Rank'},
+            'css_class': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_default': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'is_on_index': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'is_tab': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'roles': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['acl.Role']", 'null': 'True', 'blank': 'True'}),
+            'slug': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'})
+        },
+        'users.user': {
+            'Meta': {'object_name': 'User'},
+            'acl_key': ('django.db.models.fields.CharField', [], {'max_length': '12', 'null': 'True', 'blank': 'True'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '255', 'db_index': 'True'}),
+            'email_hash': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'joined_on': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['users.Rank']", 'null': 'True', 'on_delete': 'models.PROTECT', 'blank': 'True'}),
+            'roles': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['acl.Role']", 'symmetrical': 'False'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
+            'username': ('django.db.models.fields.CharField', [], {'max_length': '30'}),
+            'username_slug': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+        }
+    }
+
+    complete_apps = ['users']

+ 30 - 1
misago/users/models/usermodel.py

@@ -74,10 +74,15 @@ class User(AbstractBaseUser, PermissionsMixin):
     email = models.EmailField(max_length=255, db_index=True)
     email = models.EmailField(max_length=255, db_index=True)
     email_hash = models.CharField(max_length=32, unique=True)
     email_hash = models.CharField(max_length=32, unique=True)
     joined_on = models.DateTimeField(_('joined on'), default=timezone.now)
     joined_on = models.DateTimeField(_('joined on'), default=timezone.now)
-    rank = models.ForeignKey('users.Rank', on_delete=models.PROTECT)
+    rank = models.ForeignKey(
+        'users.Rank', null=True, blank=True, on_delete=models.PROTECT)
+    title = models.CharField(max_length=255, null=True, blank=True)
     is_staff = models.BooleanField(
     is_staff = models.BooleanField(
         _('staff status'), default=False, db_index=True,
         _('staff status'), default=False, db_index=True,
         help_text=_('Designates whether the user can log into admin sites.'))
         help_text=_('Designates whether the user can log into admin sites.'))
+    roles = models.ManyToManyField('acl.Role')
+    acl_key = models.CharField(max_length=12, null=True, blank=True)
+
     is_active = True
     is_active = True
 
 
     USERNAME_FIELD = 'username_slug'
     USERNAME_FIELD = 'username_slug'
@@ -88,6 +93,27 @@ class User(AbstractBaseUser, PermissionsMixin):
     class Meta:
     class Meta:
         app_label = 'users'
         app_label = 'users'
 
 
+    @property
+    def staff_level(self):
+        if self.is_superuser:
+            return 2
+        elif self.is_staff:
+            return 1
+        else:
+            return 0
+
+    @staff_level.setter
+    def staff_level(self, new_level):
+        if new_level == 2:
+            self.is_superuser = True
+            self.is_staff = True
+        elif new_level == 1:
+            self.is_superuser = False
+            self.is_staff = True
+        else:
+            self.is_superuser = False
+            self.is_staff = False
+
     def get_username(self):
     def get_username(self):
         """
         """
         Dirty hack: return real username instead of normalized slug
         Dirty hack: return real username instead of normalized slug
@@ -108,6 +134,9 @@ class User(AbstractBaseUser, PermissionsMixin):
         self.email = UserManager.normalize_email(new_email)
         self.email = UserManager.normalize_email(new_email)
         self.email_hash = hash_email(new_email)
         self.email_hash = hash_email(new_email)
 
 
+    def update_acl_token(self):
+        pass
+
 
 
 """register model in misago admin"""
 """register model in misago admin"""
 site.add_node(
 site.add_node(

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

@@ -1,6 +1,6 @@
 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, NewUser, EditUser
 from misago.users.views.rankadmin import (RanksList, NewRank, EditRank,
 from misago.users.views.rankadmin import (RanksList, NewRank, EditRank,
                                           DeleteRank, MoveUpRank, MoveDownRank,
                                           DeleteRank, MoveUpRank, MoveDownRank,
                                           DefaultRank)
                                           DefaultRank)
@@ -15,6 +15,8 @@ urlpatterns.namespace(r'^accounts/', 'accounts', 'users')
 urlpatterns.patterns('users:accounts',
 urlpatterns.patterns('users:accounts',
     url(r'^$', UsersList.as_view(), name='index'),
     url(r'^$', UsersList.as_view(), name='index'),
     url(r'^(?P<page>\d+)/$', UsersList.as_view(), name='index'),
     url(r'^(?P<page>\d+)/$', UsersList.as_view(), name='index'),
+    url(r'^new/$', NewUser.as_view(), name='new'),
+    url(r'^edit/(?P<user_id>\d+)/$', EditUser.as_view(), name='edit'),
 )
 )
 
 
 
 

+ 10 - 1
misago/users/validators.py

@@ -9,6 +9,9 @@ from misago.conf import settings
 USERNAME_RE = re.compile(r'^[0-9a-z]+$', re.IGNORECASE)
 USERNAME_RE = re.compile(r'^[0-9a-z]+$', re.IGNORECASE)
 
 
 
 
+"""
+Email validators
+"""
 def validate_email_available(value):
 def validate_email_available(value):
     User = get_user_model()
     User = get_user_model()
 
 
@@ -31,16 +34,22 @@ def validate_email(value):
     validate_email_banned(value)
     validate_email_banned(value)
 
 
 
 
+"""
+Password validators
+"""
 def validate_password(value):
 def validate_password(value):
     if len(value) < settings.password_length_min:
     if len(value) < settings.password_length_min:
         message = ungettext(
         message = ungettext(
             'Valid password must be at least one character long.',
             'Valid password must be at least one character long.',
-            'valid password must be at least %(length)d characters long.',
+            'Valid password must be at least %(length)d characters long.',
             settings.password_length_min)
             settings.password_length_min)
         message = message % {'length': settings.password_length_min}
         message = message % {'length': settings.password_length_min}
         raise ValidationError(message)
         raise ValidationError(message)
 
 
 
 
+"""
+Username validators
+"""
 def validate_username_available(value):
 def validate_username_available(value):
     User = get_user_model()
     User = get_user_model()
 
 

+ 56 - 0
misago/users/views/useradmin.py

@@ -1,6 +1,10 @@
+from django.contrib import messages
 from django.contrib.auth import get_user_model
 from django.contrib.auth import get_user_model
+from django.shortcuts import redirect
 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.forms.admin import (StaffFlagUserFormFactory, NewUserForm,
+                                      EditUserForm)
 
 
 
 
 class UserAdmin(generic.AdminBaseMixin):
 class UserAdmin(generic.AdminBaseMixin):
@@ -10,6 +14,10 @@ class UserAdmin(generic.AdminBaseMixin):
     def get_model(self):
     def get_model(self):
         return get_user_model()
         return get_user_model()
 
 
+    def create_form_type(self, request, target):
+        return StaffFlagUserFormFactory(
+            self.Form, target, add_staff_field=request.user.is_superuser)
+
 
 
 class UsersList(UserAdmin, generic.ListView):
 class UsersList(UserAdmin, generic.ListView):
     items_per_page = 20
     items_per_page = 20
@@ -19,3 +27,51 @@ class UsersList(UserAdmin, generic.ListView):
         ('username', _("A to z")),
         ('username', _("A to z")),
         ('-username', _("Z to a")),
         ('-username', _("Z to a")),
         )
         )
+
+
+class NewUser(UserAdmin, generic.ModelFormView):
+    Form = NewUserForm
+    template = 'new.html'
+    message_submit = _('New user "%s" has been registered.')
+
+    def handle_form(self, form, request, target):
+        User = get_user_model()
+        new_user = User.objects.create_user(
+            form.cleaned_data['username'],
+            form.cleaned_data['email'],
+            form.cleaned_data['new_password'],
+            title=form.cleaned_data['title'],
+            rank=form.cleaned_data.get('rank'))
+
+        if form.cleaned_data.get('staff_level'):
+            new_user.staff_level = form.cleaned_data['staff_level']
+
+        if form.cleaned_data.get('roles'):
+            new_user.roles.add(*form.cleaned_data['roles'])
+
+        new_user.update_acl_token()
+        new_user.save()
+
+        messages.success(request, self.message_submit % target.username)
+        return redirect('misago:admin:users:accounts:edit',
+                        user_id=new_user.pk)
+
+
+class EditUser(UserAdmin, generic.ModelFormView):
+    Form = EditUserForm
+    template = 'edit.html'
+    message_submit = _('User "%s" has been edited.')
+
+    def handle_form(self, form, request, target):
+        form.instance.save()
+
+        if form.cleaned_data.get('staff_level'):
+            form.instance.staff_level = form.cleaned_data['staff_level']
+
+        if form.cleaned_data.get('roles'):
+            form.instance.roles.add(*form.cleaned_data['roles'])
+
+        form.instance.update_acl_token()
+        form.instance.save()
+
+        messages.success(request, self.message_submit % target.username)