From 090302676df2cee34b528e1cc6a4d8768a07c891 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sun, 11 Jan 2015 17:38:47 -0400 Subject: [PATCH] Implement GUI language and timezone as user preferences, issue #114 --- mayan/apps/common/__init__.py | 28 +++++- mayan/apps/common/forms.py | 15 ++++ mayan/apps/common/links.py | 3 + mayan/apps/common/middleware/timezone.py | 16 ++++ mayan/apps/common/models.py | 19 +++- .../0009_auto__add_userlocaleprofile.py | 90 +++++++++++++++++++ .../0010_create_locale_profiles.py | 90 +++++++++++++++++++ mayan/apps/common/urls.py | 3 + mayan/apps/common/views.py | 50 ++++++++++- mayan/apps/main/templates/main/base.html | 14 +-- mayan/settings/base.py | 1 + 11 files changed, 311 insertions(+), 18 deletions(-) create mode 100644 mayan/apps/common/middleware/timezone.py create mode 100644 mayan/apps/common/south_migrations/0009_auto__add_userlocaleprofile.py create mode 100644 mayan/apps/common/south_migrations/0010_create_locale_profiles.py diff --git a/mayan/apps/common/__init__.py b/mayan/apps/common/__init__.py index 66faa8b727..6027795d9c 100644 --- a/mayan/apps/common/__init__.py +++ b/mayan/apps/common/__init__.py @@ -3,27 +3,33 @@ from __future__ import absolute_import import logging import tempfile +from django.conf import settings from django.contrib.auth import models as auth_models from django.contrib.auth.models import User from django.db.models.signals import post_save from django.dispatch import receiver +from django.contrib.auth.signals import user_logged_in + from south.signals import post_migrate from navigation.api import register_links, register_top_menu from common import settings as common_settings from .links import (link_about, link_current_user_details, - link_current_user_edit, link_license, + link_current_user_edit, + link_current_user_locale_profile_details, + link_current_user_locale_profile_edit, link_license, link_password_change) -from .models import AnonymousUserSingleton, AutoAdminSingleton +from .models import (AnonymousUserSingleton, AutoAdminSingleton, + UserLocaleProfile) from .settings import (AUTO_ADMIN_USERNAME, AUTO_ADMIN_PASSWORD, AUTO_CREATE_ADMIN, TEMPORARY_DIRECTORY) from .utils import validate_path logger = logging.getLogger(__name__) -register_links(['common:current_user_details', 'common:current_user_edit', 'common:password_change_view'], [link_current_user_details, link_current_user_edit, link_password_change], menu_name='secondary_menu') +register_links(['common:current_user_details', 'common:current_user_edit', 'common:current_user_locale_profile_details', 'common:current_user_locale_profile_edit', 'common:password_change_view'], [link_current_user_details, link_current_user_edit, link_current_user_locale_profile_details, link_current_user_locale_profile_edit, link_password_change], menu_name='secondary_menu') register_links(['common:about_view', 'common:license_view', 'registration:form_view'], [link_about, link_license], menu_name='secondary_menu') register_top_menu('about', link_about, position=-1) @@ -73,5 +79,21 @@ def auto_admin_account_passwd_change(sender, instance, **kwargs): auto_admin_properties.save() +@receiver(user_logged_in, dispatch_uid='user_locale_profile_session_config', sender=User) +def user_locale_profile_session_config(sender, request, user, **kwargs): + if hasattr(request, 'session'): + user_locale_profile, created = UserLocaleProfile.objects.get_or_create(user=user) + request.session['django_language'] = user_locale_profile.language + request.session['django_timezone'] = user_locale_profile.timezone + else: + request.set_cookie(settings.LANGUAGE_COOKIE_NAME, user_locale_profile.language) + + +@receiver(post_save, dispatch_uid='user_locale_profile_create', sender=User) +def user_locale_profile_create(sender, instance, created, **kwargs): + if created: + UserLocaleProfile.objects.create(user=instance) + + if (not validate_path(TEMPORARY_DIRECTORY)) or (not TEMPORARY_DIRECTORY): setattr(common_settings, 'TEMPORARY_DIRECTORY', tempfile.mkdtemp()) diff --git a/mayan/apps/common/forms.py b/mayan/apps/common/forms.py index 2068d6b487..8659615490 100644 --- a/mayan/apps/common/forms.py +++ b/mayan/apps/common/forms.py @@ -11,6 +11,7 @@ from django.db import models from django.utils.html import escape from django.utils.translation import ugettext_lazy as _ +from .models import UserLocaleProfile from .utils import return_attrib from .widgets import DetailSelectMultiple, EmailInput, PlainWidget @@ -107,6 +108,7 @@ class UserForm_view(DetailForm): """ Form used to display an user's public details """ + class Meta: model = User fields = ('username', 'first_name', 'last_name', 'email', 'is_staff', 'is_superuser', 'last_login', 'date_joined', 'groups') @@ -116,11 +118,24 @@ class UserForm(forms.ModelForm): """ Form used to edit an user's mininal fields by the user himself """ + class Meta: model = User fields = ('username', 'first_name', 'last_name', 'email') +class LocaleProfileForm(forms.ModelForm): + class Meta: + model = UserLocaleProfile + fields = ('language', 'timezone') + + +class LocaleProfileForm_view(DetailForm): + class Meta: + model = UserLocaleProfile + fields = ('language', 'timezone') + + class EmailAuthenticationForm(forms.Form): """ A form to use email address authentication diff --git a/mayan/apps/common/links.py b/mayan/apps/common/links.py index 70669d0a36..69d2131a10 100644 --- a/mayan/apps/common/links.py +++ b/mayan/apps/common/links.py @@ -13,3 +13,6 @@ link_current_user_edit = {'text': _(u'Edit details'), 'view': 'common:current_us link_about = {'text': _(u'About'), 'view': 'common:about_view', 'famfam': 'information'} link_license = {'text': _(u'License'), 'view': 'common:license_view', 'famfam': 'script'} + +link_current_user_locale_profile_details = {'text': _(u'Locale profile'), 'view': 'common:current_user_locale_profile_details', 'famfam': 'world'} +link_current_user_locale_profile_edit = {'text': _(u'Edit locale profile'), 'view': 'common:current_user_locale_profile_edit', 'famfam': 'world_edit'} diff --git a/mayan/apps/common/middleware/timezone.py b/mayan/apps/common/middleware/timezone.py new file mode 100644 index 0000000000..e8208f628e --- /dev/null +++ b/mayan/apps/common/middleware/timezone.py @@ -0,0 +1,16 @@ +import pytz + +from django.utils import timezone + + +class TimezoneMiddleware(object): + def process_request(self, request): + if hasattr(request, 'session'): + tzname = request.session.get('django_timezone') + if tzname: + timezone.activate(pytz.timezone(tzname)) + else: + timezone.deactivate() + else: + # TODO: Cookie, browser based timezone + timezone.deactivate() diff --git a/mayan/apps/common/models.py b/mayan/apps/common/models.py index 8eaa6c3de8..7f4102e3f8 100644 --- a/mayan/apps/common/models.py +++ b/mayan/apps/common/models.py @@ -1,9 +1,12 @@ from __future__ import absolute_import +from pytz import common_timezones + +from django.conf import settings +from django.contrib.auth.models import User from django.db import models from django.utils.translation import ugettext from django.utils.translation import ugettext_lazy as _ -from django.contrib.auth.models import User from solo.models import SingletonModel @@ -52,3 +55,17 @@ class SharedUploadedFile(models.Model): def delete(self, *args, **kwargs): self.file.storage.delete(self.file.path) return super(SharedUploadedFile, self).delete(*args, **kwargs) + + +class UserLocaleProfile(models.Model): + user = models.OneToOneField(User, related_name='locale_profile', verbose_name=_('User')) + + timezone = models.CharField(choices=zip(common_timezones, common_timezones), max_length=48, verbose_name=_('Timezone')) + language = models.CharField(choices=settings.LANGUAGES, max_length=8, verbose_name=_('Language')) + + def __unicode__(self): + return unicode(self.user) + + class Meta: + verbose_name = _(u'User locale profile') + verbose_name_plural = _(u'User locale profiles') diff --git a/mayan/apps/common/south_migrations/0009_auto__add_userlocaleprofile.py b/mayan/apps/common/south_migrations/0009_auto__add_userlocaleprofile.py new file mode 100644 index 0000000000..57d95f38f8 --- /dev/null +++ b/mayan/apps/common/south_migrations/0009_auto__add_userlocaleprofile.py @@ -0,0 +1,90 @@ +# -*- 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 model 'UserLocaleProfile' + db.create_table(u'common_userlocaleprofile', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('user', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['auth.User'], unique=True)), + ('timezone', self.gf('django.db.models.fields.CharField')(max_length=48)), + ('language', self.gf('django.db.models.fields.CharField')(max_length=8)), + )) + db.send_create_signal(u'common', ['UserLocaleProfile']) + + + def backwards(self, orm): + # Deleting model 'UserLocaleProfile' + db.delete_table(u'common_userlocaleprofile') + + + models = { + 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'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + '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_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + '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', [], {'unique': 'True', 'max_length': '30'}) + }, + u'common.anonymoususersingleton': { + 'Meta': {'object_name': 'AnonymousUserSingleton'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + u'common.autoadminsingleton': { + 'Meta': {'object_name': 'AutoAdminSingleton'}, + 'account': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'auto_admin_account'", 'null': 'True', 'to': u"orm['auth.User']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), + 'password_hash': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}) + }, + u'common.shareduploadedfile': { + 'Meta': {'object_name': 'SharedUploadedFile'}, + 'datatime': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}), + 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + u'common.userlocaleprofile': { + 'Meta': {'object_name': 'UserLocaleProfile'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'language': ('django.db.models.fields.CharField', [], {'max_length': '8'}), + 'timezone': ('django.db.models.fields.CharField', [], {'max_length': '48'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True'}) + }, + 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'}) + } + } + + complete_apps = ['common'] \ No newline at end of file diff --git a/mayan/apps/common/south_migrations/0010_create_locale_profiles.py b/mayan/apps/common/south_migrations/0010_create_locale_profiles.py new file mode 100644 index 0000000000..70c80cd5f3 --- /dev/null +++ b/mayan/apps/common/south_migrations/0010_create_locale_profiles.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models + + +class Migration(DataMigration): + + def forwards(self, orm): + "Write your forwards methods here." + # Note: Don't use "from appname.models import ModelName". + # Use orm.ModelName to refer to models in this application, + # and orm['appname.ModelName'] for models in other applications. + + for user in orm['auth.user'].objects.all(): + try: + orm.UserLocaleProfile.objects.create(user=user) + except Exception: + pass + + def backwards(self, orm): + "Write your backwards methods here." + + models = { + 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'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + '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_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + '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', [], {'unique': 'True', 'max_length': '30'}) + }, + u'common.anonymoususersingleton': { + 'Meta': {'object_name': 'AnonymousUserSingleton'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + u'common.autoadminsingleton': { + 'Meta': {'object_name': 'AutoAdminSingleton'}, + 'account': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'auto_admin_account'", 'null': 'True', 'to': u"orm['auth.User']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), + 'password_hash': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}) + }, + u'common.shareduploadedfile': { + 'Meta': {'object_name': 'SharedUploadedFile'}, + 'datatime': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}), + 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + u'common.userlocaleprofile': { + 'Meta': {'object_name': 'UserLocaleProfile'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'language': ('django.db.models.fields.CharField', [], {'max_length': '8'}), + 'timezone': ('django.db.models.fields.CharField', [], {'max_length': '48'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'locale_profile'", 'unique': 'True', 'to': u"orm['auth.User']"}) + }, + 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'}) + } + } + + complete_apps = ['common', 'auth', 'common'] + symmetrical = True diff --git a/mayan/apps/common/urls.py b/mayan/apps/common/urls.py index de414d6e9a..1722167590 100644 --- a/mayan/apps/common/urls.py +++ b/mayan/apps/common/urls.py @@ -12,6 +12,9 @@ urlpatterns = patterns('common.views', url(r'^user/$', 'current_user_details', (), name='current_user_details'), url(r'^user/edit/$', 'current_user_edit', (), name='current_user_edit'), + url(r'^user/locale/$', 'current_user_locale_profile_details', (), name='current_user_locale_profile_details'), + url(r'^user/locale/edit/$', 'current_user_locale_profile_edit', (), name='current_user_locale_profile_edit'), + url(r'^login/$', 'login_view', (), name='login_view'), url(r'^password/change/$', 'password_change_view', (), name='password_change_view'), ) diff --git a/mayan/apps/common/views.py b/mayan/apps/common/views.py index 9564d3f038..586fae3c4a 100644 --- a/mayan/apps/common/views.py +++ b/mayan/apps/common/views.py @@ -22,7 +22,8 @@ from acls.models import AccessEntry from permissions.models import Permission from .forms import (ChoiceForm, EmailAuthenticationForm, LicenseForm, - UserForm, UserForm_view) + LocaleProfileForm, LocaleProfileForm_view, UserForm, + UserForm_view) from .settings import LOGIN_METHOD @@ -173,6 +174,21 @@ def current_user_details(request): context_instance=RequestContext(request)) +def current_user_locale_profile_details(request): + """ + Display the current user's locale profile details + """ + form = LocaleProfileForm_view(instance=request.user.locale_profile) + + return render_to_response( + 'main/generic_form.html', { + 'form': form, + 'title': _(u'Current user locale profile details'), + 'read_only': True, + }, + context_instance=RequestContext(request)) + + def current_user_edit(request): """ Allow an user to edit his own details @@ -201,6 +217,38 @@ def current_user_edit(request): context_instance=RequestContext(request)) +def current_user_locale_profile_edit(request): + """ + Allow an user to edit his own locale profile + """ + + next = request.POST.get('next', request.GET.get('next', request.META.get('HTTP_REFERER', reverse('common:current_user_locale_profile_details')))) + + if request.method == 'POST': + form = LocaleProfileForm(instance=request.user.locale_profile, data=request.POST) + if form.is_valid(): + form.save() + + if hasattr(request, 'session'): + request.session['django_language'] = form.cleaned_data['language'] + request.session['django_timezone'] = form.cleaned_data['timezone'] + else: + request.set_cookie(settings.LANGUAGE_COOKIE_NAME, form.cleaned_data['language']) + + messages.success(request, _(u'Current user\'s locale profile details updated.')) + return HttpResponseRedirect(next) + else: + form = LocaleProfileForm(instance=request.user.locale_profile) + + return render_to_response( + 'main/generic_form.html', { + 'form': form, + 'next': next, + 'title': _(u'Edit current user locale profile details'), + }, + context_instance=RequestContext(request)) + + def login_view(request): """ Control how the use is to be authenticated, options are 'email' and diff --git a/mayan/apps/main/templates/main/base.html b/mayan/apps/main/templates/main/base.html index 9d59813dc3..4bffb54c86 100644 --- a/mayan/apps/main/templates/main/base.html +++ b/mayan/apps/main/templates/main/base.html @@ -214,7 +214,7 @@
diff --git a/mayan/settings/base.py b/mayan/settings/base.py index 82a1cb782c..07c209443b 100644 --- a/mayan/settings/base.py +++ b/mayan/settings/base.py @@ -109,6 +109,7 @@ MIDDLEWARE_CLASSES = ( 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.locale.LocaleMiddleware', + 'common.middleware.timezone.TimezoneMiddleware', 'common.middleware.strip_spaces_widdleware.SpacelessMiddleware', 'common.middleware.login_required_middleware.LoginRequiredMiddleware', 'permissions.middleware.permission_denied_middleware.PermissionDeniedMiddleware',