Browse Source

WIP #385: Namechanges history

Rafał Pitoń 11 years ago
parent
commit
572dc27d87

+ 2 - 2
docs/developers/user_sites.rst

@@ -16,9 +16,9 @@ Each site is associated to instance of special object, :py:class:`misago.users.s
 
 
 
 
 .. function:: add_page(link, name, icon=None, after=None, before=None,
 .. function:: add_page(link, name, icon=None, after=None, before=None,
-                 	   visibility_condition=None, badge=None)
+                 	   visible_if=None, badge=None)
 
 
-Adds page to site. ``after`` and ``before`` arguments should be value of other page ``link``. ``visibility_condition`` argument accepts callable that will be called with args passed to ``get_pages`` function on page render to resolve if link to page should be displayed. ``badge`` argument should be none or callable that will be called to get button badge, like number of posts on tab.
+Adds page to site. ``after`` and ``before`` arguments should be value of other page ``link``. ``visible_if`` argument accepts callable that will be called with args passed to ``get_pages`` function on page render to resolve if link to page should be displayed. ``badge`` argument should be none or callable that will be called to get button badge, like number of posts on tab.
 
 
 
 
 .. function:: get_pages(link, request, profile=None)
 .. function:: get_pages(link, request, profile=None)

+ 4 - 1
misago/acl/migrations/0003_default_roles.py

@@ -34,6 +34,7 @@ def create_default_roles(apps, schema_editor):
             # profiles perms
             # profiles perms
             'misago.users.permissions.profiles': {
             'misago.users.permissions.profiles': {
                 'can_search_users': 1,
                 'can_search_users': 1,
+                'can_see_users_name_history': 0,
                 'can_see_users_emails': 0,
                 'can_see_users_emails': 0,
                 'can_see_users_ips': 0,
                 'can_see_users_ips': 0,
                 'can_see_hidden_users': 0,
                 'can_see_hidden_users': 0,
@@ -62,6 +63,7 @@ def create_default_roles(apps, schema_editor):
             # profiles perms
             # profiles perms
             'misago.users.permissions.profiles': {
             'misago.users.permissions.profiles': {
                 'can_search_users': 1,
                 'can_search_users': 1,
+                'can_see_users_name_history': 0,
                 'can_see_users_emails': 0,
                 'can_see_users_emails': 0,
                 'can_see_users_ips': 0,
                 'can_see_users_ips': 0,
                 'can_see_hidden_users': 0,
                 'can_see_hidden_users': 0,
@@ -90,6 +92,7 @@ def create_default_roles(apps, schema_editor):
             # profiles perms
             # profiles perms
             'misago.users.permissions.profiles': {
             'misago.users.permissions.profiles': {
                 'can_search_users': 1,
                 'can_search_users': 1,
+                'can_see_users_name_history': 1,
                 'can_see_users_emails': 1,
                 'can_see_users_emails': 1,
                 'can_see_users_ips': 1,
                 'can_see_users_ips': 1,
                 'can_see_hidden_users': 1,
                 'can_see_hidden_users': 1,
@@ -103,7 +106,7 @@ def create_default_roles(apps, schema_editor):
         })
         })
     role.save()
     role.save()
 
 
-    role = Role(name=_("Spam accounts destroyer"))
+    role = Role(name=_("Spammers Deleter"))
     pickle_permissions(role,
     pickle_permissions(role,
         {
         {
             # destroy users perms
             # destroy users perms

+ 2 - 1
misago/static/misago/css/misago/misago.less

@@ -6,8 +6,9 @@
 @import "navs.less";
 @import "navs.less";
 @import "modals.less";
 @import "modals.less";
 @import "markup.less";
 @import "markup.less";
-@import "yesnoswitch.less";
+@import "pager.less";
 @import "states.less";
 @import "states.less";
+@import "yesnoswitch.less";
 
 
 // Layout elements
 // Layout elements
 @import "navbar.less";
 @import "navbar.less";

+ 76 - 0
misago/static/misago/css/misago/pager.less

@@ -0,0 +1,76 @@
+//
+// Pager
+// --------------------------------------------------
+
+
+// Default pager
+//
+//==
+.pager {
+  margin: 0px;
+  padding-bottom: 2px;
+
+  &>li {
+    &.page {
+      display: inline-block;
+      padding: 7px 0px;
+      padding-right: @line-height-computed / 3;
+    }
+
+    a {
+      &, &:link, &:visited {
+        background-color: @btn-default-bg;
+        border-color: @btn-default-bg;
+        border-radius: @border-radius-base;
+        box-shadow: 0px 2px 0px 0px @btn-default-border;
+        padding: 6px 9px;
+
+        color: @btn-default-color;
+      }
+
+      &:hover {
+        background-color: darken(@btn-default-bg, 10%);
+        border-color: darken(@btn-default-bg, 10%);
+        box-shadow: 0px 2px 0px 0px darken(@btn-default-border, 10%);
+
+        color: @btn-default-color;
+      }
+
+      &:active, &:focus {
+        background-color: darken(@btn-default-bg, 15%);
+        border-color: darken(@btn-default-bg, 15%);
+        box-shadow: none;
+        position: relative;
+        top: 2px;
+
+        color: darken(@btn-default-color, 10%);
+      }
+    }
+  }
+}
+
+
+// Wide pager
+//
+//==
+.pager {
+  &.pager-wide {
+    overflow: auto;
+  }
+}
+
+
+/* Big displays */
+@media (min-width: @screen-sm-min) {
+  .pager {
+    &.pager-wide {
+      .pull-left {
+        margin-right: @line-height-computed / 3;
+      }
+
+      .pull-right {
+        margin-left: @line-height-computed / 3;
+      }
+    }
+  }
+}

+ 31 - 3
misago/static/misago/css/misago/profile.less

@@ -14,8 +14,6 @@
   .nav-tabs {
   .nav-tabs {
     border-bottom: none;
     border-bottom: none;
 
 
-    font-size: @font-size-large;
-
     &>li{
     &>li{
       &>a {
       &>a {
         .label {
         .label {
@@ -23,7 +21,7 @@
           padding-left: 5px;
           padding-left: 5px;
           padding-right: 5px;
           padding-right: 5px;
           position: relative;
           position: relative;
-          top: -2px;
+          top: -1px;
 
 
           color: fadeOut(@text-color, 60%);
           color: fadeOut(@text-color, 60%);
           font-size: @font-size-small;
           font-size: @font-size-small;
@@ -118,3 +116,33 @@
     }
     }
   }
   }
 }
 }
+
+
+// Username history
+//
+//==
+.user-profile {
+  .username-history {
+    p {
+      margin: 0px;
+      margin-top: 3px;
+
+      img {
+        border-radius: @border-radius-small;
+        margin-top: -1px;
+        margin-right: @line-height-computed / 5;
+        height: 22px;
+        width: 22px;
+      }
+
+      abbr {
+        text-decoration: none;
+        border-bottom: none;
+      }
+    }
+
+    hr {
+      margin: (@line-height-computed / 2) 0px;
+    }
+  }
+}

+ 3 - 1
misago/templates/misago/profile/base.html

@@ -3,7 +3,7 @@
 
 
 
 
 {% block title %}
 {% block title %}
-{{ profile.username }} | {{ block.super }}
+{{ profile.username }}: {{ active_page.name }} {% if page_number > 1 %}({{ number }}) {% endif %}| {{ block.super }}
 {% endblock title %}
 {% endblock title %}
 
 
 
 
@@ -26,6 +26,7 @@
           {% endfor %}
           {% endfor %}
         </ul>
         </ul>
 
 
+        {% comment %}
         <div class="page-actions">
         <div class="page-actions">
 
 
           <a href="#" class="btn btn-primary">Primary</a>
           <a href="#" class="btn btn-primary">Primary</a>
@@ -47,6 +48,7 @@
           </div>
           </div>
 
 
         </div>
         </div>
+        {% endcomment %}
 
 
       </div>
       </div>
     </div>
     </div>

+ 99 - 0
misago/templates/misago/profile/name_history.html

@@ -0,0 +1,99 @@
+{% extends "misago/profile/base.html" %}
+{% load i18n misago_avatars misago_capture %}
+
+
+{% block page %}
+{% if name_changes.object_list %}
+<div class="username-history">
+  {% for change in name_changes.object_list %}
+  {% capture trimmed as new_username %}
+    <strong>{{ change.new_username }}</strong>
+  {% endcapture %}
+  <p>
+    {% if not change.changed_by or change.changed_by_id != profile.pk %}
+      {% if change.changed_by %}
+      <a href="{% url USER_PROFILE_URL user_slug=change.changed_by_slug user_id=change.changed_by_id %}">
+        <img src="{{ change.changed_by_id|avatar:22 }}" class="tooltip-top" title="{{ change.changed_by_username }}">
+      </a>
+      {% else %}
+      <img src="{% blankavatar 22 %}" class="tooltip-top" title="{{ change.changed_by_username }}">
+      {% endif %}
+      {% capture trimmed as changed_by %}
+        {% if change.changed_by %}
+        <a href="{% url USER_PROFILE_URL user_slug=change.changed_by_slug user_id=change.changed_by_id %}">{{ change.changed_by_username }}</a>
+        {% else %}
+        {{ change.changed_by_username }}
+        {% endif %}
+      {% endcapture %}
+      {% blocktrans trimmed with changed_by=changed_by|safe old_username=change.old_username new_username=new_username|safe %}
+      {{ changed_by }} changed {{ old_username }}'s name to {{ new_username }}
+      {% endblocktrans %}
+    {% else %}
+      <img src="{{ profile.pk|avatar:22 }}" class="tooltip-top" title="{{ profile.username }}">
+      {% blocktrans trimmed with old_username=change.old_username new_username=new_username|safe %}
+      {{ old_username }} changed his name to {{ new_username }}
+      {% endblocktrans %}
+    {% endif %}
+    <abbr class="pull-right text-muted tooltip-top dynamic time-ago" title="{{ change.changed_on }}" data-timestamp="{{ change.changed_on|date:"c" }}">
+      {{ change.changed_on|date }}
+    </abbr>
+  </p>
+  <hr>
+  {% endfor%}
+</div>
+
+{% if name_changes.paginator.num_pages > 1 %}
+<ul class="pager pager-wide">
+  {% if name_changes.has_previous %}
+    <li class="pull-left">
+      <a href="{% url 'misago:user_name_history' user_slug=profile.slug user_id=profile.pk %}" class="tooltip-top" title="{% trans "Go to first page" %}">
+        {% trans "Latest" %}
+      </a>
+    </li>
+    {% if name_changes.number > 2 %}
+    <li class="pull-left">
+      <a href="{% url 'misago:user_name_history' user_slug=profile.slug user_id=profile.pk page=name_changes.previous_page_number %}" class="tooltip-top" title="{% trans "Go to previous page" %}">
+        {% trans "Later" %}
+      </a>
+    </li>
+    {% endif %}
+  {% endif %}
+  {% if name_changes.has_next %}
+    <li class="pull-right">
+      <a href="{% url 'misago:user_name_history' user_slug=profile.slug user_id=profile.pk page=name_changes.paginator.num_pages %}" class="tooltip-top" title="{% trans "Go to last page" %}">
+        {% trans "Oldest" %}
+      </a>
+    </li>
+    {% if name_changes.next_page_number < name_changes.paginator.num_pages %}
+    <li class="pull-right">
+      <a href="{% url 'misago:user_name_history' user_slug=profile.slug user_id=profile.pk page=name_changes.next_page_number %}" class="tooltip-top" title="{% trans "Go to next page" %}">
+        {% trans "Older" %}
+      </a>
+    </li>
+    {% endif %}
+  {% endif %}
+  {% if name_changes.has_next %}
+  <li class="page text-muted">
+    {% blocktrans trimmed count left=items_left %}
+    There is {{ left }} more changes
+    {% plural %}
+    There are {{ left }} more changes
+    {% endblocktrans %}
+  </li>
+  {% endif %}
+</ul>
+{% endif %}
+{% else %}
+<p class="lead">
+  {% if is_authenticated_user %}
+    {% blocktrans trimmed with user=profile.username %}
+    Your username was never changed, {{ user }}.
+    {% endblocktrans %}
+  {% else %}
+    {% blocktrans trimmed with user=profile.username %}
+    {{ user }}'s username was never changed.
+    {% endblocktrans %}
+  {% endif %}
+</p>
+{% endif %}
+{% endblock page %}

+ 11 - 1
misago/users/apps.py

@@ -26,7 +26,7 @@ class MisagoUsersConfig(AppConfig):
         usercp.add_page(link='misago:usercp_edit_signature',
         usercp.add_page(link='misago:usercp_edit_signature',
                         name=_('Edit your signature'),
                         name=_('Edit your signature'),
                         icon='fa fa-pencil',
                         icon='fa fa-pencil',
-                        visibility_condition=show_signature_cp)
+                        visible_if=show_signature_cp)
         usercp.add_page(link='misago:usercp_change_username',
         usercp.add_page(link='misago:usercp_change_username',
                         name=_('Change username'),
                         name=_('Change username'),
                         icon='fa fa-credit-card')
                         icon='fa fa-credit-card')
@@ -44,6 +44,13 @@ class MisagoUsersConfig(AppConfig):
             return profile.posts
             return profile.posts
         def threads_badge(request, profile):
         def threads_badge(request, profile):
             return profile.threads
             return profile.threads
+        def can_see_names_history(request, profile):
+            if request.user.is_authenticated():
+                is_account_owner = profile.pk == request.user.pk
+                has_permission = request.user.acl['can_see_users_name_history']
+                return is_account_owner or has_permission
+            else:
+                return False
 
 
         user_profile.add_page(link='misago:user_posts',
         user_profile.add_page(link='misago:user_posts',
                               name=_("Posts"),
                               name=_("Posts"),
@@ -51,3 +58,6 @@ class MisagoUsersConfig(AppConfig):
         user_profile.add_page(link='misago:user_threads',
         user_profile.add_page(link='misago:user_threads',
                               name=_("Threads"),
                               name=_("Threads"),
                               badge=threads_badge)
                               badge=threads_badge)
+        user_profile.add_page(link='misago:user_name_history',
+                              name=_("Name history"),
+                              visible_if=can_see_names_history)

+ 5 - 0
misago/users/migrations/0001_initial.py

@@ -95,11 +95,16 @@ class Migration(migrations.Migration):
             name='UsernameChange',
             name='UsernameChange',
             fields=[
             fields=[
                 ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
                 ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                ('changed_by_username', models.CharField(max_length=30)),
+                ('changed_by_slug', models.CharField(max_length=30)),
                 ('changed_on', models.DateTimeField(default=django.utils.timezone.now)),
                 ('changed_on', models.DateTimeField(default=django.utils.timezone.now)),
+                ('new_username', models.CharField(max_length=255)),
                 ('old_username', models.CharField(max_length=255)),
                 ('old_username', models.CharField(max_length=255)),
+                ('changed_by', models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, blank=True, to=settings.AUTH_USER_MODEL, null=True)),
                 ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
                 ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
             ],
             ],
             options={
             options={
+                'get_latest_by': b'changed_on',
             },
             },
             bases=(models.Model,),
             bases=(models.Model,),
         ),
         ),

+ 22 - 2
misago/users/models/user.py

@@ -288,16 +288,25 @@ class User(AbstractBaseUser, PermissionsMixin):
     def get_short_name(self):
     def get_short_name(self):
         return self.username
         return self.username
 
 
-    def set_username(self, new_username):
+    def set_username(self, new_username, changed_by=None):
         if new_username != self.username:
         if new_username != self.username:
             old_username = self.username
             old_username = self.username
             self.username = new_username
             self.username = new_username
             self.slug = slugify(new_username)
             self.slug = slugify(new_username)
 
 
             if self.pk:
             if self.pk:
-                self.namechanges.create(old_username=old_username)
+                changed_by = changed_by or self
+                self.record_name_change(
+                    changed_by, new_username, old_username)
                 username_changed.send(sender=self)
                 username_changed.send(sender=self)
 
 
+    def record_name_change(self, changed_by, new_username, old_username):
+        self.namechanges.create(new_username=new_username,
+                                old_username=old_username,
+                                changed_by=changed_by,
+                                changed_by_username=changed_by.username,
+                                changed_by_slug=changed_by.slug)
+
     def set_email(self, new_email):
     def set_email(self, new_email):
         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)
@@ -346,12 +355,23 @@ class Online(models.Model):
 
 
 class UsernameChange(models.Model):
 class UsernameChange(models.Model):
     user = models.ForeignKey(User, related_name='namechanges')
     user = models.ForeignKey(User, related_name='namechanges')
+    changed_by = models.ForeignKey(User, null=True, blank=True,
+                                   related_name='user_renames',
+                                   on_delete=models.SET_NULL)
+    changed_by_username = models.CharField(max_length=30)
+    changed_by_slug = models.CharField(max_length=30)
     changed_on = models.DateTimeField(default=timezone.now)
     changed_on = models.DateTimeField(default=timezone.now)
+    new_username = models.CharField(max_length=255)
     old_username = models.CharField(max_length=255)
     old_username = models.CharField(max_length=255)
 
 
     class Meta:
     class Meta:
         get_latest_by = "changed_on"
         get_latest_by = "changed_on"
 
 
+    def set_change_author(self, user):
+        self.changed_by = user
+        self.changed_by_username = user.username
+        self.changed_by_slug = user.slug
+
 
 
 class AnonymousUser(DjangoAnonymousUser):
 class AnonymousUser(DjangoAnonymousUser):
     acl_key = 'anonymous'
     acl_key = 'anonymous'

+ 1 - 1
misago/users/namechanges.py

@@ -19,7 +19,7 @@ class UsernameChanges(object):
         name_changes_allowed = user.acl['name_changes_allowed']
         name_changes_allowed = user.acl['name_changes_allowed']
         name_changes_expire = user.acl['name_changes_expire']
         name_changes_expire = user.acl['name_changes_expire']
 
 
-        valid_changes_qs = user.namechanges
+        valid_changes_qs = user.namechanges.filter(changed_by=user)
         if name_changes_expire:
         if name_changes_expire:
             cutoff = timezone.now() - timedelta(days=name_changes_expire)
             cutoff = timezone.now() - timedelta(days=name_changes_expire)
             valid_changes_qs = used_changes_qs.filter(changed_on__gte=cutoff)
             valid_changes_qs = used_changes_qs.filter(changed_on__gte=cutoff)

+ 3 - 3
misago/users/permissions/destroying.py

@@ -9,15 +9,15 @@ from misago.core import forms
 Admin Permissions Form
 Admin Permissions Form
 """
 """
 class PermissionsForm(forms.Form):
 class PermissionsForm(forms.Form):
-    legend = _("Destroying user accounts")
+    legend = _("Deleting spammer accounts")
 
 
     can_destroy_user_newer_than = forms.IntegerField(
     can_destroy_user_newer_than = forms.IntegerField(
-        label=_("Maximum age of destroyed account (in days)"),
+        label=_("Maximum age of deleted account (in days)"),
         help_text=_("Enter zero to disable this check."),
         help_text=_("Enter zero to disable this check."),
         min_value=0,
         min_value=0,
         initial=0)
         initial=0)
     can_destroy_users_with_less_posts_than = forms.IntegerField(
     can_destroy_users_with_less_posts_than = forms.IntegerField(
-        label=_("Maximum number of posts on destroyed account"),
+        label=_("Maximum number of posts on deleted account"),
         help_text=_("Enter zero to disable this check."),
         help_text=_("Enter zero to disable this check."),
         min_value=0,
         min_value=0,
         initial=0)
         initial=0)

+ 4 - 0
misago/users/permissions/profiles.py

@@ -14,6 +14,8 @@ class PermissionsForm(forms.Form):
     can_search_users = forms.YesNoSwitch(
     can_search_users = forms.YesNoSwitch(
         label=_("Can search user profiles"),
         label=_("Can search user profiles"),
         initial=1)
         initial=1)
+    can_see_users_name_history = forms.YesNoSwitch(
+        label=_("Can see other members name history"))
     can_see_users_emails = forms.YesNoSwitch(
     can_see_users_emails = forms.YesNoSwitch(
         label=_("Can see members e-mails"))
         label=_("Can see members e-mails"))
     can_see_users_ips = forms.YesNoSwitch(
     can_see_users_ips = forms.YesNoSwitch(
@@ -35,6 +37,7 @@ ACL Builder
 def build_acl(acl, roles, key_name):
 def build_acl(acl, roles, key_name):
     new_acl = {
     new_acl = {
         'can_search_users': 0,
         'can_search_users': 0,
+        'can_see_users_name_history': 0,
         'can_see_users_emails': 0,
         'can_see_users_emails': 0,
         'can_see_users_ips': 0,
         'can_see_users_ips': 0,
         'can_see_hidden_users': 0,
         'can_see_hidden_users': 0,
@@ -43,6 +46,7 @@ def build_acl(acl, roles, key_name):
 
 
     return algebra.sum_acls(
     return algebra.sum_acls(
             new_acl, roles=roles, key=key_name,
             new_acl, roles=roles, key=key_name,
+            can_see_users_name_history=algebra.greater,
             can_search_users=algebra.greater,
             can_search_users=algebra.greater,
             can_see_users_emails=algebra.greater,
             can_see_users_emails=algebra.greater,
             can_see_users_ips=algebra.greater,
             can_see_users_ips=algebra.greater,

+ 10 - 0
misago/users/signals.py

@@ -1,5 +1,15 @@
 import django.dispatch
 import django.dispatch
+from django.dispatch import receiver
 
 
 
 
 delete_user_content = django.dispatch.Signal()
 delete_user_content = django.dispatch.Signal()
 username_changed = django.dispatch.Signal()
 username_changed = django.dispatch.Signal()
+
+
+"""
+Register default signal handlers
+"""
+@receiver(username_changed)
+def sync_username_in_user_models(sender, **kwargs):
+    sender.user_renames.update(changed_by_username=sender.username,
+                               changed_by_slug=sender.slug)

+ 6 - 6
misago/users/sites.py

@@ -64,7 +64,7 @@ class Site(object):
             return True
             return True
 
 
     def add_page(self, link, name, icon=None, after=None, before=None,
     def add_page(self, link, name, icon=None, after=None, before=None,
-                 visibility_condition=None, badge=None):
+                 visible_if=None, badge=None):
         if self._finalized:
         if self._finalized:
             message = ("%s site was initialized already and no longer "
             message = ("%s site was initialized already and no longer "
                        "accepts new pages")
                        "accepts new pages")
@@ -80,7 +80,7 @@ class Site(object):
             'after': after,
             'after': after,
             'before': before,
             'before': before,
             'badge': badge,
             'badge': badge,
-            'visibility_condition': visibility_condition,
+            'visible_if': visible_if,
             })
             })
 
 
     def _active_link_name(self, request):
     def _active_link_name(self, request):
@@ -103,12 +103,12 @@ class Site(object):
         else:
         else:
             test_args = (request,)
             test_args = (request,)
 
 
-        for _page in self._sorted_list:
-            page = _page.copy()
+        for page_definition in self._sorted_list:
+            page = page_definition.copy()
 
 
             is_visible = True
             is_visible = True
-            if page['visibility_condition']:
-                is_visible = page['visibility_condition'](*test_args)
+            if page['visible_if']:
+                is_visible = page['visible_if'](*test_args)
 
 
             if is_visible:
             if is_visible:
                 if page['badge']:
                 if page['badge']:

+ 8 - 0
misago/users/tests/test_profile_views.py

@@ -11,6 +11,14 @@ class UserProfileViewsTests(AdminTestCase):
             'user_id': self.test_admin.pk
             'user_id': self.test_admin.pk
         }
         }
 
 
+    def test_outdated_slugs(self):
+        """user profile view redirects to valid slig"""
+        invalid_kwargs = {'user_slug': 'baww', 'user_id': self.test_admin.pk}
+        response = self.client.get(reverse('misago:user_posts',
+                                           kwargs=invalid_kwargs))
+
+        self.assertEqual(response.status_code, 302)
+
     def test_user_posts_list(self):
     def test_user_posts_list(self):
         """user profile posts list has no showstoppers"""
         """user profile posts list has no showstoppers"""
         response = self.client.get(reverse('misago:user_posts',
         response = self.client.get(reverse('misago:user_posts',

+ 2 - 0
misago/users/urls.py

@@ -54,6 +54,8 @@ urlpatterns += patterns('misago.users.views.usercp',
 urlpatterns += patterns('misago.users.views.profile',
 urlpatterns += patterns('misago.users.views.profile',
     url(r'^user/(?P<user_slug>[a-z0-9]+)-(?P<user_id>\d+)/$', 'user_posts', name="user_posts"),
     url(r'^user/(?P<user_slug>[a-z0-9]+)-(?P<user_id>\d+)/$', 'user_posts', name="user_posts"),
     url(r'^user/(?P<user_slug>[a-z0-9]+)-(?P<user_id>\d+)/threads/$', 'user_threads', name="user_threads"),
     url(r'^user/(?P<user_slug>[a-z0-9]+)-(?P<user_id>\d+)/threads/$', 'user_threads', name="user_threads"),
+    url(r'^user/(?P<user_slug>[a-z0-9]+)-(?P<user_id>\d+)/name-history/$', 'name_history', name="user_name_history"),
+    url(r'^user/(?P<user_slug>[a-z0-9]+)-(?P<user_id>\d+)/name-history/(?P<page>\d+)/$', 'name_history', name="user_name_history"),
 )
 )
 
 
 urlpatterns += patterns('misago.users.views.avatarserver',
 urlpatterns += patterns('misago.users.views.avatarserver',

+ 4 - 1
misago/users/views/admin/users.py

@@ -205,7 +205,10 @@ class EditUser(UserAdmin, generic.ModelFormView):
 
 
     def handle_form(self, form, request, target):
     def handle_form(self, form, request, target):
         target.username = target.old_username
         target.username = target.old_username
-        target.set_username(form.cleaned_data.get('username'))
+
+        if target.username != form.cleaned_data.get('username'):
+            target.set_username(form.cleaned_data.get('username'),
+                                changed_by=request.user)
 
 
         if form.cleaned_data.get('new_password'):
         if form.cleaned_data.get('new_password'):
             target.set_password(form.cleaned_data['new_password'])
             target.set_password(form.cleaned_data['new_password'])

+ 19 - 5
misago/users/views/profile.py

@@ -1,7 +1,7 @@
 from django.contrib.auth import get_user_model
 from django.contrib.auth import get_user_model
 from django.shortcuts import redirect, render as django_render
 from django.shortcuts import redirect, render as django_render
 
 
-from misago.core.shortcuts import get_object_or_404, validate_slug
+from misago.core.shortcuts import get_object_or_404, paginate, validate_slug
 
 
 from misago.users import online
 from misago.users import online
 from misago.users.sites import user_profile
 from misago.users.sites import user_profile
@@ -28,14 +28,14 @@ def render(request, template, context):
             break
             break
 
 
     if request.user.is_authenticated():
     if request.user.is_authenticated():
-        authenticateds_profile = context['profile'].pk == request.user.pk
+        is_authenticated_user = context['profile'].pk == request.user.pk
     else:
     else:
-        authenticateds_profile = False
-    context['authenticateds_profile'] = authenticateds_profile
+        is_authenticated_user = False
+    context['is_authenticated_user'] = is_authenticated_user
 
 
     user_acl = request.user.acl
     user_acl = request.user.acl
     if request.user.is_authenticated():
     if request.user.is_authenticated():
-        if authenticateds_profile:
+        if is_authenticated_user:
             context['show_email'] = True
             context['show_email'] = True
         else:
         else:
             context['show_email'] = user_acl['can_see_users_emails']
             context['show_email'] = user_acl['can_see_users_emails']
@@ -55,3 +55,17 @@ def user_posts(request, profile=None, page=0):
 @profile_view
 @profile_view
 def user_threads(request, profile=None, page=0):
 def user_threads(request, profile=None, page=0):
     return render(request, 'misago/profile/threads.html', {'profile': profile})
     return render(request, 'misago/profile/threads.html', {'profile': profile})
+
+
+@profile_view
+def name_history(request, profile=None, page=0):
+    name_changes_sq = profile.namechanges.all().order_by('-id')
+    name_changes = paginate(name_changes_sq, page, 24, 6)
+    items_left = name_changes.paginator.count - name_changes.end_index()
+
+    return render(request, 'misago/profile/name_history.html', {
+        'profile': profile,
+        'name_changes': name_changes,
+        'page_number': name_changes.number,
+        'items_left': items_left
+    })