From 624dc3a3a5b0861b88a8cebfc76dc51bb33361d8 Mon Sep 17 00:00:00 2001 From: Jacob Coffee Date: Thu, 12 Sep 2024 14:38:12 -0500 Subject: [PATCH] feat: migration from 2.2 -> 3.2 -> 4.2 (#2520) * feat: initial 2.2 -> 4.2 migration work * feat: minimum dependency changes required to get a live webserver:port log line * test(minutes): fixed minutes test * test(mailing): fixed mailing test * test(codesamples): fixed codesamples tests * test(downloads): fixed downloads tests note: unsure if "valid" * test(pages): fixed pages tests * test(successstories): fixed successstories tests * test(users): fixed users forms tests * test(events): fixed events tests * test(users): fixed users views tests * chore: hide my shame * chore: apply code review * harmonize docker-compose and ci.yml PG versions to prod * Update base-requirements.txt Co-authored-by: Ezio Melotti * chore: remove migrations, rebase latest * chore: apply code review * feat(db): generate migrations * fix: revert allauth version bump Resolves https://github.com/python/pythondotorg/pull/2520#discussion_r1747192645 * fix: revert allauth account middlware used in later versions * chore: bump allauth version * Fixup tests that interact with django-allauth ACCOUNT_PREVENT_ENUMERATION ACCOUNT_PREVENT_ENUMERATION was introduced in django-allauth 0.52.0, and interferes with our expectations. This should probably be turned on! But for now disable it by default to keep the changeset minimal. Allauth _used_ to iterate over users to check for email uniquenss but stopped at some point, so we have to create an EmailAdress object for the user in the relevant test-case for duplicate emails * chore: bump django package version --------- Co-authored-by: Ee Durbin Co-authored-by: Ezio Melotti --- .github/workflows/ci.yml | 2 +- banners/__init__.py | 1 - base-requirements.txt | 21 ++-- blogs/__init__.py | 1 - blogs/admin.py | 4 +- ...0003_alter_relatedblog_creator_and_more.py | 26 +++++ blogs/parser.py | 4 +- boxes/__init__.py | 1 - ..._box_creator_alter_box_last_modified_by.py | 26 +++++ cms/__init__.py | 1 - codesamples/__init__.py | 1 - .../0004_alter_codesample_creator_and_more.py | 26 +++++ codesamples/tests.py | 6 +- community/__init__.py | 1 - ...or_alter_link_last_modified_by_and_more.py | 76 +++++++++++++ community/models.py | 2 +- community/tests/test_managers.py | 6 +- companies/__init__.py | 1 - docker-compose.yml | 2 +- downloads/__init__.py | 1 - ...ator_alter_os_last_modified_by_and_more.py | 46 ++++++++ downloads/tests/test_views.py | 16 +-- events/__init__.py | 1 - events/importer.py | 5 +- ...r_alter_alarm_last_modified_by_and_more.py | 46 ++++++++ jobs/__init__.py | 1 - ...tor_alter_job_last_modified_by_and_more.py | 36 +++++++ jobs/signals.py | 8 +- mailing/tests/__init__.py | 2 +- mailing/tests/forms.py | 13 +++ mailing/tests/models.py | 12 +++ mailing/tests/test_forms.py | 9 +- membership/__init__.py | 1 - minutes/__init__.py | 1 - ..._creator_alter_minutes_last_modified_by.py | 26 +++++ minutes/tests/test_models.py | 10 +- nominations/__init__.py | 1 - pages/__init__.py | 1 - ...age_creator_alter_page_last_modified_by.py | 26 +++++ pages/tests/test_api.py | 6 +- pages/tests/test_models.py | 8 +- peps/__init__.py | 1 - prod-requirements.txt | 4 +- pydotorg/settings/base.py | 19 +++- pydotorg/settings/cabotage.py | 10 +- pydotorg/settings/static.py | 10 +- pydotorg/urls.py | 3 +- pydotorg/urls_api.py | 6 +- sponsors/__init__.py | 1 - sponsors/admin.py | 102 +++++++++++++----- ...nefitfeature_polymorphic_ctype_and_more.py | 47 ++++++++ sponsors/tests/test_api.py | 34 +++--- successstories/__init__.py | 1 - successstories/admin.py | 4 +- ...ry_creator_alter_story_last_modified_by.py | 26 +++++ successstories/tests/test_models.py | 9 +- users/__init__.py | 1 - users/admin.py | 6 +- .../migrations/0015_alter_user_first_name.py | 18 ++++ users/tests/test_forms.py | 18 ++-- users/tests/test_views.py | 6 +- work_groups/__init__.py | 1 - .../0005_alter_workgroup_creator_and_more.py | 26 +++++ 63 files changed, 684 insertions(+), 153 deletions(-) create mode 100644 blogs/migrations/0003_alter_relatedblog_creator_and_more.py create mode 100644 boxes/migrations/0004_alter_box_creator_alter_box_last_modified_by.py create mode 100644 codesamples/migrations/0004_alter_codesample_creator_and_more.py create mode 100644 community/migrations/0005_alter_link_creator_alter_link_last_modified_by_and_more.py create mode 100644 downloads/migrations/0011_alter_os_creator_alter_os_last_modified_by_and_more.py create mode 100644 events/migrations/0008_alter_alarm_creator_alter_alarm_last_modified_by_and_more.py create mode 100644 jobs/migrations/0021_alter_job_creator_alter_job_last_modified_by_and_more.py create mode 100644 mailing/tests/forms.py create mode 100644 mailing/tests/models.py create mode 100644 minutes/migrations/0003_alter_minutes_creator_alter_minutes_last_modified_by.py create mode 100644 pages/migrations/0004_alter_page_creator_alter_page_last_modified_by.py create mode 100644 sponsors/migrations/0103_alter_benefitfeature_polymorphic_ctype_and_more.py create mode 100644 successstories/migrations/0012_alter_story_creator_alter_story_last_modified_by.py create mode 100644 users/migrations/0015_alter_user_first_name.py create mode 100644 work_groups/migrations/0005_alter_workgroup_creator_and_more.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7d0becf27..5bd57ab9c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,7 +5,7 @@ jobs: runs-on: ubuntu-latest services: postgres: - image: postgres:10.1 + image: postgres:15.3 env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres diff --git a/banners/__init__.py b/banners/__init__.py index 010b54570..e69de29bb 100644 --- a/banners/__init__.py +++ b/banners/__init__.py @@ -1 +0,0 @@ -default_app_config = 'banners.apps.BannersAppConfig' diff --git a/base-requirements.txt b/base-requirements.txt index 07f0f5a3f..a86bf74ae 100644 --- a/base-requirements.txt +++ b/base-requirements.txt @@ -1,10 +1,10 @@ dj-database-url==0.5.0 -django-pipeline==2.0.6 -django-sitetree==1.18.0 +django-pipeline==3.0.0 # 3.0.0 is first version that supports Django 4.2 +django-sitetree==1.18.0 # >=1.17.1 is (?) first version that supports Django 4.2 django-apptemplates==1.5 django-admin-interface==0.24.2 django-translation-aliases==0.1.0 -Django==2.2.28 +Django==4.2.16 docutils==0.12 Markdown==3.3.4 cmarkgfm==0.6.0 @@ -22,32 +22,31 @@ chardet==4.0.0 celery[redis]==5.3.6 django-celery-beat==2.5.0 # TODO: We may drop 'django-imagekit' completely. -django-imagekit==4.0.2 +django-imagekit==5.0 # 5.0 is first version that supports Django 4.2 django-haystack==3.2.1 elasticsearch>=7,<8 # TODO: 0.14.0 only supports Django 1.8 and 1.11. -django-tastypie==0.14.3 +django-tastypie==0.14.6 # 0.14.6 is first version that supports Django 4.2 pytz==2021.1 python-dateutil==2.8.2 requests[security]>=2.26.0 -django-honeypot==1.0.1 -django-markupfield==2.0.0 -django-markupfield-helpers==0.1.1 +django-honeypot==1.0.4 # 1.0.4 is first version that supports Django 4.2 +django-markupfield==2.0.1 -django-allauth==0.50.0 +django-allauth==0.57.2 # 0.55.0 is first version that supports Django 4.2 django-waffle==2.2.1 -djangorestframework==3.12.2 +djangorestframework==3.14.0 # 3.14.0 is first version that supports Django 4.1, 4.2 support hasnt been "released" django-filter==2.4.0 django-ordered-model==3.4.3 django-widget-tweaks==1.4.8 django-countries==7.2.1 num2words==0.5.10 -django-polymorphic==3.0.0 +django-polymorphic==3.1.0 # 3.1.0 is first version that supports Django 4.0, unsure if it fully supports 4.2 sorl-thumbnail==12.7.0 django-extensions==3.1.4 django-import-export==2.7.1 diff --git a/blogs/__init__.py b/blogs/__init__.py index 620291c46..e69de29bb 100644 --- a/blogs/__init__.py +++ b/blogs/__init__.py @@ -1 +0,0 @@ -default_app_config = 'blogs.apps.BlogsAppConfig' diff --git a/blogs/admin.py b/blogs/admin.py index 055431ae9..e5fea1cfb 100644 --- a/blogs/admin.py +++ b/blogs/admin.py @@ -10,11 +10,13 @@ class BlogEntryAdmin(admin.ModelAdmin): date_hierarchy = 'pub_date' actions = ['sync_new_entries'] + @admin.action( + description="Sync new blog entries" + ) def sync_new_entries(self, request, queryset): call_command('update_blogs') self.message_user(request, "Blog entries updated.") - sync_new_entries.short_description = "Sync new blog entries" @admin.register(FeedAggregate) diff --git a/blogs/migrations/0003_alter_relatedblog_creator_and_more.py b/blogs/migrations/0003_alter_relatedblog_creator_and_more.py new file mode 100644 index 000000000..9e71084a8 --- /dev/null +++ b/blogs/migrations/0003_alter_relatedblog_creator_and_more.py @@ -0,0 +1,26 @@ +# Generated by Django 4.2.11 on 2024-09-05 17:10 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('blogs', '0002_remove_translations_and_contributors'), + ] + + operations = [ + migrations.AlterField( + model_name='relatedblog', + name='creator', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_creator', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='relatedblog', + name='last_modified_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_modified', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/blogs/parser.py b/blogs/parser.py index 8ac8dc684..fd5e4b54d 100644 --- a/blogs/parser.py +++ b/blogs/parser.py @@ -3,7 +3,7 @@ from django.conf import settings from django.template.loader import render_to_string -from django.utils.timezone import make_aware, utc +from django.utils.timezone import make_aware from boxes.models import Box from .models import BlogEntry, Feed @@ -16,7 +16,7 @@ def get_all_entries(feed_url): for e in d['entries']: published = make_aware( - datetime.datetime(*e['published_parsed'][:7]), timezone=utc + datetime.datetime(*e['published_parsed'][:7]), timezone=datetime.timezone.utc ) entry = { diff --git a/boxes/__init__.py b/boxes/__init__.py index 401a83d2e..e69de29bb 100644 --- a/boxes/__init__.py +++ b/boxes/__init__.py @@ -1 +0,0 @@ -default_app_config = 'boxes.apps.BoxesAppConfig' diff --git a/boxes/migrations/0004_alter_box_creator_alter_box_last_modified_by.py b/boxes/migrations/0004_alter_box_creator_alter_box_last_modified_by.py new file mode 100644 index 000000000..3829382ec --- /dev/null +++ b/boxes/migrations/0004_alter_box_creator_alter_box_last_modified_by.py @@ -0,0 +1,26 @@ +# Generated by Django 4.2.11 on 2024-09-05 17:10 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('boxes', '0003_auto_20171101_2138'), + ] + + operations = [ + migrations.AlterField( + model_name='box', + name='creator', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_creator', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='box', + name='last_modified_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_modified', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/cms/__init__.py b/cms/__init__.py index 92d29195c..e69de29bb 100644 --- a/cms/__init__.py +++ b/cms/__init__.py @@ -1 +0,0 @@ -default_app_config = 'cms.apps.CmsAppConfig' diff --git a/codesamples/__init__.py b/codesamples/__init__.py index f51a992fa..e69de29bb 100644 --- a/codesamples/__init__.py +++ b/codesamples/__init__.py @@ -1 +0,0 @@ -default_app_config = 'codesamples.apps.CodesamplesAppConfig' diff --git a/codesamples/migrations/0004_alter_codesample_creator_and_more.py b/codesamples/migrations/0004_alter_codesample_creator_and_more.py new file mode 100644 index 000000000..0b29294ad --- /dev/null +++ b/codesamples/migrations/0004_alter_codesample_creator_and_more.py @@ -0,0 +1,26 @@ +# Generated by Django 4.2.11 on 2024-09-05 17:10 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('codesamples', '0003_auto_20170821_2000'), + ] + + operations = [ + migrations.AlterField( + model_name='codesample', + name='creator', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_creator', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='codesample', + name='last_modified_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_modified', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/codesamples/tests.py b/codesamples/tests.py index 73c85c164..7ddf51119 100644 --- a/codesamples/tests.py +++ b/codesamples/tests.py @@ -16,9 +16,7 @@ def setUp(self): is_published=False) def test_published(self): - self.assertQuerysetEqual(CodeSample.objects.published(), - ['']) + self.assertQuerySetEqual(CodeSample.objects.published(),[''], transform=repr) def test_draft(self): - self.assertQuerysetEqual(CodeSample.objects.draft(), - ['']) + self.assertQuerySetEqual(CodeSample.objects.draft(),[''], transform=repr) diff --git a/community/__init__.py b/community/__init__.py index bc11cfaf6..e69de29bb 100644 --- a/community/__init__.py +++ b/community/__init__.py @@ -1 +0,0 @@ -default_app_config = 'community.apps.CommunityAppConfig' diff --git a/community/migrations/0005_alter_link_creator_alter_link_last_modified_by_and_more.py b/community/migrations/0005_alter_link_creator_alter_link_last_modified_by_and_more.py new file mode 100644 index 000000000..9372dbf0e --- /dev/null +++ b/community/migrations/0005_alter_link_creator_alter_link_last_modified_by_and_more.py @@ -0,0 +1,76 @@ +# Generated by Django 4.2.11 on 2024-09-05 17:10 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('community', '0001_squashed_0004_auto_20170831_0541'), + ] + + operations = [ + migrations.AlterField( + model_name='link', + name='creator', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_creator', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='link', + name='last_modified_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_modified', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='link', + name='post', + field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='related_%(class)s', to='community.post'), + ), + migrations.AlterField( + model_name='photo', + name='creator', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_creator', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='photo', + name='last_modified_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_modified', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='photo', + name='post', + field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='related_%(class)s', to='community.post'), + ), + migrations.AlterField( + model_name='post', + name='creator', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_creator', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='post', + name='last_modified_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_modified', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='post', + name='meta', + field=models.JSONField(blank=True, default=dict), + ), + migrations.AlterField( + model_name='video', + name='creator', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_creator', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='video', + name='last_modified_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_modified', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='video', + name='post', + field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='related_%(class)s', to='community.post'), + ), + ] diff --git a/community/models.py b/community/models.py index 1e199b590..75ee94cd8 100644 --- a/community/models.py +++ b/community/models.py @@ -1,4 +1,4 @@ -from django.contrib.postgres.fields import JSONField +from django.db.models import JSONField from django.urls import reverse from django.db import models from django.utils.translation import gettext_lazy as _ diff --git a/community/tests/test_managers.py b/community/tests/test_managers.py index 004e5ee2e..8e91e5523 100644 --- a/community/tests/test_managers.py +++ b/community/tests/test_managers.py @@ -16,6 +16,6 @@ def test_post_manager(self): status=Post.STATUS_PUBLIC ) - self.assertQuerysetEqual(Post.objects.all(), [public_post, private_post], lambda x: x) - self.assertQuerysetEqual(Post.objects.public(), [public_post], lambda x: x) - self.assertQuerysetEqual(Post.objects.private(), [private_post], lambda x: x) + self.assertQuerySetEqual(Post.objects.all(), [public_post, private_post], lambda x: x) + self.assertQuerySetEqual(Post.objects.public(), [public_post], lambda x: x) + self.assertQuerySetEqual(Post.objects.private(), [private_post], lambda x: x) diff --git a/companies/__init__.py b/companies/__init__.py index 1a15cc943..e69de29bb 100644 --- a/companies/__init__.py +++ b/companies/__init__.py @@ -1 +0,0 @@ -default_app_config = 'companies.apps.CompaniesAppConfig' diff --git a/docker-compose.yml b/docker-compose.yml index 2e5f8bf16..4406c0ff5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: "3.9" services: postgres: - image: postgres:10-bullseye + image: postgres:15.3-bullseye ports: - "5433:5432" environment: diff --git a/downloads/__init__.py b/downloads/__init__.py index 0f460f952..e69de29bb 100644 --- a/downloads/__init__.py +++ b/downloads/__init__.py @@ -1 +0,0 @@ -default_app_config = 'downloads.apps.DownloadsAppConfig' diff --git a/downloads/migrations/0011_alter_os_creator_alter_os_last_modified_by_and_more.py b/downloads/migrations/0011_alter_os_creator_alter_os_last_modified_by_and_more.py new file mode 100644 index 000000000..368d575c2 --- /dev/null +++ b/downloads/migrations/0011_alter_os_creator_alter_os_last_modified_by_and_more.py @@ -0,0 +1,46 @@ +# Generated by Django 4.2.11 on 2024-09-05 17:10 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('downloads', '0010_releasefile_sbom_spdx2_file'), + ] + + operations = [ + migrations.AlterField( + model_name='os', + name='creator', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_creator', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='os', + name='last_modified_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_modified', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='release', + name='creator', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_creator', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='release', + name='last_modified_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_modified', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='releasefile', + name='creator', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_creator', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='releasefile', + name='last_modified_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_modified', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/downloads/tests/test_views.py b/downloads/tests/test_views.py index 50270c556..e495b9e93 100644 --- a/downloads/tests/test_views.py +++ b/downloads/tests/test_views.py @@ -41,7 +41,7 @@ def test_download_release_detail(self): self.assertEqual(response.status_code, 200) with self.subTest("Release file sizes should be human-readable"): - self.assertInHTML("11.8 MB", response.content.decode()) + self.assertInHTML("11.8 MB", response.content.decode()) url = reverse('download:download_release_detail', kwargs={'release_slug': 'fake_slug'}) response = self.client.get(url) @@ -122,7 +122,7 @@ def test_invalid_token(self): self.assertEqual(response.status_code, 401) url = self.create_url('os') - response = self.client.get(url, HTTP_AUTHORIZATION=self.Authorization_invalid) + response = self.client.get(url, headers={"authorization": self.Authorization_invalid}) # TODO: API v1 returns 200 for a GET request even if token is invalid. # 'StaffAuthorization.read_list` returns 'object_list' unconditionally, # and 'StaffAuthorization.read_detail` returns 'True'. @@ -222,7 +222,7 @@ def test_get_release(self): self.assertEqual(len(content), 4) # Login to get all releases. - response = self.client.get(url, HTTP_AUTHORIZATION=self.Authorization) + response = self.client.get(url, headers={"authorization": self.Authorization}) self.assertEqual(response.status_code, 200) content = self.get_json(response) self.assertEqual(len(content), 5) @@ -258,7 +258,7 @@ def test_post_release(self): response = self.client.get(new_url) # TODO: API v1 returns 401; and API v2 returns 404. self.assertIn(response.status_code, [401, 404]) - response = self.client.get(new_url, HTTP_AUTHORIZATION=self.Authorization) + response = self.client.get(new_url, headers={"authorization": self.Authorization}) self.assertEqual(response.status_code, 200) content = self.get_json(response) self.assertEqual(content['name'], data['name']) @@ -490,15 +490,15 @@ def test_throttling_anon(self): ) def test_throttling_user(self): url = self.create_url('os') - response = self.client.get(url, HTTP_AUTHORIZATION=self.Authorization) + response = self.client.get(url, headers={"authorization": self.Authorization}) self.assertEqual(response.status_code, 200) # Second request should be okay for a user. - response = self.client.get(url, HTTP_AUTHORIZATION=self.Authorization) + response = self.client.get(url, headers={"authorization": self.Authorization}) self.assertEqual(response.status_code, 200) # Third request should return '429 TOO MANY REQUESTS'. - response = self.client.get(url, HTTP_AUTHORIZATION=self.Authorization) + response = self.client.get(url, headers={"authorization": self.Authorization}) self.assertEqual(response.status_code, 429) def test_filter_release_file_delete_by_release(self): @@ -552,6 +552,6 @@ def test_filter_release_file_delete_by_release(self): 'release_file/delete_by_release', filters={'release': self.release_275.pk}, ), - HTTP_AUTHORIZATION=self.Authorization, + headers={"authorization": self.Authorization} ) self.assertEqual(response.status_code, 405) diff --git a/events/__init__.py b/events/__init__.py index 28291bff9..e69de29bb 100644 --- a/events/__init__.py +++ b/events/__init__.py @@ -1 +0,0 @@ -default_app_config = 'events.apps.EventsAppConfig' diff --git a/events/importer.py b/events/importer.py index fa303184b..e47775060 100644 --- a/events/importer.py +++ b/events/importer.py @@ -48,12 +48,13 @@ def import_event(self, event_data): ) defaults = { 'title': title, - 'description': description, - 'description_markup_type': 'html', 'venue': location, 'calendar': self.calendar, } event, _ = Event.objects.update_or_create(uid=uid, defaults=defaults) + event.description.raw = description + event.description.markup_type = "html" + event.save() self.import_occurrence(event, event_data) def fetch(self, url): diff --git a/events/migrations/0008_alter_alarm_creator_alter_alarm_last_modified_by_and_more.py b/events/migrations/0008_alter_alarm_creator_alter_alarm_last_modified_by_and_more.py new file mode 100644 index 000000000..371ae3aae --- /dev/null +++ b/events/migrations/0008_alter_alarm_creator_alter_alarm_last_modified_by_and_more.py @@ -0,0 +1,46 @@ +# Generated by Django 4.2.11 on 2024-09-05 17:10 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('events', '0007_auto_20180705_0352'), + ] + + operations = [ + migrations.AlterField( + model_name='alarm', + name='creator', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_creator', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='alarm', + name='last_modified_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_modified', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='calendar', + name='creator', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_creator', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='calendar', + name='last_modified_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_modified', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='event', + name='creator', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_creator', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='event', + name='last_modified_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_modified', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/jobs/__init__.py b/jobs/__init__.py index 3716a978d..e69de29bb 100644 --- a/jobs/__init__.py +++ b/jobs/__init__.py @@ -1 +0,0 @@ -default_app_config = 'jobs.apps.JobsAppConfig' diff --git a/jobs/migrations/0021_alter_job_creator_alter_job_last_modified_by_and_more.py b/jobs/migrations/0021_alter_job_creator_alter_job_last_modified_by_and_more.py new file mode 100644 index 000000000..a82f65ac9 --- /dev/null +++ b/jobs/migrations/0021_alter_job_creator_alter_job_last_modified_by_and_more.py @@ -0,0 +1,36 @@ +# Generated by Django 4.2.11 on 2024-09-05 17:10 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('jobs', '0020_auto_20191101_1601'), + ] + + operations = [ + migrations.AlterField( + model_name='job', + name='creator', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_creator', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='job', + name='last_modified_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_modified', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='jobreviewcomment', + name='creator', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_creator', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='jobreviewcomment', + name='last_modified_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_modified', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/jobs/signals.py b/jobs/signals.py index 9c470d83a..0317ff716 100644 --- a/jobs/signals.py +++ b/jobs/signals.py @@ -1,10 +1,10 @@ from django.dispatch import Signal # Sent after job offer was submitted for review -job_was_submitted = Signal(providing_args=['job']) +job_was_submitted = Signal() # Sent after job offer was approved -job_was_approved = Signal(providing_args=['approving_user', 'job']) +job_was_approved = Signal() # Sent after job offer was rejected -job_was_rejected = Signal(providing_args=['rejecting_user', 'job']) +job_was_rejected = Signal() # Sent after comment was posted -comment_was_posted = Signal(providing_args=['comment']) +comment_was_posted = Signal() diff --git a/mailing/tests/__init__.py b/mailing/tests/__init__.py index d85108ab5..8d8b4f5c7 100644 --- a/mailing/tests/__init__.py +++ b/mailing/tests/__init__.py @@ -1 +1 @@ -# Create your tests here +"""Tests for the mailing app.""" diff --git a/mailing/tests/forms.py b/mailing/tests/forms.py new file mode 100644 index 000000000..b433adea6 --- /dev/null +++ b/mailing/tests/forms.py @@ -0,0 +1,13 @@ +"""Forms to be used in mailing tests.""" + +from mailing.forms import BaseEmailTemplateForm +from mailing.tests.models import MockEmailTemplate + + +class TestBaseEmailTemplateForm(BaseEmailTemplateForm): + """Base email template form for testing.""" + + class Meta: + """Metaclass for the form.""" + model = MockEmailTemplate + fields = "__all__" diff --git a/mailing/tests/models.py b/mailing/tests/models.py new file mode 100644 index 000000000..917e8dfb9 --- /dev/null +++ b/mailing/tests/models.py @@ -0,0 +1,12 @@ +"""Models to be used in mailing tests.""" + +from mailing.models import BaseEmailTemplate + + +class MockEmailTemplate(BaseEmailTemplate): + """Mock model for BaseEmailTemplate to use in tests.""" + + class Meta: + """Metaclass for MockEmailTemplate to avoid creating a table in the database.""" + app_label = 'mailing' + managed = False diff --git a/mailing/tests/test_forms.py b/mailing/tests/test_forms.py index 1e9165a0c..f7a0c6890 100644 --- a/mailing/tests/test_forms.py +++ b/mailing/tests/test_forms.py @@ -1,6 +1,7 @@ +"""Tests for mailing app forms.""" from django.test import TestCase -from mailing.forms import BaseEmailTemplateForm +from mailing.tests.forms import TestBaseEmailTemplateForm class BaseEmailTemplateFormTests(TestCase): @@ -14,16 +15,16 @@ def setUp(self): def test_validate_required_fields(self): required = set(self.data) - form = BaseEmailTemplateForm(data={}) + form = TestBaseEmailTemplateForm(data={}) self.assertFalse(form.is_valid()) self.assertEqual(required, set(form.errors)) def test_validate_with_correct_data(self): - form = BaseEmailTemplateForm(data=self.data) + form = TestBaseEmailTemplateForm(data=self.data) self.assertTrue(form.is_valid()) def test_invalid_form_if_broken_template_syntax(self): self.data["content"] = "Invalid syntax {% invalid %}" - form = BaseEmailTemplateForm(data=self.data) + form = TestBaseEmailTemplateForm(data=self.data) self.assertFalse(form.is_valid()) self.assertIn("content", form.errors, form.errors) diff --git a/membership/__init__.py b/membership/__init__.py index 2e00144a5..e69de29bb 100644 --- a/membership/__init__.py +++ b/membership/__init__.py @@ -1 +0,0 @@ -default_app_config = 'membership.apps.MembershipAppConfig' diff --git a/minutes/__init__.py b/minutes/__init__.py index 862de63c6..e69de29bb 100644 --- a/minutes/__init__.py +++ b/minutes/__init__.py @@ -1 +0,0 @@ -default_app_config = 'minutes.apps.MinutesAppConfig' diff --git a/minutes/migrations/0003_alter_minutes_creator_alter_minutes_last_modified_by.py b/minutes/migrations/0003_alter_minutes_creator_alter_minutes_last_modified_by.py new file mode 100644 index 000000000..07e512874 --- /dev/null +++ b/minutes/migrations/0003_alter_minutes_creator_alter_minutes_last_modified_by.py @@ -0,0 +1,26 @@ +# Generated by Django 4.2.11 on 2024-09-05 17:10 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('minutes', '0002_auto_20150416_1853'), + ] + + operations = [ + migrations.AlterField( + model_name='minutes', + name='creator', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_creator', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='minutes', + name='last_modified_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_modified', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/minutes/tests/test_models.py b/minutes/tests/test_models.py index fcd9bf571..4b5603641 100644 --- a/minutes/tests/test_models.py +++ b/minutes/tests/test_models.py @@ -21,16 +21,10 @@ def setUp(self): ) def test_draft(self): - self.assertQuerysetEqual( - Minutes.objects.draft(), - [''] - ) + self.assertQuerySetEqual(Minutes.objects.draft(), [''], transform=repr) def test_published(self): - self.assertQuerysetEqual( - Minutes.objects.published(), - [''] - ) + self.assertQuerySetEqual(Minutes.objects.published(), [''], transform=repr) def test_date_methods(self): self.assertEqual(self.m1.get_date_year(), '2012') diff --git a/nominations/__init__.py b/nominations/__init__.py index 368a604a5..e69de29bb 100644 --- a/nominations/__init__.py +++ b/nominations/__init__.py @@ -1 +0,0 @@ -default_app_config = 'nominations.apps.NominationsAppConfig' diff --git a/pages/__init__.py b/pages/__init__.py index 6f1e657eb..e69de29bb 100644 --- a/pages/__init__.py +++ b/pages/__init__.py @@ -1 +0,0 @@ -default_app_config = 'pages.apps.PagesAppConfig' diff --git a/pages/migrations/0004_alter_page_creator_alter_page_last_modified_by.py b/pages/migrations/0004_alter_page_creator_alter_page_last_modified_by.py new file mode 100644 index 000000000..19c5a6082 --- /dev/null +++ b/pages/migrations/0004_alter_page_creator_alter_page_last_modified_by.py @@ -0,0 +1,26 @@ +# Generated by Django 4.2.11 on 2024-09-05 17:10 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('pages', '0003_auto_20230214_2113'), + ] + + operations = [ + migrations.AlterField( + model_name='page', + name='creator', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_creator', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='page', + name='last_modified_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_modified', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/pages/tests/test_api.py b/pages/tests/test_api.py index 4026bac75..1c4cc6184 100644 --- a/pages/tests/test_api.py +++ b/pages/tests/test_api.py @@ -30,7 +30,7 @@ def test_get_published_pages(self): def test_get_all_pages(self): # Login to get all pages. url = self.create_url('page') - response = self.client.get(url, HTTP_AUTHORIZATION=self.Authorization) + response = self.client.get(url, headers={"authorization": self.Authorization}) self.assertEqual(response.status_code, 200) self.assertEqual(len(response.data), 3) @@ -41,7 +41,7 @@ def test_filter_page(self): self.assertEqual(len(response.data), 1) # Login to filter all pages. - response = self.client.get(url, HTTP_AUTHORIZATION=self.Authorization) + response = self.client.get(url, headers={"authorization": self.Authorization}) self.assertEqual(response.status_code, 200) self.assertEqual(len(response.data), 2) @@ -53,7 +53,7 @@ def test_filter_page(self): self.assertEqual(len(response.data), 0) # This should return only unpublished pages. - response = self.client.get(url, HTTP_AUTHORIZATION=self.Authorization) + response = self.client.get(url, headers={"authorization": self.Authorization}) self.assertEqual(response.status_code, 200) self.assertEqual(len(response.data), 1) diff --git a/pages/tests/test_models.py b/pages/tests/test_models.py index 2215cc812..eac62f102 100644 --- a/pages/tests/test_models.py +++ b/pages/tests/test_models.py @@ -8,11 +8,11 @@ class PageModelTests(BasePageTests): - def test_published(self): - self.assertQuerysetEqual(Page.objects.published(), ['']) - def test_draft(self): - self.assertQuerysetEqual(Page.objects.draft(), ['']) + self.assertQuerySetEqual(Page.objects.draft(), [''], transform=repr) + + def test_published(self): + self.assertQuerySetEqual(Page.objects.published(), [''], transform=repr) def test_get_title(self): one = Page.objects.get(path='one') diff --git a/peps/__init__.py b/peps/__init__.py index c71df1925..e69de29bb 100644 --- a/peps/__init__.py +++ b/peps/__init__.py @@ -1 +0,0 @@ -default_app_config = 'peps.apps.PepsAppConfig' diff --git a/prod-requirements.txt b/prod-requirements.txt index a99aad083..a7c4022f1 100644 --- a/prod-requirements.txt +++ b/prod-requirements.txt @@ -3,6 +3,6 @@ gunicorn==22.0.0 raven==6.10.0 # Heroku -Whitenoise==6.0.0 # 6.0.0 is latest version that supports Django 2.2 -django-storages==1.12.3 # 1.12.3 is latest version that supports Django 2.2 +Whitenoise==6.6.0 # 6.4.0 is first version that supports Django 4.2 +django-storages==1.42.2 # 1.42.2 is first version that supports Django 4.2 boto3==1.26.165 diff --git a/pydotorg/settings/base.py b/pydotorg/settings/base.py index a602dff7e..9697a6ea8 100644 --- a/pydotorg/settings/base.py +++ b/pydotorg/settings/base.py @@ -31,6 +31,12 @@ ) } +DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' +"""The default primary key field type for Django models. + +Required during the Django 2.2 -> 4.2 migration. +""" + # celery settings _REDIS_URL = config("REDIS_URL", default="redis://redis:6379/0") @@ -53,7 +59,6 @@ TIME_ZONE = 'UTC' LANGUAGE_CODE = 'en-us' USE_I18N = True -USE_L10N = True USE_TZ = True DATE_FORMAT = 'Y-m-d' @@ -74,7 +79,14 @@ STATICFILES_DIRS = [ os.path.join(BASE, 'static'), ] -STATICFILES_STORAGE = 'pipeline.storage.PipelineStorage' +STORAGES = { + "default": { + "BACKEND": "django.core.files.storage.FileSystemStorage", + }, + "staticfiles": { + "BACKEND": 'pipeline.storage.PipelineStorage', + }, +} STATICFILES_FINDERS = ( 'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.AppDirectoriesFinder', @@ -98,6 +110,8 @@ ACCOUNT_UNIQUE_EMAIL = True ACCOUNT_EMAIL_VERIFICATION = 'mandatory' ACCOUNT_AUTHENTICATION_METHOD = 'username_email' +# TODO: Enable enumeration prevention +ACCOUNT_PREVENT_ENUMERATION = False SOCIALACCOUNT_EMAIL_REQUIRED = True SOCIALACCOUNT_EMAIL_VERIFICATION = True SOCIALACCOUNT_QUERY_EMAIL = True @@ -157,6 +171,7 @@ 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'pages.middleware.PageFallbackMiddleware', 'django.contrib.redirects.middleware.RedirectFallbackMiddleware', + 'allauth.account.middleware.AccountMiddleware', ] AUTH_USER_MODEL = 'users.User' diff --git a/pydotorg/settings/cabotage.py b/pydotorg/settings/cabotage.py index d73beb83c..4661fbf66 100644 --- a/pydotorg/settings/cabotage.py +++ b/pydotorg/settings/cabotage.py @@ -44,8 +44,14 @@ ] + MIDDLEWARE MEDIAFILES_LOCATION = 'media' -DEFAULT_FILE_STORAGE = 'custom_storages.storages.MediaStorage' -STATICFILES_STORAGE = 'custom_storages.storages.PipelineManifestStorage' +STORAGES = { + "default": { + "BACKEND": 'custom_storages.storages.MediaStorage', + }, + "staticfiles": { + "BACKEND": 'custom_storages.storages.PipelineManifestStorage', + }, +} EMAIL_HOST = config('EMAIL_HOST') EMAIL_HOST_USER = config('EMAIL_HOST_USER') diff --git a/pydotorg/settings/static.py b/pydotorg/settings/static.py index 3d93e113e..5dcbf6f92 100644 --- a/pydotorg/settings/static.py +++ b/pydotorg/settings/static.py @@ -21,5 +21,11 @@ ] + MIDDLEWARE MEDIAFILES_LOCATION = 'media' -DEFAULT_FILE_STORAGE = 'custom_storages.storages.MediaStorage' -STATICFILES_STORAGE = 'custom_storages.storages.PipelineManifestStorage' +STORAGES = { + "default": { + "BACKEND": 'custom_storages.storages.MediaStorage', + }, + "staticfiles": { + "BACKEND": 'custom_storages.storages.PipelineManifestStorage', + }, +} diff --git a/pydotorg/urls.py b/pydotorg/urls.py index f6ee8001d..f87ab496b 100644 --- a/pydotorg/urls.py +++ b/pydotorg/urls.py @@ -1,7 +1,8 @@ -from django.conf.urls import handler404, include +from django.conf.urls import handler404 from django.contrib import admin from django.contrib.staticfiles.urls import staticfiles_urlpatterns from django.conf.urls.static import static +from django.urls import include from django.urls import path, re_path from django.views.generic.base import TemplateView from django.conf import settings diff --git a/pydotorg/urls_api.py b/pydotorg/urls_api.py index 0c27699b1..4afc7122e 100644 --- a/pydotorg/urls_api.py +++ b/pydotorg/urls_api.py @@ -1,4 +1,4 @@ -from django.conf.urls import url +from django.urls import re_path from rest_framework import routers from tastypie.api import Api @@ -22,6 +22,6 @@ router.register(r'downloads/release_file', ReleaseFileViewSet) urlpatterns = [ - url(r'sponsors/logo-placement/', LogoPlacementeAPIList.as_view(), name="logo_placement_list"), - url(r'sponsors/sponsorship-assets/', SponsorshipAssetsAPIList.as_view(), name="assets_list"), + re_path(r'sponsors/logo-placement/', LogoPlacementeAPIList.as_view(), name="logo_placement_list"), + re_path(r'sponsors/sponsorship-assets/', SponsorshipAssetsAPIList.as_view(), name="assets_list"), ] diff --git a/sponsors/__init__.py b/sponsors/__init__.py index 016e79088..e69de29bb 100644 --- a/sponsors/__init__.py +++ b/sponsors/__init__.py @@ -1 +0,0 @@ -default_app_config = 'sponsors.apps.SponsorsAppConfig' diff --git a/sponsors/admin.py b/sponsors/admin.py index e16cffbc6..dc7278c08 100644 --- a/sponsors/admin.py +++ b/sponsors/admin.py @@ -38,18 +38,22 @@ class AssetsInline(GenericTabularInline): has_delete_permission = lambda self, request, obj: False readonly_fields = ["internal_name", "user_submitted_info", "value"] + @admin.display( + description="Submitted information" + ) def value(self, obj=None): if not obj or not obj.value: return "" return obj.value - value.short_description = "Submitted information" + @admin.display( + description="Fullfilled data?", + boolean=True, + ) def user_submitted_info(self, obj=None): return bool(self.value(obj)) - user_submitted_info.short_description = "Fullfilled data?" - user_submitted_info.boolean = True @admin.register(SponsorshipProgram) @@ -235,10 +239,12 @@ class SponsorshipsInline(admin.TabularInline): can_delete = False extra = 0 + @admin.display( + description="ID" + ) def link(self, obj): url = reverse("admin:sponsors_sponsorship_change", args=[obj.id]) return mark_safe(f"{obj.id}") - link.short_description = "ID" @admin.register(Sponsor) @@ -489,10 +495,12 @@ def get_queryset(self, *args, **kwargs): qs = super().get_queryset(*args, **kwargs) return qs.select_related("sponsor", "package", "submited_by") + @admin.action( + description='Send notifications to selected' + ) def send_notifications(self, request, queryset): return views_admin.send_sponsorship_notifications_action(self, request, queryset) - send_notifications.short_description = 'Send notifications to selected' def get_readonly_fields(self, request, obj): readonly_fields = [ @@ -528,11 +536,16 @@ def get_readonly_fields(self, request, obj): return readonly_fields + @admin.display( + description="Sponsor" + ) def sponsor_link(self, obj): url = reverse("admin:sponsors_sponsor_change", args=[obj.sponsor.id]) return mark_safe(f"{obj.sponsor.name}") - sponsor_link.short_description = "Sponsor" + @admin.display( + description="Estimated cost" + ) def get_estimated_cost(self, obj): cost = None html = "This sponsorship has not customizations so there's no estimated cost" @@ -542,8 +555,10 @@ def get_estimated_cost(self, obj): html = f"{cost} USD
Important: {msg}" return mark_safe(html) - get_estimated_cost.short_description = "Estimated cost" + @admin.display( + description="Contract" + ) def get_contract(self, obj): if not obj.contract: return "---" @@ -551,7 +566,6 @@ def get_contract(self, obj): html = f"{obj.contract}" return mark_safe(html) - get_contract.short_description = "Contract" def get_urls(self): urls = super().get_urls() @@ -597,21 +611,30 @@ def get_urls(self): ] return my_urls + urls + @admin.display( + description="Name" + ) def get_sponsor_name(self, obj): return obj.sponsor.name - get_sponsor_name.short_description = "Name" + @admin.display( + description="Description" + ) def get_sponsor_description(self, obj): return obj.sponsor.description - get_sponsor_description.short_description = "Description" + @admin.display( + description="Landing Page URL" + ) def get_sponsor_landing_page_url(self, obj): return obj.sponsor.landing_page_url - get_sponsor_landing_page_url.short_description = "Landing Page URL" + @admin.display( + description="Web Logo" + ) def get_sponsor_web_logo(self, obj): html = "{% load thumbnail %}{% thumbnail sponsor.web_logo '150x150' format='PNG' quality=100 as im %}{% endthumbnail %}" template = Template(html) @@ -619,8 +642,10 @@ def get_sponsor_web_logo(self, obj): html = template.render(context) return mark_safe(html) - get_sponsor_web_logo.short_description = "Web Logo" + @admin.display( + description="Print Logo" + ) def get_sponsor_print_logo(self, obj): img = obj.sponsor.print_logo html = "" @@ -631,13 +656,17 @@ def get_sponsor_print_logo(self, obj): html = template.render(context) return mark_safe(html) if html else "---" - get_sponsor_print_logo.short_description = "Print Logo" + @admin.display( + description="Primary Phone" + ) def get_sponsor_primary_phone(self, obj): return obj.sponsor.primary_phone - get_sponsor_primary_phone.short_description = "Primary Phone" + @admin.display( + description="Mailing/Billing Address" + ) def get_sponsor_mailing_address(self, obj): sponsor = obj.sponsor city_row = ( @@ -655,8 +684,10 @@ def get_sponsor_mailing_address(self, obj): html += f"

{sponsor.postal_code}

" return mark_safe(html) - get_sponsor_mailing_address.short_description = "Mailing/Billing Address" + @admin.display( + description="Contacts" + ) def get_sponsor_contacts(self, obj): html = "" contacts = obj.sponsor.contacts.all() @@ -676,8 +707,10 @@ def get_sponsor_contacts(self, obj): html += "" return mark_safe(html) - get_sponsor_contacts.short_description = "Contacts" + @admin.display( + description="Added by User" + ) def get_custom_benefits_added_by_user(self, obj): benefits = obj.user_customizations["added_by_user"] if not benefits: @@ -688,8 +721,10 @@ def get_custom_benefits_added_by_user(self, obj): ) return mark_safe(html) - get_custom_benefits_added_by_user.short_description = "Added by User" + @admin.display( + description="Removed by User" + ) def get_custom_benefits_removed_by_user(self, obj): benefits = obj.user_customizations["removed_by_user"] if not benefits: @@ -700,7 +735,6 @@ def get_custom_benefits_removed_by_user(self, obj): ) return mark_safe(html) - get_custom_benefits_removed_by_user.short_description = "Removed by User" def rollback_to_editing_view(self, request, pk): return views_admin.rollback_to_editing_view(self, request, pk) @@ -747,6 +781,9 @@ def get_urls(self): ] return my_urls + urls + @admin.display( + description="Links" + ) def links(self, obj): clone_form = CloneApplicationConfigForm() configured_years = clone_form.configured_years @@ -768,8 +805,10 @@ def links(self, obj): html += f"
  • {preview_label}" html += "" return mark_safe(html) - links.short_description = "Links" + @admin.display( + description="Other configured years" + ) def other_years(self, obj): clone_form = CloneApplicationConfigForm() configured_years = clone_form.configured_years @@ -800,7 +839,6 @@ def other_years(self, obj): html += "
  • " html += "" return mark_safe(html) - other_years.short_description = "Other configured years" def clone_application_config(self, request): return views_admin.clone_application_config(self, request) @@ -828,10 +866,12 @@ def get_queryset(self, *args, **kwargs): qs = super().get_queryset(*args, **kwargs) return qs.select_related("sponsorship__sponsor") + @admin.display( + description="Revision" + ) def get_revision(self, obj): return obj.revision if obj.is_draft else "Final" - get_revision.short_description = "Revision" fieldsets = [ ( @@ -899,6 +939,9 @@ def get_readonly_fields(self, request, obj): return readonly_fields + @admin.display( + description="Contract document" + ) def document_link(self, obj): html, url, msg = "---", "", "" @@ -916,8 +959,10 @@ def document_link(self, obj): html = f'{msg}' return mark_safe(html) - document_link.short_description = "Contract document" + @admin.display( + description="Sponsorship" + ) def get_sponsorship_url(self, obj): if not obj.sponsorship: return "---" @@ -925,7 +970,6 @@ def get_sponsorship_url(self, obj): html = f"{obj.sponsorship}" return mark_safe(html) - get_sponsorship_url.short_description = "Sponsorship" def get_urls(self): urls = super().get_urls() @@ -1090,14 +1134,19 @@ def all_sponsorships(self): qs = Sponsorship.objects.all().select_related("package", "sponsor") return {sp.id: sp for sp in qs} + @admin.display( + description="Value" + ) def get_value(self, obj): html = obj.value if obj.value and getattr(obj.value, "url", None): html = f"{obj.value}" return mark_safe(html) - get_value.short_description = "Value" + @admin.display( + description="Associated with" + ) def get_related_object(self, obj): """ Returns the content_object as an URL and performs better because @@ -1115,11 +1164,12 @@ def get_related_object(self, obj): html = f"{content_object}" return mark_safe(html) - get_related_object.short_description = "Associated with" + @admin.action( + description="Export selected" + ) def export_assets_as_zipfile(self, request, queryset): return views_admin.export_assets_as_zipfile(self, request, queryset) - export_assets_as_zipfile.short_description = "Export selected" class GenericAssetChildModelAdmin(PolymorphicChildModelAdmin): diff --git a/sponsors/migrations/0103_alter_benefitfeature_polymorphic_ctype_and_more.py b/sponsors/migrations/0103_alter_benefitfeature_polymorphic_ctype_and_more.py new file mode 100644 index 000000000..e9eb9e3a2 --- /dev/null +++ b/sponsors/migrations/0103_alter_benefitfeature_polymorphic_ctype_and_more.py @@ -0,0 +1,47 @@ +# Generated by Django 4.2.11 on 2024-09-05 17:10 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('sponsors', '0102_auto_20240509_2037'), + ] + + operations = [ + migrations.AlterField( + model_name='benefitfeature', + name='polymorphic_ctype', + field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype'), + ), + migrations.AlterField( + model_name='benefitfeatureconfiguration', + name='polymorphic_ctype', + field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype'), + ), + migrations.AlterField( + model_name='genericasset', + name='polymorphic_ctype', + field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype'), + ), + migrations.AlterField( + model_name='sponsor', + name='creator', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_creator', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='sponsor', + name='last_modified_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_modified', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='sponsorshipbenefit', + name='conflicts', + field=models.ManyToManyField(blank=True, help_text='For benefits that conflict with one another,', to='sponsors.sponsorshipbenefit', verbose_name='Conflicts'), + ), + ] diff --git a/sponsors/tests/test_api.py b/sponsors/tests/test_api.py index caabd6aa1..3575e59e6 100644 --- a/sponsors/tests/test_api.py +++ b/sponsors/tests/test_api.py @@ -41,7 +41,7 @@ def tearDown(self): sponsor.print_logo.delete() def test_list_logo_placement_as_expected(self): - response = self.client.get(self.url, HTTP_AUTHORIZATION=self.authorization) + response = self.client.get(self.url, headers={"authorization": self.authorization}) data = response.json() self.assertEqual(200, response.status_code) @@ -71,7 +71,7 @@ def test_list_logo_placement_as_expected(self): def test_invalid_token(self): Token.objects.all().delete() - response = self.client.get(self.url, HTTP_AUTHORIZATION=self.authorization) + response = self.client.get(self.url, headers={"authorization": self.authorization}) self.assertEqual(401, response.status_code) def test_superuser_user_have_permission_by_default(self): @@ -79,19 +79,19 @@ def test_superuser_user_have_permission_by_default(self): self.user.is_superuser = True self.user.is_staff = True self.user.save() - response = self.client.get(self.url, HTTP_AUTHORIZATION=self.authorization) + response = self.client.get(self.url, headers={"authorization": self.authorization}) self.assertEqual(200, response.status_code) def test_staff_have_permission_by_default(self): self.user.user_permissions.remove(self.permission) self.user.is_staff = True self.user.save() - response = self.client.get(self.url, HTTP_AUTHORIZATION=self.authorization) + response = self.client.get(self.url, headers={"authorization": self.authorization}) self.assertEqual(200, response.status_code) def test_user_must_have_required_permission(self): self.user.user_permissions.remove(self.permission) - response = self.client.get(self.url, HTTP_AUTHORIZATION=self.authorization) + response = self.client.get(self.url, headers={"authorization": self.authorization}) self.assertEqual(403, response.status_code) def test_filter_sponsorship_by_publisher(self): @@ -99,7 +99,7 @@ def test_filter_sponsorship_by_publisher(self): "publisher": PublisherChoices.PYPI.value, }) url = f"{self.url}?{querystring}" - response = self.client.get(url, HTTP_AUTHORIZATION=self.authorization) + response = self.client.get(url, headers={"authorization": self.authorization}) data = response.json() self.assertEqual(200, response.status_code) @@ -111,7 +111,7 @@ def test_filter_sponsorship_by_flight(self): "flight": LogoPlacementChoices.SIDEBAR.value, }) url = f"{self.url}?{querystring}" - response = self.client.get(url, HTTP_AUTHORIZATION=self.authorization) + response = self.client.get(url, headers={"authorization": self.authorization}) data = response.json() self.assertEqual(200, response.status_code) @@ -125,7 +125,7 @@ def test_bad_request_for_invalid_filters(self): "publisher": "invalid-publisher" }) url = f"{self.url}?{querystring}" - response = self.client.get(url, HTTP_AUTHORIZATION=self.authorization) + response = self.client.get(url, headers={"authorization": self.authorization}) data = response.json() self.assertEqual(400, response.status_code) @@ -162,7 +162,7 @@ def tearDown(self): def test_invalid_token(self): Token.objects.all().delete() - response = self.client.get(self.url, HTTP_AUTHORIZATION=self.authorization) + response = self.client.get(self.url, headers={"authorization": self.authorization}) self.assertEqual(401, response.status_code) def test_superuser_user_have_permission_by_default(self): @@ -170,30 +170,30 @@ def test_superuser_user_have_permission_by_default(self): self.user.is_superuser = True self.user.is_staff = True self.user.save() - response = self.client.get(self.url, HTTP_AUTHORIZATION=self.authorization) + response = self.client.get(self.url, headers={"authorization": self.authorization}) self.assertEqual(200, response.status_code) def test_staff_have_permission_by_default(self): self.user.user_permissions.remove(self.permission) self.user.is_staff = True self.user.save() - response = self.client.get(self.url, HTTP_AUTHORIZATION=self.authorization) + response = self.client.get(self.url, headers={"authorization": self.authorization}) self.assertEqual(200, response.status_code) def test_user_must_have_required_permission(self): self.user.user_permissions.remove(self.permission) - response = self.client.get(self.url, HTTP_AUTHORIZATION=self.authorization) + response = self.client.get(self.url, headers={"authorization": self.authorization}) self.assertEqual(403, response.status_code) def test_bad_request_if_no_internal_name(self): url = reverse_lazy("assets_list") - response = self.client.get(url, HTTP_AUTHORIZATION=self.authorization) + response = self.client.get(url, headers={"authorization": self.authorization}) self.assertEqual(400, response.status_code) self.assertIn("internal_name", response.json()) def test_list_assets_by_internal_name(self): # by default exclude assets with no value - response = self.client.get(self.url, HTTP_AUTHORIZATION=self.authorization) + response = self.client.get(self.url, headers={"authorization": self.authorization}) data = response.json() self.assertEqual(200, response.status_code) self.assertEqual(0, len(data)) @@ -202,7 +202,7 @@ def test_list_assets_by_internal_name(self): self.txt_asset.value = "Text Content" self.txt_asset.save() - response = self.client.get(self.url, HTTP_AUTHORIZATION=self.authorization) + response = self.client.get(self.url, headers={"authorization": self.authorization}) data = response.json() self.assertEqual(1, len(data)) @@ -216,7 +216,7 @@ def test_list_assets_by_internal_name(self): def test_enable_to_filter_by_assets_with_no_value_via_querystring(self): self.url += "&list_empty=true" - response = self.client.get(self.url, HTTP_AUTHORIZATION=self.authorization) + response = self.client.get(self.url, headers={"authorization": self.authorization}) data = response.json() self.assertEqual(1, len(data)) @@ -230,7 +230,7 @@ def test_serialize_img_value_as_url_to_image(self): self.img_asset.save() url = reverse_lazy("assets_list") + f"?internal_name={self.img_asset.internal_name}" - response = self.client.get(url, HTTP_AUTHORIZATION=self.authorization) + response = self.client.get(url, headers={"authorization": self.authorization}) data = response.json() self.assertEqual(1, len(data)) diff --git a/successstories/__init__.py b/successstories/__init__.py index e2c2c1446..e69de29bb 100644 --- a/successstories/__init__.py +++ b/successstories/__init__.py @@ -1 +0,0 @@ -default_app_config = 'successstories.apps.SuccessstoriesAppConfig' diff --git a/successstories/admin.py b/successstories/admin.py index fdde8878c..bc15d2d11 100644 --- a/successstories/admin.py +++ b/successstories/admin.py @@ -24,6 +24,8 @@ def get_list_display(self, request): fields = list(super().get_list_display(request)) return fields + ['show_link', 'is_published', 'featured'] + @admin.display( + description='View on site' + ) def show_link(self, obj): return format_html(f'\U0001F517') - show_link.short_description = 'View on site' diff --git a/successstories/migrations/0012_alter_story_creator_alter_story_last_modified_by.py b/successstories/migrations/0012_alter_story_creator_alter_story_last_modified_by.py new file mode 100644 index 000000000..dee246421 --- /dev/null +++ b/successstories/migrations/0012_alter_story_creator_alter_story_last_modified_by.py @@ -0,0 +1,26 @@ +# Generated by Django 4.2.11 on 2024-09-05 17:10 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('successstories', '0011_auto_20220127_1923'), + ] + + operations = [ + migrations.AlterField( + model_name='story', + name='creator', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_creator', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='story', + name='last_modified_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_modified', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/successstories/tests/test_models.py b/successstories/tests/test_models.py index de5c0d577..418d27062 100644 --- a/successstories/tests/test_models.py +++ b/successstories/tests/test_models.py @@ -15,12 +15,13 @@ def test_published(self): self.assertEqual(len(Story.objects.published()), 2) def test_draft(self): - self.assertQuerysetEqual(Story.objects.draft(), - [f'']) + draft_stories = Story.objects.draft() + self.assertTrue(all(story.name == 'Fraft Story' for story in draft_stories)) def test_featured(self): - self.assertQuerysetEqual(Story.objects.featured(), - [f'']) + featured_stories = Story.objects.featured() + expected_repr = [f''] + self.assertQuerysetEqual(featured_stories, expected_repr, transform=repr) def test_get_admin_url(self): self.assertEqual(self.story1.get_admin_url(), diff --git a/users/__init__.py b/users/__init__.py index 1bf67ae9c..e69de29bb 100644 --- a/users/__init__.py +++ b/users/__init__.py @@ -1 +0,0 @@ -default_app_config = 'users.apps.UsersAppConfig' diff --git a/users/admin.py b/users/admin.py index 1c003655c..36d7e30f3 100644 --- a/users/admin.py +++ b/users/admin.py @@ -26,7 +26,7 @@ class ApiKeyInline(TastypieApiKeyInline): @admin.register(User) class UserAdmin(BaseUserAdmin): - inlines = BaseUserAdmin.inlines + [ApiKeyInline, MembershipInline] + inlines = BaseUserAdmin.inlines + (ApiKeyInline, MembershipInline,) fieldsets = ( (None, {'fields': ('username', 'password')}), (_('Personal info'), {'fields': ( @@ -44,9 +44,11 @@ class UserAdmin(BaseUserAdmin): def has_add_permission(self, request): return False + @admin.display( + description='Name' + ) def full_name(self, obj): return obj.get_full_name() - full_name.short_description = 'Name' @admin.register(Membership) diff --git a/users/migrations/0015_alter_user_first_name.py b/users/migrations/0015_alter_user_first_name.py new file mode 100644 index 000000000..ac7715204 --- /dev/null +++ b/users/migrations/0015_alter_user_first_name.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.11 on 2024-09-05 17:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0014_auto_20210801_2332'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='first_name', + field=models.CharField(blank=True, max_length=150, verbose_name='first name'), + ), + ] diff --git a/users/tests/test_forms.py b/users/tests/test_forms.py index 10ab95e32..897f41d6c 100644 --- a/users/tests/test_forms.py +++ b/users/tests/test_forms.py @@ -2,6 +2,7 @@ from django.test import TestCase from allauth.account.forms import SignupForm +from allauth.account.models import EmailAddress from users.forms import UserProfileForm, MembershipForm @@ -50,14 +51,16 @@ def test_duplicate_username(self): self.assertIn('username', form.errors) def test_duplicate_email(self): - User.objects.create_user('test1', 'test@example.com', 'testpass') + user = User.objects.create_user('test1', 'test@example.com', 'testpass') + EmailAddress.objects.create(user=user, email="test@example.com") - form = SignupForm({ + form = SignupForm(data={ 'username': 'username2', 'email': 'test@example.com', 'password1': 'password', - 'password2': 'password' + 'password2': 'password', }) + self.assertFalse(form.is_valid()) self.assertIn('email', form.errors) @@ -92,13 +95,8 @@ def test_non_ascii_username(self): 'password2': 'password', }) self.assertFalse(form.is_valid()) - self.assertEqual( - form.errors['username'], - [ - 'Enter a valid username. This value may contain only ' - 'English letters, numbers, and @/./+/-/_ characters.' - ] - ) + expected_error = 'Enter a valid username. This value may contain only unaccented lowercase a-z and uppercase A-Z letters, numbers, and @/./+/-/_ characters.' + self.assertIn(expected_error, form.errors['username']) def test_user_membership(self): form = MembershipForm({ diff --git a/users/tests/test_views.py b/users/tests/test_views.py index 13c226e5f..83b8330f9 100644 --- a/users/tests/test_views.py +++ b/users/tests/test_views.py @@ -2,7 +2,7 @@ from django.conf import settings from django.contrib.auth import get_user_model from django.urls import reverse -from django.test import TestCase +from django.test import TestCase, override_settings from sponsors.forms import SponsorUpdateForm, SponsorRequiredAssetsForm from sponsors.models import Sponsorship, RequiredTextAssetConfiguration, SponsorBenefit @@ -11,8 +11,6 @@ from users.factories import UserFactory from users.models import Membership -from ..factories import MembershipFactory - User = get_user_model() @@ -245,7 +243,7 @@ def test_user_duplicate_username_email(self): response, 'A user with that username already exists.' ) self.assertContains( - response, 'A user is already registered with this e-mail address.' + response, 'A user is already registered with this email address.' ) def test_usernames(self): diff --git a/work_groups/__init__.py b/work_groups/__init__.py index ef2fbcce9..e69de29bb 100644 --- a/work_groups/__init__.py +++ b/work_groups/__init__.py @@ -1 +0,0 @@ -default_app_config = 'work_groups.apps.WorkGroupsAppConfig' diff --git a/work_groups/migrations/0005_alter_workgroup_creator_and_more.py b/work_groups/migrations/0005_alter_workgroup_creator_and_more.py new file mode 100644 index 000000000..a316aa482 --- /dev/null +++ b/work_groups/migrations/0005_alter_workgroup_creator_and_more.py @@ -0,0 +1,26 @@ +# Generated by Django 4.2.11 on 2024-09-05 17:10 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('work_groups', '0004_auto_20180705_0352'), + ] + + operations = [ + migrations.AlterField( + model_name='workgroup', + name='creator', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_creator', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='workgroup', + name='last_modified_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_modified', to=settings.AUTH_USER_MODEL), + ), + ]