From fba524786eb378be6c1f960505a99dd02cb7e491 Mon Sep 17 00:00:00 2001 From: esteban Date: Mon, 30 Mar 2015 03:07:50 -0300 Subject: [PATCH] user profile --- README.md | 16 +---- spirit/forms/admin.py | 13 +++- spirit/forms/topic.py | 2 +- spirit/forms/user.py | 19 +++-- spirit/managers/comment.py | 2 +- spirit/managers/topic.py | 4 +- spirit/middleware.py | 12 ++-- spirit/migrations/0006_auto_20150327_0204.py | 4 +- spirit/migrations/0007_userprofile.py | 39 +++++++++++ spirit/models/user.py | 44 ++++++++++-- spirit/settings.py | 2 - spirit/signals/handlers/__init__.py | 1 + spirit/signals/handlers/user.py | 21 ++++++ spirit/templates/spirit/_base.html | 2 +- spirit/templates/spirit/_header.html | 4 +- .../admin/comment_flag/flag_detail.html | 4 +- .../spirit/admin/user/user_edit.html | 1 + .../spirit/comment/_render_list.html | 12 ++-- .../spirit/comment_history/detail.html | 2 +- .../templates/spirit/topic/topic_detail.html | 6 +- .../topic_notification/_render_list.html | 2 +- spirit/templates/spirit/user/_profile.html | 14 ++-- .../spirit/user/_render_comments_list.html | 2 +- spirit/templates/spirit/user/menu.html | 4 +- .../templates/spirit/user/profile_update.html | 1 + spirit/templatetags/tags/utils/avatar.py | 2 +- spirit/utils/decorators.py | 6 +- spirit/utils/markdown/inline.py | 4 +- spirit/utils/models.py | 6 +- spirit/utils/user/tokens.py | 2 +- spirit/views/admin/user.py | 24 ++++--- spirit/views/user.py | 33 +++++---- tests/tests_admin.py | 36 +++++++--- tests/tests_comment.py | 22 +++--- tests/tests_topic.py | 2 +- tests/tests_topic_moderate.py | 16 ++--- tests/tests_user.py | 70 +++++++++++-------- tests/tests_utils.py | 34 +++++---- 38 files changed, 321 insertions(+), 169 deletions(-) create mode 100644 spirit/migrations/0007_userprofile.py create mode 100644 spirit/signals/handlers/user.py diff --git a/README.md b/README.md index c8b27811..207a8481 100644 --- a/README.md +++ b/README.md @@ -9,27 +9,13 @@ To see it in action, please visit [The Spirit Project](http://spirit-project.com Spirit requires the following software to be installed: * Python 2.7, 3.3 or 3.4 (recommended) -* Django 1.7 +* Django 1.8 * PostgreSQL or MySQL or Oracle Database ## Dependencies Check out the [requirements](https://github.com/nitely/Spirit/blob/master/requirements.txt) provided. -## Integration - -Spirit can be integrated with any other Django application without much of a hassle. - -The only thing to notice is that Spirit uses its own *AUTH_USER_MODEL*. - -If you want to roll your own user app, your user model must inherit from `spirit.models.user.AbstractForumUser`. - -If you want to extend the Spirit user model (adding new fields or methods), -your model must inherit from `spirit.models.user.AbstractUser`. - -If you just want to integrate Spirit's user profile to your *existing* project -and you are using the default Django's user model, check out the [Spirit-User-Profile](https://github.com/nitely/Spirit-User-Profile) app. - ## Installing (Advanced) Check out the [example](https://github.com/nitely/Spirit/tree/master/example) provided. diff --git a/spirit/forms/admin.py b/spirit/forms/admin.py index f0c25e0c..5039c81e 100644 --- a/spirit/forms/admin.py +++ b/spirit/forms/admin.py @@ -10,17 +10,24 @@ from django.contrib.auth import get_user_model from spirit.models.category import Category from spirit.models.comment_flag import CommentFlag +from spirit.models.user import UserProfile User = get_user_model() -class UserEditForm(forms.ModelForm): +class UserForm(forms.ModelForm): class Meta: model = User - fields = ("username", "email", "location", - "timezone", "is_administrator", "is_moderator", "is_active") + fields = ("username", "email", "is_active") + + +class UserProfileForm(forms.ModelForm): + + class Meta: + model = UserProfile + fields = ("location", "timezone", "is_administrator", "is_moderator") class CategoryForm(forms.ModelForm): diff --git a/spirit/forms/topic.py b/spirit/forms/topic.py index 7bbc2d30..d9e47fb4 100644 --- a/spirit/forms/topic.py +++ b/spirit/forms/topic.py @@ -27,7 +27,7 @@ class TopicForm(forms.ModelForm): label=_("Category"), empty_label=_("Chose a category")) - if self.instance.pk and not user.is_moderator: + if self.instance.pk and not user.st.is_moderator: del self.fields['category'] def save(self, commit=True): diff --git a/spirit/forms/user.py b/spirit/forms/user.py index d1bea7bc..022d0472 100644 --- a/spirit/forms/user.py +++ b/spirit/forms/user.py @@ -9,6 +9,8 @@ from django.contrib.auth import get_user_model from django.utils import timezone from django.template import defaultfilters +from ..models.user import UserProfile + User = get_user_model() @@ -22,7 +24,7 @@ class RegistrationForm(UserCreationForm): fields = ("username", "email") def clean_honeypot(self): - """Check that nothing's been entered into the honeypot.""" + """Check that nothing has been entered into the honeypot.""" value = self.cleaned_data["honeypot"] if value: @@ -41,16 +43,25 @@ class RegistrationForm(UserCreationForm): raise forms.ValidationError(_("The username is taken.")) + # TODO: check email is unique + def save(self, commit=True): self.instance.is_active = False return super(RegistrationForm, self).save(commit) -class UserProfileForm(forms.ModelForm): +class UserForm(forms.ModelForm): class Meta: model = User - fields = ("first_name", "last_name", "location", "timezone") + fields = ("first_name", "last_name") + + +class UserProfileForm(forms.ModelForm): + + class Meta: + model = UserProfile + fields = ("location", "timezone") def __init__(self, *args, **kwargs): super(UserProfileForm, self).__init__(*args, **kwargs) @@ -108,7 +119,7 @@ class ResendActivationForm(forms.Form): except User.DoesNotExist: raise forms.ValidationError(_("The provided email does not exists.")) - if self.user.is_verified: + if self.user.st.is_verified: raise forms.ValidationError(_("This account is verified, try logging-in.")) return email diff --git a/spirit/managers/comment.py b/spirit/managers/comment.py index 20efdb08..d3525ad1 100644 --- a/spirit/managers/comment.py +++ b/spirit/managers/comment.py @@ -48,7 +48,7 @@ class CommentQuerySet(models.QuerySet): return self.unremoved()._access(user=user) def for_update_or_404(self, pk, user): - if user.is_moderator: + if user.st.is_moderator: return get_object_or_404(self._access(user=user), pk=pk) else: return get_object_or_404(self.for_access(user), user=user, pk=pk) diff --git a/spirit/managers/topic.py b/spirit/managers/topic.py index 7ec3be14..51c53664 100644 --- a/spirit/managers/topic.py +++ b/spirit/managers/topic.py @@ -52,7 +52,7 @@ class TopicQuerySet(models.QuerySet): return self.prefetch_related(prefetch) def get_public_or_404(self, pk, user): - if user.is_authenticated() and user.is_moderator: + if user.is_authenticated() and user.st.is_moderator: return get_object_or_404(self.public() .select_related('category__parent'), pk=pk) @@ -62,7 +62,7 @@ class TopicQuerySet(models.QuerySet): pk=pk) def for_update_or_404(self, pk, user): - if user.is_moderator: + if user.st.is_moderator: return get_object_or_404(self.public(), pk=pk) else: return get_object_or_404(self.visible().opened(), pk=pk, user=user) diff --git a/spirit/middleware.py b/spirit/middleware.py index df53fe34..4f568e87 100644 --- a/spirit/middleware.py +++ b/spirit/middleware.py @@ -10,6 +10,8 @@ from django.contrib.auth import logout from django.core.urlresolvers import resolve from django.contrib.auth.views import redirect_to_login +from .models.user import UserProfile + User = get_user_model() @@ -25,7 +27,7 @@ class TimezoneMiddleware(object): def process_request(self, request): if request.user.is_authenticated(): - timezone.activate(request.user.timezone) + timezone.activate(request.user.st.timezone) else: timezone.deactivate() @@ -38,10 +40,10 @@ class LastIPMiddleware(object): last_ip = request.META['REMOTE_ADDR'].strip() - if request.user.last_ip == last_ip: + if request.user.st.last_ip == last_ip: return - User.objects.filter(pk=request.user.pk)\ + UserProfile.objects.filter(user__pk=request.user.pk)\ .update(last_ip=last_ip) @@ -52,12 +54,12 @@ class LastSeenMiddleware(object): return threshold = settings.ST_USER_LAST_SEEN_THRESHOLD_MINUTES * 60 - delta = timezone.now() - request.user.last_seen + delta = timezone.now() - request.user.st.last_seen if delta.seconds < threshold: return - User.objects.filter(pk=request.user.pk)\ + UserProfile.objects.filter(user__pk=request.user.pk)\ .update(last_seen=timezone.now()) diff --git a/spirit/migrations/0006_auto_20150327_0204.py b/spirit/migrations/0006_auto_20150327_0204.py index da08de3d..6860e631 100644 --- a/spirit/migrations/0006_auto_20150327_0204.py +++ b/spirit/migrations/0006_auto_20150327_0204.py @@ -7,7 +7,9 @@ from django.conf import settings def verify_active_users(apps, schema_editor): User = apps.get_model(settings.AUTH_USER_MODEL) - User.objects.filter(is_active=True).update(is_verified=True) + + if hasattr(User, 'is_verified'): + User.objects.filter(is_active=True).update(is_verified=True) class Migration(migrations.Migration): diff --git a/spirit/migrations/0007_userprofile.py b/spirit/migrations/0007_userprofile.py new file mode 100644 index 00000000..0feacbf4 --- /dev/null +++ b/spirit/migrations/0007_userprofile.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import spirit.utils.models +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('spirit', '0006_auto_20150327_0204'), + ] + + operations = [ + migrations.CreateModel( + name='UserProfile', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False, verbose_name='ID', auto_created=True)), + ('slug', spirit.utils.models.AutoSlugField(db_index=False, blank=True, populate_from='user.username')), + ('location', models.CharField(blank=True, verbose_name='location', max_length=75)), + ('last_seen', models.DateTimeField(auto_now=True, verbose_name='last seen')), + ('last_ip', models.GenericIPAddressField(blank=True, verbose_name='last ip', null=True)), + ('timezone', models.CharField(max_length=32, verbose_name='time zone', choices=[('Etc/GMT+12', '(GMT -12:00) Eniwetok, Kwajalein'), ('Etc/GMT+11', '(GMT -11:00) Midway Island, Samoa'), ('Etc/GMT+10', '(GMT -10:00) Hawaii'), ('Pacific/Marquesas', '(GMT -9:30) Marquesas Islands'), ('Etc/GMT+9', '(GMT -9:00) Alaska'), ('Etc/GMT+8', '(GMT -8:00) Pacific Time (US & Canada)'), ('Etc/GMT+7', '(GMT -7:00) Mountain Time (US & Canada)'), ('Etc/GMT+6', '(GMT -6:00) Central Time (US & Canada), Mexico City'), ('Etc/GMT+5', '(GMT -5:00) Eastern Time (US & Canada), Bogota, Lima'), ('America/Caracas', '(GMT -4:30) Venezuela'), ('Etc/GMT+4', '(GMT -4:00) Atlantic Time (Canada), Caracas, La Paz'), ('Etc/GMT+3', '(GMT -3:00) Brazil, Buenos Aires, Georgetown'), ('Etc/GMT+2', '(GMT -2:00) Mid-Atlantic'), ('Etc/GMT+1', '(GMT -1:00) Azores, Cape Verde Islands'), ('UTC', '(GMT) Western Europe Time, London, Lisbon, Casablanca'), ('Etc/GMT-1', '(GMT +1:00) Brussels, Copenhagen, Madrid, Paris'), ('Etc/GMT-2', '(GMT +2:00) Kaliningrad, South Africa'), ('Etc/GMT-3', '(GMT +3:00) Baghdad, Riyadh, Moscow, St. Petersburg'), ('Etc/GMT-4', '(GMT +4:00) Abu Dhabi, Muscat, Baku, Tbilisi'), ('Asia/Kabul', '(GMT +4:30) Afghanistan'), ('Etc/GMT-5', '(GMT +5:00) Ekaterinburg, Islamabad, Karachi, Tashkent'), ('Asia/Kolkata', '(GMT +5:30) India, Sri Lanka'), ('Asia/Kathmandu', '(GMT +5:45) Nepal'), ('Etc/GMT-6', '(GMT +6:00) Almaty, Dhaka, Colombo'), ('Indian/Cocos', '(GMT +6:30) Cocos Islands, Myanmar'), ('Etc/GMT-7', '(GMT +7:00) Bangkok, Hanoi, Jakarta'), ('Etc/GMT-8', '(GMT +8:00) Beijing, Perth, Singapore, Hong Kong'), ('Australia/Eucla', '(GMT +8:45) Australia (Eucla)'), ('Etc/GMT-9', '(GMT +9:00) Tokyo, Seoul, Osaka, Sapporo, Yakutsk'), ('Australia/North', '(GMT +9:30) Australia (Northern Territory)'), ('Etc/GMT-10', '(GMT +10:00) Eastern Australia, Guam, Vladivostok'), ('Etc/GMT-11', '(GMT +11:00) Magadan, Solomon Islands, New Caledonia'), ('Pacific/Norfolk', '(GMT +11:30) Norfolk Island'), ('Etc/GMT-12', '(GMT +12:00) Auckland, Wellington, Fiji, Kamchatka')], default='UTC')), + ('is_administrator', models.BooleanField(verbose_name='administrator status', default=False)), + ('is_moderator', models.BooleanField(verbose_name='moderator status', default=False)), + ('is_verified', models.BooleanField(verbose_name='verified', help_text='Designates whether the user has verified his account by email or by other means. Un-select this to let the user activate his account.', default=False)), + ('topic_count', models.PositiveIntegerField(verbose_name='topic count', default=0)), + ('comment_count', models.PositiveIntegerField(verbose_name='comment count', default=0)), + ('user', models.OneToOneField(to=settings.AUTH_USER_MODEL, verbose_name='profile', related_name='st')), + ], + options={ + 'verbose_name': 'forum profile', + 'verbose_name_plural': 'forum profiles', + }, + bases=(models.Model,), + ), + ] diff --git a/spirit/models/user.py b/spirit/models/user.py index 1ff8bd4b..cdd3a1fe 100644 --- a/spirit/models/user.py +++ b/spirit/models/user.py @@ -11,11 +11,49 @@ from django.core.mail import send_mail from django.core import validators from django.utils.translation import ugettext_lazy as _ from django.utils import timezone +from django.conf import settings from spirit.utils.timezone import TIMEZONE_CHOICES from spirit.utils.models import AutoSlugField +class UserProfile(models.Model): + user = models.OneToOneField(settings.AUTH_USER_MODEL, verbose_name=_("profile"), related_name='st') + + slug = AutoSlugField(populate_from="user.username", db_index=False, blank=True) + location = models.CharField(_("location"), max_length=75, blank=True) + last_seen = models.DateTimeField(_("last seen"), auto_now=True) + last_ip = models.GenericIPAddressField(_("last ip"), blank=True, null=True) + timezone = models.CharField(_("time zone"), max_length=32, choices=TIMEZONE_CHOICES, default='UTC') + is_administrator = models.BooleanField(_('administrator status'), default=False) + is_moderator = models.BooleanField(_('moderator status'), default=False) + is_verified = models.BooleanField(_('verified'), default=False, + help_text=_('Designates whether the user has verified his ' + 'account by email or by other means. Un-select this ' + 'to let the user activate his account.')) + + topic_count = models.PositiveIntegerField(_("topic count"), default=0) + comment_count = models.PositiveIntegerField(_("comment count"), default=0) + + class Meta: + verbose_name = _("forum profile") + verbose_name_plural = _("forum profiles") + + def save(self, *args, **kwargs): + if self.user.is_superuser: + self.is_administrator = True + + if self.is_administrator: + self.is_moderator = True + + super(UserProfile, self).save(*args, **kwargs) + + def get_absolute_url(self): + return reverse('spirit:profile-detail', kwargs={'pk': self.pk, + 'slug': self.slug}) + + + class AbstractForumUser(models.Model): slug = AutoSlugField(populate_from="username", db_index=False, blank=True) location = models.CharField(_("location"), max_length=75, blank=True) @@ -46,12 +84,6 @@ class AbstractForumUser(models.Model): class AbstractUser(AbstractBaseUser, PermissionsMixin, AbstractForumUser): - # almost verbatim copy from the auth user model - # adds email(unique=True, blank=False, max_length=254) - - # TODO: Django 1.8 sets email to max_length=254, so there is no point in keeping this, - # uniqueness can be checked at app level, although it's better to have a db index (for login) - # this should be change to the good old OneToOneField. username = models.CharField(_("username"), max_length=30, unique=True, db_index=True, help_text=_('Required. 30 characters or fewer. Letters, numbers and ' '@/./+/-/_ characters'), diff --git a/spirit/settings.py b/spirit/settings.py index 1b0caf67..af3017a6 100644 --- a/spirit/settings.py +++ b/spirit/settings.py @@ -55,8 +55,6 @@ CACHES = { }, } -AUTH_USER_MODEL = 'spirit.User' - AUTHENTICATION_BACKENDS = ( 'django.contrib.auth.backends.ModelBackend', 'spirit.backends.user.EmailAuthBackend', diff --git a/spirit/signals/handlers/__init__.py b/spirit/signals/handlers/__init__.py index bcae9a39..ba2f0a4d 100644 --- a/spirit/signals/handlers/__init__.py +++ b/spirit/signals/handlers/__init__.py @@ -7,3 +7,4 @@ from . import topic from . import topic_notification from . import topic_poll from . import topic_unread +from . import user \ No newline at end of file diff --git a/spirit/signals/handlers/user.py b/spirit/signals/handlers/user.py new file mode 100644 index 00000000..f8043f59 --- /dev/null +++ b/spirit/signals/handlers/user.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- + +from __future__ import unicode_literals + +from django.db.models.signals import post_save +from django.contrib.auth import get_user_model + +from ...models.user import UserProfile + +User = get_user_model() + + +def update_or_create_user_profile(sender, instance, created, **kwargs): + user = instance + + if created: + UserProfile.objects.create(user=user) + else: + user.st.save() + +post_save.connect(update_or_create_user_profile, sender=User, dispatch_uid=__name__) \ No newline at end of file diff --git a/spirit/templates/spirit/_base.html b/spirit/templates/spirit/_base.html index e54be366..5f4bb611 100644 --- a/spirit/templates/spirit/_base.html +++ b/spirit/templates/spirit/_base.html @@ -36,7 +36,7 @@ {% endif %} - {% if user.is_moderator %} + {% if user.st.is_moderator %} {% endif %} diff --git a/spirit/templates/spirit/_header.html b/spirit/templates/spirit/_header.html index 5ae239a3..7946e63a 100644 --- a/spirit/templates/spirit/_header.html +++ b/spirit/templates/spirit/_header.html @@ -17,12 +17,12 @@
@@ -73,7 +73,7 @@
- {{ f.user.username }} + {{ f.user.username }}
diff --git a/spirit/templates/spirit/admin/user/user_edit.html b/spirit/templates/spirit/admin/user/user_edit.html index db31eaef..50633ca0 100644 --- a/spirit/templates/spirit/admin/user/user_edit.html +++ b/spirit/templates/spirit/admin/user/user_edit.html @@ -10,6 +10,7 @@
{% csrf_token %} + {% include "spirit/_form.html" with form=uform %} {% include "spirit/_form.html" %} diff --git a/spirit/templates/spirit/comment/_render_list.html b/spirit/templates/spirit/comment/_render_list.html index b73b99e7..7e9507a0 100644 --- a/spirit/templates/spirit/comment/_render_list.html +++ b/spirit/templates/spirit/comment/_render_list.html @@ -17,7 +17,7 @@
- {{ c.user.username }}{{ c.user.get_full_name }} + {{ c.user.username }}{{ c.user.get_full_name }}
    @@ -43,7 +43,7 @@ {% if not c.action %}
      {% if user.is_authenticated %} - {% if user.is_moderator %} + {% if user.st.is_moderator %}
    • {% trans "delete" %}
    • {% endif %} @@ -63,7 +63,7 @@ {% endifnotequal %} {% endif %} - {% if user.is_moderator or c.user.pk == user.pk %} + {% if user.st.is_moderator or c.user.pk == user.pk %}
    • {% trans "edit" %}
    • {% endif %} @@ -97,14 +97,14 @@
      - {% if user.is_moderator %} + {% if user.st.is_moderator %} {{ c.comment_html|safe }} {% else %} {% trans "This comment was deleted" %}. @@ -113,7 +113,7 @@
      - {% if user.is_moderator %} + {% if user.st.is_moderator %} diff --git a/spirit/templates/spirit/comment_history/detail.html b/spirit/templates/spirit/comment_history/detail.html index e11fa792..96686aeb 100644 --- a/spirit/templates/spirit/comment_history/detail.html +++ b/spirit/templates/spirit/comment_history/detail.html @@ -24,7 +24,7 @@
        diff --git a/spirit/templates/spirit/topic/topic_detail.html b/spirit/templates/spirit/topic/topic_detail.html index 221b27b0..0b5b40ee 100644 --- a/spirit/templates/spirit/topic/topic_detail.html +++ b/spirit/templates/spirit/topic/topic_detail.html @@ -26,14 +26,14 @@ {{ topic.title }} - {% if user.is_moderator %} + {% if user.st.is_moderator %} {% trans "edit" %} {% elif user.pk == topic.user.pk and not topic.is_closed %} {% trans "edit" %} {% endif %} - {% if user.is_moderator %} + {% if user.st.is_moderator %}
        @@ -120,7 +120,7 @@ } ); - {% if user.is_moderator %} + {% if user.st.is_moderator %} $('.js-show-move-comments').move_comments( { csrfToken: "{{ csrf_token }}", target: "{% url "spirit:comment-move" topic.pk %}", diff --git a/spirit/templates/spirit/topic_notification/_render_list.html b/spirit/templates/spirit/topic_notification/_render_list.html index 26075756..198dcdea 100644 --- a/spirit/templates/spirit/topic_notification/_render_list.html +++ b/spirit/templates/spirit/topic_notification/_render_list.html @@ -2,7 +2,7 @@ {% for n in notifications %}
        - {% url "spirit:profile-detail" pk=n.comment.user.pk slug=n.comment.user.slug as url_profile %} + {% url "spirit:profile-detail" pk=n.comment.user.pk slug=n.comment.user.st.slug as url_profile %} {% url "spirit:comment-find" pk=n.comment.pk as url_topic %} {% if n.is_comment %} diff --git a/spirit/templates/spirit/user/_profile.html b/spirit/templates/spirit/user/_profile.html index 8e67be4d..9d810d91 100644 --- a/spirit/templates/spirit/user/_profile.html +++ b/spirit/templates/spirit/user/_profile.html @@ -14,13 +14,13 @@
      • {% trans "Seen" %}
        -
        {{ p_user.last_seen|shortnaturaltime }}
        +
        {{ p_user.st.last_seen|shortnaturaltime }}
      • - {% if user.is_administrator %} + {% if user.st.is_administrator %}
      • {% trans "Last IP" %}
        -
        {{ p_user.last_ip }}
        +
        {{ p_user.st.last_ip }}
      • {% endif %}
      @@ -36,14 +36,14 @@
      {% endifequal %} - {% if user.is_administrator %} + {% if user.st.is_administrator %} {% endif %} \ No newline at end of file diff --git a/spirit/templates/spirit/user/_render_comments_list.html b/spirit/templates/spirit/user/_render_comments_list.html index 430c53be..ed29bad5 100644 --- a/spirit/templates/spirit/user/_render_comments_list.html +++ b/spirit/templates/spirit/user/_render_comments_list.html @@ -15,7 +15,7 @@
        diff --git a/spirit/templates/spirit/user/menu.html b/spirit/templates/spirit/user/menu.html index 5a8c199c..6a121200 100644 --- a/spirit/templates/spirit/user/menu.html +++ b/spirit/templates/spirit/user/menu.html @@ -11,12 +11,12 @@

        {% trans "Menu" %}