diff --git a/CHANGELOG.md b/CHANGELOG.md index c10a33f..c3e5076 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## Upcoming + +* Add `TimeZoneMixin` for custom `User` models. + ## v14.0.0 * Clarify error message when your old and new passwords match, you will need to update translations. diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..0eae09c --- /dev/null +++ b/manage.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "user_management.tests.settings") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/requirements.txt b/requirements.txt index 8771cdb..8545813 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ coverage==3.7.1 dj-database-url==0.3.0 dj-inmemorystorage==1.3.0 factory_boy==2.5.2 -flake8==2.3.0 +flake8==2.5.0 flake8-import-order==0.5.1 incuna-test-utils==6.0.0 mkdocs==0.11.1 diff --git a/user_management/models/mixins.py b/user_management/models/mixins.py index bd3dd90..1725336 100644 --- a/user_management/models/mixins.py +++ b/user_management/models/mixins.py @@ -1,3 +1,4 @@ +import pytz from django.contrib.auth.models import BaseUserManager from django.contrib.auth.tokens import default_token_generator from django.contrib.sites.models import Site @@ -9,6 +10,7 @@ from django.utils.translation import ugettext_lazy as _ from user_management.utils import notifications +from .utils import timezone_choices class UserManager(BaseUserManager): @@ -55,6 +57,16 @@ class Meta: abstract = True +class TimeZoneMixin(models.Model): + timezone = models.CharField( + max_length=255, + choices=timezone_choices(pytz.common_timezones), + ) + + class Meta: + abstract = True + + class EmailUserMixin(models.Model): email = models.EmailField( verbose_name=_('Email address'), diff --git a/user_management/models/tests/models.py b/user_management/models/tests/models.py index 1bb2221..24f1f7c 100644 --- a/user_management/models/tests/models.py +++ b/user_management/models/tests/models.py @@ -11,20 +11,23 @@ EmailVerifyUserMixin, IsStaffUserMixin, NameUserMethodsMixin, + TimeZoneMixin, VerifyEmailMixin, ) -class User(AvatarMixin, VerifyEmailMixin, PermissionsMixin, AbstractBaseUser): - pass +class User( + AvatarMixin, TimeZoneMixin, VerifyEmailMixin, PermissionsMixin, + AbstractBaseUser): + """A User model using all the custom mixins.""" class BasicUser(BasicUserFieldsMixin, AbstractBaseUser): - pass + """A User model using just the BasicUserFieldsMixin.""" class VerifyEmailUser(VerifyEmailMixin, AbstractBaseUser): - pass + """A User model using just the VerifyEmailMixin.""" class CustomVerifyEmailUser(VerifyEmailMixin, AbstractBaseUser): @@ -35,6 +38,12 @@ class CustomVerifyEmailUser(VerifyEmailMixin, AbstractBaseUser): class CustomBasicUserFieldsMixin( NameUserMethodsMixin, EmailUserMixin, DateJoinedUserMixin, IsStaffUserMixin): + """ + A replacement for BasicUserFieldsMixin with a custom name field. + + Uses NameUserMethodsMixin instead of NameUserMixin. + """ + name = models.TextField() USERNAME_FIELD = 'email' @@ -46,4 +55,4 @@ class Meta: class CustomNameUser( AvatarMixin, EmailVerifyUserMixin, CustomBasicUserFieldsMixin, AbstractBaseUser): - pass + """A User model using the CustomBasicUserFieldsMixin.""" diff --git a/user_management/models/tests/test_models.py b/user_management/models/tests/test_models.py index 23d6b2f..cecd9d1 100644 --- a/user_management/models/tests/test_models.py +++ b/user_management/models/tests/test_models.py @@ -48,6 +48,7 @@ def test_fields(self): 'last_login', 'password', 'avatar', + 'timezone', # Incoming 'groups', # Django permission groups diff --git a/user_management/models/tests/test_utils.py b/user_management/models/tests/test_utils.py new file mode 100644 index 0000000..03b7ad8 --- /dev/null +++ b/user_management/models/tests/test_utils.py @@ -0,0 +1,53 @@ +from unittest import TestCase + +from ..utils import timezone_choices + + +class TestTimeZoneChoices(TestCase): + timezones = [ + 'Arctic/Longyearbyen', + 'Asia/Qyzylorda', + 'America/Argentina/Ushuaia', + 'America/Iqaluit', + 'GMT', + 'Indian/Christmas', + 'UTC', + ] + + def setUp(self): + self.choices = timezone_choices(self.timezones) + + def test_exclude_GMT_UTC(self): + self.assertNotIn('GMT', self.choices) + self.assertNotIn('UTC', self.choices) + + def test_choice_groups(self): + expected_choices = ( + ( + 'Arctic', + ( + ('Arctic/Longyearbyen', 'Longyearbyen'), + ), + ), + ( + 'Asia', + ( + ('Asia/Qyzylorda', 'Qyzylorda'), + ), + ), + ( + 'America', + ( + ('America/Argentina/Ushuaia', 'Argentina/Ushuaia'), + ('America/Iqaluit', 'Iqaluit'), + ), + ), + ( + 'Indian', + ( + ('Indian/Christmas', 'Christmas'), + ), + ), + ) + + self.assertEqual(self.choices, expected_choices) diff --git a/user_management/models/utils.py b/user_management/models/utils.py new file mode 100644 index 0000000..6fb0b76 --- /dev/null +++ b/user_management/models/utils.py @@ -0,0 +1,20 @@ +from collections import defaultdict, OrderedDict + + +class DefaultOrderedDict(OrderedDict, defaultdict): + def __init__(self, default_factory=None, *args, **kwargs): + super().__init__(*args, **kwargs) + self.default_factory = default_factory + + +def timezone_choices(timezones, exclude=('GMT', 'UTC')): + choices = DefaultOrderedDict(list) + + for timezone in timezones: + if timezone in exclude: + continue + + continent, _sep, town = timezone.partition('/') + choices[continent].append((timezone, town)) + + return tuple((c, tuple(t)) for c, t in choices.items()) diff --git a/user_management/tests/settings.py b/user_management/tests/settings.py new file mode 100644 index 0000000..4d12821 --- /dev/null +++ b/user_management/tests/settings.py @@ -0,0 +1,35 @@ +SECRET_KEY = 'not-for-production' +DEBUG = True + +ALLOWED_HOSTS = [] + +INSTALLED_APPS = ( + # Put contenttypes before auth to work around test issue. + # See: https://code.djangoproject.com/ticket/10827#comment:12 + 'django.contrib.sites', + 'django.contrib.contenttypes', + 'django.contrib.auth', + 'django.contrib.sessions', + 'django.contrib.admin', + + 'rest_framework.authtoken', + + # Added for templates + 'user_management.api', + 'user_management.models.tests', +) + +AUTH_USER_MODEL = 'tests.User' + +MIDDLEWARE_CLASSES = () + +LANGUAGE_CODE = 'en-us' +TIME_ZONE = 'UTC' +USE_I18N = True +USE_L10N = True +USE_TZ = True + +MIGRATION_MODULES = { + 'api': 'user_management.tests.testmigrations.api', + 'tests': 'user_management.tests.testmigrations.tests', +} diff --git a/user_management/tests/testmigrations/tests/0002_user_timezone.py b/user_management/tests/testmigrations/tests/0002_user_timezone.py new file mode 100644 index 0000000..4a7168e --- /dev/null +++ b/user_management/tests/testmigrations/tests/0002_user_timezone.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tests', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='timezone', + field=models.CharField(default='UTC', max_length=255), + ), + ] diff --git a/user_management/tests/testmigrations/tests/0003_user_timezone_choices.py b/user_management/tests/testmigrations/tests/0003_user_timezone_choices.py new file mode 100644 index 0000000..a835076 --- /dev/null +++ b/user_management/tests/testmigrations/tests/0003_user_timezone_choices.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import pytz +from django.db import migrations, models +from user_management.models.utils import timezone_choices + + +class Migration(migrations.Migration): + + dependencies = [ + ('tests', '0002_user_timezone'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='timezone', + field=models.CharField(choices=timezone_choices(pytz.common_timezones), max_length=255), + ), + ]