├── tests ├── __init__.py ├── apps │ └── custom_user_test │ │ ├── __init__.py │ │ ├── migrations │ │ ├── __init__.py │ │ └── 0001_initial.py │ │ ├── models.py │ │ ├── apps.py │ │ └── admin.py ├── urls.py ├── settings.py └── test.py ├── example ├── example │ ├── __init__.py │ ├── wsgi.py │ ├── urls.py │ └── settings.py ├── custom_user │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ └── 0001_initial.py │ ├── models.py │ ├── apps.py │ └── admin.py ├── manage.py └── README.rst ├── docs ├── history.rst ├── readme.rst ├── contributing.rst ├── index.rst ├── Makefile └── conf.py ├── django_use_email_as_username ├── __init__.py ├── management │ └── commands │ │ ├── app_template │ │ ├── __init__.py-tpl │ │ ├── migrations │ │ │ └── __init__.py-tpl │ │ ├── models.py-tpl │ │ ├── apps.py-tpl │ │ └── admin.py-tpl │ │ └── create_custom_user_app.py ├── apps.py ├── admin.py └── models.py ├── .coveragerc ├── .github ├── ISSUE_TEMPLATE.md ├── stale.yml └── workflows │ └── tests.yml ├── manage.py ├── runtests.py ├── tox.ini ├── HISTORY.rst ├── LICENSE ├── pyproject.toml ├── .gitignore ├── CONTRIBUTING.rst └── README.rst /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/example/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/custom_user/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/custom_user/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/apps/custom_user_test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/history.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../HISTORY.rst 2 | -------------------------------------------------------------------------------- /docs/readme.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | -------------------------------------------------------------------------------- /tests/apps/custom_user_test/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CONTRIBUTING.rst 2 | -------------------------------------------------------------------------------- /django_use_email_as_username/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.4.0" 2 | -------------------------------------------------------------------------------- /django_use_email_as_username/management/commands/app_template/__init__.py-tpl: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/urls.py: -------------------------------------------------------------------------------- 1 | """Setup urls to run tests.""" 2 | 3 | urlpatterns = [] 4 | -------------------------------------------------------------------------------- /django_use_email_as_username/management/commands/app_template/migrations/__init__.py-tpl: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = true 3 | 4 | [report] 5 | omit = 6 | *site-packages* 7 | *tests* 8 | *.tox* 9 | show_missing = True 10 | -------------------------------------------------------------------------------- /example/custom_user/models.py: -------------------------------------------------------------------------------- 1 | from django_use_email_as_username.models import BaseUser, BaseUserManager 2 | 3 | 4 | class User(BaseUser): 5 | objects = BaseUserManager() 6 | -------------------------------------------------------------------------------- /tests/apps/custom_user_test/models.py: -------------------------------------------------------------------------------- 1 | from django_use_email_as_username.models import BaseUser, BaseUserManager 2 | 3 | 4 | class User(BaseUser): 5 | objects = BaseUserManager() 6 | -------------------------------------------------------------------------------- /example/custom_user/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CustomUserConfig(AppConfig): 5 | name = 'custom_user' 6 | verbose_name = 'Custom User Management' 7 | -------------------------------------------------------------------------------- /tests/apps/custom_user_test/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CustomUserTestConfig(AppConfig): 5 | name = 'custom_user_test' 6 | verbose_name = 'Custom User Management' 7 | -------------------------------------------------------------------------------- /example/custom_user/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django_use_email_as_username.admin import BaseUserAdmin 3 | 4 | from .models import User 5 | 6 | admin.site.register(User, BaseUserAdmin) 7 | -------------------------------------------------------------------------------- /tests/apps/custom_user_test/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django_use_email_as_username.admin import BaseUserAdmin 3 | 4 | from .models import User 5 | 6 | admin.site.register(User, BaseUserAdmin) 7 | -------------------------------------------------------------------------------- /django_use_email_as_username/management/commands/app_template/models.py-tpl: -------------------------------------------------------------------------------- 1 | from django_use_email_as_username.models import BaseUser, BaseUserManager 2 | 3 | 4 | class User(BaseUser): 5 | objects = BaseUserManager() 6 | -------------------------------------------------------------------------------- /django_use_email_as_username/management/commands/app_template/apps.py-tpl: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class {{ camel_case_app_name }}Config(AppConfig): 5 | name = '{{ app_name }}' 6 | verbose_name = 'Custom User Management' 7 | -------------------------------------------------------------------------------- /django_use_email_as_username/management/commands/app_template/admin.py-tpl: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django_use_email_as_username.admin import BaseUserAdmin 3 | 4 | from .models import User 5 | 6 | admin.site.register(User, BaseUserAdmin) 7 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to Django use Email as Username's documentation! 2 | ================================================================= 3 | 4 | Contents: 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | 9 | readme 10 | contributing 11 | history 12 | -------------------------------------------------------------------------------- /django_use_email_as_username/apps.py: -------------------------------------------------------------------------------- 1 | """Declare app configuration for Django Use Email as Username.""" 2 | from django.apps import AppConfig 3 | 4 | 5 | class DjangoUseEmailAsUsernameConfig(AppConfig): 6 | """Define app configuration class.""" 7 | 8 | name = "django_use_email_as_username" 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | * Django use Email as Username version: 2 | * Django version: 3 | * Python version: 4 | * Operating System: 5 | 6 | ### Description 7 | 8 | Describe what you were trying to get done. 9 | Tell us what happened, what went wrong, and what you expected to happen. 10 | 11 | ### What I did 12 | 13 | ``` 14 | Paste the command(s) you ran and the output. 15 | If there was a crash, please include the traceback here. 16 | ``` 17 | -------------------------------------------------------------------------------- /example/example/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for example project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") 15 | 16 | application = get_wsgi_application() 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", "example.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError as exc: 10 | raise ImportError( 11 | "Couldn't import Django. Are you sure it's installed and " 12 | "available on your PYTHONPATH environment variable? Did you " 13 | "forget to activate a virtual environment?" 14 | ) from exc 15 | execute_from_command_line(sys.argv) 16 | -------------------------------------------------------------------------------- /example/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", "example.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError as exc: 10 | raise ImportError( 11 | "Couldn't import Django. Are you sure it's installed and " 12 | "available on your PYTHONPATH environment variable? Did you " 13 | "forget to activate a virtual environment?" 14 | ) from exc 15 | execute_from_command_line(sys.argv) 16 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | # Comment to post when marking an issue as stale. Set to `false` to disable 10 | markComment: > 11 | This issue has been automatically marked as stale because it has not had 12 | recent activity. It will be closed if no further activity occurs. Thank you 13 | for your contributions. 14 | # Comment to post when closing a stale issue. Set to `false` to disable 15 | closeComment: false 16 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = PROJECTNAMEAAA 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | """Entry point to run tests.""" 2 | 3 | import os 4 | import sys 5 | 6 | import django 7 | from django.conf import settings 8 | from django.test.utils import get_runner 9 | 10 | 11 | def run_tests(*test_args): 12 | """Prepare Django environment and run tests.""" 13 | if not test_args: 14 | test_args = ['tests'] 15 | 16 | sys.path.append(os.path.dirname(os.path.abspath(__file__)) + '/tests/apps') 17 | os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.settings' 18 | django.setup() 19 | TestRunner = get_runner(settings) 20 | test_runner = TestRunner() 21 | failures = test_runner.run_tests(test_args) 22 | sys.exit(bool(failures)) 23 | 24 | 25 | if __name__ == '__main__': 26 | run_tests(*sys.argv[1:]) 27 | -------------------------------------------------------------------------------- /example/example/urls.py: -------------------------------------------------------------------------------- 1 | """example URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.0/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path 18 | 19 | urlpatterns = [ 20 | path('admin/', admin.site.urls), 21 | ] 22 | -------------------------------------------------------------------------------- /example/README.rst: -------------------------------------------------------------------------------- 1 | ================================================ 2 | Example Project for Django use Email as Username 3 | ================================================ 4 | 5 | This example is provided as a convenience feature to allow potential users to try the app straight from the app repo without having to create a Django project. 6 | 7 | It can also be used to develop the app in place. 8 | 9 | To run this example, follow these instructions: 10 | 11 | 1. Navigate to the `example` directory 12 | 2. Install the requirements for the package:: 13 | 14 | pdm install --prod 15 | 16 | 3. Make and apply migrations:: 17 | 18 | pdm run python manage.py makemigrations 19 | pdm run python manage.py migrate 20 | 21 | 4. Run the server:: 22 | 23 | pdm run python manage.py runserver 24 | 25 | 5. Access from the browser at http://127.0.0.1:8000 26 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py3{8,9}-django{32,41,42} 4 | py310-django{32,41,42,50,51,52} 5 | py311-django{41,42,50,51,52} 6 | py312-django{42,50,51,52} 7 | py313-django{51,52} 8 | 9 | [gh] 10 | python = 11 | 3.8 = py38-django{32,41,42} 12 | 3.9 = py39-django{32,41,42} 13 | 3.10 = py310-django{32,41,42,50,51,52} 14 | 3.11 = py311-django{41,42,50,51,52} 15 | 3.12 = py312-django{42,50,51,52} 16 | 3.13 = py313-django{51,52} 17 | 18 | [testenv] 19 | commands = 20 | pip list 21 | coverage run --source django_use_email_as_username runtests.py 22 | coverage xml 23 | extras = testing 24 | deps = 25 | django32: Django~=3.2.0 26 | django41: Django~=4.1.0 27 | django42: Django~=4.2.0 28 | django50: Django~=5.0.0 29 | django51: Django~=5.1.0 30 | django52: Django~=5.1.2 31 | passenv = 32 | CI 33 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | on: 3 | push: 4 | pull_request: 5 | 6 | concurrency: 7 | group: check-${{ github.ref }} 8 | cancel-in-progress: true 9 | 10 | jobs: 11 | test: 12 | name: test with ${{ matrix.py }} on ${{ matrix.os }} 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | py: 18 | - "3.13" 19 | - "3.12" 20 | - "3.11" 21 | - "3.10" 22 | - "3.9" 23 | - "3.8" 24 | os: 25 | - ubuntu-22.04 26 | steps: 27 | - uses: actions/checkout@v3 28 | with: 29 | fetch-depth: 0 30 | - name: Setup python for test ${{ matrix.py }} 31 | uses: actions/setup-python@v4 32 | with: 33 | python-version: ${{ matrix.py }} 34 | - name: Install tox 35 | run: python -m pip install tox-gh>=1.2 36 | - name: Setup test suite 37 | run: tox -vv --notest 38 | - name: Run test suite 39 | run: tox --skip-pkg-install 40 | - uses: codecov/codecov-action@v3 41 | -------------------------------------------------------------------------------- /django_use_email_as_username/admin.py: -------------------------------------------------------------------------------- 1 | """Integrate DjangoUseEmailAsUsername with admin module.""" 2 | 3 | from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin 4 | from django.utils.translation import gettext_lazy as _ 5 | 6 | 7 | class BaseUserAdmin(DjangoUserAdmin): 8 | """Define admin model for custom User model with no username field.""" 9 | 10 | fieldsets = ( 11 | (None, {"fields": ("email", "password")}), 12 | (_("Personal info"), {"fields": ("first_name", "last_name")}), 13 | ( 14 | _("Permissions"), 15 | { 16 | "fields": ( 17 | "is_active", 18 | "is_staff", 19 | "is_superuser", 20 | "groups", 21 | "user_permissions", 22 | ) 23 | }, 24 | ), 25 | (_("Important dates"), {"fields": ("last_login", "date_joined")}), 26 | ) 27 | add_fieldsets = ( 28 | (None, {"classes": ("wide",), "fields": ("email", "password1", "password2")}), 29 | ) 30 | list_display = ("email", "first_name", "last_name", "is_staff") 31 | search_fields = ("email", "first_name", "last_name") 32 | ordering = ("email",) 33 | -------------------------------------------------------------------------------- /django_use_email_as_username/management/commands/create_custom_user_app.py: -------------------------------------------------------------------------------- 1 | """Define create_emailuser_app command.""" 2 | 3 | import os 4 | 5 | from django.core.management import call_command 6 | from django.core.management.templates import BaseCommand 7 | 8 | 9 | class Command(BaseCommand): 10 | """Define create_emailuser_app command.""" 11 | 12 | requires_system_checks = [] 13 | help = ( 14 | "Creates a Django app with a custom User model that subclasses the" 15 | " User model declared by Django Use Email as Username." 16 | ) 17 | 18 | def add_arguments(self, parser): 19 | """Define arguments for create_emailuser_app command.""" 20 | parser.add_argument( 21 | "name", 22 | nargs="?", 23 | default="custom_user", 24 | help="Optional name of the application or project. [custom_user]", 25 | ) 26 | parser.add_argument( 27 | "directory", nargs="?", help="Optional destination directory" 28 | ) 29 | 30 | def handle(self, name, **options): 31 | """Call "startapp" to generate app with custom user model.""" 32 | options["template"] = ( 33 | os.path.dirname(os.path.abspath(__file__)) + "/app_template" 34 | ) 35 | try: 36 | del options["skip_checks"] 37 | except KeyError: 38 | pass 39 | call_command("startapp", name, **options) 40 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | """Minimum settings to run tests.""" 2 | 3 | # SECURITY WARNING: keep the secret key used in production secret! 4 | SECRET_KEY = "7a%8!3)_=_c04_i@ai1yfw=fz&gf6(b5vp6(@_#h4&n9276vjj" 5 | 6 | DEBUG = True 7 | 8 | INSTALLED_APPS = [ 9 | "django.contrib.admin", 10 | "django.contrib.auth", 11 | "django.contrib.contenttypes", 12 | "django.contrib.sessions", 13 | "django.contrib.messages", 14 | "django_use_email_as_username.apps.DjangoUseEmailAsUsernameConfig", 15 | "custom_user_test.apps.CustomUserTestConfig", 16 | ] 17 | 18 | TEMPLATES = [ 19 | { 20 | "BACKEND": "django.template.backends.django.DjangoTemplates", 21 | "OPTIONS": { 22 | "context_processors": [ 23 | "django.contrib.auth.context_processors.auth", 24 | "django.contrib.messages.context_processors.messages", 25 | "django.template.context_processors.request", 26 | ], 27 | }, 28 | }, 29 | ] 30 | 31 | MIDDLEWARE = [ 32 | "django.contrib.sessions.middleware.SessionMiddleware", 33 | "django.contrib.auth.middleware.AuthenticationMiddleware", 34 | "django.contrib.messages.middleware.MessageMiddleware", 35 | ] 36 | 37 | ROOT_URLCONF = "tests.urls" 38 | 39 | DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}} 40 | 41 | USE_TZ = True 42 | 43 | AUTH_USER_MODEL = "custom_user_test.User" 44 | 45 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 46 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | .. :changelog: 2 | 3 | History 4 | ------- 5 | 6 | 1.4.0 7 | ++++++++++++++++++ 8 | 9 | * Drop support for Python 3.5 and 3.6 10 | * Compatible with Django 4.2 11 | * Replace Poetry with PDM 12 | 13 | 1.3.0 14 | ++++++++++++++++++ 15 | 16 | * Add tests for Python 3.11 17 | * Compatible with Django 4.1 18 | 19 | 20 | 1.2.0 21 | ++++++++++++++++++ 22 | 23 | * Add tests for Python 3.9 24 | * Remove support for deprecated Python 3.4 25 | * Compatible with Django 3.1 26 | * Upgrade build system to poetry-core>=1.0.0 27 | * Remove dependencies version ceiling 28 | * Fix "django40 warning: django.utils.translation.ugettext_lazy() is deprecated" 29 | * Add tests for Django 3.2 30 | * Add tests for Django 4.0 31 | * Add tests for Python 3.10 32 | 33 | 1.1.2 34 | ++++++++++++++++++ 35 | 36 | * Fix distributed build - remove extra unnecessary files. 37 | 38 | 1.1.1 (2020-02-10) 39 | ++++++++++++++++++ 40 | 41 | * Fix `KeyError: 'skip checks'` error `[#5] `_ 42 | 43 | 1.1.0 (2020-01-15) 44 | ++++++++++++++++++ 45 | 46 | * Add tests for Python 3.8 47 | * Compatible with Django 3.0 48 | 49 | 1.0.2 (2019-07-07) 50 | ++++++++++++++++++ 51 | 52 | * Format code with Black 53 | * Fix tests for Django 1.11 54 | * Add tests for Django 2.2 55 | 56 | 1.0.1 (2018-12-30) 57 | ++++++++++++++++++ 58 | 59 | * Add tests for Python 3.7 60 | * Add tests for Django 2.1 61 | * Remove Pipenv 62 | * Change packaging and dependency manager to Poetry 63 | 64 | 1.0.0 (2018-02-18) 65 | ++++++++++++++++++ 66 | 67 | * First release on PyPI. 68 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2018, Federico Jaramillo Martínez 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /django_use_email_as_username/models.py: -------------------------------------------------------------------------------- 1 | """Declare models for DjangoUseEmailAsUsername app.""" 2 | 3 | from django.contrib.auth.models import AbstractUser 4 | from django.contrib.auth.models import BaseUserManager as DjangoBaseUserManager 5 | from django.db import models 6 | from django.utils.translation import gettext_lazy as _ 7 | 8 | 9 | class BaseUserManager(DjangoBaseUserManager): 10 | """Define a model manager for User model with no username field.""" 11 | 12 | use_in_migrations = True 13 | 14 | def _create_user(self, email, password, **extra_fields): 15 | """Create and save a User with the given email and password.""" 16 | if not email: 17 | raise ValueError("The given email must be set") 18 | email = self.normalize_email(email) 19 | user = self.model(email=email, **extra_fields) 20 | user.set_password(password) 21 | user.save(using=self._db) 22 | return user 23 | 24 | def create_user(self, email, password=None, **extra_fields): 25 | """Create and save a regular User with the given email and password.""" 26 | extra_fields.setdefault("is_staff", False) 27 | extra_fields.setdefault("is_superuser", False) 28 | return self._create_user(email, password, **extra_fields) 29 | 30 | def create_superuser(self, email, password, **extra_fields): 31 | """Create and save a SuperUser with the given email and password.""" 32 | extra_fields.setdefault("is_staff", True) 33 | extra_fields.setdefault("is_superuser", True) 34 | 35 | if extra_fields.get("is_staff") is not True: 36 | raise ValueError("Superuser must have is_staff=True.") 37 | if extra_fields.get("is_superuser") is not True: 38 | raise ValueError("Superuser must have is_superuser=True.") 39 | 40 | return self._create_user(email, password, **extra_fields) 41 | 42 | 43 | class BaseUser(AbstractUser): 44 | """User model.""" 45 | 46 | username = None 47 | email = models.EmailField(_("email address"), unique=True) 48 | 49 | USERNAME_FIELD = "email" 50 | REQUIRED_FIELDS = [] 51 | 52 | class Meta: 53 | """Define Meta class for BaseUser model.""" 54 | 55 | abstract = True 56 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | authors = [ 3 | {name = "Federico Jaramillo Martinez", email = "federicojaramillom@gmail.com"}, 4 | ] 5 | requires-python = ">=3.7" 6 | dependencies = [ 7 | "django", 8 | ] 9 | name = "django-use-email-as-username" 10 | version = "1.5.0.dev0" 11 | description = "A Django app to use email as username for user authentication." 12 | readme = "README.rst" 13 | keywords = [ 14 | "django", 15 | "email", 16 | "auth", 17 | "username", 18 | ] 19 | classifiers = [ 20 | "Development Status :: 5 - Production/Stable", 21 | "Framework :: Django", 22 | "Framework :: Django :: 3.2", 23 | "Framework :: Django :: 4.0", 24 | "Framework :: Django :: 4.1", 25 | "Framework :: Django :: 4.2", 26 | "Framework :: Django :: 5.0", 27 | "Framework :: Django :: 5.1", 28 | "Framework :: Django :: 5.2", 29 | "Intended Audience :: Developers", 30 | "Natural Language :: English", 31 | ] 32 | license = {text = "BSD-3-Clause"} 33 | 34 | [project.urls] 35 | repository = "https://github.com/jmfederico/django-use-email-as-username" 36 | 37 | [project.optional-dependencies] 38 | # Abuse extras to make testing easier with TOX 39 | testing = [ 40 | "codecov", 41 | "coverage", 42 | ] 43 | 44 | 45 | [tool.pdm.dev-dependencies] 46 | dev = [ 47 | "sphinx", 48 | "sphinx-autobuild", 49 | "sphinx-rtd-theme", 50 | ] 51 | 52 | [tool.pdm.build] 53 | includes = [ 54 | "django_use_email_as_username", 55 | ] 56 | 57 | [build-system] 58 | requires = ["pdm-backend"] 59 | build-backend = "pdm.backend" 60 | 61 | 62 | [tool.pdm.scripts] 63 | coverage = {shell = """ 64 | coverage run --source django_use_email_as_username runtests.py && \ 65 | coverage report -m && \ 66 | coverage html && \ 67 | open htmlcov/index.html 68 | """, help="check code coverage quickly with the default Python"} 69 | 70 | docs = {shell = """ 71 | rm -f docs/django-use-email-as-username.rst 72 | rm -f docs/modules.rst 73 | make -C docs clean 74 | make -C docs html 75 | open docs/_build/html/index.html 76 | """, help="generate Sphinx HTML documentation"} 77 | 78 | clean_build = {shell = """ 79 | rm -fr build/ 80 | rm -fr dist/ 81 | rm -fr *.egg-info 82 | """, help="remove build artifacts"} 83 | 84 | clean_pyc = {shell = """ 85 | find . -type f -name '*.pyc' -exec rm -f {} + 86 | find . -type f -name '*.pyo' -exec rm -f {} + 87 | find . -type d -name '__pycache__' -exec rm -rf {} + 88 | find . -name '*~' -exec rm -f {} + 89 | """, help="remove Python file artifacts"} 90 | 91 | clean = {composite = ["clean_build", "clean_pyc"]} 92 | -------------------------------------------------------------------------------- /example/custom_user/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.13 on 2020-01-15 17:05 2 | 3 | from django.db import migrations, models 4 | import django.utils.timezone 5 | import django_use_email_as_username.models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ('auth', '0008_alter_user_username_max_length'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='User', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('password', models.CharField(max_length=128, verbose_name='password')), 22 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 23 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), 24 | ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), 25 | ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), 26 | ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), 27 | ('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')), 28 | ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), 29 | ('email', models.EmailField(max_length=254, unique=True, verbose_name='email address')), 30 | ('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')), 31 | ('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')), 32 | ], 33 | options={ 34 | 'abstract': False, 35 | }, 36 | managers=[ 37 | ('objects', django_use_email_as_username.models.BaseUserManager()), 38 | ], 39 | ), 40 | ] 41 | -------------------------------------------------------------------------------- /tests/apps/custom_user_test/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.13 on 2020-01-15 17:05 2 | 3 | from django.db import migrations, models 4 | import django.utils.timezone 5 | import django_use_email_as_username.models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ('auth', '0008_alter_user_username_max_length'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='User', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('password', models.CharField(max_length=128, verbose_name='password')), 22 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 23 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), 24 | ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), 25 | ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), 26 | ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), 27 | ('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')), 28 | ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), 29 | ('email', models.EmailField(max_length=254, unique=True, verbose_name='email address')), 30 | ('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')), 31 | ('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')), 32 | ], 33 | options={ 34 | 'abstract': False, 35 | }, 36 | managers=[ 37 | ('objects', django_use_email_as_username.models.BaseUserManager()), 38 | ], 39 | ), 40 | ] 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/django 2 | # Edit at https://www.gitignore.io/?templates=django 3 | 4 | ### Django ### 5 | *.log 6 | *.pot 7 | *.pyc 8 | __pycache__/ 9 | local_settings.py 10 | db.sqlite3 11 | media 12 | 13 | # If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/ 14 | # in your Git repository. Update and uncomment the following line accordingly. 15 | # /staticfiles/ 16 | 17 | ### Django.Python Stack ### 18 | # Byte-compiled / optimized / DLL files 19 | *.py[cod] 20 | *$py.class 21 | 22 | # C extensions 23 | *.so 24 | 25 | # Distribution / packaging 26 | .Python 27 | build/ 28 | develop-eggs/ 29 | dist/ 30 | downloads/ 31 | eggs/ 32 | .eggs/ 33 | lib/ 34 | lib64/ 35 | parts/ 36 | sdist/ 37 | var/ 38 | wheels/ 39 | pip-wheel-metadata/ 40 | share/python-wheels/ 41 | *.egg-info/ 42 | .installed.cfg 43 | *.egg 44 | MANIFEST 45 | 46 | # PyInstaller 47 | # Usually these files are written by a python script from a template 48 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 49 | *.manifest 50 | *.spec 51 | 52 | # Installer logs 53 | pip-log.txt 54 | pip-delete-this-directory.txt 55 | 56 | # Unit test / coverage reports 57 | htmlcov/ 58 | .tox/ 59 | .nox/ 60 | .coverage 61 | .coverage.* 62 | .cache 63 | nosetests.xml 64 | coverage.xml 65 | *.cover 66 | .hypothesis/ 67 | .pytest_cache/ 68 | 69 | # Translations 70 | *.mo 71 | 72 | # Django stuff: 73 | db.sqlite3-journal 74 | 75 | # Flask stuff: 76 | instance/ 77 | .webassets-cache 78 | 79 | # Scrapy stuff: 80 | .scrapy 81 | 82 | # Sphinx documentation 83 | docs/_build/ 84 | 85 | # PyBuilder 86 | target/ 87 | 88 | # Jupyter Notebook 89 | .ipynb_checkpoints 90 | 91 | # IPython 92 | profile_default/ 93 | ipython_config.py 94 | 95 | # pyenv 96 | .python-version 97 | 98 | # pipenv 99 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 100 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 101 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 102 | # install all needed dependencies. 103 | #Pipfile.lock 104 | 105 | # celery beat schedule file 106 | celerybeat-schedule 107 | 108 | # SageMath parsed files 109 | *.sage.py 110 | 111 | # Environments 112 | .env 113 | .venv 114 | env/ 115 | venv/ 116 | ENV/ 117 | env.bak/ 118 | venv.bak/ 119 | 120 | # Spyder project settings 121 | .spyderproject 122 | .spyproject 123 | 124 | # Rope project settings 125 | .ropeproject 126 | 127 | # mkdocs documentation 128 | /site 129 | 130 | # mypy 131 | .mypy_cache/ 132 | .dmypy.json 133 | dmypy.json 134 | 135 | # Pyre type checker 136 | .pyre/ 137 | 138 | # End of https://www.gitignore.io/api/django 139 | 140 | .vscode 141 | pdm.lock 142 | .pdm-python 143 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Contributing 3 | ============ 4 | 5 | Contributions are welcome, and they are greatly appreciated! Every 6 | little bit helps, and credit will always be given. 7 | 8 | You can contribute in many ways: 9 | 10 | Types of Contributions 11 | ---------------------- 12 | 13 | Report Bugs 14 | ~~~~~~~~~~~ 15 | 16 | Report bugs at https://github.com/jmfederico/django-use-email-as-username/issues. 17 | 18 | If you are reporting a bug, please include: 19 | 20 | * Your operating system name and version. 21 | * Any details about your local setup that might be helpful in troubleshooting. 22 | * Detailed steps to reproduce the bug. 23 | 24 | Fix Bugs 25 | ~~~~~~~~ 26 | 27 | Look through the GitHub issues for bugs. Anything tagged with "bug" 28 | is open to whoever wants to implement it. 29 | 30 | Implement Features 31 | ~~~~~~~~~~~~~~~~~~ 32 | 33 | Look through the GitHub issues for features. Anything tagged with "feature" 34 | is open to whoever wants to implement it. 35 | 36 | Write Documentation 37 | ~~~~~~~~~~~~~~~~~~~ 38 | 39 | Django use Email as Username could always use more documentation, whether as part of the 40 | official Django use Email as Username docs, in docstrings, or even on the web in blog posts, 41 | articles, and such. 42 | 43 | Submit Feedback 44 | ~~~~~~~~~~~~~~~ 45 | 46 | The best way to send feedback is to file an issue at https://github.com/jmfederico/django-use-email-as-username/issues. 47 | 48 | If you are proposing a feature: 49 | 50 | * Explain in detail how it would work. 51 | * Keep the scope as narrow as possible, to make it easier to implement. 52 | * Remember that this is a volunteer-driven project, and that contributions 53 | are welcome :) 54 | 55 | Get Started! 56 | ------------ 57 | 58 | Ready to contribute? Here's how to set up `django-use-email-as-username` for local development. 59 | 60 | 1. Fork the `django-use-email-as-username` repo on GitHub. 61 | 2. Clone your fork locally:: 62 | 63 | $ git clone https://github.com/YOUR_NAME_HERE/django-use-email-as-username.git 64 | 65 | 3. Install for local development:: 66 | 67 | $ cd django-use-email-as-username/ 68 | $ pdm install -G testing 69 | 70 | 4. Create a branch for local development:: 71 | 72 | $ git checkout -b name-of-your-bugfix-or-feature 73 | 74 | Now you can make your changes locally. 75 | 76 | 5. When you're done making changes, run the tests, adding new as required:: 77 | 78 | $ pdm run tox 79 | 80 | 6. Commit your changes and push your branch to GitHub:: 81 | 82 | $ git add . 83 | $ git commit -m "Your detailed description of your changes." 84 | $ git push origin name-of-your-bugfix-or-feature 85 | 86 | 7. Submit a pull request through the GitHub website. 87 | 88 | Pull Request Guidelines 89 | ----------------------- 90 | 91 | Before you submit a pull request, check that it meets these guidelines: 92 | 93 | 1. The pull request should include tests. 94 | 2. If the pull request adds functionality, the docs should be updated. Put 95 | your new functionality into a function with a docstring, and add the 96 | feature to the list in README.rst. 97 | 3. Make sure that the tests pass for all supported Python versions, check 98 | https://github.com/jmfederico/django-use-email-as-username/actions?query=event%3Apull_request 99 | -------------------------------------------------------------------------------- /example/example/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for example project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.0.2. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.0/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = '1)b(#)zui9#b^t3g7$gx!1)^__!e10%5jv&s)kgrx@1p52%7v(' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 41 | 'django_use_email_as_username.apps.DjangoUseEmailAsUsernameConfig', 42 | 'custom_user.apps.CustomUserConfig', 43 | ] 44 | 45 | MIDDLEWARE = [ 46 | 'django.middleware.security.SecurityMiddleware', 47 | 'django.contrib.sessions.middleware.SessionMiddleware', 48 | 'django.middleware.common.CommonMiddleware', 49 | 'django.middleware.csrf.CsrfViewMiddleware', 50 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 51 | 'django.contrib.messages.middleware.MessageMiddleware', 52 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 53 | ] 54 | 55 | ROOT_URLCONF = 'example.urls' 56 | 57 | TEMPLATES = [ 58 | { 59 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 60 | 'DIRS': [], 61 | 'APP_DIRS': True, 62 | 'OPTIONS': { 63 | 'context_processors': [ 64 | 'django.template.context_processors.debug', 65 | 'django.template.context_processors.request', 66 | 'django.contrib.auth.context_processors.auth', 67 | 'django.contrib.messages.context_processors.messages', 68 | ], 69 | }, 70 | }, 71 | ] 72 | 73 | WSGI_APPLICATION = 'example.wsgi.application' 74 | 75 | 76 | # Database 77 | # https://docs.djangoproject.com/en/2.0/ref/settings/#databases 78 | 79 | DATABASES = { 80 | 'default': { 81 | 'ENGINE': 'django.db.backends.sqlite3', 82 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 83 | } 84 | } 85 | 86 | 87 | # Password validation 88 | # https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators 89 | 90 | AUTH_PASSWORD_VALIDATORS = [ 91 | { 92 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 93 | }, 94 | { 95 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 96 | }, 97 | { 98 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 99 | }, 100 | { 101 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 102 | }, 103 | ] 104 | 105 | 106 | # Internationalization 107 | # https://docs.djangoproject.com/en/2.0/topics/i18n/ 108 | 109 | LANGUAGE_CODE = 'en-us' 110 | 111 | TIME_ZONE = 'UTC' 112 | 113 | USE_I18N = True 114 | 115 | USE_L10N = True 116 | 117 | USE_TZ = True 118 | 119 | 120 | # Static files (CSS, JavaScript, Images) 121 | # https://docs.djangoproject.com/en/2.0/howto/static-files/ 122 | 123 | STATIC_URL = '/static/' 124 | 125 | AUTH_USER_MODEL = 'custom_user.User' 126 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ================== 2 | 🔔 PROJECT STATUS 🔔 3 | ================== 4 | Life has taken me to now work in GO_, and do not have the time to actively maintain this project. 5 | 6 | .. _GO: https://go.dev/ 7 | 8 | Which means this project is looking for new maintainer, please `open an issue and postulate yourself`_ if interested. 9 | 10 | 11 | .. _`open an issue and postulate yourself`: https://github.com/jmfederico/django-use-email-as-username/issues/new 12 | 13 | ============================ 14 | Django use Email as Username 15 | ============================ 16 | 17 | .. image:: https://badge.fury.io/py/django-use-email-as-username.svg 18 | :target: https://badge.fury.io/py/django-use-email-as-username 19 | 20 | .. image:: https://github.com/jmfederico/django-use-email-as-username/actions/workflows/tests.yml/badge.svg 21 | :target: https://github.com/jmfederico/django-use-email-as-username/actions/workflows/tests.yml 22 | 23 | .. image:: https://codecov.io/gh/jmfederico/django-use-email-as-username/branch/master/graph/badge.svg 24 | :target: https://codecov.io/gh/jmfederico/django-use-email-as-username 25 | 26 | .. image:: https://img.shields.io/badge/code%20style-black-000000.svg 27 | :target: https://github.com/ambv/black 28 | 29 | A Django app to use email as username for user authentication. 30 | 31 | 32 | Features 33 | -------- 34 | 35 | * Custom User model with no username field 36 | * Use email as username 37 | * Includes a django-admin command for quick install 38 | * Follow Django `best practices`_ for new Django projects and User models. 39 | 40 | .. _`best practices`: https://docs.djangoproject.com/en/dev/topics/auth/customizing/#using-a-custom-user-model-when-starting-a-project 41 | 42 | 43 | Quickstart 44 | ---------- 45 | 46 | #. Install **Django use Email as Username**: 47 | 48 | .. code-block:: shell 49 | 50 | # Run in your terminal 51 | pip install django-use-email-as-username 52 | 53 | #. Add it to your *INSTALLED_APPS*: 54 | 55 | .. code-block:: python 56 | 57 | # In your settings.py file 58 | INSTALLED_APPS = ( 59 | ... 60 | 'django_use_email_as_username.apps.DjangoUseEmailAsUsernameConfig', 61 | ... 62 | ) 63 | 64 | #. Create your new django app: 65 | 66 | .. code-block:: shell 67 | 68 | # Run in your terminal 69 | python manage.py create_custom_user_app 70 | 71 | #. Add the new app to your *INSTALLED_APPS*: 72 | 73 | .. code-block:: python 74 | 75 | # In your settings.py file 76 | INSTALLED_APPS = ( 77 | ... 78 | 'django_use_email_as_username.apps.DjangoUseEmailAsUsernameConfig', 79 | 'custom_user.apps.CustomUserConfig', 80 | ... 81 | ) 82 | 83 | #. Now instruct Django to use your new model: 84 | 85 | .. code-block:: python 86 | 87 | # In your settings.py file 88 | AUTH_USER_MODEL = 'custom_user.User' 89 | 90 | #. Create and run migrations: 91 | 92 | .. code-block:: shell 93 | 94 | # Run in your terminal 95 | python manage.py makemigrations 96 | python manage.py migrate 97 | 98 | You now have a new Django app which provides a custom User model. 99 | 100 | You can further modify the new User Model any time in the future, just remember 101 | to create and run the migrations. 102 | 103 | 104 | Notes 105 | ----- 106 | 107 | This app gives you a custom User model, which is `good practice`_ for new 108 | Django projects. 109 | 110 | `Changing to a custom user model mid-project`_ is not easy. 111 | 112 | .. _`good practice`: https://docs.djangoproject.com/en/dev/topics/auth/customizing/#using-a-custom-user-model-when-starting-a-project 113 | .. _`Changing to a custom user model mid-project`: https://docs.djangoproject.com/en/dev/topics/auth/customizing/#changing-to-a-custom-user-model-mid-project 114 | 115 | It is recommended to always create a custom User model at the beginning of every 116 | Django project. 117 | 118 | Credits 119 | ------- 120 | 121 | Tools used in rendering this package: 122 | 123 | * Cookiecutter_ 124 | * `Cookiecutter Django Package`_ by jmfederico_ 125 | 126 | .. _Cookiecutter: https://github.com/audreyr/cookiecutter 127 | .. _`Cookiecutter Django Package`: https://github.com/jmfederico/cookiecutter-djangopackage 128 | .. _jmfederico: https://github.com/jmfederico 129 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/stable/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | # import os 16 | # import sys 17 | # sys.path.insert(0, os.path.abspath('.')) 18 | 19 | 20 | # -- Project information ----------------------------------------------------- 21 | 22 | import django_use_email_as_username 23 | 24 | project = 'Django use Email as Username' 25 | copyright = 'Federico Jaramillo Martínez' 26 | author = 'Federico Jaramillo Martínez' 27 | 28 | # The short X.Y version 29 | version = django_use_email_as_username.__version__ 30 | # The full version, including alpha/beta/rc tags 31 | release = django_use_email_as_username.__version__ 32 | 33 | 34 | # -- General configuration --------------------------------------------------- 35 | 36 | # If your documentation needs a minimal Sphinx version, state it here. 37 | # 38 | # needs_sphinx = '1.0' 39 | 40 | # Add any Sphinx extension module names here, as strings. They can be 41 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 42 | # ones. 43 | extensions = [ 44 | ] 45 | 46 | # Add any paths that contain templates here, relative to this directory. 47 | templates_path = ['_templates'] 48 | 49 | # The suffix(es) of source filenames. 50 | # You can specify multiple suffix as a list of string: 51 | # 52 | # source_suffix = ['.rst', '.md'] 53 | source_suffix = '.rst' 54 | 55 | # The master toctree document. 56 | master_doc = 'index' 57 | 58 | # The language for content autogenerated by Sphinx. Refer to documentation 59 | # for a list of supported languages. 60 | # 61 | # This is also used if you do content translation via gettext catalogs. 62 | # Usually you set "language" from the command line for these cases. 63 | language = None 64 | 65 | # List of patterns, relative to source directory, that match files and 66 | # directories to ignore when looking for source files. 67 | # This pattern also affects html_static_path and html_extra_path . 68 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 69 | 70 | # The name of the Pygments (syntax highlighting) style to use. 71 | pygments_style = 'sphinx' 72 | 73 | 74 | # -- Options for HTML output ------------------------------------------------- 75 | 76 | # The theme to use for HTML and HTML Help pages. See the documentation for 77 | # a list of builtin themes. 78 | # 79 | html_theme = 'sphinx_rtd_theme' 80 | 81 | # Theme options are theme-specific and customize the look and feel of a theme 82 | # further. For a list of options available for each theme, see the 83 | # documentation. 84 | # 85 | # html_theme_options = {} 86 | 87 | # Add any paths that contain custom static files (such as style sheets) here, 88 | # relative to this directory. They are copied after the builtin static files, 89 | # so a file named "default.css" will overwrite the builtin "default.css". 90 | html_static_path = ['_static'] 91 | 92 | # Custom sidebar templates, must be a dictionary that maps document names 93 | # to template names. 94 | # 95 | # The default sidebars (for documents that don't match any pattern) are 96 | # defined by theme itself. Builtin themes are using these templates by 97 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 98 | # 'searchbox.html']``. 99 | # 100 | # html_sidebars = {} 101 | 102 | 103 | # -- Options for HTMLHelp output --------------------------------------------- 104 | 105 | # Output file base name for HTML help builder. 106 | htmlhelp_basename = 'django-use-email-as-usernamedoc' 107 | 108 | 109 | # -- Options for LaTeX output ------------------------------------------------ 110 | 111 | latex_elements = { 112 | # The paper size ('letterpaper' or 'a4paper'). 113 | # 114 | # 'papersize': 'letterpaper', 115 | 116 | # The font size ('10pt', '11pt' or '12pt'). 117 | # 118 | # 'pointsize': '10pt', 119 | 120 | # Additional stuff for the LaTeX preamble. 121 | # 122 | # 'preamble': '', 123 | 124 | # Latex figure (float) alignment 125 | # 126 | # 'figure_align': 'htbp', 127 | } 128 | 129 | # Grouping the document tree into LaTeX files. List of tuples 130 | # (source start file, target name, title, 131 | # author, documentclass [howto, manual, or own class]). 132 | latex_documents = [ 133 | (master_doc, 'django-use-email-as-username.tex', 'Django use Email as Username Documentation', 134 | 'Federico Jaramillo Martínez', 'manual'), 135 | ] 136 | 137 | 138 | # -- Options for manual page output ------------------------------------------ 139 | 140 | # One entry per manual page. List of tuples 141 | # (source start file, name, description, authors, manual section). 142 | man_pages = [ 143 | (master_doc, 'django-use-email-as-username', 'Django use Email as Username Documentation', 144 | [author], 1) 145 | ] 146 | 147 | 148 | # -- Options for Texinfo output ---------------------------------------------- 149 | 150 | # Grouping the document tree into Texinfo files. List of tuples 151 | # (source start file, target name, title, author, 152 | # dir menu entry, description, category) 153 | texinfo_documents = [ 154 | (master_doc, 'django-use-email-as-username', 'Django use Email as Username Documentation', 155 | author, 'django-use-email-as-username', 'A Django app that uses email as username for user authentication..', 156 | 'Miscellaneous'), 157 | ] 158 | -------------------------------------------------------------------------------- /tests/test.py: -------------------------------------------------------------------------------- 1 | """Define tests for django-use-email-as-username.""" 2 | 3 | import filecmp 4 | import os 5 | import shutil 6 | import sys 7 | import tempfile 8 | from pathlib import Path 9 | from unittest import mock 10 | 11 | from django.core.exceptions import FieldDoesNotExist 12 | from django.core.management import call_command 13 | from django.test import TestCase 14 | 15 | from custom_user_test.models import User 16 | from django_use_email_as_username.admin import BaseUserAdmin 17 | from django_use_email_as_username.models import BaseUserManager 18 | 19 | 20 | def contains_recursive(nl, target): 21 | """Recursive search for an element in list, sets and tuples.""" 22 | if type(nl) is dict: 23 | nl = nl.values() 24 | for thing in nl: 25 | if type(thing) in (list, set, tuple, dict): 26 | if contains_recursive(thing, target): 27 | return True 28 | if thing == target: 29 | return True 30 | return False 31 | 32 | 33 | class TestContainsRecursive(TestCase): 34 | """Quick test for our recursive function.""" 35 | 36 | test_list = [1, "a"] 37 | test_tuple = [2, "b"] 38 | test_set = {3, "c"} 39 | test_dict = {"int": 4, "str": "d"} 40 | 41 | test_nested = { 42 | "list": [test_list, test_tuple, test_set, test_dict], 43 | } 44 | 45 | def test_list_search(self): 46 | """Test it finds elements in lists.""" 47 | self.assertTrue(contains_recursive(self.test_list, 1)) 48 | self.assertFalse(contains_recursive(self.test_list, 10)) 49 | self.assertTrue(contains_recursive(self.test_list, "a")) 50 | self.assertFalse(contains_recursive(self.test_list, "zz")) 51 | 52 | def test_tuple_search(self): 53 | """Test it finds elements in tuples.""" 54 | self.assertTrue(contains_recursive(self.test_tuple, 2)) 55 | self.assertTrue(contains_recursive(self.test_tuple, "b")) 56 | self.assertFalse(contains_recursive(self.test_tuple, 10)) 57 | self.assertFalse(contains_recursive(self.test_tuple, "zz")) 58 | 59 | def test_set_search(self): 60 | """Test it finds elements in sets.""" 61 | self.assertTrue(contains_recursive(self.test_set, 3)) 62 | self.assertTrue(contains_recursive(self.test_set, "c")) 63 | self.assertFalse(contains_recursive(self.test_set, 10)) 64 | self.assertFalse(contains_recursive(self.test_set, "zz")) 65 | 66 | def test_dict_search(self): 67 | """Test it finds elements in dictionaries.""" 68 | self.assertTrue(contains_recursive(self.test_dict, 4)) 69 | self.assertTrue(contains_recursive(self.test_dict, "d")) 70 | self.assertFalse(contains_recursive(self.test_dict, 10)) 71 | self.assertFalse(contains_recursive(self.test_dict, "zz")) 72 | 73 | def test_nested_search(self): 74 | """Test it finds elements in nested structures.""" 75 | self.assertTrue(contains_recursive(self.test_nested, 1)) 76 | self.assertTrue(contains_recursive(self.test_nested, "a")) 77 | self.assertTrue(contains_recursive(self.test_nested, 2)) 78 | self.assertTrue(contains_recursive(self.test_nested, "b")) 79 | self.assertTrue(contains_recursive(self.test_nested, 3)) 80 | self.assertTrue(contains_recursive(self.test_nested, "c")) 81 | self.assertTrue(contains_recursive(self.test_nested, 4)) 82 | self.assertTrue(contains_recursive(self.test_nested, "d")) 83 | self.assertFalse(contains_recursive(self.test_nested, 10)) 84 | self.assertFalse(contains_recursive(self.test_nested, "zz")) 85 | 86 | 87 | class TestAppGeneration(TestCase): 88 | """Test custom app generations.""" 89 | 90 | def setUp(self): 91 | """ 92 | Prepare test environment. 93 | 94 | - Create temp directory to use in tests. 95 | - Avoid `CommandError` from `validate_name: 96 | - Ensure `apps` is not in sys.path to . 97 | - Remove `custom_user_test` from `sys.modules`. 98 | """ 99 | self.test_dir = tempfile.mkdtemp() 100 | self.original_path = sys.path[:] 101 | sys.path.remove(os.path.dirname(os.path.abspath(__file__)) + "/apps") 102 | self.custom_user_test_module = sys.modules.pop("custom_user_test") 103 | 104 | def tearDown(self): 105 | """ 106 | Revert modifications made by the test. 107 | 108 | - Remove temp directory used in tests. 109 | - Reset `sys.path`. 110 | - Add `custom_user_test` back to `sys.modules`. 111 | """ 112 | shutil.rmtree(self.test_dir) 113 | sys.path = self.original_path 114 | sys.modules["custom_user_test"] = self.custom_user_test_module 115 | 116 | def test_custom_app_is_created(self): 117 | """Test that create_custom_user_app command creates the app.""" 118 | call_command( 119 | "create_custom_user_app", "custom_user_test", directory=self.test_dir 120 | ) 121 | custom_user_test_path = ( 122 | os.path.dirname(os.path.abspath(__file__)) + "/apps/custom_user_test" 123 | ) 124 | 125 | base_files = set() 126 | for path in Path(self.test_dir).glob("**/*.py"): 127 | base_files.add(str(path.relative_to(self.test_dir))) 128 | 129 | test_files = set() 130 | for path in Path(custom_user_test_path).glob("**/*.py"): 131 | test_files.add(str(path.relative_to(custom_user_test_path))) 132 | 133 | all_files = base_files.union(test_files) 134 | difference = base_files.symmetric_difference(test_files) 135 | 136 | comparison = filecmp.cmpfiles(self.test_dir, custom_user_test_path, all_files) 137 | 138 | self.assertEqual(difference, {"migrations/0001_initial.py"}) 139 | self.assertEqual(difference, set(comparison[2])) 140 | self.assertEqual(base_files, set(comparison[0])) 141 | 142 | 143 | class TestUserModel(TestCase): 144 | """Test User model.""" 145 | 146 | def test_user_has_no_username(self): 147 | """Test that the user model has no username field.""" 148 | self.assertRaises(FieldDoesNotExist, User._meta.get_field, "username") 149 | 150 | def test_username_field_is_email(self): 151 | """Test that the value of USERNAME_FIELD is email.""" 152 | self.assertEqual(User.USERNAME_FIELD, "email") 153 | 154 | def test_email_is_unique(self): 155 | """Test that the email field is unique.""" 156 | email_field = User._meta.get_field("email") 157 | self.assertTrue(email_field.unique) 158 | 159 | def test_email_is_not_null(self): 160 | """Test that the email field is not null.""" 161 | email_field = User._meta.get_field("email") 162 | self.assertFalse(email_field.null) 163 | 164 | def test_email_is_not_blank(self): 165 | """Test that the email field is not blank.""" 166 | email_field = User._meta.get_field("email") 167 | self.assertFalse(email_field.blank) 168 | 169 | 170 | class TestUserManager(TestCase): 171 | """Test User Manager.""" 172 | 173 | def test_objects(self): 174 | """Test default manager (objects) is BaseUserManager.""" 175 | self.assertIsInstance(User.objects, BaseUserManager) 176 | 177 | def test_create_superuser(self): 178 | """Test a super user is created.""" 179 | user = User.objects.create_superuser("foo@domain.com", "bar") 180 | self.assertTrue(user.is_staff) 181 | self.assertTrue(user.is_superuser) 182 | self.assertEqual(User.objects.last(), user) 183 | 184 | def test_create_superuser_forces_is_staff(self): 185 | """Test is_staff must be true.""" 186 | with self.assertRaises(ValueError): 187 | User.objects.create_superuser("foo@domain.com", "bar", is_staff=False) 188 | 189 | def test_create_superuser_forces_is_superuser(self): 190 | """Test is_superuser must be true.""" 191 | with self.assertRaises(ValueError): 192 | User.objects.create_superuser("foo@domain.com", "bar", is_superuser=False) 193 | 194 | def test_create_user(self): 195 | """Test a user is created.""" 196 | user = User.objects.create_user("foo@domain.com") 197 | self.assertFalse(user.is_staff) 198 | self.assertFalse(user.is_superuser) 199 | self.assertEqual(User.objects.last(), user) 200 | 201 | def test_create_user_respects_is_staff(self): 202 | """Test is_staff value is respected.""" 203 | user = User.objects.create_user("foo@domain.com", is_staff=True) 204 | self.assertTrue(user.is_staff) 205 | 206 | def test_create_user_respects_is_superuser(self): 207 | """Test is_superuser value is respected.""" 208 | user = User.objects.create_user("foo@domain.com", is_superuser=True) 209 | self.assertTrue(user.is_superuser) 210 | 211 | def test_create_user_passes_other_params(self): 212 | """Test is_superuser value is respected.""" 213 | user = User.objects.create_user("foo@domain.com", is_active=False) 214 | self.assertFalse(user.is_active) 215 | 216 | user = User.objects.create_user("bar@domain.com", is_active=True) 217 | self.assertTrue(user.is_active) 218 | 219 | def test_password_is_set(self): 220 | """Test a user gets its password.""" 221 | with self.subTest("For users"): 222 | user = User.objects.create_user("foo@domain.com", "bar") 223 | self.assertTrue(user.check_password("bar")) 224 | 225 | with self.subTest("For superusers"): 226 | user = User.objects.create_superuser("bar@domain.com", "foo") 227 | self.assertTrue(user.check_password("foo")) 228 | 229 | def test_user_is_created_with__create_user(self): 230 | """Test that _create_user is called by create_user.""" 231 | User.objects._create_user = mock.MagicMock() 232 | 233 | User.objects.create_user("foo@domain.com") 234 | self.assertEqual(User.objects._create_user.call_count, 1) 235 | 236 | def test_superuser_is_created_with__create_user(self): 237 | """Test that _create_user is called by create_superuser.""" 238 | User.objects._create_user = mock.MagicMock() 239 | 240 | User.objects.create_superuser("bar@domain.com", "foo") 241 | self.assertEqual(User.objects._create_user.call_count, 1) 242 | 243 | def test_email_is_normalized(self): 244 | """Test that _create_user is called.""" 245 | User.objects.normalize_email = mock.MagicMock( 246 | wraps=User.objects.normalize_email 247 | ) 248 | 249 | user = User.objects._create_user("foo@DOMAIN.com", "bar") 250 | 251 | User.objects.normalize_email.assert_called_once_with("foo@DOMAIN.com") 252 | self.assertEqual(user.email, "foo@domain.com") 253 | 254 | def test_email_is_required(self): 255 | """Test email is required by _create_user.""" 256 | with self.assertRaises(ValueError): 257 | User.objects.create_superuser(False, None) 258 | 259 | with self.assertRaises(ValueError): 260 | User.objects.create_superuser("", None) 261 | 262 | 263 | class TestUserAdmin(TestCase): 264 | """Test User Admin.""" 265 | 266 | def test_fieldset_has_no_username(self): 267 | """Test username is not in the admin filedsets.""" 268 | self.assertFalse(contains_recursive(BaseUserAdmin.fieldsets, "username")) 269 | 270 | def test_fieldset_has_email(self): 271 | """Test email is in the admin filedsets.""" 272 | self.assertTrue(contains_recursive(BaseUserAdmin.fieldsets, "email")) 273 | 274 | def test_add_fieldsets_has_no_username(self): 275 | """Test username is not in the admin add_fieldsets.""" 276 | self.assertFalse(contains_recursive(BaseUserAdmin.add_fieldsets, "username")) 277 | 278 | def test_add_fieldsets_has_email(self): 279 | """Test email is in the admin add_fieldsets.""" 280 | self.assertTrue(contains_recursive(BaseUserAdmin.add_fieldsets, "email")) 281 | 282 | def test_list_display_has_no_username(self): 283 | """Test username is not in the admin list_display.""" 284 | self.assertFalse(contains_recursive(BaseUserAdmin.list_display, "username")) 285 | 286 | def test_list_display_has_email(self): 287 | """Test email is in the admin list_display.""" 288 | self.assertTrue(contains_recursive(BaseUserAdmin.list_display, "email")) 289 | 290 | def test_search_fields_has_no_username(self): 291 | """Test username is not in the admin search_fields.""" 292 | self.assertFalse(contains_recursive(BaseUserAdmin.search_fields, "username")) 293 | 294 | def test_search_fields_has_email(self): 295 | """Test email is in the admin search_fields.""" 296 | self.assertTrue(contains_recursive(BaseUserAdmin.search_fields, "email")) 297 | 298 | def test_ordering_has_no_username(self): 299 | """Test username is not in the admin ordering.""" 300 | self.assertFalse(contains_recursive(BaseUserAdmin.ordering, "username")) 301 | 302 | def test_ordering_has_email(self): 303 | """Test email is in the admin ordering.""" 304 | self.assertTrue(contains_recursive(BaseUserAdmin.ordering, "email")) 305 | --------------------------------------------------------------------------------