├── .gitignore ├── LICENSE ├── README.md ├── __init__.py ├── admin.py ├── forms.py ├── migrations ├── 0001_initial.py └── __init__.py ├── models.py ├── urls.py └── views.py /.gitignore: -------------------------------------------------------------------------------- 1 | ### https://raw.github.com/github/gitignore/058d4918973c034c1c53215f28845c0342a0f6b1/python.gitignore 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *,cover 48 | .hypothesis/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask instance folder 59 | instance/ 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # IPython Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # dotenv 80 | .env 81 | 82 | # Spyder project settings 83 | .spyderproject 84 | 85 | 86 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Brenton Cleeland (https://brntn.me) 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-authuser 2 | 3 | 4 | A reusable custom user model for Django projects. 5 | 6 | Includes: 7 | 8 | - The user model (email address to sign in, full-name only) 9 | - Sign up form and view 10 | - Logout view with redirect to the homepage 11 | - Admin registration 12 | 13 | 14 | ### Usage 15 | 16 | Add the `authuser` app as a git submodule: 17 | 18 | ``` 19 | git submodule add git@github.com:sesh/django-authuser.git authuser 20 | ``` 21 | 22 | Add the app to your `INSTALLED_APPS`, and configure your `AUTH_USER_MODEL` setting: 23 | 24 | ``` 25 | INSTALLED_APPS = [ 26 | ... 27 | "authuser", 28 | ] 29 | 30 | AUTH_USER_MODEL = "authuser.User" 31 | ``` 32 | 33 | Add the following to your `settings.py` to allow signups: 34 | 35 | ``` 36 | AUTH_USER_ALLOW_SIGNUP = True 37 | ``` 38 | 39 | Update your `urls.py` in include the signup and logout urls: 40 | 41 | ``` 42 | urlpatterns = [ 43 | ..., 44 | path('accounts/', include('authuser.urls')), 45 | ] 46 | ``` 47 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sesh/django-authuser/19e046c54f6988d33ac9e2bc5aa5f86bccae1e1f/__init__.py -------------------------------------------------------------------------------- /admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth.admin import UserAdmin 3 | 4 | from .models import User 5 | 6 | 7 | class CustomUserAdmin(UserAdmin): 8 | add_fieldsets = ( 9 | (None, {"fields": ("name", "email", "password1", "password2")}), 10 | ( 11 | "Permissions", 12 | { 13 | "fields": ( 14 | "is_active", 15 | "is_staff", 16 | "is_superuser", 17 | "groups", 18 | "user_permissions", 19 | ) 20 | }, 21 | ), 22 | ) 23 | fieldsets = ( 24 | (None, {"fields": ("name", "email", "password")}), 25 | ( 26 | "Permissions", 27 | { 28 | "fields": ( 29 | "is_active", 30 | "is_staff", 31 | "is_superuser", 32 | "groups", 33 | "user_permissions", 34 | ) 35 | }, 36 | ), 37 | ("Important dates", {"fields": ("last_login", "date_joined")}), 38 | ) 39 | list_display = ("email", "name", "is_staff") 40 | search_fields = ("name", "email") 41 | ordering = ["email"] 42 | 43 | 44 | admin.site.register(User, CustomUserAdmin) 45 | -------------------------------------------------------------------------------- /forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from .models import User 4 | 5 | 6 | class SignUpForm(forms.Form): 7 | email = forms.EmailField() 8 | password = forms.CharField(widget=forms.PasswordInput()) 9 | password_again = forms.CharField(widget=forms.PasswordInput()) 10 | 11 | def clean_email(self): 12 | value = self.cleaned_data["email"].strip() 13 | if User.objects.filter(email=value): 14 | raise forms.ValidationError("That email is already in user") 15 | return value 16 | 17 | def clean(self): 18 | cleaned_data = super().clean() 19 | if cleaned_data["password"] != cleaned_data["password_again"]: 20 | raise forms.ValidationError("The passwords you entered did not match") 21 | -------------------------------------------------------------------------------- /migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.4 on 2022-05-19 03:42 2 | 3 | import authuser.models 4 | from django.db import migrations, models 5 | import django.utils.timezone 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ("auth", "0012_alter_user_first_name_max_length"), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name="User", 19 | fields=[ 20 | ( 21 | "id", 22 | models.BigAutoField( 23 | auto_created=True, 24 | primary_key=True, 25 | serialize=False, 26 | verbose_name="ID", 27 | ), 28 | ), 29 | ("password", models.CharField(max_length=128, verbose_name="password")), 30 | ( 31 | "email", 32 | models.EmailField( 33 | blank=True, default="", max_length=254, unique=True 34 | ), 35 | ), 36 | ("name", models.CharField(blank=True, default="", max_length=200)), 37 | ("is_active", models.BooleanField(default=True)), 38 | ("is_staff", models.BooleanField(default=False)), 39 | ("is_superuser", models.BooleanField(default=False)), 40 | ("last_login", models.DateTimeField(blank=True, null=True)), 41 | ( 42 | "date_joined", 43 | models.DateTimeField(default=django.utils.timezone.now), 44 | ), 45 | ( 46 | "groups", 47 | models.ManyToManyField( 48 | blank=True, 49 | help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", 50 | related_name="user_set", 51 | related_query_name="user", 52 | to="auth.group", 53 | verbose_name="groups", 54 | ), 55 | ), 56 | ( 57 | "user_permissions", 58 | models.ManyToManyField( 59 | blank=True, 60 | help_text="Specific permissions for this user.", 61 | related_name="user_set", 62 | related_query_name="user", 63 | to="auth.permission", 64 | verbose_name="user permissions", 65 | ), 66 | ), 67 | ], 68 | options={ 69 | "verbose_name": "User", 70 | "verbose_name_plural": "Users", 71 | }, 72 | managers=[ 73 | ("objects", authuser.models.CustomUserManager()), 74 | ], 75 | ), 76 | ] 77 | -------------------------------------------------------------------------------- /migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sesh/django-authuser/19e046c54f6988d33ac9e2bc5aa5f86bccae1e1f/migrations/__init__.py -------------------------------------------------------------------------------- /models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, UserManager 3 | from django.utils import timezone 4 | 5 | 6 | class CustomUserManager(UserManager): 7 | def _create_user(self, email, password, **extra_fields): 8 | """ 9 | Create and save a User with the provided email and password. 10 | """ 11 | if not email: 12 | raise ValueError("The given email address must be set") 13 | 14 | email = self.normalize_email(email) 15 | user = self.model(email=email, **extra_fields) 16 | user.set_password(password) 17 | user.save(using=self._db) 18 | return user 19 | 20 | def create_user(self, email=None, password=None, **extra_fields): 21 | extra_fields.setdefault("is_staff", False) 22 | extra_fields.setdefault("is_superuser", False) 23 | return self._create_user(email, password, **extra_fields) 24 | 25 | def create_superuser(self, email, password, **extra_fields): 26 | extra_fields.setdefault("is_staff", True) 27 | extra_fields.setdefault("is_superuser", True) 28 | 29 | if extra_fields.get("is_staff") is not True: 30 | raise ValueError("Superuser must have is_staff=True.") 31 | if extra_fields.get("is_superuser") is not True: 32 | raise ValueError("Superuser must have is_superuser=True.") 33 | 34 | return self._create_user(email, password, **extra_fields) 35 | 36 | 37 | class User(AbstractBaseUser, PermissionsMixin): 38 | """ 39 | User model that uses email addresses instead of usernames, and 40 | name instead of first / last name fields. 41 | 42 | All other fields from the Django auth.User model are kept to 43 | ensure maximum compatibility with the built in management 44 | commands. 45 | """ 46 | 47 | email = models.EmailField(blank=True, default="", unique=True) 48 | name = models.CharField(max_length=200, blank=True, default="") 49 | 50 | is_active = models.BooleanField(default=True) 51 | is_staff = models.BooleanField(default=False) 52 | is_superuser = models.BooleanField(default=False) 53 | 54 | last_login = models.DateTimeField(blank=True, null=True) 55 | date_joined = models.DateTimeField(default=timezone.now) 56 | 57 | objects = CustomUserManager() 58 | 59 | USERNAME_FIELD = "email" 60 | EMAIL_FIELD = "email" 61 | REQUIRED_FIELDS = [] 62 | 63 | class Meta: 64 | verbose_name = "User" 65 | verbose_name_plural = "Users" 66 | 67 | def get_full_name(self): 68 | return self.name 69 | 70 | def get_short_name(self): 71 | return self.name or self.email.split("@")[0] 72 | -------------------------------------------------------------------------------- /urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import signup 4 | 5 | 6 | urlpatterns = [ 7 | path("signup/", signup, name="signup"), 8 | ] 9 | -------------------------------------------------------------------------------- /views.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.http import HttpResponseForbidden 3 | from django.shortcuts import render, redirect 4 | from django.contrib.auth import logout as logout_user 5 | from django.contrib.auth import login as login_user 6 | 7 | from .forms import SignUpForm 8 | from .models import User 9 | 10 | 11 | def signup(request): 12 | if not getattr(settings, "AUTH_USER_ALLOW_SIGNUP", False): 13 | return HttpResponseForbidden("Forbidden") 14 | 15 | form = SignUpForm() 16 | 17 | if request.method == "POST": 18 | form = SignUpForm(request.POST) 19 | if form.is_valid(): 20 | user = User.objects.create_user( 21 | email=form.cleaned_data["email"], 22 | ) 23 | user.set_password(form.cleaned_data["password"]) 24 | user.save() 25 | login_user(request, user) 26 | return redirect("/") 27 | 28 | return render( 29 | request, 30 | "registration/signup.html", 31 | { 32 | "form": form, 33 | }, 34 | ) 35 | --------------------------------------------------------------------------------