Merge branch 'master' into drf-auth-token

This commit is contained in:
Bruno Alla 2020-03-15 19:19:36 +00:00
commit 729cd2adab
25 changed files with 115 additions and 119 deletions

4
.github/FUNDING.yml vendored
View File

@ -1,7 +1,7 @@
# These are supported funding model platforms # These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] github: pydanny
patreon: danielroygreenfeld patreon: roygreenfeld
open_collective: # Replace with a single Open Collective username open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel

View File

@ -15,10 +15,6 @@ matrix:
include: include:
- name: Test results - name: Test results
script: tox -e py37 script: tox -e py37
- name: Run flake8 on result
script: tox -e flake8
- name: Run black on result
script: tox -e black
- name: Black template - name: Black template
script: tox -e black-template script: tox -e black-template
- name: Basic Docker - name: Basic Docker

View File

@ -49,6 +49,7 @@ Listed in alphabetical order.
Adam Dobrawy `@ad-m`_ Adam Dobrawy `@ad-m`_
Adam Steele `@adammsteele`_ Adam Steele `@adammsteele`_
Agam Dua Agam Dua
Agustín Scaramuzza `@scaramagus`_ @scaramagus
Alberto Sanchez `@alb3rto`_ Alberto Sanchez `@alb3rto`_
Alex Tsai `@caffodian`_ Alex Tsai `@caffodian`_
Alvaro [Andor] `@andor-pierdelacabeza`_ Alvaro [Andor] `@andor-pierdelacabeza`_
@ -118,6 +119,7 @@ Listed in alphabetical order.
Garry Cairns `@garry-cairns`_ Garry Cairns `@garry-cairns`_
Garry Polley `@garrypolley`_ Garry Polley `@garrypolley`_
Gilbishkosma `@Gilbishkosma`_ Gilbishkosma `@Gilbishkosma`_
Guilherme Guy `@guilherme1guy`_
Hamish Durkin `@durkode`_ Hamish Durkin `@durkode`_
Hana Quadara `@hanaquadara`_ Hana Quadara `@hanaquadara`_
Harry Moreno `@morenoh149`_ @morenoh149 Harry Moreno `@morenoh149`_ @morenoh149
@ -275,6 +277,7 @@ Listed in alphabetical order.
.. _@dhepper: https://github.com/dhepper .. _@dhepper: https://github.com/dhepper
.. _@dot2dotseurat: https://github.com/dot2dotseurat .. _@dot2dotseurat: https://github.com/dot2dotseurat
.. _@dsclementsen: https://github.com/dsclementsen .. _@dsclementsen: https://github.com/dsclementsen
.. _@guilherme1guy: https://github.com/guilherme1guy
.. _@durkode: https://github.com/durkode .. _@durkode: https://github.com/durkode
.. _@Egregors: https://github.com/Egregors .. _@Egregors: https://github.com/Egregors
.. _@epileptic-fish: https://gihub.com/epileptic-fish .. _@epileptic-fish: https://gihub.com/epileptic-fish
@ -350,6 +353,7 @@ Listed in alphabetical order.
.. _@rolep: https://github.com/rolep .. _@rolep: https://github.com/rolep
.. _@romanosipenko: https://github.com/romanosipenko .. _@romanosipenko: https://github.com/romanosipenko
.. _@saschalalala: https://github.com/saschalalala .. _@saschalalala: https://github.com/saschalalala
.. _@scaramagus: https://github.com/scaramagus
.. _@shireenrao: https://github.com/shireenrao .. _@shireenrao: https://github.com/shireenrao
.. _@show0k: https://github.com/show0k .. _@show0k: https://github.com/show0k
.. _@shultz: https://github.com/shultz .. _@shultz: https://github.com/shultz

View File

