├── .coveragerc ├── .gitignore ├── AUTHORS ├── LICENSE ├── README.rst ├── customuser ├── __init__.py ├── accounts │ ├── __init__.py │ ├── admin.py │ ├── forms.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ └── models.py ├── settings.py ├── testing.py ├── urls.py └── wsgi.py ├── dev-requirements.txt ├── manage.py ├── requirements.txt ├── setup.cfg └── tests ├── __init__.py ├── factories.py ├── test_simple_assert.py ├── test_user_custom_model.py └── test_user_custom_model_forms.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = customuser/ 3 | omit = '*tests*,*commands*,*migrations*,*admin*,*wsgi*' 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python bytecode 2 | *.py[co] 3 | 4 | # Packages 5 | *.egg* 6 | dist 7 | eggs 8 | parts 9 | var 10 | sdist 11 | develop-eggs 12 | .installed.cfg 13 | 14 | # Installer logs 15 | pip-log.txt 16 | 17 | # Unit test / coverage reports 18 | .coverage 19 | .tox 20 | 21 | # Translations 22 | *.mo 23 | 24 | # SQLite3 database files: 25 | *.db 26 | *.sqlite3 27 | 28 | # Django 29 | *.log 30 | *.pot 31 | 32 | # Sphinx docs: 33 | build 34 | 35 | # Mac OS X specific 36 | .DS_Store 37 | 38 | # VirtualEnv 39 | venv/ 40 | .direnv 41 | 42 | # Cache 43 | .cache 44 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Simon Luijk <@simonluijk> 2 | Aniket Maithani <@aniketmaithani> 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2013 Jonathan Chu 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 19 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 20 | OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ========================== 2 | Django Custom User Example 3 | ========================== 4 | 5 | This is an example Django project that demonstrates the configurable User model available in Django 1.5. It is inspired by `Dr. Russell Keith-Magee's `_ talk at DjangoCon US 2013 `Red User, Blue User, MyUser, auth.User `_. 6 | 7 | Getting Started 8 | --------------- 9 | Clone this repository: 10 | :: 11 | 12 | $ git clone https://github.com/jonathanchu/django-custom-user-example.git 13 | $ cd django-custom-user-example 14 | 15 | Create a virtual environment for this project and install Django (1.5.4+ recommended): 16 | :: 17 | 18 | $ mkvirtualenv customuser 19 | (customuser) $ pip install django 20 | 21 | Run `syncdb` or `migrate` (depending on your Django version) and create a superuser when prompted: 22 | (Django < 1.9) 23 | :: 24 | 25 | (customuser) $ python manage.py syncdb 26 | ... 27 | 28 | (Django 1.9+) 29 | :: 30 | 31 | (customuser) $ python manage.py migrate 32 | ... 33 | (customuser) $ python manage.py createsuperuser 34 | ... 35 | 36 | Run `runserver`: 37 | :: 38 | 39 | (customuser) $ python manage.py runserver 40 | 41 | 42 | 43 | Finally, open up http://127.0.0.1:8000/admin in your browser and login with the superuser just created. You should see your custom user under "Accounts". 44 | 45 | Screenshots 46 | ----------- 47 | 48 | .. image:: http://i.imgur.com/As2xDEg.png 49 | .. image:: http://i.imgur.com/uaG4qaH.png 50 | 51 | 52 | Running Tests Locally 53 | ----------- 54 | - Tests have been added to this sample project. You can install these tests from 55 | `dev-requirements`. 56 | - `Py.Test` has been integrated with the test suite. In order to run these tests just run the following command 57 | `py.test` 58 | 59 | - In case you want to see your test coverage, just run `py.test --cov` . 60 | 61 | Comments/Feedback 62 | ----------------- 63 | 64 | Suggestions for any modifications, please feel free to fork and contribute! 65 | 66 | Please file bugs at `https://github.com/jonathanchu/django-custom-user-example/issues `_. 67 | -------------------------------------------------------------------------------- /customuser/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanchu/django-custom-user-example/a80e47b2c2f7113feadaa60253adac63f8afe3b4/customuser/__init__.py -------------------------------------------------------------------------------- /customuser/accounts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanchu/django-custom-user-example/a80e47b2c2f7113feadaa60253adac63f8afe3b4/customuser/accounts/__init__.py -------------------------------------------------------------------------------- /customuser/accounts/admin.py: -------------------------------------------------------------------------------- 1 | from django.db import transaction 2 | from django.conf import settings 3 | from django.contrib import admin 4 | from django.contrib.auth.forms import AdminPasswordChangeForm 5 | from django.contrib import messages 6 | from django.core.exceptions import PermissionDenied 7 | from django.http import HttpResponseRedirect, Http404 8 | from django.shortcuts import get_object_or_404 9 | from django.template.response import TemplateResponse 10 | from django.utils.html import escape 11 | from django.utils.decorators import method_decorator 12 | from django.utils.translation import ugettext, ugettext_lazy as _ 13 | from django.views.decorators.csrf import csrf_protect 14 | from django.views.decorators.debug import sensitive_post_parameters 15 | 16 | from .forms import CustomUserChangeForm, CustomUserCreationForm 17 | from .models import CustomUser 18 | 19 | csrf_protect_m = method_decorator(csrf_protect) 20 | sensitive_post_parameters_m = method_decorator(sensitive_post_parameters()) 21 | 22 | 23 | class CustomUserAdmin(admin.ModelAdmin): 24 | """ 25 | The default UserAdmin class, but with changes for our CustomUser 26 | where `first_name` and `last_name` are replaced by `full_name` and 27 | `short_name` 28 | """ 29 | add_form_template = 'admin/auth/user/add_form.html' 30 | change_user_password_template = None 31 | fieldsets = ( 32 | (None, {'fields': ('username', 'password')}), 33 | (_('Personal info'), {'fields': ('full_name', 'short_name', 'email')}), 34 | (_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser', 35 | 'groups', 'user_permissions')}), 36 | (_('Important dates'), {'fields': ('last_login', 'date_joined')}), 37 | ) 38 | add_fieldsets = ( 39 | (None, { 40 | 'classes': ('wide',), 41 | 'fields': ('username', 'password1', 'password2')} 42 | ), 43 | ) 44 | form = CustomUserChangeForm 45 | add_form = CustomUserCreationForm 46 | change_password_form = AdminPasswordChangeForm 47 | list_display = ('username', 'email', 'full_name', 'short_name', 'is_staff') 48 | list_filter = ('is_staff', 'is_superuser', 'is_active', 'groups') 49 | search_fields = ('username', 'full_name', 'short_name', 'email') 50 | ordering = ('username',) 51 | filter_horizontal = ('groups', 'user_permissions',) 52 | 53 | def get_fieldsets(self, request, obj=None): 54 | if not obj: 55 | return self.add_fieldsets 56 | return super(CustomUserAdmin, self).get_fieldsets(request, obj) 57 | 58 | def get_form(self, request, obj=None, **kwargs): 59 | """ 60 | Use special form during user creation 61 | """ 62 | defaults = {} 63 | if obj is None: 64 | defaults.update({ 65 | 'form': self.add_form, 66 | }) 67 | defaults.update(kwargs) 68 | return super(CustomUserAdmin, self).get_form(request, obj, **defaults) 69 | 70 | def get_urls(self): 71 | from django.conf.urls import url 72 | 73 | urlpatterns = [ 74 | url(r'^(\d+)/password/$', self.admin_site.admin_view(self.user_change_password)), 75 | 76 | ] 77 | urlpatterns += super(CustomUserAdmin, self).get_urls() 78 | return urlpatterns 79 | 80 | def lookup_allowed(self, lookup, value): 81 | # See #20078: we don't want to allow any lookups involving passwords. 82 | if lookup.startswith('password'): 83 | return False 84 | return super(CustomUserAdmin, self).lookup_allowed(lookup, value) 85 | 86 | @sensitive_post_parameters_m 87 | @csrf_protect_m 88 | @transaction.atomic 89 | def add_view(self, request, form_url='', extra_context=None): 90 | # It's an error for a user to have add permission but NOT change 91 | # permission for users. If we allowed such users to add users, they 92 | # could create superusers, which would mean they would essentially have 93 | # the permission to change users. To avoid the problem entirely, we 94 | # disallow users from adding users if they don't have change 95 | # permission. 96 | if not self.has_change_permission(request): 97 | if self.has_add_permission(request) and settings.DEBUG: 98 | # Raise Http404 in debug mode so that the user gets a helpful 99 | # error message. 100 | raise Http404( 101 | 'Your user does not have the "Change user" permission. In ' 102 | 'order to add users, Django requires that your user ' 103 | 'account have both the "Add user" and "Change user" ' 104 | 'permissions set.') 105 | raise PermissionDenied 106 | if extra_context is None: 107 | extra_context = {} 108 | username_field = self.model._meta.get_field(self.model.USERNAME_FIELD) 109 | defaults = { 110 | 'auto_populated_fields': (), 111 | 'username_help_text': username_field.help_text, 112 | } 113 | extra_context.update(defaults) 114 | return super(CustomUserAdmin, self).add_view(request, form_url, 115 | extra_context) 116 | 117 | @sensitive_post_parameters_m 118 | def user_change_password(self, request, id, form_url=''): 119 | if not self.has_change_permission(request): 120 | raise PermissionDenied 121 | user = get_object_or_404(self.queryset(request), pk=id) 122 | if request.method == 'POST': 123 | form = self.change_password_form(user, request.POST) 124 | if form.is_valid(): 125 | form.save() 126 | msg = ugettext('Password changed successfully.') 127 | messages.success(request, msg) 128 | return HttpResponseRedirect('..') 129 | else: 130 | form = self.change_password_form(user) 131 | 132 | fieldsets = [(None, {'fields': list(form.base_fields)})] 133 | adminForm = admin.helpers.AdminForm(form, fieldsets, {}) 134 | 135 | context = { 136 | 'title': _('Change password: %s') % escape(user.get_username()), 137 | 'adminForm': adminForm, 138 | 'form_url': form_url, 139 | 'form': form, 140 | 'is_popup': '_popup' in request.REQUEST, 141 | 'add': True, 142 | 'change': False, 143 | 'has_delete_permission': False, 144 | 'has_change_permission': True, 145 | 'has_absolute_url': False, 146 | 'opts': self.model._meta, 147 | 'original': user, 148 | 'save_as': False, 149 | 'show_save': True, 150 | } 151 | return TemplateResponse(request, 152 | self.change_user_password_template or 153 | 'admin/auth/user/change_password.html', 154 | context, current_app=self.admin_site.name) 155 | 156 | def response_add(self, request, obj, post_url_continue=None): 157 | """ 158 | Determines the HttpResponse for the add_view stage. It mostly defers to 159 | its superclass implementation but is customized because the User model 160 | has a slightly different workflow. 161 | """ 162 | # We should allow further modification of the user just added i.e. the 163 | # 'Save' button should behave like the 'Save and continue editing' 164 | # button except in two scenarios: 165 | # * The user has pressed the 'Save and add another' button 166 | # * We are adding a user in a popup 167 | if '_addanother' not in request.POST and '_popup' not in request.POST: 168 | request.POST['_continue'] = 1 169 | return super(CustomUserAdmin, self).response_add(request, obj, 170 | post_url_continue) 171 | 172 | 173 | admin.site.register(CustomUser, CustomUserAdmin) 174 | -------------------------------------------------------------------------------- /customuser/accounts/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.contrib.auth.forms import ReadOnlyPasswordHashField 3 | from django.utils.translation import ugettext_lazy as _ 4 | 5 | from .models import CustomUser 6 | 7 | 8 | class CustomUserCreationForm(forms.ModelForm): 9 | """ 10 | A form that creates a user, with no privileges, from the given username and 11 | password. 12 | """ 13 | error_messages = { 14 | 'duplicate_username': _("A user with that username already exists."), 15 | 'password_mismatch': _("The two password fields didn't match."), 16 | } 17 | username = forms.RegexField(label=_("Username"), max_length=30, 18 | regex=r'^[\w.@+-]+$', 19 | help_text=_("Required. 30 characters or fewer. Letters, digits and " 20 | "@/./+/-/_ only."), 21 | error_messages={ 22 | 'invalid': _("This value may contain only letters, numbers and " 23 | "@/./+/-/_ characters.")}) 24 | password1 = forms.CharField(label=_("Password"), 25 | widget=forms.PasswordInput) 26 | password2 = forms.CharField(label=_("Password confirmation"), 27 | widget=forms.PasswordInput, 28 | help_text=_("Enter the same password as above, for verification.")) 29 | 30 | class Meta: 31 | # Point to our CustomUser here instead of default `User` 32 | model = CustomUser 33 | fields = ("username",) 34 | 35 | def clean_username(self): 36 | # Since User.username is unique, this check is redundant, 37 | # but it sets a nicer error message than the ORM. See #13147. 38 | username = self.cleaned_data["username"] 39 | try: 40 | # Refer to our CustomUser here instead of default `User` 41 | CustomUser._default_manager.get(username=username) 42 | except CustomUser.DoesNotExist: 43 | return username 44 | raise forms.ValidationError(self.error_messages['duplicate_username']) 45 | 46 | def clean_password2(self): 47 | password1 = self.cleaned_data.get("password1") 48 | password2 = self.cleaned_data.get("password2") 49 | if password1 and password2 and password1 != password2: 50 | raise forms.ValidationError( 51 | self.error_messages['password_mismatch']) 52 | return password2 53 | 54 | def save(self, commit=True): 55 | # Make sure we pass back in our CustomUserCreationForm and not the 56 | # default `UserCreationForm` 57 | user = super(CustomUserCreationForm, self).save(commit=False) 58 | user.set_password(self.cleaned_data["password1"]) 59 | if commit: 60 | user.save() 61 | return user 62 | 63 | 64 | class CustomUserChangeForm(forms.ModelForm): 65 | username = forms.RegexField( 66 | label=_("Username"), max_length=30, regex=r"^[\w.@+-]+$", 67 | help_text=_("Required. 30 characters or fewer. Letters, digits and " 68 | "@/./+/-/_ only."), 69 | error_messages={ 70 | 'invalid': _("This value may contain only letters, numbers and " 71 | "@/./+/-/_ characters.")}) 72 | password = ReadOnlyPasswordHashField(label=_("Password"), 73 | help_text=_("Raw passwords are not stored, so there is no way to see " 74 | "this user's password, but you can change the password " 75 | "using this form.")) 76 | 77 | class Meta: 78 | # Point to our CustomUser here instead of default `User` 79 | model = CustomUser 80 | fields = "__all__" 81 | 82 | def __init__(self, *args, **kwargs): 83 | # Make sure we pass back in our CustomUserChangeForm and not the 84 | # default `UserChangeForm` 85 | super(CustomUserChangeForm, self).__init__(*args, **kwargs) 86 | f = self.fields.get('user_permissions', None) 87 | if f is not None: 88 | f.queryset = f.queryset.select_related('content_type') 89 | 90 | def clean_password(self): 91 | # Regardless of what the user provides, return the initial value. 92 | # This is done here, rather than on the field, because the 93 | # field does not have access to the initial value 94 | return self.initial["password"] 95 | -------------------------------------------------------------------------------- /customuser/accounts/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.1 on 2016-01-11 06:18 3 | from __future__ import unicode_literals 4 | 5 | import django.contrib.auth.models 6 | import django.core.validators 7 | from django.db import migrations, models 8 | import django.utils.timezone 9 | import re 10 | 11 | 12 | class Migration(migrations.Migration): 13 | 14 | initial = True 15 | 16 | dependencies = [ 17 | ('auth', '0007_alter_validators_add_error_messages'), 18 | ] 19 | 20 | operations = [ 21 | migrations.CreateModel( 22 | name='CustomUser', 23 | fields=[ 24 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 25 | ('password', models.CharField(max_length=128, verbose_name='password')), 26 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 27 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), 28 | ('username', models.CharField(help_text='Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters', max_length=30, unique=True, validators=[django.core.validators.RegexValidator(re.compile('^[\\w.@+-]+$', 32), 'Enter a valid username.', 'invalid')], verbose_name='username')), 29 | ('full_name', models.CharField(blank=True, max_length=254, verbose_name='full name')), 30 | ('short_name', models.CharField(blank=True, max_length=30, verbose_name='short name')), 31 | ('email', models.EmailField(max_length=254, unique=True, verbose_name='email address')), 32 | ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), 33 | ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), 34 | ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), 35 | ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), 36 | ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), 37 | ], 38 | options={ 39 | 'verbose_name_plural': 'users', 40 | 'verbose_name': 'user', 41 | }, 42 | managers=[ 43 | ('objects', django.contrib.auth.models.UserManager()), 44 | ], 45 | ), 46 | ] 47 | -------------------------------------------------------------------------------- /customuser/accounts/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonathanchu/django-custom-user-example/a80e47b2c2f7113feadaa60253adac63f8afe3b4/customuser/accounts/migrations/__init__.py -------------------------------------------------------------------------------- /customuser/accounts/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | import re 3 | 4 | from django.contrib.auth.models import (AbstractBaseUser, PermissionsMixin, 5 | UserManager) 6 | from django.core.mail import send_mail 7 | from django.core import validators 8 | from django.db import models 9 | from django.utils.translation import ugettext_lazy as _ 10 | from django.utils import timezone 11 | from django.utils.http import urlquote 12 | 13 | 14 | class CustomUser(AbstractBaseUser, PermissionsMixin): 15 | """ 16 | A custom user class that basically mirrors Django's `AbstractUser` class 17 | and doesn't force `first_name` or `last_name` with sensibilities for 18 | international names. 19 | 20 | http://www.w3.org/International/questions/qa-personal-names 21 | """ 22 | username = models.CharField(_('username'), max_length=30, unique=True, 23 | help_text=_('Required. 30 characters or fewer. Letters, numbers and ' 24 | '@/./+/-/_ characters'), 25 | validators=[ 26 | validators.RegexValidator(re.compile( 27 | '^[\w.@+-]+$'), _('Enter a valid username.'), 'invalid') 28 | ]) 29 | full_name = models.CharField(_('full name'), max_length=254, blank=True) 30 | short_name = models.CharField(_('short name'), max_length=30, blank=True) 31 | email = models.EmailField(_('email address'), max_length=254, unique=True) 32 | is_staff = models.BooleanField(_('staff status'), default=False, 33 | help_text=_('Designates whether the user can log into this admin ' 34 | 'site.')) 35 | is_active = models.BooleanField(_('active'), default=True, 36 | help_text=_('Designates whether this user should be treated as ' 37 | 'active. Unselect this instead of deleting accounts.')) 38 | date_joined = models.DateTimeField(_('date joined'), default=timezone.now) 39 | 40 | objects = UserManager() 41 | 42 | USERNAME_FIELD = 'username' 43 | REQUIRED_FIELDS = ['email'] 44 | 45 | class Meta: 46 | verbose_name = _('user') 47 | verbose_name_plural = _('users') 48 | 49 | def __unicode__(self): 50 | return self.username 51 | 52 | def get_absolute_url(self): 53 | return "/users/%s/" % urlquote(self.username) 54 | 55 | def get_full_name(self): 56 | """ 57 | Returns the first_name plus the last_name, with a space in between. 58 | """ 59 | full_name = self.full_name 60 | return full_name.strip() 61 | 62 | def get_short_name(self): 63 | "Returns the short name for the user." 64 | return self.short_name.strip() 65 | 66 | def email_user(self, subject, message, from_email=None): 67 | """ 68 | Sends an email to this User. 69 | """ 70 | send_mail(subject, message, from_email, [self.email]) 71 | -------------------------------------------------------------------------------- /customuser/settings.py: -------------------------------------------------------------------------------- 1 | # Django settings for django-custom-user-example project. 2 | import os 3 | 4 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 5 | 6 | 7 | DEBUG = True 8 | TEMPLATE_DEBUG = DEBUG 9 | 10 | ADMINS = ( 11 | # ('Your Name', 'your_email@example.com'), 12 | ) 13 | 14 | MANAGERS = ADMINS 15 | 16 | DATABASES = { 17 | 'default': { 18 | 'ENGINE': 'django.db.backends.sqlite3', 19 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 20 | } 21 | } 22 | 23 | 24 | # Local time zone for this installation. Choices can be found here: 25 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 26 | # although not all choices may be available on all operating systems. 27 | # In a Windows environment this must be set to your system time zone. 28 | TIME_ZONE = 'America/Chicago' 29 | 30 | # Language code for this installation. All choices can be found here: 31 | # http://www.i18nguy.com/unicode/language-identifiers.html 32 | LANGUAGE_CODE = 'en-us' 33 | 34 | SITE_ID = 1 35 | 36 | # If you set this to False, Django will make some optimizations so as not 37 | # to load the internationalization machinery. 38 | USE_I18N = True 39 | 40 | # If you set this to False, Django will not format dates, numbers and 41 | # calendars according to the current locale. 42 | USE_L10N = True 43 | 44 | # If you set this to False, Django will not use timezone-aware datetimes. 45 | USE_TZ = True 46 | 47 | # Absolute filesystem path to the directory that will hold user-uploaded files. 48 | # Example: "/var/www/example.com/media/" 49 | MEDIA_ROOT = '' 50 | 51 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 52 | # trailing slash. 53 | # Examples: "http://example.com/media/", "http://media.example.com/" 54 | MEDIA_URL = '' 55 | 56 | # Absolute path to the directory static files should be collected to. 57 | # Don't put anything in this directory yourself; store your static files 58 | # in apps' "static/" subdirectories and in STATICFILES_DIRS. 59 | # Example: "/var/www/example.com/static/" 60 | STATIC_ROOT = '' 61 | 62 | # URL prefix for static files. 63 | # Example: "http://example.com/static/", "http://static.example.com/" 64 | STATIC_URL = '/static/' 65 | 66 | # Additional locations of static files 67 | STATICFILES_DIRS = ( 68 | # Put strings here, like "/home/html/static" or "C:/www/django/static". 69 | # Always use forward slashes, even on Windows. 70 | # Don't forget to use absolute paths, not relative paths. 71 | ) 72 | 73 | # List of finder classes that know how to find static files in 74 | # various locations. 75 | STATICFILES_FINDERS = ( 76 | 'django.contrib.staticfiles.finders.FileSystemFinder', 77 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 78 | # 'django.contrib.staticfiles.finders.DefaultStorageFinder', 79 | ) 80 | 81 | # Make this unique, and don't share it with anybody. 82 | SECRET_KEY = 'hhpbe@1_8x4us$v14+h31pjy)udqz(fg@92a@p-yd=6kk04smp' 83 | 84 | # List of callables that know how to import templates from various sources. 85 | TEMPLATE_LOADERS = ( 86 | 'django.template.loaders.filesystem.Loader', 87 | 'django.template.loaders.app_directories.Loader', 88 | # 'django.template.loaders.eggs.Loader', 89 | ) 90 | 91 | MIDDLEWARE_CLASSES = ( 92 | 'django.middleware.common.CommonMiddleware', 93 | 'django.contrib.sessions.middleware.SessionMiddleware', 94 | 'django.middleware.csrf.CsrfViewMiddleware', 95 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 96 | 'django.contrib.messages.middleware.MessageMiddleware', 97 | # Uncomment the next line for simple clickjacking protection: 98 | # 'django.middleware.clickjacking.XFrameOptionsMiddleware', 99 | ) 100 | 101 | ROOT_URLCONF = 'customuser.urls' 102 | 103 | # Python dotted path to the WSGI application used by Django's runserver. 104 | WSGI_APPLICATION = 'customuser.wsgi.application' 105 | 106 | TEMPLATE_DIRS = ( 107 | # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". 108 | # Always use forward slashes, even on Windows. 109 | # Don't forget to use absolute paths, not relative paths. 110 | ) 111 | 112 | INSTALLED_APPS = ( 113 | 'django.contrib.auth', 114 | 'django.contrib.contenttypes', 115 | 'django.contrib.sessions', 116 | 'django.contrib.sites', 117 | 'django.contrib.messages', 118 | 'django.contrib.staticfiles', 119 | # Uncomment the next line to enable the admin: 120 | 'django.contrib.admin', 121 | # Uncomment the next line to enable admin documentation: 122 | # 'django.contrib.admindocs', 123 | 'customuser.accounts', 124 | 'django_extensions', # Don't use this in production environment 125 | ) 126 | 127 | # A sample logging configuration. The only tangible logging 128 | # performed by this configuration is to send an email to 129 | # the site admins on every HTTP 500 error when DEBUG=False. 130 | # See http://docs.djangoproject.com/en/dev/topics/logging for 131 | # more details on how to customize your logging configuration. 132 | LOGGING = { 133 | 'version': 1, 134 | 'disable_existing_loggers': False, 135 | 'filters': { 136 | 'require_debug_false': { 137 | '()': 'django.utils.log.RequireDebugFalse' 138 | } 139 | }, 140 | 'handlers': { 141 | 'mail_admins': { 142 | 'level': 'ERROR', 143 | 'filters': ['require_debug_false'], 144 | 'class': 'django.utils.log.AdminEmailHandler' 145 | } 146 | }, 147 | 'loggers': { 148 | 'django.request': { 149 | 'handlers': ['mail_admins'], 150 | 'level': 'ERROR', 151 | 'propagate': True, 152 | }, 153 | } 154 | } 155 | 156 | AUTH_USER_MODEL = 'accounts.CustomUser' 157 | -------------------------------------------------------------------------------- /customuser/testing.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | from .settings import * # noqa 4 | 5 | MEDIA_ROOT = "/tmp" 6 | 7 | SECRET_KEY = 'top-secret!' 8 | 9 | EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" 10 | INSTALLED_APPS += ("tests", ) 11 | -------------------------------------------------------------------------------- /customuser/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import include, url 2 | 3 | # Uncomment the next two lines to enable the admin: 4 | from django.contrib import admin 5 | admin.autodiscover() 6 | 7 | urlpatterns = [ 8 | # Examples: 9 | # url(r'^$', 'django_custom_user_example.views.home', name='home'), 10 | # url(r'^django_custom_user_example/', 11 | # include('django_custom_user_example.foo.urls')), 12 | 13 | # Uncomment the admin/doc line below to enable admin documentation: 14 | # url(r'^admin/doc/', 15 | # include('django.contrib.admindocs.urls')), 16 | 17 | # Uncomment the next line to enable the admin: 18 | url(r'^admin/', include(admin.site.urls)), 19 | 20 | ] 21 | -------------------------------------------------------------------------------- /customuser/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for django-custom-user-example project. 3 | 4 | This module contains the WSGI application used by Django's development server 5 | and any production WSGI deployments. It should expose a module-level variable 6 | named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover 7 | this application via the ``WSGI_APPLICATION`` setting. 8 | 9 | Usually you will have the standard Django WSGI application here, but it also 10 | might make sense to replace the whole Django WSGI application with a custom one 11 | that later delegates to the Django one. For example, you could introduce WSGI 12 | middleware here, or combine a Django application with an application of another 13 | framework. 14 | 15 | """ 16 | import os 17 | 18 | # We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks 19 | # if running multiple sites in the same mod_wsgi process. To fix this, use 20 | # mod_wsgi daemon mode with each site in its own daemon process, or use 21 | # os.environ["DJANGO_SETTINGS_MODULE"] = "django_custom_user_example.settings" 22 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "customuser.settings") 23 | 24 | # This application object is used by any WSGI server configured to use this 25 | # file. This includes Django's development server, if the WSGI_APPLICATION 26 | # setting points here. 27 | from django.core.wsgi import get_wsgi_application 28 | application = get_wsgi_application() 29 | 30 | # Apply WSGI middleware here. 31 | # from helloworld.wsgi import HelloWorldApplication 32 | # application = HelloWorldApplication(application) 33 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | # Debugging 2 | # ------------------------------------- 3 | django-debug-toolbar==1.4 4 | ipdb==0.8.1 5 | ipython==4.0.1 6 | 7 | # Testing and coverage 8 | # ------------------------------------- 9 | pytest-django==2.9.1 10 | pytest-pythonpath==0.7 11 | pytest-cov==2.2.0 12 | factory_boy==2.6.0 13 | pdbpp==0.8.3 14 | mock==1.3.0 15 | flake8==2.5.1 16 | django-extensions == 1.6.1 17 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "customuser.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django==1.9.1 2 | 3 | 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | exclude = .tox,.git,*/migrations/*,*/static/*,docs,venv 4 | 5 | [pytest] 6 | DJANGO_SETTINGS_MODULE = customuser.testing 7 | norecursedirs = .tox .git */migrations/* */static/* docs venv 8 | python_paths = . 9 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | -------------------------------------------------------------------------------- /tests/factories.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import factory 4 | from django.conf import settings 5 | 6 | 7 | class Factory(factory.DjangoModelFactory): 8 | 9 | class Meta: 10 | strategy = factory.CREATE_STRATEGY 11 | model = None 12 | abstract = True 13 | 14 | 15 | class UserFactory(Factory): 16 | 17 | class Meta: 18 | model = settings.AUTH_USER_MODEL 19 | 20 | username = factory.Sequence(lambda n: 'user%04d' % n) 21 | email = factory.Sequence(lambda n: 'user%04d@email.com' % n) 22 | password = factory.PostGeneration( 23 | lambda obj, *args, **kwargs: obj.set_password('123123')) 24 | 25 | # You can define more fields over here for User Factory as per your custom model 26 | -------------------------------------------------------------------------------- /tests/test_simple_assert.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | def test_simple_assert(): 6 | x = 2 7 | assert x == 2 8 | -------------------------------------------------------------------------------- /tests/test_user_custom_model.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from .factories import UserFactory 5 | from customuser.accounts.models import CustomUser 6 | import pytest 7 | 8 | 9 | pytestmark = pytest.mark.django_db 10 | 11 | 12 | def test_user_custom_model(): 13 | user = UserFactory.create(username="sampleuser") 14 | assert user.username == "sampleuser" 15 | assert user.is_staff is False 16 | assert user.is_superuser is False 17 | 18 | # You Can Add More Test Case w.r.t to your custom model 19 | 20 | 21 | def test_custom_model_required_fields(): 22 | user = CustomUser(username="user") 23 | user.save() 24 | # assert user.is_valid() is False 25 | -------------------------------------------------------------------------------- /tests/test_user_custom_model_forms.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from customuser.accounts.forms import (CustomUserCreationForm) 5 | import pytest 6 | 7 | 8 | pytestmark = pytest.mark.django_db 9 | 10 | 11 | def test_user_custom_model_forms(): 12 | 13 | data = {'password1': 'a', 'password2': 'a', 'username': 'user'} 14 | user_form = CustomUserCreationForm(data) 15 | assert user_form.is_valid() is True 16 | 17 | data = {'password1': 'a', 'password2': 'b', 'username': 'user'} 18 | user_form = CustomUserCreationForm(data) 19 | assert user_form.is_valid() is False 20 | 21 | invalid_data = {'password1': 'a', 'password2': 'a', 'username': 'foqw;efnwldksfalsdkj \ 22 | fosaidfj;alsdfjsadlfkjslakdjfsladkfjslakdjfsdalfkjaslkfaslkdfjaslkdjf'} 23 | user_form = CustomUserCreationForm(invalid_data) 24 | assert user_form.is_valid() is False # Username more than 30 character 25 | 26 | # To Add more test for custom forms 27 | --------------------------------------------------------------------------------