diff --git a/.github/contributors.json b/.github/contributors.json index 6fd74677e..ab1575605 100644 --- a/.github/contributors.json +++ b/.github/contributors.json @@ -1202,5 +1202,20 @@ "name": "Bogdan Mateescu", "github_login": "mateesville93", "twitter_username": "" + }, + { + "name": "Fuzzwah", + "github_login": "Fuzzwah", + "twitter_username": "" + }, + { + "name": "Thibault J.", + "github_login": "thibault", + "twitter_username": "thibault" + }, + { + "name": "Pedro Campos", + "github_login": "pcampos119104", + "twitter_username": "" } ] \ No newline at end of file diff --git a/.github/workflows/update-contributors.yml b/.github/workflows/update-contributors.yml index 2e6e65b86..335159444 100644 --- a/.github/workflows/update-contributors.yml +++ b/.github/workflows/update-contributors.yml @@ -26,7 +26,7 @@ jobs: run: python scripts/update_contributors.py - name: Commit changes - uses: stefanzweifel/git-auto-commit-action@v4.12.0 + uses: stefanzweifel/git-auto-commit-action@v4.13.1 with: commit_message: Update Contributors file_pattern: CONTRIBUTORS.md .github/contributors.json diff --git a/CHANGELOG.md b/CHANGELOG.md index d9d07507e..cf4ffefac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,37 @@ All enhancements and patches to Cookiecutter Django will be documented in this f +## 2022.01.14 + +### Updated +- Update uvicorn to 0.17.0 ([#3534](https://github.com/cookiecutter/cookiecutter-django/pull/3534)) +- Bump stefanzweifel/git-auto-commit-action from 4.13.0 to 4.13.1 ([#3532](https://github.com/cookiecutter/cookiecutter-django/pull/3532)) + +## 2022.01.13 + +### Changed +- Add UserSignupForm and UserSocialSignupForm ([#3515](https://github.com/cookiecutter/cookiecutter-django/pull/3515)) +### Fixed +- Fix high CPU usage when running `runserver_plus` in Docker ([#3531](https://github.com/cookiecutter/cookiecutter-django/pull/3531)) +- Fix out-of-sync sequence for Site ID ([#3511](https://github.com/cookiecutter/cookiecutter-django/pull/3511)) + +## 2022.01.11 + +### Updated +- Bump stefanzweifel/git-auto-commit-action from 4.12.0 to 4.13.0 ([#3527](https://github.com/cookiecutter/cookiecutter-django/pull/3527)) + +## 2022.01.10 + +### Updated +- Update django-cors-headers to 3.11.0 ([#3526](https://github.com/cookiecutter/cookiecutter-django/pull/3526)) +- Update sentry-sdk to 1.5.2 ([#3525](https://github.com/cookiecutter/cookiecutter-django/pull/3525)) +- Update gitpython to 3.1.26 ([#3524](https://github.com/cookiecutter/cookiecutter-django/pull/3524)) + +## 2022.01.09 + +### Changed +- Fix broken center align of image links in README ([#3522](https://github.com/cookiecutter/cookiecutter-django/pull/3522)) + ## 2022.01.07 ### Fixed diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index cbeda8503..fe69a718a 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -733,6 +733,13 @@ Listed in alphabetical order. + + Fuzzwah + + Fuzzwah + + + Gabriel Mejia @@ -1342,6 +1349,13 @@ Listed in alphabetical order. + + Pedro Campos + + pcampos119104 + + + Peter Bittner @@ -1531,6 +1545,13 @@ Listed in alphabetical order. + + Thibault J. + + thibault + + thibault + Théo Segonds diff --git a/README.md b/README.md index 26f63fc3c..1e40a4190 100644 --- a/README.md +++ b/README.md @@ -63,13 +63,17 @@ Projects that provide financial support to the maintainers: --- -[![Two Scoops of Django](https://cdn.shopify.com/s/files/1/0304/6901/products/Two-Scoops-of-Django-3-Alpha-Cover_540x_26507b15-e489-470b-8a97-02773dd498d1_1080x.jpg){#Two Scoops of Django 3.x .align-center}](https://www.feldroy.com/products//two-scoops-of-django-3-x) +

+ +