@ -1,7 +1,4 @@
[pytest] [pytest]
addopts = -x --tb=short addopts = -v --tb=short
python_paths = . python_paths = .
norecursedirs = .tox .git */migrations/* */static/* docs venv */{{cookiecutter.project_slug}}/* norecursedirs = .tox .git */migrations/* */static/* docs venv */{{cookiecutter.project_slug}}/*
markers =
flake8: Run flake8 on all possible template combinations
black: Run black on all possible template combinations

View File

@ -6,12 +6,12 @@ binaryornot==0.4.4
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
black==19.10b0 black==19.10b0
flake8==3.7.9 flake8==3.7.9
flake8-isort==2.8.0
# Testing # Testing
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
tox==3.14.5 tox==3.14.5
pytest==5.3.5 pytest==5.4.1
pytest_cases==1.12.2
pytest-cookies==0.5.1 pytest-cookies==0.5.1
pytest-xdist==1.31.0 pytest-instafail==0.4.1.post0
pyyaml==5.3 pyyaml==5.3

View File

@ -3,7 +3,6 @@ import re
import pytest import pytest
from cookiecutter.exceptions import FailedHookException from cookiecutter.exceptions import FailedHookException
from pytest_cases import fixture_plus
import sh import sh
import yaml import yaml
from binaryornot.check import is_binary from binaryornot.check import is_binary
@ -26,49 +25,62 @@ def context():
} }
@fixture_plus SUPPORTED_COMBINATIONS = [
@pytest.mark.parametrize("windows", ["y", "n"], ids=lambda yn: f"win:{yn}") {"open_source_license": "MIT"},
@pytest.mark.parametrize("use_docker", ["y", "n"], ids=lambda yn: f"docker:{yn}") {"open_source_license": "BSD"},
@pytest.mark.parametrize("use_celery", ["y", "n"], ids=lambda yn: f"celery:{yn}") {"open_source_license": "GPLv3"},
@pytest.mark.parametrize("use_mailhog", ["y", "n"], ids=lambda yn: f"mailhog:{yn}") {"open_source_license": "Apache Software License 2.0"},
@pytest.mark.parametrize("use_sentry", ["y", "n"], ids=lambda yn: f"sentry:{yn}") {"open_source_license": "Not open source"},
@pytest.mark.parametrize("use_compressor", ["y", "n"], ids=lambda yn: f"cmpr:{yn}") {"windows": "y"},
@pytest.mark.parametrize("use_drf", ["y", "n"], ids=lambda yn: f"drf:{yn}") {"windows": "n"},
@pytest.mark.parametrize( {"use_pycharm": "y"},
"use_whitenoise,cloud_provider", {"use_pycharm": "n"},
[ {"use_docker": "y"},
("y", "AWS"), {"use_docker": "n"},
("y", "GCP"), {"postgresql_version": "11.3"},
("y", "None"), {"postgresql_version": "10.8"},
("n", "AWS"), {"postgresql_version": "9.6"},
("n", "GCP"), {"postgresql_version": "9.5"},
# no whitenoise + no cloud provider is not supported {"postgresql_version": "9.4"},
], {"cloud_provider": "AWS", "use_whitenoise": "y"},
ids=lambda id: f"wnoise:{id[0]}-cloud:{id[1]}", {"cloud_provider": "AWS", "use_whitenoise": "n"},
) {"cloud_provider": "GCP", "use_whitenoise": "y"},
def context_combination( {"cloud_provider": "GCP", "use_whitenoise": "n"},
windows, {"cloud_provider": "None", "use_whitenoise": "y"},
use_docker, # Note: cloud_provider=None AND use_whitenoise=n is not supported
use_celery, {"use_drf": "y"},
use_mailhog, {"use_drf": "n"},
use_sentry, {"js_task_runner": "None"},
use_compressor, {"js_task_runner": "Gulp"},
use_whitenoise, {"custom_bootstrap_compilation": "y"},
use_drf, {"custom_bootstrap_compilation": "n"},
cloud_provider, {"use_compressor": "y"},
): {"use_compressor": "n"},
"""Fixture that parametrize the function where it's used.""" {"use_celery": "y"},
return { {"use_celery": "n"},
"windows": windows, {"use_mailhog": "y"},
"use_docker": use_docker, {"use_mailhog": "n"},
"use_compressor": use_compressor, {"use_sentry": "y"},
"use_celery": use_celery, {"use_sentry": "n"},
"use_mailhog": use_mailhog, {"use_whitenoise": "y"},
"use_sentry": use_sentry, {"use_whitenoise": "n"},
"use_whitenoise": use_whitenoise, {"use_heroku": "y"},
"use_drf": use_drf, {"use_heroku": "n"},
"cloud_provider": cloud_provider, {"ci_tool": "None"},
} {"ci_tool": "Travis"},
{"ci_tool": "Gitlab"},
{"keep_local_envs_in_vcs": "y"},
{"keep_local_envs_in_vcs": "n"},
{"debug": "y"},
{"debug": "n"},
]
UNSUPPORTED_COMBINATIONS = [{"cloud_provider": "None", "use_whitenoise": "n"}]
def _fixture_id(ctx):
"""Helper to get a user friendly test name from the parametrized context."""
return "-".join(f"{key}:{value}" for key, value in ctx.items())
def build_files_list(root_dir): def build_files_list(root_dir):
@ -81,9 +93,7 @@ def build_files_list(root_dir):
def check_paths(paths): def check_paths(paths):
"""Method to check all paths have correct substitutions, """Method to check all paths have correct substitutions."""
used by other tests cases
"""
# Assert that no match is found in any of the files # Assert that no match is found in any of the files
for path in paths: for path in paths:
if is_binary(path): if is_binary(path):
@ -95,13 +105,10 @@ def check_paths(paths):
assert match is None, msg.format(path) assert match is None, msg.format(path)
def test_project_generation(cookies, context, context_combination): @pytest.mark.parametrize("context_override", SUPPORTED_COMBINATIONS, ids=_fixture_id)
""" def test_project_generation(cookies, context, context_override):
Test that project is generated and fully rendered. """Test that project is generated and fully rendered."""
result = cookies.bake(extra_context={**context, **context_override})
This is parametrized for each combination from ``context_combination`` fixture
"""
result = cookies.bake(extra_context={**context, **context_combination})
assert result.exit_code == 0 assert result.exit_code == 0
assert result.exception is None assert result.exception is None
assert result.project.basename == context["project_slug"] assert result.project.basename == context["project_slug"]
@ -112,34 +119,26 @@ def test_project_generation(cookies, context, context_combination):
check_paths(paths) check_paths(paths)
@pytest.mark.flake8 @pytest.mark.parametrize("context_override", SUPPORTED_COMBINATIONS, ids=_fixture_id)
def test_flake8_passes(cookies, context_combination): def test_flake8_passes(cookies, context_override):
""" """Generated project should pass flake8."""
Generated project should pass flake8. result = cookies.bake(extra_context=context_override)
This is parametrized for each combination from ``context_combination`` fixture
"""
result = cookies.bake(extra_context=context_combination)
try: try:
sh.flake8(str(result.project)) sh.flake8(str(result.project))
except sh.ErrorReturnCode as e: except sh.ErrorReturnCode as e:
pytest.fail(e) pytest.fail(e.stdout.decode())
@pytest.mark.black @pytest.mark.parametrize("context_override", SUPPORTED_COMBINATIONS, ids=_fixture_id)
def test_black_passes(cookies, context_combination): def test_black_passes(cookies, context_override):
""" """Generated project should pass black."""
Generated project should pass black. result = cookies.bake(extra_context=context_override)
This is parametrized for each combination from ``context_combination`` fixture
"""
result = cookies.bake(extra_context=context_combination)
try: try:
sh.black("--check", "--diff", "--exclude", "migrations", f"{result.project}/") sh.black("--check", "--diff", "--exclude", "migrations", f"{result.project}/")
except sh.ErrorReturnCode as e: except sh.ErrorReturnCode as e:
pytest.fail(e) pytest.fail(e.stdout.decode())
def test_travis_invokes_pytest(cookies, context): def test_travis_invokes_pytest(cookies, context):
@ -187,9 +186,10 @@ def test_invalid_slug(cookies, context, slug):
assert isinstance(result.exception, FailedHookException) assert isinstance(result.exception, FailedHookException)
def test_no_whitenoise_and_no_cloud_provider(cookies, context): @pytest.mark.parametrize("invalid_context", UNSUPPORTED_COMBINATIONS)
"""It should not generate project if neither whitenoise or cloud provider are set""" def test_error_if_incompatible(cookies, context, invalid_context):
context.update({"use_whitenoise": "n", "cloud_provider": "None"}) """It should not generate project an incompatible combination is selected."""
context.update(invalid_context)
result = cookies.bake(extra_context=context) result = cookies.bake(extra_context=context)
assert result.exit_code != 0 assert result.exit_code != 0

12
tox.ini
View File

@ -1,18 +1,10 @@
[tox] [tox]
skipsdist = true skipsdist = true
envlist = py37,flake8,black,black-template envlist = py37,black-template
[testenv] [testenv]
deps = -rrequirements.txt deps = -rrequirements.txt
commands = pytest -m "not flake8" -m "not black" {posargs:./tests} commands = pytest {posargs:./tests}
[testenv:flake8]
deps = -rrequirements.txt
commands = pytest -m flake8 {posargs:./tests}
[testenv:black]
deps = -rrequirements.txt
commands = pytest -m black {posargs:./tests}
[testenv:black-template] [testenv:black-template]
deps = black deps = black

View File

@ -13,8 +13,8 @@ indent_style = space
indent_size = 4 indent_size = 4
[*.py] [*.py]
line_length = 120 line_length = 88
known_first_party = {{ cookiecutter.project_slug }} known_first_party = {{cookiecutter.project_slug}},config
multi_line_output = 3 multi_line_output = 3
default_section = THIRDPARTY default_section = THIRDPARTY
recursive = true recursive = true

View File

@ -6,6 +6,7 @@ variables:
POSTGRES_USER: '{{ cookiecutter.project_slug }}' POSTGRES_USER: '{{ cookiecutter.project_slug }}'
POSTGRES_PASSWORD: '' POSTGRES_PASSWORD: ''
POSTGRES_DB: 'test_{{ cookiecutter.project_slug }}' POSTGRES_DB: 'test_{{ cookiecutter.project_slug }}'
POSTGRES_HOST_AUTH_METHOD: trust
flake8: flake8:
stage: lint stage: lint

View File

@ -2,4 +2,5 @@ release: python manage.py migrate
web: gunicorn config.wsgi:application web: gunicorn config.wsgi:application
{% if cookiecutter.use_celery == "y" -%} {% if cookiecutter.use_celery == "y" -%}
worker: celery worker --app=config.celery_app --loglevel=info worker: celery worker --app=config.celery_app --loglevel=info
beat: celery beat --app=config.celery_app --loglevel=info
{%- endif %} {%- endif %}

View File

@ -1,6 +1,7 @@
FROM python:3.7-slim-buster FROM python:3.7-slim-buster
ENV PYTHONUNBUFFERED 1 ENV PYTHONUNBUFFERED 1
ENV PYTHONDONTWRITEBYTECODE 1
RUN apt-get update \ RUN apt-get update \
# dependencies for building Python packages # dependencies for building Python packages

View File

@ -1,5 +1,6 @@
from rest_framework.routers import DefaultRouter, SimpleRouter
from django.conf import settings from django.conf import settings
from rest_framework.routers import DefaultRouter, SimpleRouter
from {{ cookiecutter.project_slug }}.users.api.views import UserViewSet from {{ cookiecutter.project_slug }}.users.api.views import UserViewSet
if settings.DEBUG: if settings.DEBUG:

View File

@ -1,4 +1,5 @@
import os import os
from celery import Celery from celery import Celery
# set the default Django settings module for the 'celery' program. # set the default Django settings module for the 'celery' program.

View File

@ -1,3 +1,4 @@
"""isort:skip_file"""
{% if cookiecutter.use_sentry == 'y' -%} {% if cookiecutter.use_sentry == 'y' -%}
import logging import logging

View File

@ -1,10 +1,10 @@
from django.conf import settings from django.conf import settings
from django.urls import include, path
from django.conf.urls.static import static from django.conf.urls.static import static
from django.contrib import admin from django.contrib import admin
from django.views.generic import TemplateView from django.urls import include, path
from django.views import defaults as default_views from django.views import defaults as default_views
{% if cookiecutter.use_drf == 'y' -%} from django.views.generic import TemplateView
{%- if cookiecutter.use_drf == 'y' %}
from rest_framework.authtoken.views import obtain_auth_token from rest_framework.authtoken.views import obtain_auth_token
{%- endif %} {%- endif %}

View File

@ -10,7 +10,7 @@ whitenoise==5.0.1 # https://github.com/evansd/whitenoise
{%- endif %} {%- endif %}
redis==3.4.1 # https://github.com/andymccurdy/redis-py redis==3.4.1 # https://github.com/andymccurdy/redis-py
{%- if cookiecutter.use_celery == "y" %} {%- if cookiecutter.use_celery == "y" %}
celery==4.4.0 # pyup: < 5.0 # https://github.com/celery/celery celery==4.4.1 # pyup: < 5.0 # https://github.com/celery/celery
django-celery-beat==2.0.0 # https://github.com/celery/django-celery-beat django-celery-beat==2.0.0 # https://github.com/celery/django-celery-beat
{%- if cookiecutter.use_docker == 'y' %} {%- if cookiecutter.use_docker == 'y' %}
flower==0.9.3 # https://github.com/mher/flower flower==0.9.3 # https://github.com/mher/flower
@ -19,7 +19,7 @@ flower==0.9.3 # https://github.com/mher/flower
# Django # Django
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
django==2.2.10 # pyup: < 3.0 # https://www.djangoproject.com/ django==2.2.11 # pyup: < 3.0 # https://www.djangoproject.com/
django-environ==0.4.5 # https://github.com/joke2k/django-environ django-environ==0.4.5 # https://github.com/joke2k/django-environ
django-model-utils==4.0.0 # https://github.com/jazzband/django-model-utils django-model-utils==4.0.0 # https://github.com/jazzband/django-model-utils
django-allauth==0.41.0 # https://github.com/pennersr/django-allauth django-allauth==0.41.0 # https://github.com/pennersr/django-allauth

View File

@ -1,8 +1,8 @@
-r ./base.txt -r ./base.txt
Werkzeug==1.0.0 # https://github.com/pallets/werkzeug Werkzeug==1.0.0 # https://github.com/pallets/werkzeug
ipdb==0.13.1 # https://github.com/gotcha/ipdb ipdb==0.13.2 # https://github.com/gotcha/ipdb
Sphinx==2.4.3 # https://github.com/sphinx-doc/sphinx Sphinx==2.4.4 # https://github.com/sphinx-doc/sphinx
{%- if cookiecutter.use_docker == 'y' %} {%- if cookiecutter.use_docker == 'y' %}
psycopg2==2.8.4 --no-binary psycopg2 # https://github.com/psycopg/psycopg2 psycopg2==2.8.4 --no-binary psycopg2 # https://github.com/psycopg/psycopg2
{%- else %} {%- else %}
@ -11,21 +11,22 @@ psycopg2-binary==2.8.4 # https://github.com/psycopg/psycopg2
# Testing # Testing
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
mypy==0.761 # https://github.com/python/mypy mypy==0.770 # https://github.com/python/mypy
django-stubs==1.4.0 # https://github.com/typeddjango/django-stubs django-stubs==1.5.0 # https://github.com/typeddjango/django-stubs
pytest==5.3.5 # https://github.com/pytest-dev/pytest pytest==5.3.5 # https://github.com/pytest-dev/pytest
pytest-sugar==0.9.2 # https://github.com/Frozenball/pytest-sugar pytest-sugar==0.9.2 # https://github.com/Frozenball/pytest-sugar
# Code quality # Code quality
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
flake8==3.7.9 # https://github.com/PyCQA/flake8 flake8==3.7.9 # https://github.com/PyCQA/flake8
flake8-isort==2.8.0 # https://github.com/gforcada/flake8-isort
coverage==5.0.3 # https://github.com/nedbat/coveragepy coverage==5.0.3 # https://github.com/nedbat/coveragepy
black==19.10b0 # https://github.com/ambv/black black==19.10b0 # https://github.com/ambv/black
pylint-django==2.0.14 # https://github.com/PyCQA/pylint-django pylint-django==2.0.14 # https://github.com/PyCQA/pylint-django
{%- if cookiecutter.use_celery == 'y' %} {%- if cookiecutter.use_celery == 'y' %}
pylint-celery==0.3 # https://github.com/PyCQA/pylint-celery pylint-celery==0.3 # https://github.com/PyCQA/pylint-celery
{%- endif %} {%- endif %}
pre-commit==2.1.1 # https://github.com/pre-commit/pre-commit pre-commit==2.2.0 # https://github.com/pre-commit/pre-commit
# Django # Django
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View File

@ -5,7 +5,7 @@
gunicorn==20.0.4 # https://github.com/benoitc/gunicorn gunicorn==20.0.4 # https://github.com/benoitc/gunicorn
psycopg2==2.8.4 --no-binary psycopg2 # https://github.com/psycopg/psycopg2 psycopg2==2.8.4 --no-binary psycopg2 # https://github.com/psycopg/psycopg2
{%- if cookiecutter.use_whitenoise == 'n' %} {%- if cookiecutter.use_whitenoise == 'n' %}
Collectfast==2.0.1 # https://github.com/antonagestam/collectfast Collectfast==2.1.0 # https://github.com/antonagestam/collectfast
{%- endif %} {%- endif %}
{%- if cookiecutter.use_sentry == "y" %} {%- if cookiecutter.use_sentry == "y" %}
sentry-sdk==0.14.2 # https://github.com/getsentry/sentry-python sentry-sdk==0.14.2 # https://github.com/getsentry/sentry-python

View File

@ -1,4 +1,5 @@
from rest_framework import serializers from rest_framework import serializers
from {{ cookiecutter.project_slug }}.users.models import User from {{ cookiecutter.project_slug }}.users.models import User

View File

@ -1,7 +1,7 @@
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from rest_framework import status from rest_framework import status
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.mixins import RetrieveModelMixin, ListModelMixin, UpdateModelMixin from rest_framework.mixins import ListModelMixin, RetrieveModelMixin, UpdateModelMixin
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet from rest_framework.viewsets import GenericViewSet

View File

@ -1,4 +1,4 @@
from django.contrib.auth import get_user_model, forms from django.contrib.auth import forms, get_user_model
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _

View File

@ -1,7 +1,6 @@
import pytest import pytest
from celery.result import EagerResult from celery.result import EagerResult
from {{ cookiecutter.project_slug }}.users.tasks import get_users_count from {{ cookiecutter.project_slug }}.users.tasks import get_users_count
from {{ cookiecutter.project_slug }}.users.tests.factories import UserFactory from {{ cookiecutter.project_slug }}.users.tests.factories import UserFactory

View File

@ -1,5 +1,5 @@
import pytest import pytest
from django.urls import reverse, resolve from django.urls import resolve, reverse
from {{ cookiecutter.project_slug }}.users.models import User from {{ cookiecutter.project_slug }}.users.models import User

View File

@ -1,9 +1,9 @@
from django.urls import path from django.urls import path
from {{ cookiecutter.project_slug }}.users.views import ( from {{ cookiecutter.project_slug }}.users.views import (
user_detail_view,
user_redirect_view, user_redirect_view,
user_update_view, user_update_view,
user_detail_view,
) )
app_name = "users" app_name = "users"

View File

@ -1,9 +1,9 @@
from django.contrib import messages
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse from django.urls import reverse
from django.views.generic import DetailView, RedirectView, UpdateView
from django.contrib import messages
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.views.generic import DetailView, RedirectView, UpdateView
User = get_user_model() User = get_user_model()