Two Scoops of Django 3.x is the best ice cream-themed Django reference in the universe! ### PyUp -[![pyup](https://pyup.io/static/images/logo.png){#pyup .align-center}](https://pyup.io/) +

+ +

PyUp brings you automated security and dependency updates used by Google and other organizations. Free for open source projects! diff --git a/docs/developing-locally-docker.rst b/docs/developing-locally-docker.rst index 0af97842b..f70363dc9 100644 --- a/docs/developing-locally-docker.rst +++ b/docs/developing-locally-docker.rst @@ -191,16 +191,18 @@ docker The ``container_name`` from the yml file can be used to check on containers with docker commands, for example: :: - $ docker logs worker - $ docker top worker + $ docker logs _local_celeryworker + $ docker top _local_celeryworker +Notice that the ``container_name`` is generated dynamically using your project slug as a prefix + Mailhog ~~~~~~~ When developing locally you can go with MailHog_ for email testing provided ``use_mailhog`` was set to ``y`` on setup. To proceed, -#. make sure ``mailhog`` container is up and running; +#. make sure ``_local_mailhog`` container is up and running; #. open up ``http://127.0.0.1:8025``. diff --git a/docs/requirements.txt b/docs/requirements.txt index ed09babbb..d9286ce24 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1,2 @@ -sphinx==4.3.2 +sphinx==4.4.0 sphinx-rtd-theme==1.0.0 diff --git a/requirements.txt b/requirements.txt index 4ef925e99..426091f9e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,6 +21,6 @@ pyyaml==6.0 # Scripting # ------------------------------------------------------------------------------ PyGithub==1.55 -gitpython==3.1.25 +gitpython==3.1.26 jinja2==3.0.3 requests==2.27.1 diff --git a/setup.py b/setup.py index e86a2cf71..28abc0d47 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ except ImportError: from distutils.core import setup # We use calendar versioning -version = "2022.01.07" +version = "2022.01.14" with open("README.rst") as readme_file: long_description = readme_file.read() diff --git a/{{cookiecutter.project_slug}}/config/settings/base.py b/{{cookiecutter.project_slug}}/config/settings/base.py index 2d4dbdd03..afae0ec4f 100644 --- a/{{cookiecutter.project_slug}}/config/settings/base.py +++ b/{{cookiecutter.project_slug}}/config/settings/base.py @@ -312,8 +312,12 @@ ACCOUNT_EMAIL_REQUIRED = True ACCOUNT_EMAIL_VERIFICATION = "mandatory" # https://django-allauth.readthedocs.io/en/latest/configuration.html ACCOUNT_ADAPTER = "{{cookiecutter.project_slug}}.users.adapters.AccountAdapter" +# https://django-allauth.readthedocs.io/en/latest/forms.html +ACCOUNT_FORMS = {"signup": "{{cookiecutter.project_slug}}.users.forms.UserSignupForm"} # https://django-allauth.readthedocs.io/en/latest/configuration.html SOCIALACCOUNT_ADAPTER = "{{cookiecutter.project_slug}}.users.adapters.SocialAccountAdapter" +# https://django-allauth.readthedocs.io/en/latest/forms.html +SOCIALACCOUNT_FORMS = {"signup": "{{cookiecutter.project_slug}}.users.forms.UserSocialSignupForm"} {% if cookiecutter.use_compressor == 'y' -%} # django-compressor # ------------------------------------------------------------------------------ diff --git a/{{cookiecutter.project_slug}}/local.yml b/{{cookiecutter.project_slug}}/local.yml index 0de5eec8f..e4ce73d9f 100644 --- a/{{cookiecutter.project_slug}}/local.yml +++ b/{{cookiecutter.project_slug}}/local.yml @@ -9,14 +9,14 @@ volumes: {{ cookiecutter.project_slug }}_local_mysql_data: {} {{ cookiecutter.project_slug }}_local_mysql_data_backups: {} {%- endif %} - + services: django:{% if cookiecutter.use_celery == 'y' %} &django{% endif %} build: context: . dockerfile: ./compose/local/django/Dockerfile image: {{ cookiecutter.project_slug }}_local_django - container_name: django + container_name: {{ cookiecutter.project_slug }}_local_django depends_on: {%- if cookiecutter.database_engine == 'postgresql' %} - postgres @@ -50,10 +50,10 @@ services: context: . dockerfile: ./compose/production/postgres/Dockerfile image: {{ cookiecutter.project_slug }}_production_postgres - container_name: postgres + container_name: {{ cookiecutter.project_slug }}_local_postgres volumes: - - local_postgres_data:/var/lib/postgresql/data:Z - - local_postgres_data_backups:/backups:z + - {{ cookiecutter.project_slug }}_local_postgres_data:/var/lib/postgresql/data:Z + - {{ cookiecutter.project_slug }}_local_postgres_data_backups:/backups:z env_file: - ./.envs/.local/.postgres {%- endif %} @@ -73,7 +73,7 @@ services: docs: image: {{ cookiecutter.project_slug }}_local_docs - container_name: docs + container_name: {{ cookiecutter.project_slug }}_local_docs build: context: . dockerfile: ./compose/local/docs/Dockerfile @@ -90,7 +90,7 @@ services: mailhog: image: mailhog/mailhog:v1.0.0 - container_name: mailhog + container_name: {{ cookiecutter.project_slug }}_local_mailhog ports: - "8025:8025" @@ -99,12 +99,12 @@ services: redis: image: redis:6 - container_name: redis + container_name: {{ cookiecutter.project_slug }}_local_redis celeryworker: <<: *django image: {{ cookiecutter.project_slug }}_local_celeryworker - container_name: celeryworker + container_name: {{ cookiecutter.project_slug }}_local_celeryworker depends_on: - redis {%- if cookiecutter.database_engine == 'postgresql' %} @@ -122,7 +122,7 @@ services: celerybeat: <<: *django image: {{ cookiecutter.project_slug }}_local_celerybeat - container_name: celerybeat + container_name: {{ cookiecutter.project_slug }}_local_celerybeat depends_on: - redis {%- if cookiecutter.database_engine == 'postgresql' %} @@ -140,7 +140,7 @@ services: flower: <<: *django image: {{ cookiecutter.project_slug }}_local_flower - container_name: flower + container_name: {{ cookiecutter.project_slug }}_local_flower ports: - "5555:5555" command: /start-flower @@ -153,7 +153,7 @@ services: context: . dockerfile: ./compose/local/node/Dockerfile image: {{ cookiecutter.project_slug }}_local_node - container_name: node + container_name: {{ cookiecutter.project_slug }}_local_node depends_on: - django volumes: diff --git a/{{cookiecutter.project_slug}}/requirements/base.txt b/{{cookiecutter.project_slug}}/requirements/base.txt index 4dcd42605..7e4bcf545 100644 --- a/{{cookiecutter.project_slug}}/requirements/base.txt +++ b/{{cookiecutter.project_slug}}/requirements/base.txt @@ -24,7 +24,7 @@ flower==1.0.0 # https://github.com/mher/flower {%- endif %} {%- endif %} {%- if cookiecutter.use_async == 'y' %} -uvicorn[standard]==0.16.0 # https://github.com/encode/uvicorn +uvicorn[standard]==0.17.0 # https://github.com/encode/uvicorn {%- endif %} # Django @@ -42,5 +42,5 @@ django-redis==5.2.0 # https://github.com/jazzband/django-redis {%- if cookiecutter.use_drf == "y" %} # Django REST Framework djangorestframework==3.13.1 # https://github.com/encode/django-rest-framework -django-cors-headers==3.10.1 # https://github.com/adamchainz/django-cors-headers +django-cors-headers==3.11.0 # https://github.com/adamchainz/django-cors-headers {%- endif %} diff --git a/{{cookiecutter.project_slug}}/requirements/local.txt b/{{cookiecutter.project_slug}}/requirements/local.txt index 00bd56cc1..ee99e04e9 100644 --- a/{{cookiecutter.project_slug}}/requirements/local.txt +++ b/{{cookiecutter.project_slug}}/requirements/local.txt @@ -1,6 +1,6 @@ -r base.txt -Werkzeug==2.0.2 # https://github.com/pallets/werkzeug +Werkzeug[watchdog]==2.0.2 # https://github.com/pallets/werkzeug ipdb==0.13.9 # https://github.com/gotcha/ipdb {%- if cookiecutter.database_engine == "postgresql" %} {%- if cookiecutter.use_docker == 'y' %} @@ -28,7 +28,7 @@ djangorestframework-stubs==1.4.0 # https://github.com/typeddjango/djangorestfra # Documentation # ------------------------------------------------------------------------------ -sphinx==4.3.2 # https://github.com/sphinx-doc/sphinx +sphinx==4.4.0 # https://github.com/sphinx-doc/sphinx sphinx-autobuild==2021.3.14 # https://github.com/GaretJax/sphinx-autobuild # Code quality diff --git a/{{cookiecutter.project_slug}}/requirements/production.txt b/{{cookiecutter.project_slug}}/requirements/production.txt index 25404ed5d..67d0ae0c5 100644 --- a/{{cookiecutter.project_slug}}/requirements/production.txt +++ b/{{cookiecutter.project_slug}}/requirements/production.txt @@ -12,7 +12,7 @@ mysqlclient==2.1.0 # https://github.com/PyMySQL/mysqlclient Collectfast==2.2.0 # https://github.com/antonagestam/collectfast {%- endif %} {%- if cookiecutter.use_sentry == "y" %} -sentry-sdk==1.5.1 # https://github.com/getsentry/sentry-python +sentry-sdk==1.5.2 # https://github.com/getsentry/sentry-python {%- endif %} {%- if cookiecutter.use_docker == "n" and cookiecutter.windows == "y" %} hiredis==2.0.0 # https://github.com/redis/hiredis-py diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/contrib/sites/migrations/0003_set_site_domain_and_name.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/contrib/sites/migrations/0003_set_site_domain_and_name.py index 8f4a8f997..080c734bb 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/contrib/sites/migrations/0003_set_site_domain_and_name.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/contrib/sites/migrations/0003_set_site_domain_and_name.py @@ -7,23 +7,52 @@ from django.conf import settings from django.db import migrations +def _update_or_create_site_with_sequence(site_model, connection, domain, name): + """Update or create the site with default ID and keep the DB sequence in sync.""" + site, created = site_model.objects.update_or_create( + id=settings.SITE_ID, + defaults={ + "domain": domain, + "name": name, + }, + ) + if created: + # We provided the ID explicitly when creating the Site entry, therefore the DB + # sequence to auto-generate them wasn't used and is now out of sync. If we + # don't do anything, we'll get a unique constraint violation the next time a + # site is created. + # To avoid this, we need to manually update DB sequence and make sure it's + # greater than the maximum value. + max_id = site_model.objects.order_by('-id').first().id + with connection.cursor() as cursor: + cursor.execute("SELECT last_value from django_site_id_seq") + (current_id,) = cursor.fetchone() + if current_id <= max_id: + cursor.execute( + "alter sequence django_site_id_seq restart with %s", + [max_id + 1], + ) + + def update_site_forward(apps, schema_editor): """Set site domain and name.""" Site = apps.get_model("sites", "Site") - Site.objects.update_or_create( - id=settings.SITE_ID, - defaults={ - "domain": "{{cookiecutter.domain_name}}", - "name": "{{cookiecutter.project_name}}", - }, + _update_or_create_site_with_sequence( + Site, + schema_editor.connection, + "{{cookiecutter.domain_name}}", + "{{cookiecutter.project_name}}", ) def update_site_backward(apps, schema_editor): """Revert site domain and name to default.""" Site = apps.get_model("sites", "Site") - Site.objects.update_or_create( - id=settings.SITE_ID, defaults={"domain": "example.com", "name": "example.com"} + _update_or_create_site_with_sequence( + Site, + schema_editor.connection, + "example.com", + "example.com", ) diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/admin.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/admin.py index 8e8e3eb2f..6675f483a 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/admin.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/admin.py @@ -3,7 +3,7 @@ from django.contrib.auth import admin as auth_admin from django.contrib.auth import get_user_model from django.utils.translation import gettext_lazy as _ -from {{ cookiecutter.project_slug }}.users.forms import UserChangeForm, UserCreationForm +from {{ cookiecutter.project_slug }}.users.forms import UserAdminChangeForm, UserAdminCreationForm User = get_user_model() @@ -11,8 +11,8 @@ User = get_user_model() @admin.register(User) class UserAdmin(auth_admin.UserAdmin): - form = UserChangeForm - add_form = UserCreationForm + form = UserAdminChangeForm + add_form = UserAdminCreationForm fieldsets = ( (None, {"fields": ("username", "password")}), (_("Personal info"), {"fields": ("name", "email")}), diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/forms.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/forms.py index 80cd97aed..6e1dd9d32 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/forms.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/forms.py @@ -1,3 +1,5 @@ +from allauth.account.forms import SignupForm +from allauth.socialaccount.forms import SignupForm as SocialSignupForm from django.contrib.auth import forms as admin_forms from django.contrib.auth import get_user_model from django.utils.translation import gettext_lazy as _ @@ -5,15 +7,36 @@ from django.utils.translation import gettext_lazy as _ User = get_user_model() -class UserChangeForm(admin_forms.UserChangeForm): +class UserAdminChangeForm(admin_forms.UserChangeForm): class Meta(admin_forms.UserChangeForm.Meta): model = User -class UserCreationForm(admin_forms.UserCreationForm): +class UserAdminCreationForm(admin_forms.UserCreationForm): + """ + Form for User Creation in the Admin Area. + To change user signup, see UserSignupForm and UserSocialSignupForm. + """ + class Meta(admin_forms.UserCreationForm.Meta): model = User error_messages = { "username": {"unique": _("This username has already been taken.")} } + + +class UserSignupForm(SignupForm): + """ + Form that will be rendered on a user sign up section/screen. + Default fields will be added automatically. + Check UserSocialSignupForm for accounts created from social. + """ + + +class UserSocialSignupForm(SocialSignupForm): + """ + Renders the form when user has signed up using social accounts. + Default fields will be added automatically. + See UserSignupForm otherwise. + """ diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/models.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/models.py index 935eee9a2..1f6f61bc0 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/models.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/models.py @@ -5,7 +5,11 @@ from django.utils.translation import gettext_lazy as _ class User(AbstractUser): - """Default user for {{cookiecutter.project_name}}.""" + """ + Default custom user model for {{cookiecutter.project_name}}. + If adding fields that need to be filled at user signup, + check forms.SignupForm and forms.SocialSignupForms accordingly. + """ #: First and last name do not cover name patterns around the globe name = CharField(_("Name of User"), blank=True, max_length=255) diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_forms.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_forms.py index 35db91b4d..e51bb6bf2 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_forms.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_forms.py @@ -4,20 +4,20 @@ Module for all Form Tests. import pytest from django.utils.translation import gettext_lazy as _ -from {{ cookiecutter.project_slug }}.users.forms import UserCreationForm +from {{ cookiecutter.project_slug }}.users.forms import UserAdminCreationForm from {{ cookiecutter.project_slug }}.users.models import User pytestmark = pytest.mark.django_db -class TestUserCreationForm: +class TestUserAdminCreationForm: """ - Test class for all tests related to the UserCreationForm + Test class for all tests related to the UserAdminCreationForm """ def test_username_validation_error_msg(self, user: User): """ - Tests UserCreation Form's unique validator functions correctly by testing: + Tests UserAdminCreation Form's unique validator functions correctly by testing: 1) A new user with an existing username cannot be added. 2) Only 1 error is raised by the UserCreation Form 3) The desired error message is raised @@ -25,7 +25,7 @@ class TestUserCreationForm: # The user already exists, # hence cannot be created. - form = UserCreationForm( + form = UserAdminCreationForm( { "username": user.username, "password1": user.password, diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_views.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_views.py index 7170bfae7..944daca52 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_views.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_views.py @@ -8,7 +8,7 @@ from django.http import HttpRequest, HttpResponseRedirect from django.test import RequestFactory from django.urls import reverse -from {{ cookiecutter.project_slug }}.users.forms import UserChangeForm +from {{ cookiecutter.project_slug }}.users.forms import UserAdminChangeForm from {{ cookiecutter.project_slug }}.users.models import User from {{ cookiecutter.project_slug }}.users.tests.factories import UserFactory from {{ cookiecutter.project_slug }}.users.views import ( @@ -62,7 +62,7 @@ class TestUserUpdateView: view.request = request # Initialize the form - form = UserChangeForm() + form = UserAdminChangeForm() form.cleaned_data = [] view.form_valid(form)