diff --git a/.bandit.yaml b/.bandit.yaml new file mode 100644 index 000000000..67e03bee0 --- /dev/null +++ b/.bandit.yaml @@ -0,0 +1,10 @@ +exclude_dirs: + - /tests/ + - ./venv/ + - ./env/ + - ./node_modules/ + - .tox + - pydotorg/settings/local.py +skips: + - B101 # B101: assert + - B311 # B311: random diff --git a/.flake8 b/.flake8 new file mode 100644 index 000000000..fe982651e --- /dev/null +++ b/.flake8 @@ -0,0 +1,13 @@ +[flake8] +max-line-length = 120 +ignore = + D100,D101,D102,D103,D105,D205,D400, + E305,E266,E231 + W504,W606,X100 +exclude = ./node_modules/*,*/migrations/*,./venv/*,./env/*,./_personal/*,.tox/* +per-file-ignores = + manage.py:INP001 + docs/source/conf.py:INP001 + */test_*.py: E501,W605 + custom_storages/__init__.py:F401 + sponsors/models/__init__.py:F401 \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f34cc73bb..8fb349910 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,33 @@ name: CI on: [push, pull_request] +concurrency: + group: ${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read + jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: '3.9.16' + cache: 'pip' # caching pip dependencies + - run: python -m pip install pre-commit + shell: bash + - run: python -m pip freeze --local + shell: bash + - uses: actions/cache@v3 + with: + path: ~/.cache/pre-commit + key: pre-commit-3|${{ env.pythonLocation }}|${{ hashFiles('.pre-commit-config.yaml') }} + - run: pre-commit run --show-diff-on-failure --color=always --all-files + shell: bash test: runs-on: ubuntu-latest services: @@ -16,10 +43,10 @@ jobs: options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - name: Check out repository - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 + uses: actions/checkout@v3 + - uses: actions/setup-python@v4 with: - python-version: 3.9.16 + python-version: "3.9.16" - name: Cache Python dependencies uses: actions/cache@v2 env: diff --git a/.github/workflows/pre-commit-autoupdate.yml b/.github/workflows/pre-commit-autoupdate.yml new file mode 100644 index 000000000..eb66bbf6d --- /dev/null +++ b/.github/workflows/pre-commit-autoupdate.yml @@ -0,0 +1,43 @@ +# Run pre-commit autoupdate every day at midnight +# and create a pull request if any changes + +name: Pre-commit auto-update + +on: + schedule: + - cron: "15 2 * * *" + workflow_dispatch: # to trigger manually + +permissions: + contents: read + +jobs: + auto-update: + # Disables this workflow from running in a repository that is not part of the indicated organization/user + if: github.repository_owner == 'pythondotorg' + permissions: + contents: write # for peter-evans/create-pull-request to create branch + pull-requests: write # for peter-evans/create-pull-request to create a PR + + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: "3.9.16" + + - name: Install pre-commit + run: pip install pre-commit + + - name: Autoupdate template + run: pre-commit autoupdate + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + branch: update/pre-commit-autoupdate + title: Auto-update pre-commit hooks + commit-message: Auto-update pre-commit hooks + body: Update versions of tools in pre-commit configs to latest version + labels: update diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..a275ac809 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,19 @@ +repos: + - repo: https://github.com/pre-commit/mirrors-autopep8 + rev: 'v2.0.0' + hooks: + - id: autopep8 + args: ['--in-place', '--select', 'E101,E112,E113,E115,E116,E117,E121,E122,E123,E124,E125,E126,E127,E128,E129,E131,E133,E201,E202,E203,E211,E221,E222,E223,E224,E225,E226,E227,E228,E231,E231,E241,E242,E251,E252,E261,E262,E265,E271,E272,E273,E274,E275,E301,E302,E303,E304,E305,E306,W391'] + - repo: https://github.com/asottile/pyupgrade + rev: v3.3.1 + hooks: + - id: pyupgrade + args: ['--py3-plus', '--py39-plus'] + - repo: https://github.com/PyCQA/flake8 + rev: 5.0.4 + hooks: + - id: flake8 + additional_dependencies: [ + 'flake8-bugbear==22.12.6', + 'flake8-no-pep420==2.3.0' + ] \ No newline at end of file diff --git a/blogs/admin.py b/blogs/admin.py index 055431ae9..0952385bd 100644 --- a/blogs/admin.py +++ b/blogs/admin.py @@ -1,7 +1,11 @@ from django.contrib import admin from django.core.management import call_command -from .models import BlogEntry, Feed, FeedAggregate +from .models import ( + BlogEntry, + Feed, + FeedAggregate, +) @admin.register(BlogEntry) @@ -22,4 +26,5 @@ class FeedAggregateAdmin(admin.ModelAdmin): list_display = ['name', 'slug', 'description'] prepopulated_fields = {'slug': ('name',)} + admin.site.register(Feed) diff --git a/blogs/management/commands/update_blogs.py b/blogs/management/commands/update_blogs.py index 8914d0a78..49ccdda20 100644 --- a/blogs/management/commands/update_blogs.py +++ b/blogs/management/commands/update_blogs.py @@ -1,8 +1,15 @@ from django.core.management.base import BaseCommand from django.utils.timezone import now -from ...models import BlogEntry, RelatedBlog, Feed -from ...parser import get_all_entries, update_blog_supernav +from ...models import ( + BlogEntry, + Feed, + RelatedBlog, +) +from ...parser import ( + get_all_entries, + update_blog_supernav, +) class Command(BaseCommand): diff --git a/blogs/models.py b/blogs/models.py index 2703fefaf..097135507 100644 --- a/blogs/models.py +++ b/blogs/models.py @@ -1,8 +1,6 @@ import feedparser - from bs4 import BeautifulSoup from bs4.element import Comment - from django.db import models from cms.models import ContentManageable diff --git a/blogs/parser.py b/blogs/parser.py index 8ac8dc684..ab538cb65 100644 --- a/blogs/parser.py +++ b/blogs/parser.py @@ -1,12 +1,18 @@ import datetime -import feedparser +import feedparser 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, + utc, +) from boxes.models import Box -from .models import BlogEntry, Feed +from .models import ( + BlogEntry, + Feed, +) def get_all_entries(feed_url): diff --git a/blogs/templatetags/blogs.py b/blogs/templatetags/blogs.py index bc055312e..ce78c1906 100644 --- a/blogs/templatetags/blogs.py +++ b/blogs/templatetags/blogs.py @@ -23,4 +23,3 @@ def feed_list(slug, limit=10): """ return BlogEntry.objects.filter( feed__feedaggregate__slug=slug).order_by('-pub_date')[:limit] - diff --git a/blogs/tests/test_models.py b/blogs/tests/test_models.py index 3c29299ca..ed8069314 100644 --- a/blogs/tests/test_models.py +++ b/blogs/tests/test_models.py @@ -1,7 +1,10 @@ from django.test import TestCase from django.utils import timezone -from ..models import BlogEntry, Feed +from ..models import ( + BlogEntry, + Feed, +) class BlogModelTest(TestCase): diff --git a/blogs/tests/test_parser.py b/blogs/tests/test_parser.py index fbfcfca38..2c6edae7d 100644 --- a/blogs/tests/test_parser.py +++ b/blogs/tests/test_parser.py @@ -1,8 +1,8 @@ import datetime import unittest -from ..parser import get_all_entries from .utils import get_test_rss_path +from ..parser import get_all_entries class BlogParserTest(unittest.TestCase): diff --git a/blogs/tests/test_templatetags.py b/blogs/tests/test_templatetags.py index c26fbd3ea..da4927c98 100644 --- a/blogs/tests/test_templatetags.py +++ b/blogs/tests/test_templatetags.py @@ -1,11 +1,18 @@ from django.core.management import call_command +from django.template import ( + Context, + Template, +) from django.test import TestCase -from django.template import Template, Context from django.utils.timezone import now -from ..templatetags.blogs import get_latest_blog_entries -from ..models import BlogEntry, Feed, FeedAggregate from .utils import get_test_rss_path +from ..models import ( + BlogEntry, + Feed, + FeedAggregate, +) +from ..templatetags.blogs import get_latest_blog_entries class BlogTemplateTagTest(TestCase): @@ -63,7 +70,6 @@ def test_feed_list(self): ) fa.feeds.add(f1, f2) - t = Template(""" {% load blogs %} {% feed_list 'test' as entries %} diff --git a/blogs/tests/test_views.py b/blogs/tests/test_views.py index ee7df723b..a9a91fbe1 100644 --- a/blogs/tests/test_views.py +++ b/blogs/tests/test_views.py @@ -1,10 +1,12 @@ from django.core.management import call_command -from django.urls import reverse from django.test import TestCase - -from ..models import BlogEntry, Feed +from django.urls import reverse from .utils import get_test_rss_path +from ..models import ( + BlogEntry, + Feed, +) class BlogViewTest(TestCase): diff --git a/blogs/urls.py b/blogs/urls.py index d315ed486..b169de5fd 100644 --- a/blogs/urls.py +++ b/blogs/urls.py @@ -1,6 +1,7 @@ -from . import views from django.urls import path +from . import views + urlpatterns = [ path('', views.BlogHome.as_view(), name='blog'), ] diff --git a/boxes/admin.py b/boxes/admin.py index d71e7810a..8711d2f8b 100644 --- a/boxes/admin.py +++ b/boxes/admin.py @@ -1,4 +1,5 @@ from django.contrib import admin + from cms.admin import ContentManageableModelAdmin from .models import Box diff --git a/boxes/factories.py b/boxes/factories.py index 5d8c7f2ad..4d088041c 100644 --- a/boxes/factories.py +++ b/boxes/factories.py @@ -2,13 +2,11 @@ import pathlib import factory - from django.conf import settings from factory.django import DjangoModelFactory -from .models import Box - from users.factories import UserFactory +from .models import Box class BoxFactory(DjangoModelFactory): diff --git a/boxes/models.py b/boxes/models.py index b7b0a3385..fff108fe5 100644 --- a/boxes/models.py +++ b/boxes/models.py @@ -11,10 +11,12 @@ from django.conf import settings from django.db import models from markupfield.fields import MarkupField + from cms.models import ContentManageable DEFAULT_MARKUP_TYPE = getattr(settings, 'DEFAULT_MARKUP_TYPE', 'restructuredtext') + class Box(ContentManageable): label = models.SlugField(max_length=100, unique=True) content = MarkupField(default_markup_type=DEFAULT_MARKUP_TYPE) diff --git a/boxes/templatetags/__init__.py b/boxes/templatetags/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/boxes/tests.py b/boxes/tests.py index 13a8e998c..785f0047a 100644 --- a/boxes/tests.py +++ b/boxes/tests.py @@ -1,14 +1,21 @@ import logging + from django import template -from django.test import TestCase, override_settings +from django.test import ( + TestCase, + override_settings, +) + from .models import Box logging.disable(logging.CRITICAL) + class BaseTestCase(TestCase): def setUp(self): self.box = Box.objects.create(label='test', content='test content') + class TemplateTagTests(BaseTestCase): def render(self, tmpl, **context): t = template.Template(tmpl) @@ -22,6 +29,7 @@ def test_tag_invalid_label(self): r = self.render('{% load boxes %}{% box "missing" %}') self.assertEqual(r, '') + class ViewTests(BaseTestCase): @override_settings(ROOT_URLCONF='boxes.urls') diff --git a/boxes/urls.py b/boxes/urls.py index 8ac457c08..e8e416256 100644 --- a/boxes/urls.py +++ b/boxes/urls.py @@ -1,6 +1,7 @@ -from .views import box from django.urls import path +from .views import box + urlpatterns = [ path('/', box, name='box'), ] diff --git a/boxes/views.py b/boxes/views.py index 02a50feb6..9e69b2647 100644 --- a/boxes/views.py +++ b/boxes/views.py @@ -1,7 +1,9 @@ from django.http import HttpResponse from django.shortcuts import get_object_or_404 + from .models import Box + def box(request, label): b = get_object_or_404(Box, label=label) return HttpResponse(b.content.rendered) diff --git a/cms/admin.py b/cms/admin.py index 3468fe320..7be12217a 100644 --- a/cms/admin.py +++ b/cms/admin.py @@ -44,7 +44,7 @@ def get_fieldsets(self, request, obj=None): # Remove created/updated/creator from any existing fieldsets. They'll # be there if the child class didn't manually declare fieldsets. fieldsets = super().get_fieldsets(request, obj) - for name, fieldset in fieldsets: + for __, fieldset in fieldsets: for f in ('created', 'updated', 'creator', 'last_modified_by'): if f in fieldset['fields']: fieldset['fields'].remove(f) diff --git a/cms/management/commands/create_initial_data.py b/cms/management/commands/create_initial_data.py index b0147704b..92dfc1a97 100644 --- a/cms/management/commands/create_initial_data.py +++ b/cms/management/commands/create_initial_data.py @@ -3,7 +3,10 @@ import pprint from django.apps import apps -from django.core.management import BaseCommand, call_command +from django.core.management import ( + BaseCommand, + call_command, +) class Command(BaseCommand): @@ -60,7 +63,7 @@ def flush_handler(self, do_flush, verbosity): 'You have provided the --flush argument, this will cleanup ' 'the database before creating new data.\n' 'Type \'y\' or \'yes\' to continue, \'n\' or \'no\' to cancel: ' - ) + ) else: msg = ( 'Note that this command won\'t cleanup the database before ' diff --git a/cms/tests.py b/cms/tests.py index 9c9e8a6d4..e7cf11d2d 100644 --- a/cms/tests.py +++ b/cms/tests.py @@ -1,12 +1,15 @@ +import datetime import unittest from unittest import mock -from django.template import Template, Context +from django.template import ( + Context, + Template, +) from django.test import TestCase from .admin import ContentManageableModelAdmin from .views import legacy_path -import datetime class ContentManageableAdminTests(unittest.TestCase): @@ -48,7 +51,10 @@ def test_get_fieldsets(self): self.assertEqual( fieldsets, [(None, {'fields': ['foo']}), - ('CMS metadata', {'fields': [('creator', 'created'), ('last_modified_by', 'updated')], 'classes': ('collapse',)})] + ('CMS metadata', { + 'fields': [('creator', 'created'), ('last_modified_by', 'updated')], + 'classes': ('collapse',) + })] ) def test_save_model(self): @@ -56,14 +62,22 @@ def test_save_model(self): request = mock.Mock() obj = mock.Mock() admin.save_model(request=request, obj=obj, form=None, change=False) - self.assertEqual(obj.creator, request.user, "save_model didn't set obj.creator to request.user") + self.assertEqual( + obj.creator, + request.user, + "save_model didn't set obj.creator to request.user" + ) def test_update_model(self): admin = self.make_admin() request = mock.Mock() obj = mock.Mock() admin.save_model(request=request, obj=obj, form=None, change=True) - self.assertEqual(obj.last_modified_by, request.user, "save_model didn't set obj.last_modified_by to request.user") + self.assertEqual( + obj.last_modified_by, + request.user, + "save_model didn't set obj.last_modified_by to request.user" + ) class TemplateTagsTest(unittest.TestCase): @@ -71,7 +85,14 @@ def test_iso_time_tag(self): now = datetime.datetime(2014, 1, 1, 12, 0) template = Template("{% load cms %}{% iso_time_tag now %}") rendered = template.render(Context({'now': now})) - self.assertIn('', rendered) + expected = ( + '' + ) + self.assertIn( + expected, + rendered + ) class Test404(TestCase): diff --git a/cms/views.py b/cms/views.py index e3d938136..8ed887815 100644 --- a/cms/views.py +++ b/cms/views.py @@ -1,7 +1,8 @@ -from django.urls import reverse -from django.shortcuts import render from urllib.parse import urljoin +from django.shortcuts import render +from django.urls import reverse + LEGACY_PYTHON_DOMAIN = 'http://legacy.python.org' PYPI_URL = 'https://pypi.org/' diff --git a/codesamples/admin.py b/codesamples/admin.py index 08da235b3..bb12f1e4f 100644 --- a/codesamples/admin.py +++ b/codesamples/admin.py @@ -1,7 +1,6 @@ from django.contrib import admin -from .models import CodeSample from cms.admin import ContentManageableModelAdmin - +from .models import CodeSample admin.site.register(CodeSample, ContentManageableModelAdmin) diff --git a/codesamples/factories.py b/codesamples/factories.py index 49a60730f..84098a53e 100644 --- a/codesamples/factories.py +++ b/codesamples/factories.py @@ -3,9 +3,8 @@ import factory from factory.django import DjangoModelFactory -from .models import CodeSample - from users.factories import UserFactory +from .models import CodeSample class CodeSampleFactory(DjangoModelFactory): diff --git a/codesamples/models.py b/codesamples/models.py index e0158fb69..d70dc6aaf 100644 --- a/codesamples/models.py +++ b/codesamples/models.py @@ -1,13 +1,14 @@ from django.conf import settings from django.db import models -from django.template.defaultfilters import truncatechars, striptags +from django.template.defaultfilters import ( + striptags, + truncatechars, +) from markupfield.fields import MarkupField from cms.models import ContentManageable - from .managers import CodeSampleQuerySet - DEFAULT_MARKUP_TYPE = getattr(settings, 'DEFAULT_MARKUP_TYPE', 'html') diff --git a/community/admin.py b/community/admin.py index b9023ac00..6da7d230c 100644 --- a/community/admin.py +++ b/community/admin.py @@ -1,7 +1,15 @@ from django.contrib import admin -from .models import Link, Photo, Post, Video -from cms.admin import ContentManageableModelAdmin, ContentManageableStackedInline +from cms.admin import ( + ContentManageableModelAdmin, + ContentManageableStackedInline, +) +from .models import ( + Link, + Photo, + Post, + Video, +) class LinkInline(ContentManageableStackedInline): diff --git a/community/models.py b/community/models.py index 1e199b590..c524276a9 100644 --- a/community/models.py +++ b/community/models.py @@ -1,15 +1,12 @@ from django.contrib.postgres.fields import JSONField -from django.urls import reverse from django.db import models +from django.urls import reverse from django.utils.translation import gettext_lazy as _ - from markupfield.fields import MarkupField from cms.models import ContentManageable - from .managers import PostQuerySet - DEFAULT_MARKUP_TYPE = 'html' diff --git a/community/urls.py b/community/urls.py index 531dfe015..f21cbbb1d 100644 --- a/community/urls.py +++ b/community/urls.py @@ -1,6 +1,7 @@ -from . import views from django.urls import path +from . import views + app_name = 'community' urlpatterns = [ path('', views.PostList.as_view(), name='post_list'), diff --git a/community/views.py b/community/views.py index c9fbe08c5..2b4f4bbe4 100644 --- a/community/views.py +++ b/community/views.py @@ -1,4 +1,7 @@ -from django.views.generic import ListView, DetailView +from django.views.generic import ( + DetailView, + ListView, +) from .models import Post diff --git a/companies/admin.py b/companies/admin.py index f5e2da58f..9f4070c49 100644 --- a/companies/admin.py +++ b/companies/admin.py @@ -1,7 +1,6 @@ from django.contrib import admin from cms.admin import NameSlugAdmin - from .models import Company diff --git a/companies/models.py b/companies/models.py index 0e97cc779..bb2ea0c04 100644 --- a/companies/models.py +++ b/companies/models.py @@ -5,7 +5,6 @@ from cms.models import NameSlugModel - DEFAULT_MARKUP_TYPE = getattr(settings, 'DEFAULT_MARKUP_TYPE', 'restructuredtext') diff --git a/companies/tests.py b/companies/tests.py index 083cd9dfe..c47673dc2 100644 --- a/companies/tests.py +++ b/companies/tests.py @@ -1,10 +1,13 @@ from django.test import TestCase -from . import admin # coverage FTW +from . import admin # noqa: F401 coverage FTW from .templatetags.companies import render_email class CompaniesTagsTests(TestCase): def test_render_email(self): self.assertEqual(render_email(''), None) - self.assertEqual(render_email('firstname.lastname@domain.com'), 'firstname.lastname@domain.com') + self.assertEqual( + render_email('firstname.lastname@domain.com'), + 'firstname.lastname@domain.com' + ) diff --git a/custom_storages/__init__.py b/custom_storages/__init__.py new file mode 100644 index 000000000..17f6aa9d9 --- /dev/null +++ b/custom_storages/__init__.py @@ -0,0 +1,4 @@ +from custom_storages.storage import ( + MediaStorage, + PipelineManifestStorage, +) diff --git a/custom_storages.py b/custom_storages/storage.py similarity index 96% rename from custom_storages.py rename to custom_storages/storage.py index e702c38d8..ba103b4f6 100644 --- a/custom_storages.py +++ b/custom_storages/storage.py @@ -1,14 +1,18 @@ import os import posixpath import re - -from urllib.parse import unquote, urldefrag +from urllib.parse import ( + unquote, + urldefrag, +) from django.conf import settings -from django.contrib.staticfiles.storage import ManifestFilesMixin, StaticFilesStorage +from django.contrib.staticfiles.storage import ( + ManifestFilesMixin, + StaticFilesStorage, +) from django.contrib.staticfiles.utils import matches_patterns from django.core.files.base import ContentFile - from pipeline.storage import PipelineMixin from storages.backends.s3boto3 import S3Boto3Storage @@ -32,10 +36,13 @@ def get_comment_blocks(self, content): for match in re.finditer(r"\/\*.*?\*\/", content, flags=re.DOTALL) ] - def url_converter(self, name, hashed_files, template=None, comment_blocks=[]): + def url_converter(self, name, hashed_files, template=None, comment_blocks=None): """ Return the custom URL converter for the given file name. """ + if comment_blocks is None: + comment_blocks = [] + if template is None: template = self.default_template diff --git a/dev-requirements.txt b/dev-requirements.txt index 9b5e0938f..a3b14e644 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,4 +1,5 @@ -r base-requirements.txt +-r prod-requirements.txt # Required for running tests @@ -13,3 +14,5 @@ django-debug-toolbar==3.2.1 coverage ddt model-bakery==1.4.0 +flake8 +pre-commit diff --git a/docs/source/conf.py b/docs/source/conf.py index 00477aaa3..2917895e4 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,7 +1,5 @@ #!/usr/bin/env python3 -import sys -import os import time extensions = [ @@ -40,22 +38,22 @@ # -- Options for LaTeX output --------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', + # The paper size ('letterpaper' or 'a4paper'). + # 'papersize': 'letterpaper', -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', + # The font size ('10pt', '11pt' or '12pt'). + # 'pointsize': '10pt', -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # Additional stuff for the LaTeX preamble. + # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - ('index', 'PythonorgWebsite.tex', 'Python.org Website Documentation', - 'Python Software Foundation', 'manual'), + ('index', 'PythonorgWebsite.tex', 'Python.org Website Documentation', + 'Python Software Foundation', 'manual'), ] # -- Options for manual page output --------------------------------------- @@ -73,7 +71,7 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'PythonorgWebsite', 'Python.org Website Documentation', - 'Python Software Foundation', 'PythonorgWebsite', '', - 'Miscellaneous'), + ('index', 'PythonorgWebsite', 'Python.org Website Documentation', + 'Python Software Foundation', 'PythonorgWebsite', '', + 'Miscellaneous'), ] diff --git a/downloads/admin.py b/downloads/admin.py index d32f97b71..06a0e3b70 100644 --- a/downloads/admin.py +++ b/downloads/admin.py @@ -1,7 +1,14 @@ from django.contrib import admin -from .models import OS, Release, ReleaseFile -from cms.admin import ContentManageableModelAdmin, ContentManageableStackedInline +from cms.admin import ( + ContentManageableModelAdmin, + ContentManageableStackedInline, +) +from .models import ( + OS, + Release, + ReleaseFile, +) @admin.register(OS) diff --git a/downloads/api.py b/downloads/api.py index bb49e588e..e934722c4 100644 --- a/downloads/api.py +++ b/downloads/api.py @@ -1,17 +1,36 @@ -from rest_framework import status, viewsets +from rest_framework import ( + status, + viewsets, +) from rest_framework.authentication import TokenAuthentication from rest_framework.decorators import action from rest_framework.response import Response - from tastypie import fields -from tastypie.constants import ALL, ALL_WITH_RELATIONS +from tastypie.constants import ( + ALL, + ALL_WITH_RELATIONS, +) from pages.api import PageResource -from pydotorg.resources import GenericResource, OnlyPublishedAuthorization -from pydotorg.drf import BaseAPIViewSet, BaseFilterSet, IsStaffOrReadOnly - -from .models import OS, Release, ReleaseFile -from .serializers import OSSerializer, ReleaseSerializer, ReleaseFileSerializer +from pydotorg.drf import ( + BaseAPIViewSet, + BaseFilterSet, + IsStaffOrReadOnly, +) +from pydotorg.resources import ( + GenericResource, + OnlyPublishedAuthorization, +) +from .models import ( + OS, + Release, + ReleaseFile, +) +from .serializers import ( + OSSerializer, + ReleaseFileSerializer, + ReleaseSerializer, +) class OSResource(GenericResource): diff --git a/downloads/factories.py b/downloads/factories.py index 4ebcbdc22..75f3d6692 100644 --- a/downloads/factories.py +++ b/downloads/factories.py @@ -5,8 +5,11 @@ from factory.django import DjangoModelFactory from users.factories import UserFactory - -from .models import OS, Release, ReleaseFile +from .models import ( + OS, + Release, + ReleaseFile, +) class OSFactory(DjangoModelFactory): diff --git a/downloads/models.py b/downloads/models.py index 7955f58f5..9dfa3c551 100644 --- a/downloads/models.py +++ b/downloads/models.py @@ -1,24 +1,24 @@ import re -from django.urls import reverse from django.conf import settings from django.core.exceptions import ValidationError from django.db import models from django.db.models.signals import post_save from django.dispatch import receiver from django.template.loader import render_to_string +from django.urls import reverse from django.utils import timezone - from markupfield.fields import MarkupField from boxes.models import Box -from cms.models import ContentManageable, NameSlugModel +from cms.models import ( + ContentManageable, + NameSlugModel, +) from fastly.utils import purge_url from pages.models import Page - from .managers import ReleaseManager - DEFAULT_MARKUP_TYPE = getattr(settings, 'DEFAULT_MARKUP_TYPE', 'restructuredtext') @@ -338,7 +338,7 @@ def validate_unique(self, exclude=None): qs = ReleaseFile.objects.filter(release=self.release, os=self.os, download_button=True).exclude(pk=self.id) if qs.count() > 0: raise ValidationError("Only one Release File per OS can have \"Download button\" enabled") - super(ReleaseFile, self).validate_unique(exclude=exclude) + super().validate_unique(exclude=exclude) class Meta: verbose_name = 'Release File' @@ -347,6 +347,6 @@ class Meta: constraints = [ models.UniqueConstraint(fields=['os', 'release'], - condition=models.Q(download_button=True), - name="only_one_download_per_os_per_release"), + condition=models.Q(download_button=True), + name="only_one_download_per_os_per_release"), ] diff --git a/downloads/search_indexes.py b/downloads/search_indexes.py index 307841283..321a085c7 100644 --- a/downloads/search_indexes.py +++ b/downloads/search_indexes.py @@ -1,8 +1,10 @@ import datetime -from django.template.defaultfilters import truncatewords_html, striptags +from django.template.defaultfilters import ( + striptags, + truncatewords_html, +) from django.utils import timezone - from haystack import indexes from .models import Release diff --git a/downloads/serializers.py b/downloads/serializers.py index f30974e02..057534d36 100644 --- a/downloads/serializers.py +++ b/downloads/serializers.py @@ -1,6 +1,10 @@ from rest_framework import serializers -from downloads.models import OS, Release, ReleaseFile +from downloads.models import ( + OS, + Release, + ReleaseFile, +) class OSSerializer(serializers.HyperlinkedModelSerializer): diff --git a/downloads/tests/base.py b/downloads/tests/base.py index e19ffe03a..b79f4c64f 100644 --- a/downloads/tests/base.py +++ b/downloads/tests/base.py @@ -4,7 +4,11 @@ from django.utils import timezone from pages.models import Page -from ..models import OS, Release, ReleaseFile +from ..models import ( + OS, + Release, + ReleaseFile, +) class DownloadMixin: diff --git a/downloads/tests/test_models.py b/downloads/tests/test_models.py index f27e9517d..361700b38 100644 --- a/downloads/tests/test_models.py +++ b/downloads/tests/test_models.py @@ -1,5 +1,5 @@ -from ..models import Release from .base import BaseDownloadTests +from ..models import Release class DownloadModelTests(BaseDownloadTests): @@ -82,8 +82,8 @@ def test_is_version_at_least(self): release_38 = Release.objects.create(name='Python 3.8.0') self.assertFalse(release_38.is_version_at_least_3_9) - self.assert_(release_38.is_version_at_least_3_5) + self.assertTrue(release_38.is_version_at_least_3_5) release_310 = Release.objects.create(name='Python 3.10.0') - self.assert_(release_310.is_version_at_least_3_9) - self.assert_(release_310.is_version_at_least_3_5) + self.assertTrue(release_310.is_version_at_least_3_9) + self.assertTrue(release_310.is_version_at_least_3_5) diff --git a/downloads/tests/test_views.py b/downloads/tests/test_views.py index 75fe76693..0faff1f36 100644 --- a/downloads/tests/test_views.py +++ b/downloads/tests/test_views.py @@ -2,17 +2,21 @@ from django.conf import settings from django.contrib.auth import get_user_model +from django.test import ( + TestCase, + override_settings, +) from django.urls import reverse -from django.test import TestCase, override_settings - -from rest_framework.authtoken.models import Token from rest_framework.test import APITestCase -from .base import BaseDownloadTests, DownloadMixin -from ..models import OS, Release from pages.factories import PageFactory from pydotorg.drf import BaseAPITestCase from users.factories import UserFactory +from .base import ( + BaseDownloadTests, + DownloadMixin, +) +from ..models import Release User = get_user_model() diff --git a/downloads/urls.py b/downloads/urls.py index d64f0a1ad..d45864841 100644 --- a/downloads/urls.py +++ b/downloads/urls.py @@ -1,5 +1,9 @@ +from django.urls import ( + path, + re_path, +) + from . import views -from django.urls import path, re_path app_name = 'downloads' urlpatterns = [ diff --git a/downloads/views.py b/downloads/views.py index 746845402..de9b0ca5d 100644 --- a/downloads/views.py +++ b/downloads/views.py @@ -1,9 +1,18 @@ from django.db.models import Prefetch -from django.urls import reverse -from django.views.generic import DetailView, TemplateView, ListView, RedirectView from django.http import Http404 +from django.urls import reverse +from django.views.generic import ( + DetailView, + ListView, + RedirectView, + TemplateView, +) -from .models import OS, Release, ReleaseFile +from .models import ( + OS, + Release, + ReleaseFile, +) class DownloadLatestPython2(RedirectView): @@ -40,6 +49,7 @@ def get_redirect_url(self, **kwargs): class DownloadBase: """ Include latest releases in all views """ + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context.update({ diff --git a/events/admin.py b/events/admin.py index ba03c28ed..fc51d000c 100644 --- a/events/admin.py +++ b/events/admin.py @@ -1,7 +1,18 @@ from django.contrib import admin -from cms.admin import ContentManageableModelAdmin, NameSlugAdmin -from .models import Calendar, EventCategory, Event, OccurringRule, RecurringRule, Alarm, EventLocation +from cms.admin import ( + ContentManageableModelAdmin, + NameSlugAdmin, +) +from .models import ( + Alarm, + Calendar, + Event, + EventCategory, + EventLocation, + OccurringRule, + RecurringRule, +) class EventInline(admin.StackedInline): diff --git a/events/importer.py b/events/importer.py index 847394fa0..edad4f822 100644 --- a/events/importer.py +++ b/events/importer.py @@ -1,8 +1,13 @@ from datetime import timedelta -from icalendar import Calendar as ICalendar + import requests +from icalendar import Calendar as ICalendar -from .models import EventLocation, Event, OccurringRule +from .models import ( + Event, + EventLocation, + OccurringRule, +) from .utils import extract_date_or_datetime DATE_RESOLUTION = timedelta(1) diff --git a/events/management/commands/import_ics_calendars.py b/events/management/commands/import_ics_calendars.py index 3ca1937a7..09f753a26 100644 --- a/events/management/commands/import_ics_calendars.py +++ b/events/management/commands/import_ics_calendars.py @@ -1,4 +1,5 @@ from django.core.management import BaseCommand + from events.models import Calendar diff --git a/events/models.py b/events/models.py index 3334ca326..ae716667a 100644 --- a/events/models.py +++ b/events/models.py @@ -1,22 +1,32 @@ import datetime -from dateutil.rrule import rrule, YEARLY, MONTHLY, WEEKLY, DAILY from operator import itemgetter +from dateutil.rrule import ( + DAILY, + MONTHLY, + WEEKLY, + YEARLY, + rrule, +) from django.conf import settings -from django.urls import reverse from django.db import models from django.db.models import Q from django.template.defaultfilters import date +from django.urls import reverse from django.utils import timezone from django.utils.functional import cached_property from django.utils.translation import gettext_lazy as _ - -from cms.models import ContentManageable, NameSlugModel - from markupfield.fields import MarkupField +from cms.models import ( + ContentManageable, + NameSlugModel, +) from .utils import ( - minutes_resolution, convert_dt_to_aware, timedelta_nice_repr, timedelta_parse, + convert_dt_to_aware, + minutes_resolution, + timedelta_nice_repr, + timedelta_parse, ) DEFAULT_MARKUP_TYPE = getattr(settings, 'DEFAULT_MARKUP_TYPE', 'restructuredtext') @@ -284,7 +294,10 @@ class RecurringRule(RuleMixin, models.Model): def __str__(self): strftime = settings.SHORT_DATETIME_FORMAT - return f'{self.event.title} every {timedelta_nice_repr(self.interval)} since {date(self.dt_start.strftime, strftime)}' + return ( + f'{self.event.title} every {timedelta_nice_repr(self.interval)} ' + f'since {date(self.dt_start.strftime, strftime)}' + ) def to_rrule(self): return rrule( diff --git a/events/search_indexes.py b/events/search_indexes.py index 9db26a93f..4204d714b 100644 --- a/events/search_indexes.py +++ b/events/search_indexes.py @@ -1,8 +1,13 @@ -from django.template.defaultfilters import truncatewords_html, striptags - +from django.template.defaultfilters import ( + striptags, + truncatewords_html, +) from haystack import indexes -from .models import Event, Calendar +from .models import ( + Calendar, + Event, +) class CalendarIndex(indexes.SearchIndex, indexes.Indexable): diff --git a/events/templatetags/events.py b/events/templatetags/events.py index fa6bf8063..f5e4b150d 100644 --- a/events/templatetags/events.py +++ b/events/templatetags/events.py @@ -3,7 +3,6 @@ from ..models import Event - register = template.Library() diff --git a/events/tests/test_importer.py b/events/tests/test_importer.py index 15a573063..c95ad7615 100644 --- a/events/tests/test_importer.py +++ b/events/tests/test_importer.py @@ -1,10 +1,16 @@ import os from django.test import TestCase -from django.utils.timezone import datetime, make_aware +from django.utils.timezone import ( + datetime, + make_aware, +) from events.importer import ICSImporter -from events.models import Calendar, Event +from events.models import ( + Calendar, + Event, +) CUR_DIR = os.path.dirname(__file__) EVENTS_CALENDAR = os.path.join(CUR_DIR, 'events.ics') diff --git a/events/tests/test_models.py b/events/tests/test_models.py index 0f3bafe76..bc618a788 100644 --- a/events/tests/test_models.py +++ b/events/tests/test_models.py @@ -1,13 +1,23 @@ import datetime +from dateutil.rrule import ( + WEEKLY, + rrule, +) from django.contrib.auth import get_user_model from django.test import TestCase from django.utils import timezone -from dateutil.rrule import rrule, WEEKLY - -from ..models import Calendar, Event, OccurringRule, RecurringRule -from ..utils import seconds_resolution, convert_dt_to_aware +from ..models import ( + Calendar, + Event, + OccurringRule, + RecurringRule, +) +from ..utils import ( + convert_dt_to_aware, + seconds_resolution, +) class EventsModelsTests(TestCase): @@ -62,7 +72,6 @@ def test_recurring_event(self): self.assertEqual(self.event.next_time.dt_start, recurring_time_dtstart) self.assertTrue(rt.valid_dt_end()) - rt.begin = now - datetime.timedelta(days=5) rt.finish = now - datetime.timedelta(days=3) rt.save() diff --git a/events/tests/test_utils.py b/events/tests/test_utils.py index 7e49223ec..1819769cb 100644 --- a/events/tests/test_utils.py +++ b/events/tests/test_utils.py @@ -1,10 +1,13 @@ import datetime -from django.utils import timezone from django.test import TestCase +from django.utils import timezone from ..utils import ( - seconds_resolution, minutes_resolution, timedelta_nice_repr, timedelta_parse, + minutes_resolution, + seconds_resolution, + timedelta_nice_repr, + timedelta_parse, ) diff --git a/events/tests/test_views.py b/events/tests/test_views.py index 691817036..aecd4ec5e 100644 --- a/events/tests/test_views.py +++ b/events/tests/test_views.py @@ -2,13 +2,22 @@ from django.contrib.auth import get_user_model from django.core import mail -from django.urls import reverse, reverse_lazy from django.test import TestCase +from django.urls import ( + reverse, + reverse_lazy, +) from django.utils import timezone -from ..models import Calendar, Event, EventCategory, EventLocation, RecurringRule -from ..templatetags.events import get_events_upcoming from users.factories import UserFactory +from ..models import ( + Calendar, + Event, + EventCategory, + EventLocation, + RecurringRule, +) +from ..templatetags.events import get_events_upcoming class EventsViewsTests(TestCase): diff --git a/events/urls.py b/events/urls.py index 8bb2d0135..1e3ac7197 100644 --- a/events/urls.py +++ b/events/urls.py @@ -1,18 +1,45 @@ +from django.urls import ( + path, + re_path, +) from django.views.generic import TemplateView from . import views -from django.urls import path, re_path app_name = 'events' urlpatterns = [ path('calendars/', views.CalendarList.as_view(), name='calendar_list'), path('submit/', views.EventSubmit.as_view(), name='event_submit'), - path('submit/thanks/', TemplateView.as_view(template_name='events/event_form_thanks.html'), name='event_thanks'), - path('/categories//', views.EventListByCategory.as_view(), name='eventlist_category'), - path('/categories/', views.EventCategoryList.as_view(), name='eventcategory_list'), - path('/locations//', views.EventListByLocation.as_view(), name='eventlist_location'), - path('/locations/', views.EventLocationList.as_view(), name='eventlocation_list'), - re_path(r'(?P[-a-zA-Z0-9_]+)/date/(?P\d{4})/(?P\d{2})/(?P\d{2})/$', views.EventListByDate.as_view(), name='eventlist_date'), + path( + 'submit/thanks/', + TemplateView.as_view(template_name='events/event_form_thanks.html'), + name='event_thanks' + ), + path( + '/categories//', + views.EventListByCategory.as_view(), + name='eventlist_category' + ), + path( + '/categories/', + views.EventCategoryList.as_view(), + name='eventcategory_list' + ), + path( + '/locations//', + views.EventListByLocation.as_view(), + name='eventlist_location' + ), + path( + '/locations/', + views.EventLocationList.as_view(), + name='eventlocation_list' + ), + re_path( + r'(?P[-a-zA-Z0-9_]+)/date/(?P\d{4})/(?P\d{2})/(?P\d{2})/$', + views.EventListByDate.as_view(), + name='eventlist_date' + ), path('//', views.EventDetail.as_view(), name='event_detail'), path('/past/', views.PastEventList.as_view(), name='event_list_past'), path('/', views.EventList.as_view(), name='event_list'), diff --git a/events/utils.py b/events/utils.py index a3801d4a6..b0d05f2d2 100644 --- a/events/utils.py +++ b/events/utils.py @@ -2,8 +2,10 @@ import re import pytz - -from django.utils.timezone import is_aware, make_aware +from django.utils.timezone import ( + is_aware, + make_aware, +) def seconds_resolution(dt): diff --git a/events/views.py b/events/views.py index 2490626e3..91f98d60e 100644 --- a/events/views.py +++ b/events/views.py @@ -2,15 +2,26 @@ from django.contrib import messages from django.core.mail import BadHeaderError -from django.shortcuts import get_object_or_404, redirect +from django.shortcuts import ( + get_object_or_404, + redirect, +) from django.urls import reverse_lazy from django.utils import timezone -from django.views.generic import DetailView, ListView, FormView +from django.views.generic import ( + DetailView, + FormView, + ListView, +) from pydotorg.mixins import LoginRequiredMixin - -from .models import Calendar, Event, EventCategory, EventLocation from .forms import EventForm +from .models import ( + Calendar, + Event, + EventCategory, + EventLocation, +) class CalendarList(ListView): @@ -68,11 +79,17 @@ def get_context_data(self, **kwargs): class EventList(EventListBase): def get_queryset(self): - return Event.objects.for_datetime(timezone.now()).filter(calendar__slug=self.kwargs['calendar_slug']).order_by('occurring_rule__dt_start') + return Event.objects.for_datetime( + timezone.now() + ).filter( + calendar__slug=self.kwargs['calendar_slug'] + ).order_by('occurring_rule__dt_start') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['events_today'] = Event.objects.until_datetime(timezone.now()).filter(calendar__slug=self.kwargs['calendar_slug'])[:2] + context['events_today'] = Event.objects.until_datetime( + timezone.now() + ).filter(calendar__slug=self.kwargs['calendar_slug'])[:2] context['calendar'] = get_object_or_404(Calendar, slug=self.kwargs['calendar_slug']) return context @@ -81,7 +98,9 @@ class PastEventList(EventList): template_name = 'events/event_list_past.html' def get_queryset(self): - return Event.objects.until_datetime(timezone.now()).filter(calendar__slug=self.kwargs['calendar_slug']) + return Event.objects.until_datetime( + timezone.now() + ).filter(calendar__slug=self.kwargs['calendar_slug']) class EventListByDate(EventList): @@ -92,12 +111,18 @@ def get_object(self): return datetime.date(year, month, day) def get_queryset(self): - return Event.objects.for_datetime(self.get_object()).filter(calendar__slug=self.kwargs['calendar_slug']) + return Event.objects.for_datetime( + self.get_object() + ).filter(calendar__slug=self.kwargs['calendar_slug']) class EventListByCategory(EventList): def get_object(self, queryset=None): - return get_object_or_404(EventCategory, calendar__slug=self.kwargs['calendar_slug'], slug=self.kwargs['slug']) + return get_object_or_404( + EventCategory, + calendar__slug=self.kwargs['calendar_slug'], + slug=self.kwargs['slug'] + ) def get_queryset(self): qs = super().get_queryset() @@ -106,7 +131,11 @@ def get_queryset(self): class EventListByLocation(EventList): def get_object(self, queryset=None): - return get_object_or_404(EventLocation, calendar__slug=self.kwargs['calendar_slug'], pk=self.kwargs['pk']) + return get_object_or_404( + EventLocation, + calendar__slug=self.kwargs['calendar_slug'], + pk=self.kwargs['pk'] + ) def get_queryset(self): qs = super().get_queryset() diff --git a/jobs/admin.py b/jobs/admin.py index 7c811e95e..eafc87283 100644 --- a/jobs/admin.py +++ b/jobs/admin.py @@ -1,7 +1,15 @@ from django.contrib import admin -from .models import JobType, JobCategory, Job, JobReviewComment -from cms.admin import NameSlugAdmin, ContentManageableModelAdmin +from cms.admin import ( + ContentManageableModelAdmin, + NameSlugAdmin, +) +from .models import ( + Job, + JobCategory, + JobReviewComment, + JobType, +) @admin.register(Job) diff --git a/jobs/apps.py b/jobs/apps.py index 92868a79c..c75cd0f1d 100644 --- a/jobs/apps.py +++ b/jobs/apps.py @@ -7,4 +7,4 @@ class JobsAppConfig(AppConfig): verbose_name = 'Jobs Application' def ready(self): - import jobs.listeners + import jobs.listeners # noqa: F401 diff --git a/jobs/factories.py b/jobs/factories.py index a8c38b423..e2751ea96 100644 --- a/jobs/factories.py +++ b/jobs/factories.py @@ -1,15 +1,17 @@ import datetime -import factory +import factory from django.contrib.auth.models import Group from django.utils import timezone from factory.django import DjangoModelFactory - from faker.providers import BaseProvider from users.factories import UserFactory - -from .models import JobType, JobCategory, Job +from .models import ( + Job, + JobCategory, + JobType, +) class JobProvider(BaseProvider): diff --git a/jobs/forms.py b/jobs/forms.py index 08b35ce00..dc8c11e35 100644 --- a/jobs/forms.py +++ b/jobs/forms.py @@ -1,10 +1,15 @@ from django import forms -from django.forms.widgets import CheckboxSelectMultiple, HiddenInput - +from django.forms.widgets import ( + CheckboxSelectMultiple, + HiddenInput, +) from markupfield.widgets import MarkupTextarea -from .models import Job, JobReviewComment from cms.forms import ContentManageableModelForm +from .models import ( + Job, + JobReviewComment, +) class JobForm(ContentManageableModelForm): diff --git a/jobs/listeners.py b/jobs/listeners.py index 2e88bc793..bf4c8b783 100644 --- a/jobs/listeners.py +++ b/jobs/listeners.py @@ -1,14 +1,15 @@ from django.conf import settings +from django.contrib.sites.models import Site from django.core.mail import send_mail -from django.db import models from django.dispatch import receiver -from django.contrib.sites.models import Site from django.template import loader from django.utils.translation import gettext_lazy as _ -from .models import Job from .signals import ( - job_was_submitted, job_was_approved, job_was_rejected, comment_was_posted, + comment_was_posted, + job_was_approved, + job_was_rejected, + job_was_submitted, ) # Python job board team email address diff --git a/jobs/management/commands/expire_jobs.py b/jobs/management/commands/expire_jobs.py index 8c5848b49..f1b85fce3 100644 --- a/jobs/management/commands/expire_jobs.py +++ b/jobs/management/commands/expire_jobs.py @@ -1,7 +1,7 @@ import datetime -from django.core.management import BaseCommand from django.conf import settings +from django.core.management import BaseCommand from django.utils import timezone from jobs.models import Job diff --git a/jobs/management/commands/jobs_monthly_report.py b/jobs/management/commands/jobs_monthly_report.py index 09f37f9b7..6ac362f23 100644 --- a/jobs/management/commands/jobs_monthly_report.py +++ b/jobs/management/commands/jobs_monthly_report.py @@ -1,12 +1,11 @@ import datetime +from django.conf import settings from django.core.mail import send_mail from django.core.management import BaseCommand from django.db.models import Count -from django.conf import settings from django.template import loader - from jobs.models import Job diff --git a/jobs/models.py b/jobs/models.py index 54722873d..ff8fc1fd8 100644 --- a/jobs/models.py +++ b/jobs/models.py @@ -1,26 +1,32 @@ import datetime from django.conf import settings -from django.urls import reverse from django.db import models from django.db.models.signals import post_save from django.dispatch import receiver from django.template.defaultfilters import slugify +from django.urls import reverse from django.utils import timezone - from markupfield.fields import MarkupField -from cms.models import ContentManageable, NameSlugModel +from cms.models import ( + ContentManageable, + NameSlugModel, +) from fastly.utils import purge_url - from users.models import User - -from .managers import JobQuerySet, JobTypeQuerySet, JobCategoryQuerySet +from .managers import ( + JobCategoryQuerySet, + JobQuerySet, + JobTypeQuerySet, +) from .signals import ( - job_was_submitted, job_was_approved, job_was_rejected, comment_was_posted + comment_was_posted, + job_was_approved, + job_was_rejected, + job_was_submitted, ) - DEFAULT_MARKUP_TYPE = getattr(settings, 'DEFAULT_MARKUP_TYPE', 'restructuredtext') @@ -222,7 +228,7 @@ def display_description(self): @property def display_location(self): location_parts = [part for part in (self.city, self.region, self.country) - if part] + if part] location_str = ', '.join(location_parts) return location_str diff --git a/jobs/search_indexes.py b/jobs/search_indexes.py index 91f56fa58..bbe84e979 100644 --- a/jobs/search_indexes.py +++ b/jobs/search_indexes.py @@ -1,9 +1,15 @@ +from django.template.defaultfilters import ( + striptags, + truncatewords_html, +) from django.urls import reverse -from django.template.defaultfilters import truncatewords_html, striptags - from haystack import indexes -from .models import JobType, JobCategory, Job +from .models import ( + Job, + JobCategory, + JobType, +) class JobTypeIndex(indexes.SearchIndex, indexes.Indexable): diff --git a/jobs/tests/test_models.py b/jobs/tests/test_models.py index 310659165..222477f39 100644 --- a/jobs/tests/test_models.py +++ b/jobs/tests/test_models.py @@ -1,11 +1,14 @@ import datetime -from django.core import mail from django.test import TestCase from django.utils import timezone from .. import factories -from ..models import Job, JobType, JobCategory +from ..models import ( + Job, + JobCategory, + JobType, +) class JobsModelsTests(TestCase): diff --git a/jobs/tests/test_views.py b/jobs/tests/test_views.py index 763dca666..a4564e59f 100644 --- a/jobs/tests/test_views.py +++ b/jobs/tests/test_views.py @@ -1,14 +1,18 @@ from django.contrib.auth import get_user_model from django.core import mail -from django.urls import reverse from django.test import TestCase +from django.urls import reverse -from ..models import Job +from users.factories import UserFactory from ..factories import ( - ApprovedJobFactory, DraftJobFactory, JobCategoryFactory, JobTypeFactory, - ReviewJobFactory, JobsBoardAdminGroupFactory, + ApprovedJobFactory, + DraftJobFactory, + JobCategoryFactory, + JobTypeFactory, + JobsBoardAdminGroupFactory, + ReviewJobFactory, ) -from users.factories import UserFactory +from ..models import Job class JobsViewTests(TestCase): @@ -362,6 +366,7 @@ def test_job_preview_404(self): self.assertEqual(response.status_code, 404) def test_job_create_prepopulate_email(self): + # TODO: Refactor create_url = reverse('jobs:job_create') user_data = { 'username': 'phrasebook', @@ -370,12 +375,12 @@ def test_job_create_prepopulate_email(self): } User = get_user_model() - creator = User.objects.create_user(**user_data) + User.objects.create_user(**user_data) # Logged in, email address is prepopulated. self.client.login(username=user_data['username'], password=user_data['password']) - response = self.client.get(create_url) + self.client.get(create_url) def test_job_types(self): job_type2 = JobTypeFactory( @@ -430,15 +435,15 @@ def test_job_telecommute(self): def test_job_display_name(self): self.assertEqual(self.job.display_name, - f"{self.job.job_title}, {self.job.company_name}") + f"{self.job.job_title}, {self.job.company_name}") self.job.company_name = 'ABC' self.assertEqual(self.job.display_name, - f"{self.job.job_title}, {self.job.company_name}") + f"{self.job.job_title}, {self.job.company_name}") self.job.company_name = '' self.assertEqual(self.job.display_name, - f"{self.job.job_title}, {self.job.company_name}") + f"{self.job.job_title}, {self.job.company_name}") def test_job_display_about(self): self.job.company_description.raw = 'XYZ' diff --git a/jobs/urls.py b/jobs/urls.py index 319ec98c3..80b21a7f9 100644 --- a/jobs/urls.py +++ b/jobs/urls.py @@ -1,8 +1,10 @@ +from django.urls import path from django.views.generic import TemplateView -from . import views -from . import feeds -from django.urls import path +from . import ( + feeds, + views, +) app_name = 'jobs' urlpatterns = [ diff --git a/jobs/views.py b/jobs/views.py index 9e781a185..000f59b48 100644 --- a/jobs/views.py +++ b/jobs/views.py @@ -1,14 +1,37 @@ from django.contrib import messages +from django.http import ( + Http404, + HttpResponse, + HttpResponseRedirect, +) +from django.shortcuts import ( + get_object_or_404, + redirect, +) from django.urls import reverse -from django.db.models import Q -from django.http import Http404, HttpResponse, HttpResponseRedirect -from django.shortcuts import get_object_or_404, redirect -from django.views.generic import ListView, DetailView, CreateView, UpdateView, TemplateView, View - -from pydotorg.mixins import GroupRequiredMixin, LoginRequiredMixin - -from .forms import JobForm, JobReviewCommentForm -from .models import Job, JobType, JobCategory, JobReviewComment +from django.views.generic import ( + CreateView, + DetailView, + ListView, + TemplateView, + UpdateView, + View, +) + +from pydotorg.mixins import ( + GroupRequiredMixin, + LoginRequiredMixin, +) +from .forms import ( + JobForm, + JobReviewCommentForm, +) +from .models import ( + Job, + JobCategory, + JobReviewComment, + JobType, +) class JobListMenu: diff --git a/mailing/admin.py b/mailing/admin.py index 72f78222b..acaa94140 100644 --- a/mailing/admin.py +++ b/mailing/admin.py @@ -1,8 +1,8 @@ from django.contrib import admin from django.forms.models import modelform_factory from django.http import HttpResponse -from django.urls import path from django.shortcuts import get_object_or_404 +from django.urls import path from mailing.forms import BaseEmailTemplateForm diff --git a/mailing/forms.py b/mailing/forms.py index 59f5676e7..606155255 100644 --- a/mailing/forms.py +++ b/mailing/forms.py @@ -1,5 +1,9 @@ from django import forms -from django.template import Template, Context, TemplateSyntaxError +from django.template import ( + Context, + Template, + TemplateSyntaxError, +) from mailing.models import BaseEmailTemplate diff --git a/mailing/models.py b/mailing/models.py index 5ba96f67b..d979f3490 100644 --- a/mailing/models.py +++ b/mailing/models.py @@ -1,6 +1,9 @@ from django.core.mail import EmailMessage from django.db import models -from django.template import Template, Context +from django.template import ( + Context, + Template, +) from django.urls import reverse diff --git a/membership/urls.py b/membership/urls.py index 8d12b46c9..ec4145ad2 100644 --- a/membership/urls.py +++ b/membership/urls.py @@ -1,6 +1,6 @@ -from . import views from django.urls import path +from . import views urlpatterns = [ path('', views.Membership.as_view(), name='membership'), diff --git a/membership/views.py b/membership/views.py index 9ed03581d..769d50704 100644 --- a/membership/views.py +++ b/membership/views.py @@ -2,6 +2,7 @@ from pydotorg.mixins import FlagMixin + # NOTE: Many aspects of 'membership' such as adjusting a user profile, signup, # etc are handled in the users app diff --git a/minutes/admin.py b/minutes/admin.py index 63f7fdd4d..6e26f4af2 100644 --- a/minutes/admin.py +++ b/minutes/admin.py @@ -1,7 +1,7 @@ from django.contrib import admin -from .models import Minutes from cms.admin import ContentManageableModelAdmin +from .models import Minutes @admin.register(Minutes) diff --git a/minutes/models.py b/minutes/models.py index 9101aa5e9..58d6fdb35 100644 --- a/minutes/models.py +++ b/minutes/models.py @@ -1,11 +1,9 @@ from django.conf import settings -from django.urls import reverse from django.db import models - +from django.urls import reverse from markupfield.fields import MarkupField from cms.models import ContentManageable - from .managers import MinutesQuerySet DEFAULT_MARKUP_TYPE = getattr(settings, 'DEFAULT_MARKUP_TYPE', 'restructuredtext') diff --git a/minutes/tests/test_views.py b/minutes/tests/test_views.py index 5bdee65db..b084abe78 100644 --- a/minutes/tests/test_views.py +++ b/minutes/tests/test_views.py @@ -1,8 +1,8 @@ import datetime -from django.urls import reverse from django.contrib.auth import get_user_model from django.test import TestCase +from django.urls import reverse from ..models import Minutes diff --git a/minutes/urls.py b/minutes/urls.py index a957df34b..6105deff8 100644 --- a/minutes/urls.py +++ b/minutes/urls.py @@ -1,10 +1,17 @@ -from .feeds import MinutesFeed -from . import views -from django.urls import path, re_path +from django.urls import ( + path, + re_path, +) +from . import views +from .feeds import MinutesFeed urlpatterns = [ path('', views.MinutesList.as_view(), name='minutes_list'), path('feed/', MinutesFeed(), name='minutes_feed'), - re_path(r'^(?P[0-9]{4})-(?P[0-9]{2})-(?P[0-9]{2})/$', views.MinutesDetail.as_view(), name='minutes_detail'), + re_path( + r'^(?P[0-9]{4})-(?P[0-9]{2})-(?P[0-9]{2})/$', + views.MinutesDetail.as_view(), + name='minutes_detail' + ), ] diff --git a/minutes/views.py b/minutes/views.py index 9a88957e4..6bac06582 100644 --- a/minutes/views.py +++ b/minutes/views.py @@ -1,6 +1,9 @@ from django.core.exceptions import ObjectDoesNotExist from django.http import Http404 -from django.views.generic import DetailView, ListView +from django.views.generic import ( + DetailView, + ListView, +) from .models import Minutes diff --git a/nominations/admin.py b/nominations/admin.py index 07e516488..02d499f2c 100644 --- a/nominations/admin.py +++ b/nominations/admin.py @@ -1,8 +1,11 @@ from django.contrib import admin - from django.db.models.functions import Lower -from nominations.models import Election, Nominee, Nomination +from nominations.models import ( + Election, + Nomination, + Nominee, +) @admin.register(Election) diff --git a/nominations/forms.py b/nominations/forms.py index 4a221fc2f..be090a1bf 100644 --- a/nominations/forms.py +++ b/nominations/forms.py @@ -1,6 +1,5 @@ from django import forms from django.utils.safestring import mark_safe - from markupfield.widgets import MarkupTextarea from .models import Nomination @@ -23,7 +22,10 @@ class Meta: help_texts = { "name": "Name of the person you are nominating.", "email": "Email address for the person you are nominating.", - "previous_board_service": "Has the person previously served on the PSF Board? If so what year(s)? Otherwise 'New board member'.", + "previous_board_service": ( + "Has the person previously served on the PSF Board? " + "If so what year(s)? Otherwise 'New board member'." + ), "employer": "Nominee's current employer.", "other_affiliations": "Any other relevant affiliations the Nominee has.", "nomination_statement": "Markdown syntax supported.", @@ -37,7 +39,10 @@ def __init__(self, *args, **kwargs): self_nomination = forms.BooleanField( required=False, - help_text="If you are nominating yourself, we will automatically associate the nomination with your python.org user.", + help_text=( + "If you are nominating yourself, we will automatically " + "associate the nomination with your python.org user." + ), ) def clean_self_nomination(self): @@ -46,7 +51,8 @@ def clean_self_nomination(self): if not self.request.user.first_name or not self.request.user.last_name: raise forms.ValidationError( mark_safe( - 'You must set your First and Last name in your User Profile to self nominate.' + 'You must set your First and Last name in your ' + 'User Profile to self nominate.' ) ) @@ -60,5 +66,8 @@ class Meta: "accepted", ) help_texts = { - "accepted": "If selected, this nomination will be considered accepted and displayed once nominations are public.", + "accepted": ( + "If selected, this nomination will be considered " + "accepted and displayed once nominations are public." + ), } diff --git a/nominations/models.py b/nominations/models.py index f52a286be..3d6d7abe4 100644 --- a/nominations/models.py +++ b/nominations/models.py @@ -5,10 +5,9 @@ from django.dispatch import receiver from django.urls import reverse from django.utils.text import slugify - -from fastly.utils import purge_url from markupfield.fields import MarkupField +from fastly.utils import purge_url from users.models import User @@ -33,9 +32,9 @@ def __str__(self): def nominations_open(self): if self.nominations_open_at and self.nominations_close_at: return ( - self.nominations_open_at - < datetime.datetime.now(datetime.timezone.utc) - < self.nominations_close_at + self.nominations_open_at < + datetime.datetime.now(datetime.timezone.utc) < + self.nominations_close_at ) return False @@ -129,8 +128,8 @@ def display_name(self): @property def display_previous_board_service(self): if ( - self.self_nomination is not None - and self.self_nomination.previous_board_service + self.self_nomination is not None and + self.self_nomination.previous_board_service ): return self.self_nomination.previous_board_service @@ -216,16 +215,16 @@ def get_accept_url(self): def editable(self, user=None): if ( - self.nominee - and user == self.nominee.user - and self.election.nominations_open + self.nominee and + user == self.nominee.user and + self.election.nominations_open ): return True if ( - user == self.nominator - and not (self.accepted or self.approved) - and self.election.nominations_open + user == self.nominator and + not (self.accepted or self.approved) and + self.election.nominations_open ): return True diff --git a/nominations/templatetags/nominations.py b/nominations/templatetags/nominations.py index 8e449dfcc..46b016108 100644 --- a/nominations/templatetags/nominations.py +++ b/nominations/templatetags/nominations.py @@ -1,4 +1,5 @@ import random + from django import template register = template.Library() diff --git a/nominations/urls.py b/nominations/urls.py index 1815ae2e7..b1e717681 100644 --- a/nominations/urls.py +++ b/nominations/urls.py @@ -1,26 +1,27 @@ -from . import views from django.urls import path +from . import views + app_name = "nominations" urlpatterns = [ path('elections/', views.ElectionsList.as_view(), name="elections_list"), path('election//', views.ElectionDetail.as_view(), name="election_detail"), path('elections//nominees/', views.NomineeList.as_view(), - name="nominees_list", - ), + name="nominees_list", + ), path('elections//nominees//', views.NomineeDetail.as_view(), - name="nominee_detail", - ), + name="nominee_detail", + ), path('/create/', views.NominationCreate.as_view(), - name="nomination_create", - ), + name="nomination_create", + ), path('//', views.NominationView.as_view(), - name="nomination_detail", - ), + name="nomination_detail", + ), path('//edit/', views.NominationEdit.as_view(), - name="nomination_edit", - ), + name="nomination_edit", + ), path('//accept/', views.NominationAccept.as_view(), - name="nomination_accept", - ), + name="nomination_accept", + ), ] diff --git a/nominations/views.py b/nominations/views.py index 484f7a7c2..43364f328 100644 --- a/nominations/views.py +++ b/nominations/views.py @@ -1,14 +1,25 @@ from django.contrib import messages from django.contrib.auth.mixins import UserPassesTestMixin - -from django.views.generic import CreateView, UpdateView, DetailView, ListView -from django.urls import reverse from django.http import Http404 +from django.urls import reverse +from django.views.generic import ( + CreateView, + DetailView, + ListView, + UpdateView, +) from pydotorg.mixins import LoginRequiredMixin - -from .models import Nomination, Nominee, Election -from .forms import NominationForm, NominationCreateForm, NominationAcceptForm +from .forms import ( + NominationAcceptForm, + NominationCreateForm, + NominationForm, +) +from .models import ( + Election, + Nomination, + Nominee, +) class ElectionsList(ListView): diff --git a/pages/admin.py b/pages/admin.py index beb8d3b46..aed6e03c2 100644 --- a/pages/admin.py +++ b/pages/admin.py @@ -1,7 +1,11 @@ from django.contrib import admin from cms.admin import ContentManageableModelAdmin -from .models import Page, Image, DocumentFile +from .models import ( + DocumentFile, + Image, + Page, +) class ImageInlineAdmin(admin.StackedInline): @@ -48,7 +52,20 @@ class PageAdmin(ContentManageableModelAdmin): list_filter = [PagePathFilter, 'is_published'] inlines = [ImageInlineAdmin, DocumentFileInlineAdmin] fieldsets = [ - (None, {'fields': ('title', 'keywords', 'description', 'path', 'content', 'content_markup_type', 'is_published')}), - ('Advanced options', {'classes': ('collapse',), 'fields': ('template_name',)}), + (None, { + 'fields': ( + 'title', + 'keywords', + 'description', + 'path', + 'content', + 'content_markup_type', + 'is_published' + ) + }), + ('Advanced options', { + 'classes': ('collapse',), + 'fields': ('template_name',) + }), ] save_as = True diff --git a/pages/api.py b/pages/api.py index 073f60e68..9e1a8a040 100644 --- a/pages/api.py +++ b/pages/api.py @@ -1,10 +1,14 @@ from rest_framework.authentication import TokenAuthentication -from pydotorg.resources import GenericResource, OnlyPublishedAuthorization from pydotorg.drf import ( - BaseReadOnlyAPIViewSet, BaseFilterSet, IsStaffOrReadOnly, + BaseFilterSet, + BaseReadOnlyAPIViewSet, + IsStaffOrReadOnly, +) +from pydotorg.resources import ( + GenericResource, + OnlyPublishedAuthorization, ) - from .models import Page from .serializers import PageSerializer diff --git a/pages/factories.py b/pages/factories.py index c525f11f4..e531f8cea 100644 --- a/pages/factories.py +++ b/pages/factories.py @@ -1,10 +1,8 @@ import factory - from django.template.defaultfilters import slugify from factory.django import DjangoModelFactory from users.factories import UserFactory - from .models import Page diff --git a/pages/management/commands/fix_success_story_images.py b/pages/management/commands/fix_success_story_images.py index 7fc9fe1de..e5391148e 100644 --- a/pages/management/commands/fix_success_story_images.py +++ b/pages/management/commands/fix_success_story_images.py @@ -1,14 +1,17 @@ -import re import os -import requests - +import re from urllib.parse import urlparse -from django.core.management.base import BaseCommand +import requests from django.conf import settings from django.core.files import File +from django.core.management.base import BaseCommand -from ...models import Page, Image, page_image_path +from ...models import ( + Image, + Page, + page_image_path, +) class Command(BaseCommand): diff --git a/pages/management/commands/import_pages_from_svn.py b/pages/management/commands/import_pages_from_svn.py index f6865c0ed..2997bd009 100644 --- a/pages/management/commands/import_pages_from_svn.py +++ b/pages/management/commands/import_pages_from_svn.py @@ -1,15 +1,17 @@ +import os import re import shutil -import os import traceback -from django.core.management.base import BaseCommand +from bs4 import BeautifulSoup from django.conf import settings from django.core.exceptions import ImproperlyConfigured +from django.core.management.base import BaseCommand -from bs4 import BeautifulSoup - -from ...models import Page, Image +from ...models import ( + Image, + Page, +) from ...parser import parse_page @@ -49,7 +51,7 @@ def copy_image(self, content_path, image): pass try: shutil.copyfile(src, dst) - except Exception as e: + except Exception: # noqa pass def save_images(self, content_path, page): @@ -77,7 +79,7 @@ def handle(self, *args, **kwargs): raise ImproperlyConfigured("PYTHON_ORG_CONTENT_SVN_PATH not defined in settings") matches = [] - for root, dirnames, filenames in os.walk(self.SVN_REPO_PATH): + for root, __dirnames, filenames in os.walk(self.SVN_REPO_PATH): for filename in filenames: if re.match(r'(content\.(ht|rst)|body\.html)$', filename): matches.append(os.path.join(root, filename)) @@ -91,7 +93,7 @@ def handle(self, *args, **kwargs): try: data = parse_page(os.path.dirname(match)) - except Exception as e: + except Exception: # noqa print(f"Unable to parse {match}") traceback.print_exc() continue @@ -107,7 +109,7 @@ def handle(self, *args, **kwargs): page_obj, _ = Page.objects.get_or_create(path=path, defaults=defaults) self.save_images(path, page_obj) - except Exception as e: + except Exception: # noqa print(f"Unable to create Page object for {match}") traceback.print_exc() continue diff --git a/pages/middleware.py b/pages/middleware.py index 46b46189a..489799dba 100644 --- a/pages/middleware.py +++ b/pages/middleware.py @@ -1,5 +1,6 @@ -from django.conf import settings from django import http +from django.conf import settings + from .models import Page from .views import PageView @@ -36,7 +37,7 @@ def __call__(self, request): except Page.DoesNotExist: pass if (settings.APPEND_SLASH and page is not None and - not request.path.endswith('/')): + not request.path.endswith('/')): scheme = "https" if request.is_secure() else "http" new_path = request.path + '/' new_url = f"{scheme}://{request.get_host()}{new_path}" diff --git a/pages/models.py b/pages/models.py index 9b67997e1..66f104c76 100644 --- a/pages/models.py +++ b/pages/models.py @@ -8,24 +8,20 @@ import os import re - from copy import deepcopy +import cmarkgfm +from cmarkgfm.cmark import Options as cmarkgfmOptions from django.conf import settings from django.core import validators from django.db import models from django.db.models.signals import post_save from django.dispatch import receiver - from markupfield.fields import MarkupField from markupfield.markup import DEFAULT_MARKUP_TYPES -import cmarkgfm -from cmarkgfm.cmark import Options as cmarkgfmOptions - from cms.models import ContentManageable from fastly.utils import purge_url - from .managers import PageQuerySet DEFAULT_MARKUP_TYPE = getattr(settings, 'DEFAULT_MARKUP_TYPE', 'restructuredtext') @@ -38,8 +34,8 @@ /? # Possibly ending with a slash $ """, - re.X -) + re.X + ) is_valid_page_path = validators.RegexValidator( regex=PAGE_PATH_RE, @@ -64,8 +60,8 @@ # tagfilter used by Github (we can be more liberal, since we know our page # editors). -def unsafe_markdown_to_html(text, options=0): +def unsafe_markdown_to_html(text, options=0): """Render the given GitHub-flavored Makrdown to HTML. This function is similar to cmarkgfm.github_flavored_markdown_to_html(), @@ -85,6 +81,7 @@ def unsafe_markdown_to_html(text, options=0): 'table', 'autolink', 'strikethrough', 'tasklist' ]) + RENDERERS.append( ( "markdown_unsafe", @@ -157,4 +154,3 @@ class DocumentFile(models.Model): def __str__(self): return self.document.url - diff --git a/pages/parser.py b/pages/parser.py index 47a1b4cfb..f754c47dd 100644 --- a/pages/parser.py +++ b/pages/parser.py @@ -1,7 +1,8 @@ -import chardet import email import os +import chardet + def read_content_file(dirpath): """(str): (str, email.Message) diff --git a/pages/search_indexes.py b/pages/search_indexes.py index b41e6f369..5ebf82119 100644 --- a/pages/search_indexes.py +++ b/pages/search_indexes.py @@ -1,5 +1,7 @@ -from django.template.defaultfilters import truncatewords_html, striptags - +from django.template.defaultfilters import ( + striptags, + truncatewords_html, +) from haystack import indexes from .models import Page diff --git a/pages/tests/base.py b/pages/tests/base.py index b5046816c..eb2ebbfa1 100644 --- a/pages/tests/base.py +++ b/pages/tests/base.py @@ -1,5 +1,6 @@ from django.contrib.auth import get_user_model from django.test import TestCase + from ..models import Page User = get_user_model() diff --git a/pages/tests/test_api.py b/pages/tests/test_api.py index 4026bac75..8ee92ba1d 100644 --- a/pages/tests/test_api.py +++ b/pages/tests/test_api.py @@ -1,8 +1,7 @@ from rest_framework.test import APITestCase -from pydotorg.drf import BaseAPITestCase - from pages.factories import PageFactory +from pydotorg.drf import BaseAPITestCase from users.factories import UserFactory diff --git a/pages/tests/test_models.py b/pages/tests/test_models.py index 2215cc812..0b6c44fc4 100644 --- a/pages/tests/test_models.py +++ b/pages/tests/test_models.py @@ -4,7 +4,10 @@ import ddt from .base import BasePageTests -from ..models import Page, PAGE_PATH_RE +from ..models import ( + PAGE_PATH_RE, + Page, +) class PageModelTests(BasePageTests): diff --git a/pages/tests/test_views.py b/pages/tests/test_views.py index d4e17f8ce..dc78f2447 100644 --- a/pages/tests/test_views.py +++ b/pages/tests/test_views.py @@ -1,7 +1,7 @@ -from .base import BasePageTests - -from django.contrib.sites.models import Site from django.contrib.redirects.models import Redirect +from django.contrib.sites.models import Site + +from .base import BasePageTests class PageViewTests(BasePageTests): diff --git a/pages/urls.py b/pages/urls.py index df60e2732..614ed4c64 100644 --- a/pages/urls.py +++ b/pages/urls.py @@ -1,6 +1,7 @@ -from .views import PageView from django.urls import path +from .views import PageView + urlpatterns = [ path('/', PageView.as_view(), name='page_detail'), ] diff --git a/peps/converters.py b/peps/converters.py index 1d63d7438..e8882d8ef 100644 --- a/peps/converters.py +++ b/peps/converters.py @@ -1,19 +1,21 @@ -import functools import datetime -import re import os +import re from bs4 import BeautifulSoup - -from django.conf import settings -from django.core.exceptions import ImproperlyConfigured from django.core.files import File from django.db.models import Max -from pages.models import Page, Image +from pages.models import ( + Image, + Page, +) PEP_TEMPLATE = 'pages/pep-page.html' -pep_url = lambda num: f'dev/peps/pep-{num}/' + + +def pep_url(num): + return f'dev/peps/pep-{num}/' def get_peps_last_updated(): @@ -76,7 +78,7 @@ def fix_headers(soup, data): if t.next_sibling.text == '': t.parent.extract() if 'Last-Modified:' in t.text: - if '$Date$'in t.next_sibling.text: + if '$Date$' in t.next_sibling.text: t.parent.extract() if t.next_sibling.text == '': t.parent.extract() diff --git a/peps/management/commands/generate_pep_pages.py b/peps/management/commands/generate_pep_pages.py index 9f9010584..9a340d822 100644 --- a/peps/management/commands/generate_pep_pages.py +++ b/peps/management/commands/generate_pep_pages.py @@ -1,19 +1,23 @@ -import re import os - +import re from contextlib import ExitStack from tarfile import TarFile -from tempfile import TemporaryDirectory, TemporaryFile +from tempfile import ( + TemporaryDirectory, + TemporaryFile, +) import requests - -from django.core.management import BaseCommand -from django.conf import settings - from dateutil.parser import parse as parsedate +from django.conf import settings +from django.core.management import BaseCommand from peps.converters import ( - get_pep0_page, get_pep_page, add_pep_image, get_peps_rss, get_peps_last_updated + add_pep_image, + get_pep0_page, + get_pep_page, + get_peps_last_updated, + get_peps_rss, ) pep_number_re = re.compile(r'pep-(\d+)') diff --git a/peps/tests/test_commands.py b/peps/tests/test_commands.py index 2579a5f99..7dbc196c8 100644 --- a/peps/tests/test_commands.py +++ b/peps/tests/test_commands.py @@ -1,19 +1,18 @@ import io +import responses from bs4 import BeautifulSoup - -from django.test import TestCase, override_settings from django.conf import settings from django.core import serializers from django.core.management import call_command - -import responses +from django.test import ( + TestCase, + override_settings, +) from pages.models import Image - from . import FAKE_PEP_ARTIFACT - PEP_ARTIFACT_URL = 'https://example.net/fake-peps.tar.gz' diff --git a/peps/tests/test_converters.py b/peps/tests/test_converters.py index 833bf7c0e..eef6bca29 100644 --- a/peps/tests/test_converters.py +++ b/peps/tests/test_converters.py @@ -1,9 +1,10 @@ -from django.test import TestCase, override_settings -from django.core.exceptions import ImproperlyConfigured +from django.test import TestCase from django.test.utils import captured_stdout -from peps.converters import get_pep0_page, get_pep_page, add_pep_image - +from peps.converters import ( + add_pep_image, + get_pep_page, +) from . import FAKE_PEP_REPO diff --git a/pydotorg/context_processors.py b/pydotorg/context_processors.py index 1c11341fd..8387c8648 100644 --- a/pydotorg/context_processors.py +++ b/pydotorg/context_processors.py @@ -1,5 +1,9 @@ from django.conf import settings -from django.urls import resolve, Resolver404, reverse +from django.urls import ( + Resolver404, + resolve, + reverse, +) def site_info(request): diff --git a/pydotorg/drf.py b/pydotorg/drf.py index 9b1ccb5ca..123a0c551 100644 --- a/pydotorg/drf.py +++ b/pydotorg/drf.py @@ -1,14 +1,20 @@ import json +from urllib.parse import ( + urlencode, + urljoin, +) -from urllib.parse import urlencode, urljoin - -from django.db.models.constants import LOOKUP_SEP from django.core.exceptions import ImproperlyConfigured - +from django.db.models.constants import LOOKUP_SEP from django_filters import rest_framework as filters -from rest_framework import serializers -from rest_framework import viewsets -from rest_framework.permissions import SAFE_METHODS, IsAuthenticatedOrReadOnly +from rest_framework import ( + serializers, + viewsets, +) +from rest_framework.permissions import ( + IsAuthenticatedOrReadOnly, + SAFE_METHODS, +) class IsStaffOrReadOnly(IsAuthenticatedOrReadOnly): diff --git a/pydotorg/resources.py b/pydotorg/resources.py index 7cc0be981..24ac26ad5 100644 --- a/pydotorg/resources.py +++ b/pydotorg/resources.py @@ -1,3 +1,4 @@ +from django.contrib.auth import get_user_model from tastypie.authentication import ApiKeyAuthentication from tastypie.authorization import Authorization from tastypie.exceptions import Unauthorized @@ -5,8 +6,6 @@ from tastypie.resources import ModelResource from tastypie.throttle import CacheThrottle -from django.contrib.auth import get_user_model - class ApiKeyOrGuestAuthentication(ApiKeyAuthentication): def _unauthorized(self): @@ -59,6 +58,7 @@ class StaffAuthorization(Authorization): """ Everybody can read everything. Staff users can write everything. """ + def read_list(self, object_list, bundle): # Everybody can read return object_list @@ -102,6 +102,7 @@ class OnlyPublishedAuthorization(StaffAuthorization): """ Only staff users can see unpublished objects. """ + def read_list(self, object_list, bundle): if not bundle.request.user.is_staff: return object_list.filter(is_published=True) @@ -119,5 +120,5 @@ class GenericResource(ModelResource): class Meta: authentication = ApiKeyOrGuestAuthentication() authorization = StaffAuthorization() - throttle = CacheThrottle(throttle_at=600) # default is 150 req/hr + throttle = CacheThrottle(throttle_at=600) # default is 150 req/hr abstract = True diff --git a/pydotorg/settings/base.py b/pydotorg/settings/base.py index 702eaa364..f903d6de4 100644 --- a/pydotorg/settings/base.py +++ b/pydotorg/settings/base.py @@ -1,7 +1,7 @@ import os -from dj_database_url import parse as dj_database_url_parser -from decouple import config +from decouple import config +from dj_database_url import parse as dj_database_url_parser from django.contrib.messages import constants ### Basic config @@ -45,6 +45,7 @@ MEDIA_ROOT = os.path.join(BASE, 'media') MEDIA_URL = '/media/' +MEDIAFILES_LOCATION = 'media' # Absolute path to the directory static files should be collected to. # Don't put anything in this directory yourself; store your static files @@ -190,10 +191,10 @@ 'allauth', 'allauth.account', 'allauth.socialaccount', - #'allauth.socialaccount.providers.facebook', - #'allauth.socialaccount.providers.github', - #'allauth.socialaccount.providers.openid', - #'allauth.socialaccount.providers.twitter', + # 'allauth.socialaccount.providers.facebook', + # 'allauth.socialaccount.providers.github', + # 'allauth.socialaccount.providers.openid', + # 'allauth.socialaccount.providers.twitter', # Tastypie needs the `users` app to be already loaded. 'tastypie', @@ -253,8 +254,7 @@ PEP_ARTIFACT_URL = 'https://pythondotorg-assets-staging.s3.amazonaws.com/fake-peps.tar.gz' ### Fastly ### -FASTLY_API_KEY = False # Set to Fastly API key in production to allow pages to - # be purged on save +FASTLY_API_KEY = False # Set to Fastly API key in production to allow pages to be purged on save # Jobs JOB_THRESHOLD_DAYS = 90 @@ -277,7 +277,7 @@ ### Pipeline -from .pipeline import PIPELINE +from .pipeline import PIPELINE # noqa ### contrib.messages diff --git a/pydotorg/settings/heroku.py b/pydotorg/settings/heroku.py index 5adff485c..54b85ec75 100644 --- a/pydotorg/settings/heroku.py +++ b/pydotorg/settings/heroku.py @@ -1,15 +1,11 @@ -import os - -import dj_database_url -import raven from decouple import Csv -from .base import * +from .base import * # noqa: F403 DEBUG = TEMPLATE_DEBUG = False DATABASE_CONN_MAX_AGE = 600 -DATABASES['default']['CONN_MAX_AGE'] = DATABASE_CONN_MAX_AGE +DATABASES['default']['CONN_MAX_AGE'] = DATABASE_CONN_MAX_AGE # noqa: F405 ## Django Caching @@ -20,7 +16,7 @@ } } -HAYSTACK_SEARCHBOX_SSL_URL = config( +HAYSTACK_SEARCHBOX_SSL_URL = config( # noqa: F405 'SEARCHBOX_SSL_URL' ) @@ -32,55 +28,55 @@ }, } -SECRET_KEY = config('SECRET_KEY') +SECRET_KEY = config('SECRET_KEY') # noqa: F405 -ALLOWED_HOSTS = config('ALLOWED_HOSTS', cast=Csv()) +ALLOWED_HOSTS = config('ALLOWED_HOSTS', cast=Csv()) # noqa: F405 MIDDLEWARE = [ 'whitenoise.middleware.WhiteNoiseMiddleware', -] + MIDDLEWARE +] + MIDDLEWARE # noqa: F405 MEDIAFILES_LOCATION = 'media' DEFAULT_FILE_STORAGE = 'custom_storages.MediaStorage' STATICFILES_STORAGE = 'custom_storages.PipelineManifestStorage' -EMAIL_HOST = config('EMAIL_HOST') -EMAIL_HOST_USER = config('EMAIL_HOST_USER') -EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD') -EMAIL_PORT = int(config('EMAIL_PORT')) +EMAIL_HOST = config('EMAIL_HOST') # noqa: F405 +EMAIL_HOST_USER = config('EMAIL_HOST_USER') # noqa: F405 +EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD') # noqa: F405 +EMAIL_PORT = int(config('EMAIL_PORT')) # noqa: F405 EMAIL_USE_TLS = True -DEFAULT_FROM_EMAIL = config('DEFAULT_FROM_EMAIL') +DEFAULT_FROM_EMAIL = config('DEFAULT_FROM_EMAIL') # noqa: F405 PEP_REPO_PATH = None -PEP_ARTIFACT_URL = config('PEP_ARTIFACT_URL') +PEP_ARTIFACT_URL = config('PEP_ARTIFACT_URL') # noqa: F405 # Fastly API Key -FASTLY_API_KEY = config('FASTLY_API_KEY') +FASTLY_API_KEY = config('FASTLY_API_KEY') # noqa: F405 SECURE_SSL_REDIRECT = True SECURE_PROXY_SSL_HEADER = ('HTTP_FASTLY_SSL', '1') SESSION_COOKIE_SECURE = True CSRF_COOKIE_SECURE = True -INSTALLED_APPS += [ +INSTALLED_APPS += [ # noqa: F405 "raven.contrib.django.raven_compat", ] RAVEN_CONFIG = { - "dsn": config('SENTRY_DSN'), - "release": config('SOURCE_VERSION'), + "dsn": config('SENTRY_DSN'), # noqa: F405 + "release": config('SOURCE_VERSION'), # noqa: F405 } -AWS_ACCESS_KEY_ID = config('AWS_ACCESS_KEY_ID') -AWS_SECRET_ACCESS_KEY = config('AWS_SECRET_ACCESS_KEY') -AWS_STORAGE_BUCKET_NAME = config('AWS_STORAGE_BUCKET_NAME') -AWS_DEFAULT_ACL = config('AWS_DEFAULT_ACL', default='public-read') +AWS_ACCESS_KEY_ID = config('AWS_ACCESS_KEY_ID') # noqa: F405 +AWS_SECRET_ACCESS_KEY = config('AWS_SECRET_ACCESS_KEY') # noqa: F405 +AWS_STORAGE_BUCKET_NAME = config('AWS_STORAGE_BUCKET_NAME') # noqa: F405 +AWS_DEFAULT_ACL = config('AWS_DEFAULT_ACL', default='public-read') # noqa: F405 AWS_AUTO_CREATE_BUCKET = False AWS_S3_OBJECT_PARAMETERS = { 'CacheControl': 'max-age=86400', } AWS_QUERYSTRING_AUTH = False AWS_S3_FILE_OVERWRITE = False -AWS_S3_REGION_NAME = config('AWS_S3_REGION_NAME', default='us-east-1') +AWS_S3_REGION_NAME = config('AWS_S3_REGION_NAME', default='us-east-1') # noqa: F405 AWS_S3_USE_SSL = True -AWS_S3_ENDPOINT_URL = config('AWS_S3_ENDPOINT_URL', default='https://s3.amazonaws.com') +AWS_S3_ENDPOINT_URL = config('AWS_S3_ENDPOINT_URL', default='https://s3.amazonaws.com') # noqa: F405 diff --git a/pydotorg/settings/local.py b/pydotorg/settings/local.py index 4ecbe35aa..013c0606d 100644 --- a/pydotorg/settings/local.py +++ b/pydotorg/settings/local.py @@ -1,6 +1,7 @@ -from .base import * import os +from .base import * # noqa: F403 + DEBUG = True ALLOWED_HOSTS = ['*'] @@ -12,14 +13,14 @@ PYTHON_ORG_CONTENT_SVN_PATH = '' DATABASES = { - 'default': config( + 'default': config( # noqa: F405 'DATABASE_URL', default='postgres:///pythondotorg', - cast=dj_database_url_parser + cast=dj_database_url_parser # noqa: F405 ) } -HAYSTACK_SEARCHBOX_SSL_URL = config( +HAYSTACK_SEARCHBOX_SSL_URL = config( # noqa: F405 'SEARCHBOX_SSL_URL', default='http://127.0.0.1:9200/' ) @@ -36,12 +37,12 @@ # Set the local pep repository path to fetch PEPs from, # or none to fallback to the tarball specified by PEP_ARTIFACT_URL. -PEP_REPO_PATH = config('PEP_REPO_PATH', default=None) # directory path or None +PEP_REPO_PATH = config('PEP_REPO_PATH', default=None) # directory path or None # noqa: F405 # Set the path to where to fetch PEP artifacts from. # The value can be a local path or a remote URL. # Ignored if PEP_REPO_PATH is set. -PEP_ARTIFACT_URL = os.path.join(BASE, 'peps/tests/peps.tar.gz') +PEP_ARTIFACT_URL = os.path.join(BASE, 'peps/tests/peps.tar.gz') # noqa: F405 # Use Dummy SASS compiler to avoid performance issues and remove the need to # have a sass compiler installed at all during local development if you aren't @@ -54,11 +55,11 @@ # yui-compressor. # PIPELINE['YUI_BINARY'] = '/usr/bin/java -Xss200048k -jar /usr/share/yui-compressor/yui-compressor.jar' -INSTALLED_APPS += [ +INSTALLED_APPS += [ # noqa: F405 'debug_toolbar', ] -MIDDLEWARE += [ +MIDDLEWARE += [ # noqa: F405 'debug_toolbar.middleware.DebugToolbarMiddleware', ] @@ -69,6 +70,6 @@ } } -REST_FRAMEWORK['DEFAULT_RENDERER_CLASSES'] += ( +REST_FRAMEWORK['DEFAULT_RENDERER_CLASSES'] += ( # noqa: F405 'rest_framework.renderers.BrowsableAPIRenderer', ) diff --git a/pydotorg/settings/pipeline.py b/pydotorg/settings/pipeline.py index 187613447..0815ea34d 100644 --- a/pydotorg/settings/pipeline.py +++ b/pydotorg/settings/pipeline.py @@ -1,6 +1,6 @@ -import os +import os # noqa: F401 -from .base import BASE +from .base import BASE # noqa: F401 PIPELINE_CSS = { 'style': { diff --git a/pydotorg/tests/test_classes.py b/pydotorg/tests/test_classes.py index d730e9cf0..2f2f069dd 100644 --- a/pydotorg/tests/test_classes.py +++ b/pydotorg/tests/test_classes.py @@ -1,4 +1,7 @@ -from django.template import Template, Context +from django.template import ( + Context, + Template, +) from django.test import TestCase diff --git a/pydotorg/tests/test_context_processors.py b/pydotorg/tests/test_context_processors.py index 8d5880a57..8bca05b80 100644 --- a/pydotorg/tests/test_context_processors.py +++ b/pydotorg/tests/test_context_processors.py @@ -1,10 +1,13 @@ +from django.conf import settings +from django.contrib.auth.models import AnonymousUser +from django.test import ( + RequestFactory, + TestCase, +) +from django.urls import reverse from model_bakery import baker -from django.urls import reverse -from django.conf import settings from pydotorg import context_processors -from django.test import TestCase, RequestFactory -from django.contrib.auth.models import AnonymousUser class TemplateProcessorsTestCase(TestCase): diff --git a/pydotorg/tests/test_middleware.py b/pydotorg/tests/test_middleware.py index d4a8eef86..3759540bc 100644 --- a/pydotorg/tests/test_middleware.py +++ b/pydotorg/tests/test_middleware.py @@ -1,7 +1,6 @@ -from django.test import TestCase - -from django.contrib.sites.models import Site from django.contrib.redirects.models import Redirect +from django.contrib.sites.models import Site +from django.test import TestCase class MiddlewareTests(TestCase): diff --git a/pydotorg/tests/test_resources.py b/pydotorg/tests/test_resources.py index 58c3af256..3474d858a 100644 --- a/pydotorg/tests/test_resources.py +++ b/pydotorg/tests/test_resources.py @@ -1,8 +1,9 @@ -from django.test import TestCase from django.contrib.auth import get_user_model from django.http import HttpRequest +from django.test import TestCase from pydotorg.resources import ApiKeyOrGuestAuthentication + User = get_user_model() diff --git a/pydotorg/tests/test_views.py b/pydotorg/tests/test_views.py index d6905a41c..0b1f825f1 100644 --- a/pydotorg/tests/test_views.py +++ b/pydotorg/tests/test_views.py @@ -1,8 +1,7 @@ -from django.urls import reverse +import factory from django.db.models import signals from django.test import TestCase - -import factory +from django.urls import reverse from downloads.models import Release diff --git a/pydotorg/urls.py b/pydotorg/urls.py index 5fc6b3f12..b1cb02a14 100644 --- a/pydotorg/urls.py +++ b/pydotorg/urls.py @@ -1,15 +1,26 @@ -from django.conf.urls import handler404, include +from django.conf import settings +from django.conf.urls.static import static from django.contrib import admin from django.contrib.staticfiles.urls import staticfiles_urlpatterns -from django.conf.urls.static import static -from django.urls import path, re_path -from django.views.generic.base import TemplateView, RedirectView -from django.conf import settings +from django.urls import include +from django.urls import ( + path, + re_path, +) +from django.views.generic.base import ( + RedirectView, + TemplateView, +) from cms.views import custom_404 -from users.views import HoneypotSignupView, CustomPasswordChangeView - -from . import views, urls_api +from users.views import ( + CustomPasswordChangeView, + HoneypotSignupView, +) +from . import ( + urls_api, + views, +) handler404 = custom_404 @@ -51,7 +62,7 @@ # Override /accounts/password/change/ to add Honeypot # and change success URL. path('accounts/password/change/', CustomPasswordChangeView.as_view(), - name='account_change_password'), + name='account_change_password'), path('accounts/', include('allauth.urls')), path('box/', include('boxes.urls')), path('community/', include('community.urls', namespace='community')), diff --git a/pydotorg/urls_api.py b/pydotorg/urls_api.py index 0c27699b1..8757daff2 100644 --- a/pydotorg/urls_api.py +++ b/pydotorg/urls_api.py @@ -1,13 +1,23 @@ -from django.conf.urls import url - +from django.urls import re_path from rest_framework import routers from tastypie.api import Api -from downloads.api import OSResource, ReleaseResource, ReleaseFileResource -from downloads.api import OSViewSet, ReleaseViewSet, ReleaseFileViewSet -from pages.api import PageResource -from pages.api import PageViewSet -from sponsors.api import LogoPlacementeAPIList, SponsorshipAssetsAPIList +from downloads.api import ( + OSResource, + OSViewSet, + ReleaseFileResource, + ReleaseFileViewSet, + ReleaseResource, + ReleaseViewSet, +) +from pages.api import ( + PageResource, + PageViewSet, +) +from sponsors.api import ( + LogoPlacementeAPIList, + SponsorshipAssetsAPIList, +) v1_api = Api(api_name='v1') v1_api.register(PageResource()) @@ -22,6 +32,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/pydotorg/views.py b/pydotorg/views.py index 476e62fd9..c6267e3e5 100644 --- a/pydotorg/views.py +++ b/pydotorg/views.py @@ -1,5 +1,8 @@ from django.conf import settings -from django.views.generic.base import RedirectView, TemplateView +from django.views.generic.base import ( + RedirectView, + TemplateView, +) from codesamples.models import CodeSample from downloads.models import Release diff --git a/pydotorg/wsgi.py b/pydotorg/wsgi.py index 92031712f..6febbfdcf 100644 --- a/pydotorg/wsgi.py +++ b/pydotorg/wsgi.py @@ -24,7 +24,7 @@ # This application object is used by any WSGI server configured to use this # file. This includes Django's development server, if the WSGI_APPLICATION # setting points here. -from django.core.wsgi import get_wsgi_application +from django.core.wsgi import get_wsgi_application # noqa: E402 application = get_wsgi_application() # Apply WSGI middleware here. diff --git a/sponsors/admin.py b/sponsors/admin.py index 4e68a7d20..9bec7bb58 100644 --- a/sponsors/admin.py +++ b/sponsors/admin.py @@ -1,25 +1,66 @@ +from django.contrib import admin from django.contrib.contenttypes.admin import GenericTabularInline from django.contrib.contenttypes.models import ContentType -from ordered_model.admin import OrderedModelAdmin -from polymorphic.admin import PolymorphicInlineSupportMixin, StackedPolymorphicInline, PolymorphicParentModelAdmin, \ - PolymorphicChildModelAdmin - -from django.db.models import Subquery -from django.template import Context, Template -from django.contrib import admin from django.contrib.humanize.templatetags.humanize import intcomma +from django.db.models import Subquery from django.forms import ModelForm -from django.urls import path, reverse, resolve +from django.template import ( + Context, + Template, +) +from django.urls import ( + path, + reverse, +) from django.utils.functional import cached_property from django.utils.html import mark_safe +from ordered_model.admin import OrderedModelAdmin +from polymorphic.admin import ( + PolymorphicChildModelAdmin, + PolymorphicInlineSupportMixin, + PolymorphicParentModelAdmin, + StackedPolymorphicInline, +) +from cms.admin import ContentManageableModelAdmin from mailing.admin import BaseEmailTemplateAdmin -from sponsors.models import * -from sponsors.models.benefits import RequiredAssetMixin from sponsors import views_admin -from sponsors.forms import SponsorshipReviewAdminForm, SponsorBenefitAdminInlineForm, RequiredImgAssetConfigurationForm, \ - SponsorshipBenefitAdminForm, CloneApplicationConfigForm -from cms.admin import ContentManageableModelAdmin +from sponsors.forms import ( + CloneApplicationConfigForm, + RequiredImgAssetConfigurationForm, + SponsorBenefitAdminInlineForm, + SponsorshipBenefitAdminForm, + SponsorshipReviewAdminForm, +) +from sponsors.models import ( + BenefitFeature, + BenefitFeatureConfiguration, + Contract, + EmailTargetableConfiguration, + FileAsset, + GenericAsset, + ImgAsset, + LegalClause, + LogoPlacementConfiguration, + ProvidedFileAssetConfiguration, + ProvidedTextAssetConfiguration, + RequiredImgAssetConfiguration, + RequiredResponseAssetConfiguration, + RequiredTextAssetConfiguration, + ResponseAsset, + SPONSOR_TEMPLATE_HELP_TEXT, + Sponsor, + SponsorBenefit, + SponsorContact, + SponsorEmailNotificationTemplate, + Sponsorship, + SponsorshipBenefit, + SponsorshipCurrentYear, + SponsorshipPackage, + SponsorshipProgram, + TextAsset, + TieredBenefitConfiguration, +) def get_url_base_name(Model): @@ -30,9 +71,11 @@ class AssetsInline(GenericTabularInline): model = GenericAsset extra = 0 max_num = 0 - has_delete_permission = lambda self, request, obj: False readonly_fields = ["internal_name", "user_submitted_info", "value"] + def has_delete_permission(self, request, obj=None): + return False + def value(self, obj=None): if not obj or not obj.value: return "" @@ -105,6 +148,7 @@ class ProvidedFileAssetConfigurationInline(StackedPolymorphicInline.Child): ProvidedFileAssetConfigurationInline, ] + @admin.register(SponsorshipBenefit) class SponsorshipBenefitAdmin(PolymorphicInlineSupportMixin, OrderedModelAdmin): change_form_template = "sponsors/admin/sponsorshipbenefit_change_form.html" @@ -119,7 +163,9 @@ class SponsorshipBenefitAdmin(PolymorphicInlineSupportMixin, OrderedModelAdmin): "unavailable", "move_up_down_links", ] - list_filter = ["program", "year", "package_only", "packages", "new", "standalone", "unavailable"] + list_filter = [ + "program", "year", "package_only", "packages", "new", "standalone", "unavailable" + ] search_fields = ["name"] form = SponsorshipBenefitAdminForm @@ -188,7 +234,9 @@ def get_readonly_fields(self, request, obj=None): def get_prepopulated_fields(self, request, obj=None): if not obj: - return {'slug': ['name']} + return { + 'slug': ['name'] + } return {} @@ -208,6 +256,7 @@ class SponsorshipsInline(admin.TabularInline): def link(self, obj): url = reverse("admin:sponsors_sponsorship_change", args=[obj.id]) return mark_safe(f"{obj.id}") + link.short_description = "ID" @@ -374,7 +423,10 @@ def get_fieldsets(self, request, obj=None): for title, cfg in super().get_fieldsets(request, obj): # disable collapse option in case of sponsorships with customizations if title == "User Customizations" and obj: - if obj.user_customizations["added_by_user"] or obj.user_customizations["removed_by_user"]: + if ( + obj.user_customizations["added_by_user"] or + obj.user_customizations["removed_by_user"] + ): cfg["classes"] = [] fieldsets.append((title, cfg)) return fieldsets @@ -388,7 +440,7 @@ def send_notifications(self, request, queryset): send_notifications.short_description = 'Send notifications to selected' - def get_readonly_fields(self, request, obj): + def get_readonly_fields(self, request, obj=None): readonly_fields = [ "for_modified_package", "sponsor_link", @@ -425,13 +477,16 @@ def get_readonly_fields(self, request, obj): 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" def get_estimated_cost(self, obj): - cost = None html = "This sponsorship has not customizations so there's no estimated cost" if obj.for_modified_package: - msg = "This sponsorship has customizations and this cost is a sum of all benefit's internal values from when this sponsorship was created" + msg = ( + "This sponsorship has customizations and this cost is a sum " + "of all benefit's internal values from when this sponsorship was created" + ) cost = intcomma(obj.estimated_cost) html = f"{cost} USD
Important: {msg}" return mark_safe(html) @@ -497,9 +552,14 @@ def get_sponsor_landing_page_url(self, obj): get_sponsor_landing_page_url.short_description = "Landing Page URL" def get_sponsor_web_logo(self, obj): - html = "{% load thumbnail %}{% thumbnail sponsor.web_logo '150x150' format='PNG' quality=100 as im %}{% endthumbnail %}" + html = ( + "{% load thumbnail %}{% thumbnail sponsor.web_logo '150x150' format='PNG' " + "quality=100 as im %}{% endthumbnail %}" + ) template = Template(html) - context = Context({'sponsor': obj.sponsor}) + context = Context({ + 'sponsor': obj.sponsor + }) html = template.render(context) return mark_safe(html) @@ -509,9 +569,14 @@ def get_sponsor_print_logo(self, obj): img = obj.sponsor.print_logo html = "" if img: - html = "{% load thumbnail %}{% thumbnail img '150x150' format='PNG' quality=100 as im %}{% endthumbnail %}" + html = ( + "{% load thumbnail %}{% thumbnail img '150x150' format='PNG' quality=100 as im %}" + "{% endthumbnail %}" + ) template = Template(html) - context = Context({'img': img}) + context = Context({ + 'img': img + }) html = template.render(context) return mark_safe(html) if html else "---" @@ -528,7 +593,10 @@ def get_sponsor_mailing_address(self, obj): f"{sponsor.city} - {sponsor.get_country_display()} ({sponsor.country})" ) if sponsor.state: - city_row = f"{sponsor.city} - {sponsor.state} - {sponsor.get_country_display()} ({sponsor.country})" + city_row = ( + f"{sponsor.city} - {sponsor.state} - " + f"{sponsor.get_country_display()} ({sponsor.country})" + ) mail_row = sponsor.mailing_address_line_1 if sponsor.mailing_address_line_2: @@ -627,11 +695,11 @@ def get_urls(self): def links(self, obj): clone_form = CloneApplicationConfigForm() - configured_years = clone_form.configured_years + configured_years = clone_form.configured_years # TODO: not used # noqa: F841 application_url = reverse("select_sponsorship_application_benefits") benefits_url = reverse("admin:sponsors_sponsorshipbenefit_changelist") - packages_url = reverse("admin:sponsors_sponsorshippackage_changelist") + packages_url = reverse("admin:sponsors_sponsorshippackage_changelist") # TODO: not used # noqa: F841 preview_label = 'View sponsorship application' year = obj.year html = "
    " @@ -646,6 +714,7 @@ def links(self, obj): html += f"
  • {preview_label}" html += "
" return mark_safe(html) + links.short_description = "Links" def other_years(self, obj): @@ -660,7 +729,7 @@ def other_years(self, obj): application_url = reverse("select_sponsorship_application_benefits") benefits_url = reverse("admin:sponsors_sponsorshipbenefit_changelist") - packages_url = reverse("admin:sponsors_sponsorshippackage_changelist") + packages_url = reverse("admin:sponsors_sponsorshippackage_changelist") # TODO: not used # noqa: F841 preview_label = 'View sponsorship application form for this year' html = "
    " for year in configured_years: @@ -678,11 +747,13 @@ 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) + @admin.register(LegalClause) class LegalClauseModelAdmin(OrderedModelAdmin): list_display = ["internal_name"] @@ -752,7 +823,7 @@ def get_revision(self, obj): ), ] - def get_readonly_fields(self, request, obj): + def get_readonly_fields(self, request, obj=None): readonly_fields = [ "status", "created_on", @@ -852,7 +923,9 @@ def get_form(self, request, obj=None, **kwargs): help_texts = { "content": SPONSOR_TEMPLATE_HELP_TEXT, } - kwargs.update({"help_texts": help_texts}) + kwargs.update({ + "help_texts": help_texts + }) return super().get_form(request, obj, **kwargs) @@ -880,8 +953,10 @@ class AssociatedBenefitListFilter(admin.SimpleListFilter): @property def benefits_with_assets(self): - qs = BenefitFeature.objects.required_assets().values_list("sponsor_benefit__sponsorship_benefit", - flat=True).distinct() + qs = BenefitFeature.objects.required_assets().values_list( + "sponsor_benefit__sponsorship_benefit", + flat=True + ).distinct() benefits = SponsorshipBenefit.objects.filter(id__in=Subquery(qs)) return {str(b.id): b for b in benefits} @@ -997,6 +1072,7 @@ def get_related_object(self, obj): 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" @@ -1017,7 +1093,7 @@ class ImgAssetModelAdmin(GenericAssetChildModelAdmin): @admin.register(FileAsset) -class ImgAssetModelAdmin(GenericAssetChildModelAdmin): +class FileAssetModelAdmin(GenericAssetChildModelAdmin): base_model = FileAsset diff --git a/sponsors/api.py b/sponsors/api.py index 0d180be6d..24e619843 100644 --- a/sponsors/api.py +++ b/sponsors/api.py @@ -1,12 +1,21 @@ -from django.utils.text import slugify from django.urls import reverse - +from django.utils.text import slugify from rest_framework import permissions -from rest_framework.views import APIView from rest_framework.response import Response -from sponsors.models import BenefitFeature, LogoPlacement, Sponsorship, GenericAsset -from sponsors.serializers import LogoPlacementSerializer, FilterLogoPlacementsSerializer, FilterAssetsSerializer, \ - AssetSerializer +from rest_framework.views import APIView + +from sponsors.models import ( + BenefitFeature, + GenericAsset, + LogoPlacement, + Sponsorship, +) +from sponsors.serializers import ( + AssetSerializer, + FilterAssetsSerializer, + FilterLogoPlacementsSerializer, + LogoPlacementSerializer, +) class SponsorPublisherPermission(permissions.BasePermission): @@ -44,15 +53,22 @@ def get(self, request, *args, **kwargs): } benefits = BenefitFeature.objects.filter(sponsor_benefit__sponsorship_id=sponsorship.pk) - logos = [l for l in benefits.instance_of(LogoPlacement) if not logo_filters.skip_logo(l)] + logos = [ + logo_ for logo_ in benefits.instance_of(LogoPlacement) + if not logo_filters.skip_logo(logo_) + ] for logo in logos: placement = base_data.copy() placement["publisher"] = logo.publisher placement["flight"] = logo.logo_place if logo.describe_as_sponsor: - placement["description"] = f"{sponsor.name} is a {sponsorship.level_name} sponsor of the Python Software Foundation." + placement["description"] = ( + f"{sponsor.name} is a {sponsorship.level_name} sponsor of " + f"the Python Software Foundation." + ) if logo.link_to_sponsors_page: - placement["sponsor_url"] = request.build_absolute_uri(reverse('psf-sponsors') + f"#{slugify(sponsor.name)}") + sponsor_relative_url = reverse('psf-sponsors') + f"#{slugify(sponsor.name)}" + placement["sponsor_url"] = request.build_absolute_uri(sponsor_relative_url) placements.append(placement) serializer = LogoPlacementSerializer(placements, many=True) diff --git a/sponsors/forms.py b/sponsors/forms.py index 01d3de4f2..fcf324c9a 100644 --- a/sponsors/forms.py +++ b/sponsors/forms.py @@ -1,5 +1,6 @@ import datetime from itertools import chain + from django import forms from django.conf import settings from django.contrib.admin.widgets import AdminDateWidget @@ -12,21 +13,28 @@ from django_countries.fields import CountryField from sponsors.models import ( - SponsorshipBenefit, - SponsorshipPackage, - SponsorshipProgram, + BenefitFeature, + RequiredImgAssetConfiguration, + SPONSOR_TEMPLATE_HELP_TEXT, Sponsor, - SponsorContact, - Sponsorship, SponsorBenefit, + SponsorContact, SponsorEmailNotificationTemplate, - RequiredImgAssetConfiguration, - BenefitFeature, - SPONSOR_TEMPLATE_HELP_TEXT, SponsorshipCurrentYear, + Sponsorship, + SponsorshipBenefit, + SponsorshipCurrentYear, + SponsorshipPackage, + SponsorshipProgram, ) +SELECTABLE_DATES = tuple( + (y, str(y)) + for y in range(2021, datetime.date.today().year + 2) +) SPONSORSHIP_YEAR_SELECT = forms.Select( - choices=(((None, '---'),) + tuple(((y, str(y)) for y in range(2021, datetime.date.today().year + 2)))) + choices=( + ((None, '---'),) + SELECTABLE_DATES + ) ) @@ -71,11 +79,15 @@ def __init__(self, *args, **kwargs): ) self.fields["a_la_carte_benefits"] = PickSponsorshipBenefitsField( required=False, - queryset=SponsorshipBenefit.objects.from_year(year).a_la_carte().select_related("program"), + queryset=SponsorshipBenefit.objects.from_year(year).a_la_carte().select_related( + "program" + ), ) self.fields["standalone_benefits"] = PickSponsorshipBenefitsField( required=False, - queryset=SponsorshipBenefit.objects.from_year(year).standalone().select_related("program"), + queryset=SponsorshipBenefit.objects.from_year(year).standalone().select_related( + "program" + ), ) benefits_qs = SponsorshipBenefit.objects.from_year(year).with_packages().select_related( @@ -173,13 +185,15 @@ def _clean_benefits(self, cleaned_data): if not package: raise forms.ValidationError( _( - "The application has 1 or more package only benefits and no sponsor package." + "The application has 1 or more package " + "only benefits and no sponsor package." ) ) elif not benefit.packages.filter(id=package.id).exists(): raise forms.ValidationError( _( - "The application has 1 or more package only benefits but wrong sponsor package." + "The application has 1 or more package only " + "benefits but wrong sponsor package." ) ) @@ -221,7 +235,11 @@ class SponsorshipApplicationForm(forms.Form): ) web_logo = forms.ImageField( label="Sponsor web logo", - help_text="For display on our sponsor webpage. High resolution PNG or JPG, smallest dimension no less than 256px", + help_text=( + "For display on our sponsor webpage. " + "High resolution PNG or JPG, smallest " + "dimension no less than 256px", + ), required=False, ) print_logo = forms.ImageField( @@ -273,7 +291,10 @@ def __init__(self, *args, **kwargs): self.contacts_formset = SponsorContactFormSet(**formset_kwargs) def clean(self): - cleaned_data = super().clean() + # TODO: Refactor. + # why does this method not return a value + # as expected by the original implementation? + cleaned_data = super().clean() # noqa sponsor = self.data.get("sponsor") if not sponsor and not self.contacts_formset.is_valid(): msg = "Errors with contact(s) information" @@ -390,7 +411,10 @@ def user_with_previous_sponsors(self): class SponsorshipReviewAdminForm(forms.ModelForm): start_date = forms.DateField(widget=AdminDateWidget(), required=False) end_date = forms.DateField(widget=AdminDateWidget(), required=False) - overlapped_by = forms.ModelChoiceField(queryset=Sponsorship.objects.select_related("sponsor", "package"), required=False) + overlapped_by = forms.ModelChoiceField( + queryset=Sponsorship.objects.select_related("sponsor", "package"), + required=False + ) def __init__(self, *args, **kwargs): force_required = kwargs.pop("force_required", False) @@ -425,7 +449,9 @@ class SignedSponsorshipReviewAdminForm(SponsorshipReviewAdminForm): """ Form to approve sponsorships that already have a signed contract """ - signed_contract = forms.FileField(help_text="Please upload the final version of the signed contract.") + signed_contract = forms.FileField( + help_text="Please upload the final version of the signed contract." + ) class SponsorBenefitAdminInlineForm(forms.ModelForm): @@ -525,9 +551,13 @@ def clean(self): custom_notification = subject or content if not (notification or custom_notification): - raise forms.ValidationError("Can not send email without notification or custom content") + raise forms.ValidationError( + "Can not send email without notification or custom content" + ) if notification and custom_notification: - raise forms.ValidationError("You must select a notification or use custom content, not both") + raise forms.ValidationError( + "You must select a notification or use custom content, not both" + ) return cleaned_data @@ -546,7 +576,10 @@ class SponsorUpdateForm(forms.ModelForm): web_logo = forms.ImageField( widget=forms.widgets.FileInput, - help_text="For display on our sponsor webpage. High resolution PNG or JPG, smallest dimension no less than 256px", + help_text=( + "For display on our sponsor webpage. High resolution " + "PNG or JPG, smallest dimension no less than 256px" + ), required=False, ) print_logo = forms.ImageField( @@ -582,7 +615,10 @@ class Meta: model = Sponsor def clean(self): - cleaned_data = super().clean() + # TODO: Refactor. + # why does this method not return a value + # as expected by the original implementation? + cleaned_data = super().clean() # noqa if not self.contacts_formset.is_valid(): msg = "Errors with contact(s) information" @@ -639,7 +675,9 @@ def __init__(self, *args, **kwargs): msg = "Form must be initialized with a sponsorship passed by the instance parameter" raise TypeError(msg) super().__init__(*args, **kwargs) - self.required_assets = BenefitFeature.objects.required_assets().from_sponsorship(self.sponsorship) + self.required_assets = BenefitFeature.objects.required_assets().from_sponsorship( + self.sponsorship + ) if required_assets_ids: self.required_assets = self.required_assets.filter(pk__in=required_assets_ids) @@ -657,9 +695,14 @@ def __init__(self, *args, **kwargs): field = required_asset.as_form_field(required=required, initial=value) if required_asset.due_date and not bool(value): - field.label = mark_safe(f"{field.label}
(Required by {required_asset.due_date})") + field.label = mark_safe( + f"{field.label}
" + f"(Required by {required_asset.due_date})" + ) if bool(value): - field.label = mark_safe(f"{field.label}
(Fulfilled, thank you!)") + field.label = mark_safe( + f"{field.label}
(Fulfilled, thank you!)" + ) fields[f_name] = field @@ -744,8 +787,12 @@ def clean(self): if from_year and target_year: if target_year < from_year: - raise forms.ValidationError("The target year must be greater the one used as source.") + raise forms.ValidationError( + "The target year must be greater the one used as source." + ) elif target_year in self.configured_years: - raise forms.ValidationError(f"The year {target_year} already have a valid confguration.") + raise forms.ValidationError( + f"The year {target_year} already have a valid confguration." + ) return self.cleaned_data diff --git a/sponsors/management/commands/check_sponsorship_assets_due_date.py b/sponsors/management/commands/check_sponsorship_assets_due_date.py index 0f980fc90..ac7d964bf 100644 --- a/sponsors/management/commands/check_sponsorship_assets_due_date.py +++ b/sponsors/management/commands/check_sponsorship_assets_due_date.py @@ -4,7 +4,10 @@ from django.db.models import Subquery from django.utils import timezone -from sponsors.models import Sponsorship, Contract, BenefitFeature +from sponsors.models import ( + BenefitFeature, + Sponsorship, +) from sponsors.notifications import AssetCloseToDueDateNotificationToSponsors @@ -18,7 +21,11 @@ class Command(BaseCommand): def add_arguments(self, parser): help = "Num of days to be used as interval up to target date" parser.add_argument("num_days", nargs="?", default="7", help=help) - parser.add_argument("--no-input", action="store_true", help="Tells Django to NOT prompt the user for input of any kind.") + parser.add_argument( + "--no-input", + action="store_true", + help="Tells Django to NOT prompt the user for input of any kind." + ) def handle(self, **options): num_days = options["num_days"] @@ -27,7 +34,9 @@ def handle(self, **options): req_assets = BenefitFeature.objects.required_assets() - sponsorship_ids = Subquery(req_assets.values_list("sponsor_benefit__sponsorship_id").distinct()) + sponsorship_ids = Subquery( + req_assets.values_list("sponsor_benefit__sponsorship_id").distinct() + ) sponsorships = Sponsorship.objects.filter(id__in=sponsorship_ids) sponsorships_to_notify = [] @@ -46,8 +55,10 @@ def handle(self, **options): user_input = "" while user_input != "Y" and ask_input: - msg = f"Contacts from {len(sponsorships_to_notify)} with pending assets with expiring due date will get " \ - f"notified. " + msg = ( + f"Contacts from {len(sponsorships_to_notify)} with pending assets with " + f"expiring due date will get notified. " + ) msg += "Do you want to proceed? [Y/n]: " user_input = input(msg).strip().upper() if user_input == "N": diff --git a/sponsors/management/commands/create_contracts.py b/sponsors/management/commands/create_contracts.py index 16bc986e0..128ac8229 100644 --- a/sponsors/management/commands/create_contracts.py +++ b/sponsors/management/commands/create_contracts.py @@ -1,6 +1,10 @@ from django.core.management import BaseCommand -from sponsors.models import Sponsorship, Contract +from sponsors.models import ( + Contract, + Sponsorship, +) + # The reason to not use a data migration but a django management command # to deal with pre existing approved Sponsorships is due to migrations @@ -12,6 +16,7 @@ # The same limitation is true for the SponsorshipQuerySet's approved method and for # the sponsorship.contract reverse lookup. + class Command(BaseCommand): """ Create Contract objects for existing approved Sponsorships. @@ -31,4 +36,4 @@ def handle(self, **options): for sponsorship in qs: Contract.new(sponsorship) - print(f"Done!") + print("Done!") diff --git a/sponsors/management/commands/fullfill_pycon_2022.py b/sponsors/management/commands/fullfill_pycon_2022.py index 86ee26a1e..aea309339 100644 --- a/sponsors/management/commands/fullfill_pycon_2022.py +++ b/sponsors/management/commands/fullfill_pycon_2022.py @@ -1,21 +1,17 @@ -import os -from hashlib import sha1 from calendar import timegm from datetime import datetime -import sys +from hashlib import sha1 from urllib.parse import urlencode import requests -from requests.exceptions import RequestException - -from django.db.models import Q from django.conf import settings from django.core.management import BaseCommand +from requests.exceptions import RequestException from sponsors.models import ( - SponsorBenefit, BenefitFeature, ProvidedTextAsset, + SponsorBenefit, TieredBenefit, ) @@ -82,7 +78,8 @@ def handle(self, **options): ) except BenefitFeature.DoesNotExist: print( - f"No quantity found for {sponsorbenefit.sponsorship.sponsor.name} and {code_type}" + f"No quantity found for {sponsorbenefit.sponsorship.sponsor.name} " + f"and {code_type}" ) continue try: @@ -91,7 +88,8 @@ def handle(self, **options): ).get(internal_name=f"{code_type}_code") except ProvidedTextAsset.DoesNotExist: print( - f"No provided asset found for {sponsorbenefit.sponsorship.sponsor.name} with internal name {code_type}_code" + f"No provided asset found for {sponsorbenefit.sponsorship.sponsor.name} " + f"with internal name {code_type}_code" ) continue @@ -105,13 +103,15 @@ def handle(self, **options): ) if result["code"] == 200: print( - f"Fullfilling {code_type} for {sponsorbenefit.sponsorship.sponsor.name}: {quantity.quantity}" + f"Fullfilling {code_type} for " + f"{sponsorbenefit.sponsorship.sponsor.name}: {quantity.quantity}" ) promo_code = result["data"]["promo_code"] asset.value = promo_code asset.save() else: print( - f"Error from PyCon when fullfilling {code_type} for {sponsorbenefit.sponsorship.sponsor.name}: {result}" + f"Error from PyCon when fullfilling {code_type} for " + f"{sponsorbenefit.sponsorship.sponsor.name}: {result}" ) - print(f"Done!") + print("Done!") diff --git a/sponsors/models/__init__.py b/sponsors/models/__init__.py index 11f1f1df4..96ce62a30 100644 --- a/sponsors/models/__init__.py +++ b/sponsors/models/__init__.py @@ -4,14 +4,54 @@ structured as a python package. """ -from .assets import GenericAsset, ImgAsset, TextAsset, FileAsset, ResponseAsset -from .notifications import SponsorEmailNotificationTemplate, SPONSOR_TEMPLATE_HELP_TEXT -from .sponsors import Sponsor, SponsorContact, SponsorBenefit -from .benefits import BaseLogoPlacement, BaseTieredBenefit, BaseEmailTargetable, BenefitFeatureConfiguration, \ - LogoPlacementConfiguration, TieredBenefitConfiguration, EmailTargetableConfiguration, BenefitFeature, \ - LogoPlacement, EmailTargetable, TieredBenefit, RequiredImgAsset, RequiredImgAssetConfiguration, \ - RequiredTextAssetConfiguration, RequiredTextAsset, RequiredResponseAssetConfiguration, RequiredResponseAsset, \ - ProvidedTextAssetConfiguration, ProvidedTextAsset, ProvidedFileAssetConfiguration, ProvidedFileAsset -from .sponsorship import Sponsorship, SponsorshipProgram, SponsorshipBenefit, Sponsorship, SponsorshipPackage, \ - SponsorshipCurrentYear -from .contract import LegalClause, Contract, signed_contract_random_path +from .assets import ( + FileAsset, + GenericAsset, + ImgAsset, + ResponseAsset, + TextAsset, +) +from .benefits import ( + BaseEmailTargetable, + BaseLogoPlacement, + BaseTieredBenefit, + BenefitFeature, + BenefitFeatureConfiguration, + EmailTargetable, + EmailTargetableConfiguration, + LogoPlacement, + LogoPlacementConfiguration, + ProvidedFileAsset, + ProvidedFileAssetConfiguration, + ProvidedTextAsset, + ProvidedTextAssetConfiguration, + RequiredImgAsset, + RequiredImgAssetConfiguration, + RequiredResponseAsset, + RequiredResponseAssetConfiguration, + RequiredTextAsset, + RequiredTextAssetConfiguration, + TieredBenefit, + TieredBenefitConfiguration, +) +from .contract import ( + Contract, + LegalClause, + signed_contract_random_path, +) +from .notifications import ( + SPONSOR_TEMPLATE_HELP_TEXT, + SponsorEmailNotificationTemplate, +) +from .sponsors import ( + Sponsor, + SponsorBenefit, + SponsorContact, +) +from .sponsorship import ( + Sponsorship, + SponsorshipBenefit, + SponsorshipCurrentYear, + SponsorshipPackage, + SponsorshipProgram, +) diff --git a/sponsors/models/assets.py b/sponsors/models/assets.py index 4db7c9671..329bdc7bf 100644 --- a/sponsors/models/assets.py +++ b/sponsors/models/assets.py @@ -6,11 +6,13 @@ from enum import Enum from pathlib import Path -from django.db import models from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType -from django.db.models.fields.files import ImageFieldFile, FileField -from polymorphic.managers import PolymorphicManager +from django.db import models +from django.db.models.fields.files import ( + FileField, + ImageFieldFile, +) from polymorphic.models import PolymorphicModel from sponsors.models.managers import GenericAssetQuerySet diff --git a/sponsors/models/benefits.py b/sponsors/models/benefits.py index 51ec1870e..1454ccbbe 100644 --- a/sponsors/models/benefits.py +++ b/sponsors/models/benefits.py @@ -7,13 +7,18 @@ from django.urls import reverse from polymorphic.models import PolymorphicModel -from sponsors.models.assets import ImgAsset, TextAsset, FileAsset, ResponseAsset, Response +from sponsors.models.assets import ( + FileAsset, + ImgAsset, + Response, + ResponseAsset, + TextAsset, +) from sponsors.models.enums import ( - PublisherChoices, - LogoPlacementChoices, AssetsRelatedTo, + LogoPlacementChoices, + PublisherChoices, ) - ######################################## # Benefit features abstract classes from sponsors.models.managers import BenefitFeatureQuerySet @@ -36,11 +41,17 @@ class BaseLogoPlacement(models.Model): ) link_to_sponsors_page = models.BooleanField( default=False, - help_text="Override URL in placement to the PSF Sponsors Page, rather than the sponsor landing page url.", + help_text=( + "Override URL in placement to the PSF Sponsors Page, " + "rather than the sponsor landing page url." + ), ) describe_as_sponsor = models.BooleanField( default=False, - help_text='Override description with "SPONSOR_NAME is a SPONSOR_LEVEL sponsor of the Python Software Foundation".', + help_text=( + 'Override description with "SPONSOR_NAME is a SPONSOR_LEVEL ' + 'sponsor of the Python Software Foundation".' + ), ) class Meta: @@ -78,7 +89,10 @@ class BaseAsset(models.Model): internal_name = models.CharField( max_length=128, verbose_name="Internal Name", - help_text="Unique name used internally to control if the sponsor/sponsorship already has the asset", + help_text=( + "Unique name used internally to control " + "if the sponsor/sponsorship already has the asset", + ), unique=False, db_index=True, ) @@ -106,7 +120,7 @@ class Meta: class BaseProvidedAsset(BaseAsset): shared = models.BooleanField( - default = False, + default=False, ) def shared_value(self): @@ -220,6 +234,7 @@ def shared_value(self): class Meta(BaseProvidedAsset.Meta): abstract = True + class BaseProvidedFileAsset(BaseProvidedAsset): ASSET_CLASS = FileAsset @@ -273,12 +288,15 @@ def user_edit_url(self): url = reverse("users:update_sponsorship_assets", args=[self.sponsor_benefit.sponsorship.pk]) return url + f"?required_asset={self.pk}" - @property def user_view_url(self): - url = reverse("users:view_provided_sponsorship_assets", args=[self.sponsor_benefit.sponsorship.pk]) + url = reverse( + "users:view_provided_sponsorship_assets", + args=[self.sponsor_benefit.sponsorship.pk] + ) return url + f"?provided_asset={self.pk}" + class RequiredAssetMixin(AssetMixin): """ This class should be used to implement required assets. @@ -287,6 +305,7 @@ class RequiredAssetMixin(AssetMixin): """ pass + class ProvidedAssetMixin(AssetMixin): """ This class should be used to implement provided assets. @@ -302,6 +321,8 @@ def value(self): ###################################################### # SponsorshipBenefit features configuration models + + class BenefitFeatureConfiguration(PolymorphicModel): """ Base class for sponsorship benefits configuration. @@ -397,7 +418,10 @@ def benefit_feature_class(self): return LogoPlacement def __str__(self): - return f"Logo Configuration for {self.get_publisher_display()} at {self.get_logo_place_display()}" + return ( + f"Logo Configuration for {self.get_publisher_display()} " + f"at {self.get_logo_place_display()}" + ) class TieredBenefitConfiguration(BaseTieredBenefit, BenefitFeatureConfiguration): @@ -419,7 +443,10 @@ def get_benefit_feature_kwargs(self, **kwargs): return None def __str__(self): - return f"Tiered Benefit Configuration for {self.benefit} and {self.package} ({self.quantity})" + return ( + f"Tiered Benefit Configuration for {self.benefit} " + f"and {self.package} ({self.quantity})" + ) def display_modifier(self, name, **kwargs): if kwargs.get("package") != self.package: @@ -446,17 +473,21 @@ def benefit_feature_class(self): return EmailTargetable def __str__(self): - return f"Email targeatable configuration" + return "Email targeatable configuration" -class RequiredImgAssetConfiguration(AssetConfigurationMixin, BaseRequiredImgAsset, BenefitFeatureConfiguration): +class RequiredImgAssetConfiguration( + AssetConfigurationMixin, + BaseRequiredImgAsset, + BenefitFeatureConfiguration +): class Meta(BaseRequiredImgAsset.Meta, BenefitFeatureConfiguration.Meta): verbose_name = "Require Image Configuration" verbose_name_plural = "Require Image Configurations" constraints = [UniqueConstraint(fields=["internal_name"], name="uniq_img_asset_cfg")] def __str__(self): - return f"Require image configuration" + return "Require image configuration" @property def benefit_feature_class(self): @@ -471,7 +502,7 @@ class Meta(BaseRequiredTextAsset.Meta, BenefitFeatureConfiguration.Meta): constraints = [UniqueConstraint(fields=["internal_name"], name="uniq_text_asset_cfg")] def __str__(self): - return f"Require text configuration" + return "Require text configuration" @property def benefit_feature_class(self): @@ -489,7 +520,7 @@ class Meta(BaseRequiredResponseAsset.Meta, BenefitFeatureConfiguration.Meta): ] def __str__(self): - return f"Require response configuration" + return "Require response configuration" @property def benefit_feature_class(self): @@ -502,10 +533,12 @@ class ProvidedTextAssetConfiguration( class Meta(BaseProvidedTextAsset.Meta, BenefitFeatureConfiguration.Meta): verbose_name = "Provided Text Configuration" verbose_name_plural = "Provided Text Configurations" - constraints = [UniqueConstraint(fields=["internal_name"], name="uniq_provided_text_asset_cfg")] + constraints = [ + UniqueConstraint(fields=["internal_name"], name="uniq_provided_text_asset_cfg") + ] def __str__(self): - return f"Provided text configuration" + return "Provided text configuration" @property def benefit_feature_class(self): @@ -517,10 +550,12 @@ class ProvidedFileAssetConfiguration(AssetConfigurationMixin, BaseProvidedFileAs class Meta(BaseProvidedFileAsset.Meta, BenefitFeatureConfiguration.Meta): verbose_name = "Provided File Configuration" verbose_name_plural = "Provided File Configurations" - constraints = [UniqueConstraint(fields=["internal_name"], name="uniq_provided_file_asset_cfg")] + constraints = [ + UniqueConstraint(fields=["internal_name"], name="uniq_provided_file_asset_cfg") + ] def __str__(self): - return f"Provided File configuration" + return "Provided File configuration" @property def benefit_feature_class(self): @@ -586,7 +621,7 @@ class Meta(BaseTieredBenefit.Meta, BenefitFeature.Meta): verbose_name_plural = "Email Targetable Benefits" def __str__(self): - return f"Email targeatable" + return "Email targeatable" class RequiredImgAsset(RequiredAssetMixin, BaseRequiredImgAsset, BenefitFeature): @@ -595,13 +630,19 @@ class Meta(BaseRequiredImgAsset.Meta, BenefitFeature.Meta): verbose_name_plural = "Require Images" def __str__(self): - return f"Require image" + return "Require image" def as_form_field(self, **kwargs): help_text = kwargs.pop("help_text", self.help_text) label = kwargs.pop("label", self.label) required = kwargs.pop("required", False) - return forms.ImageField(required=required, help_text=help_text, label=label, widget=forms.ClearableFileInput, **kwargs) + return forms.ImageField( + required=required, + help_text=help_text, + label=label, + widget=forms.ClearableFileInput, + **kwargs + ) class RequiredTextAsset(RequiredAssetMixin, BaseRequiredTextAsset, BenefitFeature): @@ -610,7 +651,7 @@ class Meta(BaseRequiredTextAsset.Meta, BenefitFeature.Meta): verbose_name_plural = "Require Texts" def __str__(self): - return f"Require text" + return "Require text" def as_form_field(self, **kwargs): help_text = kwargs.pop("help_text", self.help_text) @@ -620,7 +661,13 @@ def as_form_field(self, **kwargs): widget = forms.TextInput if max_length is None or max_length > 256: widget = forms.Textarea - return forms.CharField(required=required, help_text=help_text, label=label, widget=widget, **kwargs) + return forms.CharField( + required=required, + help_text=help_text, + label=label, + widget=widget, + **kwargs + ) class RequiredResponseAsset(RequiredAssetMixin, BaseRequiredResponseAsset, BenefitFeature): @@ -629,13 +676,20 @@ class Meta(BaseRequiredTextAsset.Meta, BenefitFeature.Meta): verbose_name_plural = "Required Responses" def __str__(self): - return f"Require response" + return "Require response" def as_form_field(self, **kwargs): help_text = kwargs.pop("help_text", self.help_text) label = kwargs.pop("label", self.label) required = kwargs.pop("required", False) - return forms.ChoiceField(required=required, choices=Response.choices(), widget=forms.RadioSelect, help_text=help_text, label=label, **kwargs) + return forms.ChoiceField( + required=required, + choices=Response.choices(), + widget=forms.RadioSelect, + help_text=help_text, + label=label, + **kwargs + ) class ProvidedTextAsset(ProvidedAssetMixin, BaseProvidedTextAsset, BenefitFeature): @@ -653,4 +707,4 @@ class Meta(BaseProvidedFileAsset.Meta, BenefitFeature.Meta): verbose_name_plural = "Provided Files" def __str__(self): - return f"Provided file" + return "Provided file" diff --git a/sponsors/models/contract.py b/sponsors/models/contract.py index 3b22de9f3..e771b55e9 100644 --- a/sponsors/models/contract.py +++ b/sponsors/models/contract.py @@ -12,8 +12,8 @@ from ordered_model.models import OrderedModel from sponsors.exceptions import InvalidStatusException -from sponsors.utils import file_from_storage from sponsors.models.sponsorship import Sponsorship +from sponsors.utils import file_from_storage class LegalClause(OrderedModel): diff --git a/sponsors/models/managers.py b/sponsors/models/managers.py index 4681532f5..23cec4752 100644 --- a/sponsors/models/managers.py +++ b/sponsors/models/managers.py @@ -1,9 +1,12 @@ from django.db import IntegrityError -from django.db.models import Count -from ordered_model.models import OrderedModelManager, OrderedModelQuerySet -from django.db.models import Q, Subquery +from django.db.models import ( + Count, + Q, + Subquery, +) from django.db.models.query import QuerySet from django.utils import timezone +from ordered_model.models import OrderedModelQuerySet from polymorphic.query import PolymorphicQuerySet @@ -41,13 +44,17 @@ def with_logo_placement(self, logo_place=None, publisher=None): feature_qs = feature_qs.filter(logo_place=logo_place) if publisher: feature_qs = feature_qs.filter(publisher=publisher) - benefit_qs = SponsorBenefit.objects.filter(id__in=Subquery(feature_qs.values_list('sponsor_benefit_id', flat=True))) + benefit_qs = SponsorBenefit.objects.filter( + id__in=Subquery(feature_qs.values_list('sponsor_benefit_id', flat=True)) + ) return self.filter(id__in=Subquery(benefit_qs.values_list('sponsorship_id', flat=True))) def includes_benefit_feature(self, feature_model): from sponsors.models import SponsorBenefit feature_qs = feature_model.objects.all() - benefit_qs = SponsorBenefit.objects.filter(id__in=Subquery(feature_qs.values_list('sponsor_benefit_id', flat=True))) + benefit_qs = SponsorBenefit.objects.filter( + id__in=Subquery(feature_qs.values_list('sponsor_benefit_id', flat=True)) + ) return self.filter(id__in=Subquery(benefit_qs.values_list('sponsorship_id', flat=True))) @@ -64,7 +71,13 @@ def get_primary_contact(self, sponsor): raise self.model.DoesNotExist() return contact - def filter_by_contact_types(self, primary=False, administrative=False, accounting=False, manager=False): + def filter_by_contact_types( + self, + primary=False, + administrative=False, + accounting=False, + manager=False + ): if not any([primary, administrative, accounting, manager]): return self.none() @@ -89,7 +102,12 @@ def without_conflicts(self): return self.filter(conflicts__isnull=True) def a_la_carte(self): - return self.annotate(num_packages=Count("packages")).filter(num_packages=0, standalone=False).exclude(unavailable=True) + return self.annotate( + num_packages=Count("packages") + ).filter( + num_packages=0, + standalone=False + ).exclude(unavailable=True) def standalone(self): return self.filter(standalone=True).exclude(unavailable=True) @@ -133,17 +151,23 @@ def delete(self): return super().delete() def from_sponsorship(self, sponsorship): - return self.filter(sponsor_benefit__sponsorship=sponsorship).select_related("sponsor_benefit__sponsorship") + return self.filter( + sponsor_benefit__sponsorship=sponsorship + ).select_related("sponsor_benefit__sponsorship") def required_assets(self): from sponsors.models.benefits import RequiredAssetMixin required_assets_classes = RequiredAssetMixin.__subclasses__() - return self.instance_of(*required_assets_classes).select_related("sponsor_benefit__sponsorship") + return self.instance_of( + *required_assets_classes + ).select_related("sponsor_benefit__sponsorship") def provided_assets(self): from sponsors.models.benefits import ProvidedAssetMixin provided_assets_classes = ProvidedAssetMixin.__subclasses__() - return self.instance_of(*provided_assets_classes).select_related("sponsor_benefit__sponsorship") + return self.instance_of( + *provided_assets_classes + ).select_related("sponsor_benefit__sponsorship") class GenericAssetQuerySet(PolymorphicQuerySet): diff --git a/sponsors/models/sponsors.py b/sponsors/models/sponsors.py index ad2c4b8b1..5982ca03d 100644 --- a/sponsors/models/sponsors.py +++ b/sponsors/models/sponsors.py @@ -3,13 +3,12 @@ """ from allauth.account.models import EmailAddress from django.conf import settings +from django.contrib.contenttypes.fields import GenericRelation from django.db import models -from django.core.exceptions import ObjectDoesNotExist from django.template.defaultfilters import slugify from django.urls import reverse from django_countries.fields import CountryField from ordered_model.models import OrderedModel -from django.contrib.contenttypes.fields import GenericRelation from cms.models import ContentManageable from sponsors.models.assets import GenericAsset @@ -277,15 +276,15 @@ def reset_attributes(self, benefit): self.added_by_user = self.added_by_user or self.standalone # generate benefit features from benefit features configurations - features = self.features.all().delete() + self.features.all().delete() for feature_config in benefit.features_config.all(): feature_config.create_benefit_feature(self) self.save() - def delete(self): + def delete(self, *args, extra_update=None, **kwargs): self.features.all().delete() - super().delete() + super().delete(*args, extra_update=extra_update, **kwargs) class Meta(OrderedModel.Meta): pass diff --git a/sponsors/models/sponsorship.py b/sponsors/models/sponsorship.py index ec22f61f1..33d861a93 100644 --- a/sponsors/models/sponsorship.py +++ b/sponsors/models/sponsorship.py @@ -8,28 +8,44 @@ from django.contrib.contenttypes.fields import GenericRelation from django.core.cache import cache from django.core.exceptions import ObjectDoesNotExist -from django.core.validators import MinValueValidator, MaxValueValidator -from django.db import models, transaction, IntegrityError -from django.db.models import Subquery, Sum +from django.core.validators import ( + MaxValueValidator, + MinValueValidator, +) +from django.db import ( + IntegrityError, + models, + transaction, +) +from django.db.models import ( + Subquery, + Sum, +) from django.template.defaultfilters import truncatechars from django.urls import reverse from django.utils import timezone from django.utils.functional import cached_property from num2words import num2words - from ordered_model.models import OrderedModel -from sponsors.exceptions import SponsorWithExistingApplicationException, InvalidStatusException, \ - SponsorshipInvalidDateRangeException +from sponsors.exceptions import ( + InvalidStatusException, + SponsorWithExistingApplicationException, + SponsorshipInvalidDateRangeException, +) from sponsors.models.assets import GenericAsset -from sponsors.models.managers import SponsorshipPackageQuerySet, SponsorshipBenefitQuerySet, \ - SponsorshipQuerySet, SponsorshipCurrentYearQuerySet from sponsors.models.benefits import TieredBenefitConfiguration +from sponsors.models.managers import ( + SponsorshipBenefitQuerySet, + SponsorshipCurrentYearQuerySet, + SponsorshipPackageQuerySet, + SponsorshipQuerySet, +) from sponsors.models.sponsors import SponsorBenefit YEAR_VALIDATORS = [ - MinValueValidator(limit_value=2022, message="The min year value is 2022."), - MaxValueValidator(limit_value=2050, message="The max year value is 2050."), + MinValueValidator(limit_value=2022, message="The min year value is 2022."), + MaxValueValidator(limit_value=2050, message="The max year value is 2050."), ] @@ -98,8 +114,8 @@ def get_user_customization(self, benefits): benefits = set(tuple(benefits)) pkg_benefits = set(tuple(self.benefits.all())) return { - "added_by_user": benefits - pkg_benefits, - "removed_by_user": pkg_benefits - benefits, + "added_by_user": benefits - pkg_benefits, + "removed_by_user": pkg_benefits - benefits, } def clone(self, year: int): @@ -172,12 +188,18 @@ class Sponsorship(models.Model): for_modified_package = models.BooleanField( default=False, - help_text="If true, it means the user customized the package's benefits. Changes are listed under section 'User Customizations'.", + help_text=( + "If true, it means the user customized the package's benefits. " + "Changes are listed under section 'User Customizations'." + ), + ) + level_name_old = models.CharField( + max_length=64, + default="", + blank=True, + help_text="DEPRECATED: shall be removed after manual data sanity check.", + verbose_name="Level name" ) - level_name_old = models.CharField(max_length=64, default="", blank=True, help_text="DEPRECATED: shall be removed " - "after manual data sanity " - "check.", verbose_name="Level " - "name") package = models.ForeignKey(SponsorshipPackage, null=True, on_delete=models.SET_NULL) sponsorship_fee = models.PositiveIntegerField(null=True, blank=True) overlapped_by = models.ForeignKey("self", null=True, on_delete=models.SET_NULL) @@ -199,11 +221,17 @@ def level_name(self, value): @cached_property def user_customizations(self): - benefits = [b.sponsorship_benefit for b in self.benefits.select_related("sponsorship_benefit")] + benefits = [ + b.sponsorship_benefit + for b in self.benefits.select_related("sponsorship_benefit") + ] return self.package.get_user_customization(benefits) def __str__(self): - repr = f"{self.level_name} - {self.year} - ({self.get_status_display()}) for sponsor {self.sponsor.name}" + repr = ( + f"{self.level_name} - {self.year} - ({self.get_status_display()}) " + f"for sponsor {self.sponsor.name}" + ) if self.start_date and self.end_date: fmt = "%m/%d/%Y" start = self.start_date.strftime(fmt) @@ -250,12 +278,9 @@ def new(cls, sponsor, benefits, package=None, submited_by=None): @property def estimated_cost(self): - return ( - self.benefits.aggregate(Sum("benefit_internal_value"))[ - "benefit_internal_value__sum" - ] - or 0 - ) + return self.benefits.aggregate( + Sum("benefit_internal_value") + )["benefit_internal_value__sum"] or 0 @property def verbose_sponsorship_fee(self): @@ -269,15 +294,18 @@ def agreed_fee(self): if self.status in valid_status: return self.sponsorship_fee try: - benefits = [sb.sponsorship_benefit for sb in self.package_benefits.all().select_related('sponsorship_benefit')] + benefits = [ + sb.sponsorship_benefit + for sb in self.package_benefits.all().select_related('sponsorship_benefit') + ] if self.package and not self.package.has_user_customization(benefits): return self.sponsorship_fee except SponsorshipPackage.DoesNotExist: # sponsorship level names can change over time return None @property - def is_active(self): - conditions = [ + def is_active(self): # TODO: Fix + conditions = [ # noqa: F841 self.status == self.FINALIZED, self.end_date and self.end_date > date.today() ] @@ -294,7 +322,7 @@ def approve(self, start_date, end_date): msg = f"Can't approve a {self.get_status_display()} sponsorship." raise InvalidStatusException(msg) if start_date >= end_date: - msg = f"Start date greater or equal than end date" + msg = "Start date greater or equal than end date" raise SponsorshipInvalidDateRangeException(msg) self.status = self.APPROVED self.start_date = start_date @@ -482,9 +510,9 @@ def has_capacity(self): if self.unavailable: return False return not ( - self.remaining_capacity is not None - and self.remaining_capacity <= 0 - and not self.soft_capacity + self.remaining_capacity is not None and + self.remaining_capacity <= 0 and + not self.soft_capacity ) @property diff --git a/sponsors/notifications.py b/sponsors/notifications.py index 196cc94b6..24f2b19af 100644 --- a/sponsors/notifications.py +++ b/sponsors/notifications.py @@ -1,11 +1,35 @@ -from django.core.mail import EmailMessage -from django.core.cache import cache -from django.template.loader import render_to_string from django.conf import settings -from django.contrib.admin.models import LogEntry, CHANGE, ADDITION +from django.contrib.admin.models import ( + ADDITION, + CHANGE, + LogEntry, +) from django.contrib.contenttypes.models import ContentType +from django.core.cache import cache +from django.core.mail import EmailMessage +from django.template.loader import render_to_string -from sponsors.models import Sponsorship, Contract, BenefitFeature +from sponsors.models import BenefitFeature + +__all__ = ( + "AppliedSponsorshipNotificationToPSF", + "AppliedSponsorshipNotificationToSponsors", + "AssetCloseToDueDateNotificationToSponsors", + "BaseEmailSponsorshipNotification", + "ClonedResourcesLogger", + "ContractNotificationToPSF", + "ContractNotificationToSponsors", + "ExecutedContractLogger", + "ExecutedExistingContractLogger", + "NullifiedContractLogger", + "RefreshSponsorshipsCache", + "RejectedSponsorshipNotificationToPSF", + "RejectedSponsorshipNotificationToSponsors", + "SendSponsorNotificationLogger", + "SentContractLogger", + "SponsorshipApprovalLogger", + "add_log_entry", +) class BaseEmailSponsorshipNotification: diff --git a/sponsors/pdf.py b/sponsors/pdf.py index 5188b8290..56ab8a986 100644 --- a/sponsors/pdf.py +++ b/sponsors/pdf.py @@ -3,15 +3,15 @@ """ import io import os + from django.conf import settings from django.http import HttpResponse from django.utils.dateformat import format - from docxtpl import DocxTemplate -from easy_pdf.rendering import render_to_pdf_response, render_to_pdf - -from markupfield_helpers.helpers import render_md -from django.utils.html import mark_safe +from easy_pdf.rendering import ( + render_to_pdf, + render_to_pdf_response, +) def _clean_split(text, separator='\n'): diff --git a/sponsors/serializers.py b/sponsors/serializers.py index c0782c12a..6f8a9c1c0 100644 --- a/sponsors/serializers.py +++ b/sponsors/serializers.py @@ -1,8 +1,11 @@ - from rest_framework import serializers from sponsors.models import GenericAsset -from sponsors.models.enums import PublisherChoices, LogoPlacementChoices +from sponsors.models.enums import ( + LogoPlacementChoices, + PublisherChoices, +) + class LogoPlacementSerializer(serializers.Serializer): publisher = serializers.CharField() diff --git a/sponsors/templatetags/sponsors.py b/sponsors/templatetags/sponsors.py index 7e2f1f462..0c6c32b4c 100644 --- a/sponsors/templatetags/sponsors.py +++ b/sponsors/templatetags/sponsors.py @@ -1,16 +1,21 @@ import math - from collections import OrderedDict -from django import template -from django.conf import settings -from django.core.cache import cache -from ..models import Sponsorship, SponsorshipPackage, TieredBenefitConfiguration -from sponsors.models.enums import PublisherChoices, LogoPlacementChoices +from django import template +from sponsors.models.enums import ( + LogoPlacementChoices, + PublisherChoices, +) +from ..models import ( + Sponsorship, + SponsorshipPackage, + TieredBenefitConfiguration, +) register = template.Library() + @register.inclusion_tag("sponsors/partials/full_sponsorship.txt") def full_sponsorship(sponsorship, display_fee=False): if not display_fee: diff --git a/sponsors/tests/baker_recipes.py b/sponsors/tests/baker_recipes.py index 8d24820d6..b789884d2 100644 --- a/sponsors/tests/baker_recipes.py +++ b/sponsors/tests/baker_recipes.py @@ -1,9 +1,23 @@ -from datetime import date, timedelta +from datetime import ( + date, + timedelta, +) -from model_bakery.recipe import Recipe, foreign_key +from model_bakery.recipe import ( + Recipe, + foreign_key, +) -from sponsors.models import Contract, LogoPlacement, Sponsorship, SponsorshipPackage -from sponsors.models.enums import LogoPlacementChoices, PublisherChoices +from sponsors.models import ( + Contract, + LogoPlacement, + Sponsorship, + SponsorshipPackage, +) +from sponsors.models.enums import ( + LogoPlacementChoices, + PublisherChoices, +) today = date.today() two_days = timedelta(days=2) diff --git a/sponsors/tests/test_admin.py b/sponsors/tests/test_admin.py index 1e94fa6df..48ac9155d 100644 --- a/sponsors/tests/test_admin.py +++ b/sponsors/tests/test_admin.py @@ -1,13 +1,19 @@ from unittest.mock import Mock from django.contrib.admin.views.main import ChangeList +from django.test import ( + RequestFactory, + TestCase, +) from model_bakery import baker -from django.test import TestCase, RequestFactory - -from sponsors.admin import SponsorshipStatusListFilter, SponsorshipAdmin +from sponsors.admin import ( + SponsorshipAdmin, + SponsorshipStatusListFilter, +) from sponsors.models import Sponsorship + class TestCustomSponsorshipStatusListFilter(TestCase): def setUp(self): diff --git a/sponsors/tests/test_api.py b/sponsors/tests/test_api.py index caabd6aa1..0c987debf 100644 --- a/sponsors/tests/test_api.py +++ b/sponsors/tests/test_api.py @@ -9,8 +9,16 @@ from rest_framework.authtoken.models import Token from rest_framework.test import APITestCase -from sponsors.models import Sponsor, Sponsorship, TextAsset, ImgAsset -from sponsors.models.enums import LogoPlacementChoices, PublisherChoices +from sponsors.models import ( + ImgAsset, + Sponsor, + Sponsorship, + TextAsset, +) +from sponsors.models.enums import ( + LogoPlacementChoices, + PublisherChoices, +) class LogoPlacementeAPIListTests(APITestCase): diff --git a/sponsors/tests/test_forms.py b/sponsors/tests/test_forms.py index 058e21625..b08ee95e7 100644 --- a/sponsors/tests/test_forms.py +++ b/sponsors/tests/test_forms.py @@ -1,22 +1,32 @@ -from model_bakery import baker - from django.conf import settings from django.test import TestCase +from model_bakery import baker from sponsors.forms import ( - SponsorshipsBenefitsForm, - SponsorshipApplicationForm, + CloneApplicationConfigForm, + SendSponsorshipNotificationForm, Sponsor, + SponsorBenefit, + SponsorBenefitAdminInlineForm, SponsorContactForm, SponsorContactFormSet, - SponsorBenefitAdminInlineForm, - SponsorBenefit, + SponsorRequiredAssetsForm, Sponsorship, + SponsorshipApplicationForm, + SponsorshipBenefitAdminForm, + SponsorshipsBenefitsForm, SponsorshipsListForm, - SendSponsorshipNotificationForm, SponsorRequiredAssetsForm, SponsorshipBenefitAdminForm, CloneApplicationConfigForm, ) -from sponsors.models import SponsorshipBenefit, SponsorContact, RequiredTextAssetConfiguration, \ - RequiredImgAssetConfiguration, ImgAsset, RequiredTextAsset, SponsorshipPackage, SponsorshipCurrentYear +from sponsors.models import ( + ImgAsset, + RequiredImgAssetConfiguration, + RequiredTextAsset, + RequiredTextAssetConfiguration, + SponsorContact, + SponsorshipBenefit, + SponsorshipCurrentYear, + SponsorshipPackage, +) from .utils import get_static_image_file_as_upload from ..models.enums import AssetsRelatedTo @@ -115,7 +125,8 @@ def test_specific_field_to_select_standalone_benefits_by_year(self): self.assertIn(benefit.id, [c[0] for c in choices]) def test_package_list_only_advertisable_ones_from_current_year(self): - ads_pkgs = baker.make( + # TODO: Refactor + baker.make( 'SponsorshipPackage', advertise=True, _quantity=2, year=self.current_year ) baker.make('SponsorshipPackage', advertise=False) @@ -700,7 +711,7 @@ def setUp(self): } def test_required_fields(self): - required_fields = set(["__all__", "contact_types"]) + required_fields = {"__all__", "contact_types"} form = SendSponsorshipNotificationForm({}) self.assertFalse(form.is_valid()) self.assertEqual(required_fields, set(form.errors)) @@ -772,8 +783,9 @@ def test_build_form_fields_from_required_assets(self): self.assertTrue(form.has_input) def test_build_form_fields_from_specific_list_of_required_assets(self): + # TODO: Refactor text_asset = self.required_text_cfg.create_benefit_feature(self.benefits[0]) - img_asset = self.required_img_cfg.create_benefit_feature(self.benefits[1]) + self.required_img_cfg.create_benefit_feature(self.benefits[1]) form = SponsorRequiredAssetsForm(instance=self.sponsorship, required_assets_ids=[text_asset.pk]) fields = dict(form.fields) @@ -804,8 +816,9 @@ def test_save_info_for_image_asset(self): self.assertEqual(expected_url, img_asset.value.url) def test_load_initial_from_assets_and_force_field_if_previous_Data(self): - img_asset = self.required_img_cfg.create_benefit_feature(self.benefits[0]) - text_asset = self.required_text_cfg.create_benefit_feature(self.benefits[0]) + # TODO: Refactor + self.required_img_cfg.create_benefit_feature(self.benefits[0]) + self.required_text_cfg.create_benefit_feature(self.benefits[0]) files = {"image_input": get_static_image_file_as_upload("psf-logo.png", "logo.png")} form = SponsorRequiredAssetsForm(instance=self.sponsorship, data={"text_input": "data"}, files=files) self.assertTrue(form.is_valid()) diff --git a/sponsors/tests/test_managers.py b/sponsors/tests/test_managers.py index c908cfc41..7e791b255 100644 --- a/sponsors/tests/test_managers.py +++ b/sponsors/tests/test_managers.py @@ -1,12 +1,28 @@ -from datetime import date, timedelta -from model_bakery import baker +from datetime import ( + date, + timedelta, +) from django.conf import settings from django.test import TestCase +from model_bakery import baker -from ..models import Sponsorship, SponsorBenefit, LogoPlacement, TieredBenefit, RequiredTextAsset, RequiredImgAsset, \ - BenefitFeature, SponsorshipPackage, SponsorshipBenefit, SponsorshipCurrentYear -from sponsors.models.enums import LogoPlacementChoices, PublisherChoices +from sponsors.models.enums import ( + LogoPlacementChoices, + PublisherChoices, +) +from ..models import ( + BenefitFeature, + LogoPlacement, + RequiredImgAsset, + RequiredTextAsset, + SponsorBenefit, + Sponsorship, + SponsorshipBenefit, + SponsorshipCurrentYear, + SponsorshipPackage, + TieredBenefit, +) class SponsorshipQuerySetTests(TestCase): @@ -86,7 +102,7 @@ def test_filter_sponsorship_with_logo_placement_benefits(self): baker.make_recipe('sponsors.tests.logo_at_download_feature', sponsor_benefit=download_logo_benefit) sponsors_logo_benefit = baker.make(SponsorBenefit, sponsorship=sponsorship_with_sponsors_logo) baker.make_recipe('sponsors.tests.logo_at_sponsors_feature', sponsor_benefit=sponsors_logo_benefit) - regular_benefit = baker.make(SponsorBenefit, sponsorship=simple_sponsorship) + baker.make(SponsorBenefit, sponsorship=simple_sponsorship) with self.assertNumQueries(1): qs = list(Sponsorship.objects.with_logo_placement()) @@ -137,7 +153,7 @@ def test_filter_benefits_from_sponsorship(self): def test_filter_only_for_required_assets(self): baker.make(TieredBenefit) text_asset = baker.make(RequiredTextAsset) - img_asset = baker.make(RequiredImgAsset) + baker.make(RequiredImgAsset) qs = BenefitFeature.objects.required_assets() @@ -154,8 +170,8 @@ def setUp(self): self.regular_benefit_unavailable = baker.make(SponsorshipBenefit, year=current_year, unavailable=True) self.regular_benefit.packages.add(package) self.regular_benefit.packages.add(package) - self.a_la_carte = baker.make(SponsorshipBenefit, year=current_year-1) - self.a_la_carte_unavail = baker.make(SponsorshipBenefit, year=current_year-1, unavailable=True) + self.a_la_carte = baker.make(SponsorshipBenefit, year=current_year - 1) + self.a_la_carte_unavail = baker.make(SponsorshipBenefit, year=current_year - 1, unavailable=True) self.standalone = baker.make(SponsorshipBenefit, standalone=True) self.standalone_unavail = baker.make(SponsorshipBenefit, standalone=True, unavailable=True) diff --git a/sponsors/tests/test_models.py b/sponsors/tests/test_models.py index 781e85c09..d4655c587 100644 --- a/sponsors/tests/test_models.py +++ b/sponsors/tests/test_models.py @@ -1,38 +1,58 @@ -from datetime import date, timedelta - -from django.core.cache import cache -from django.db import IntegrityError -from model_bakery import baker, seq +from datetime import ( + date, + timedelta, +) from django import forms from django.conf import settings +from django.core.cache import cache from django.core.mail import EmailMessage +from django.db import IntegrityError from django.test import TestCase from django.utils import timezone +from model_bakery import ( + baker, + seq, +) +from sponsors.models.enums import ( + AssetsRelatedTo, + LogoPlacementChoices, + PublisherChoices, +) +from ..exceptions import ( + InvalidStatusException, + SponsorWithExistingApplicationException, + SponsorshipInvalidDateRangeException, +) from ..models import ( Contract, + ImgAsset, LegalClause, LogoPlacement, LogoPlacementConfiguration, + RequiredImgAsset, + RequiredImgAssetConfiguration, + RequiredTextAsset, + RequiredTextAssetConfiguration, Sponsor, SponsorBenefit, SponsorContact, Sponsorship, SponsorshipBenefit, + SponsorshipCurrentYear, SponsorshipPackage, + TextAsset, TieredBenefit, - TieredBenefitConfiguration, RequiredImgAssetConfiguration, RequiredImgAsset, ImgAsset, - RequiredTextAssetConfiguration, RequiredTextAsset, TextAsset, SponsorshipCurrentYear + TieredBenefitConfiguration, ) -from ..exceptions import ( - SponsorWithExistingApplicationException, - SponsorshipInvalidDateRangeException, - InvalidStatusException, +from ..models.benefits import ( + BaseRequiredImgAsset, + BaseRequiredTextAsset, + BenefitFeature, + EmailTargetableConfiguration, + RequiredAssetMixin, ) -from sponsors.models.enums import PublisherChoices, LogoPlacementChoices, AssetsRelatedTo -from ..models.benefits import RequiredAssetMixin, BaseRequiredImgAsset, BenefitFeature, BaseRequiredTextAsset, \ - EmailTargetableConfiguration class SponsorshipBenefitModelTests(TestCase): @@ -80,7 +100,7 @@ def test_name_for_display_without_specifying_package(self): quantity=10 ) - expected_name = f"Benefit (10)" + expected_name = "Benefit (10)" name = benefit.name_for_display(package=benefit_config.package) self.assertEqual(name, expected_name) self.assertTrue(benefit.has_tiers) @@ -433,6 +453,7 @@ def test_clone_does_not_repeate_already_cloned_package(self): self.assertFalse(created) self.assertEqual(pkg_2023.pk, repeated_pkg_2023.pk) + class SponsorContactModelTests(TestCase): def test_get_primary_contact_for_sponsor(self): sponsor = baker.make(Sponsor) @@ -469,7 +490,7 @@ def test_auto_increment_draft_revision_on_save(self): self.assertEqual(contract.revision, 0) num_updates = 5 - for i in range(num_updates): + for __ in range(num_updates): contract.save() contract.refresh_from_db() @@ -506,7 +527,6 @@ def test_create_new_contract_from_sponsorship_sets_sponsor_info_and_contact( def test_create_new_contract_from_sponsorship_sets_sponsor_contact_and_primary( self, ): - sponsor = self.sponsorship.sponsor contact = baker.make( SponsorContact, sponsor=self.sponsorship.sponsor, primary=True ) @@ -673,7 +693,7 @@ def test_new_copy_also_add_benefit_feature_when_creating_sponsor_benefit(self): self.assertEqual(benefit_feature.logo_place, benefit_config.logo_place) def test_new_copy_do_not_save_unexisting_features(self): - benefit_config = baker.make( + baker.make( TieredBenefitConfiguration, package__name='Another package', benefit=self.sponsorship_benefit, @@ -693,7 +713,7 @@ def test_sponsor_benefit_name_for_display(self): # benefit name if no features self.assertEqual(sponsor_benefit.name_for_display, name) # apply display modifier from features - benefit_config = baker.make( + baker.make( TieredBenefit, sponsor_benefit=sponsor_benefit, quantity=10 @@ -843,13 +863,13 @@ def test_clone_related_objects_as_well(self): self.assertEqual(2, benefit_2023.legal_clauses.count()) def test_clone_benefit_feature_configurations(self): - cfg_1 = baker.make( + baker.make( LogoPlacementConfiguration, - publisher = PublisherChoices.FOUNDATION, - logo_place = LogoPlacementChoices.FOOTER, + publisher=PublisherChoices.FOUNDATION, + logo_place=LogoPlacementChoices.FOOTER, benefit=self.sponsorship_benefit ) - cfg_2 = baker.make( + baker.make( RequiredTextAssetConfiguration, related_to=AssetsRelatedTo.SPONSOR.value, internal_name="config_name", diff --git a/sponsors/tests/test_notifications.py b/sponsors/tests/test_notifications.py index 30151ede0..dc398a579 100644 --- a/sponsors/tests/test_notifications.py +++ b/sponsors/tests/test_notifications.py @@ -1,18 +1,29 @@ from datetime import date from unittest.mock import Mock -from model_bakery import baker - from allauth.account.models import EmailAddress from django.conf import settings +from django.contrib.admin.models import ( + ADDITION, + CHANGE, + LogEntry, +) +from django.contrib.contenttypes.models import ContentType from django.core import mail from django.template.loader import render_to_string -from django.test import TestCase, RequestFactory -from django.contrib.admin.models import LogEntry, CHANGE, ADDITION -from django.contrib.contenttypes.models import ContentType +from django.test import ( + RequestFactory, + TestCase, +) +from model_bakery import baker from sponsors import notifications -from sponsors.models import Sponsorship, Contract, RequiredTextAssetConfiguration, SponsorBenefit +from sponsors.models import ( + Contract, + RequiredTextAssetConfiguration, + SponsorBenefit, + Sponsorship, +) class AppliedSponsorshipNotificationToPSFTests(TestCase): diff --git a/sponsors/tests/test_pdf.py b/sponsors/tests/test_pdf.py index ec929d05e..c7bc9fc4e 100644 --- a/sponsors/tests/test_pdf.py +++ b/sponsors/tests/test_pdf.py @@ -1,25 +1,33 @@ from datetime import date -from docxtpl import DocxTemplate -from markupfield_helpers.helpers import render_md -from model_bakery import baker from pathlib import Path -from unittest.mock import patch, Mock +from unittest.mock import ( + Mock, + patch, +) from django.conf import settings -from django.http import HttpResponse, HttpRequest -from django.template.loader import render_to_string +from django.http import ( + HttpRequest, + HttpResponse, +) from django.test import TestCase -from django.utils.html import mark_safe from django.utils.dateformat import format +from docxtpl import DocxTemplate +from markupfield_helpers.helpers import render_md +from model_bakery import baker -from sponsors.pdf import render_contract_to_pdf_file, render_contract_to_pdf_response, render_contract_to_docx_response +from sponsors.pdf import ( + render_contract_to_docx_response, + render_contract_to_pdf_file, + render_contract_to_pdf_response, +) class TestRenderContract(TestCase): def setUp(self): self.contract = baker.make_recipe("sponsors.tests.empty_contract", sponsorship__start_date=date.today()) text = f"{self.contract.benefits_list.raw}\n\n**Legal Clauses**\n{self.contract.legal_clauses.raw}" - html = render_md(text) + self.html = render_md(text) self.context = { "contract": self.contract, "start_date": self.contract.sponsorship.start_date, diff --git a/sponsors/tests/test_templatetags.py b/sponsors/tests/test_templatetags.py index f891a6479..22517ba15 100644 --- a/sponsors/tests/test_templatetags.py +++ b/sponsors/tests/test_templatetags.py @@ -1,12 +1,9 @@ -from model_bakery import baker -from django.test import TestCase -from unittest.mock import patch, Mock +from unittest.mock import patch -from companies.models import Company +from django.test import TestCase +from model_bakery import baker from ..models import ( - Sponsor, - SponsorBenefit, SponsorshipBenefit, TieredBenefitConfiguration, ) diff --git a/sponsors/tests/test_use_cases.py b/sponsors/tests/test_use_cases.py index 433d4950e..3fd163fcb 100644 --- a/sponsors/tests/test_use_cases.py +++ b/sponsors/tests/test_use_cases.py @@ -1,19 +1,46 @@ import os -from unittest.mock import Mock, patch, call -from model_bakery import baker -from datetime import timedelta, date +from datetime import ( + date, + timedelta, +) from pathlib import Path +from unittest.mock import ( + Mock, + call, + patch, +) from django.conf import settings +from django.core.files.uploadedfile import SimpleUploadedFile +from django.core.mail import EmailMessage from django.test import TestCase from django.utils import timezone -from django.core.mail import EmailMessage -from django.core.files.uploadedfile import SimpleUploadedFile +from model_bakery import baker from sponsors import use_cases -from sponsors.notifications import * -from sponsors.models import Sponsorship, Contract, SponsorEmailNotificationTemplate, Sponsor, SponsorshipBenefit, \ - SponsorshipPackage +from sponsors.models import ( + Contract, + Sponsor, + SponsorEmailNotificationTemplate, + Sponsorship, + SponsorshipBenefit, + SponsorshipPackage, +) +from sponsors.notifications import ( + AppliedSponsorshipNotificationToPSF, + AppliedSponsorshipNotificationToSponsors, + ClonedResourcesLogger, + ContractNotificationToPSF, + ExecutedContractLogger, + ExecutedExistingContractLogger, + NullifiedContractLogger, + RefreshSponsorshipsCache, + RejectedSponsorshipNotificationToPSF, + RejectedSponsorshipNotificationToSponsors, + SendSponsorNotificationLogger, + SentContractLogger, + SponsorshipApprovalLogger, +) class CreateSponsorshipApplicationUseCaseTests(TestCase): @@ -334,7 +361,12 @@ def test_send_notifications(self, mock_get_email_message): kwargs = dict(to_accounting=False, to_administrative=True, to_manager=False, to_primary=False) mock_get_email_message.assert_has_calls([call(sponsorship, **kwargs)]) self.notifications[0].notify.assert_has_calls([ - call(notification=self.notification, sponsorship=sponsorship, contact_types=contact_types, request='request') + call( + notification=self.notification, + sponsorship=sponsorship, + contact_types=contact_types, + request='request' + ) ]) for email in emails: email.send.assert_called_once_with() @@ -364,7 +396,7 @@ def test_clone_package_and_benefits(self): baker.make(SponsorshipPackage, year=2021) # package from another year baker.make(SponsorshipPackage, year=2022, _quantity=2) baker.make(SponsorshipBenefit, year=2021) # benefit from another year - benefits_2022 = baker.make(SponsorshipBenefit, year=2022, _quantity=3) + baker.make(SponsorshipBenefit, year=2022, _quantity=3) created_objects = self.use_case.execute(clone_from_year=2022, target_year=2023, request=self.request) diff --git a/sponsors/tests/test_views.py b/sponsors/tests/test_views.py index 130eb443d..b06486d6f 100644 --- a/sponsors/tests/test_views.py +++ b/sponsors/tests/test_views.py @@ -1,5 +1,4 @@ import json -from model_bakery import baker from django.conf import settings from django.contrib import messages @@ -8,19 +7,27 @@ from django.contrib.sessions.backends.base import SessionBase from django.core import mail from django.test import TestCase -from django.urls import reverse, reverse_lazy +from django.urls import ( + reverse, + reverse_lazy, +) +from model_bakery import baker -from .utils import get_static_image_file_as_upload, assertMessage +from sponsors.forms import ( + SponsorshipApplicationForm, + SponsorshipsBenefitsForm, +) +from .utils import ( + assertMessage, + get_static_image_file_as_upload, +) from ..models import ( Sponsor, - SponsorshipBenefit, SponsorContact, - Sponsorship, SponsorshipCurrentYear, - SponsorshipPackage -) -from sponsors.forms import ( - SponsorshipsBenefitsForm, - SponsorshipApplicationForm, + Sponsorship, + SponsorshipBenefit, + SponsorshipCurrentYear, + SponsorshipPackage, ) @@ -109,15 +116,15 @@ def test_populate_form_initial_with_values_from_cookie(self): self.assertEqual(self.data, r.context["form"].initial) def test_capacity_flag(self): - psf_package = baker.make(SponsorshipPackage, advertise=True) + baker.make(SponsorshipPackage, advertise=True) r = self.client.get(self.url) self.assertEqual(False, r.context["capacities_met"]) def test_capacity_flag_when_needed(self): - at_capacity_benefit = baker.make( + baker.make( SponsorshipBenefit, program=self.psf, capacity=0, soft_capacity=False ) - psf_package = baker.make(SponsorshipPackage, advertise=True) + baker.make(SponsorshipPackage, advertise=True) r = self.client.get(self.url) self.assertEqual(True, r.context["capacities_met"]) @@ -162,7 +169,7 @@ def test_valid_only_with_standalone(self): def test_do_not_display_application_form_by_year_if_staff_user(self): custom_year = self.current_year + 1 - # move all obects to a new year instead of using the active one + # move all objects to a new year instead of using the active one SponsorshipBenefit.objects.all().update(year=custom_year) SponsorshipPackage.objects.all().update(year=custom_year) @@ -179,7 +186,7 @@ def test_display_application_form_by_year_if_staff_user_and_querystring(self): self.user.save() self.client.force_login(self.user) custom_year = self.current_year + 1 - # move all obects to a new year instead of using the active one + # move all objects to a new year instead of using the active one SponsorshipBenefit.objects.all().update(year=custom_year) SponsorshipPackage.objects.all().update(year=custom_year) diff --git a/sponsors/tests/test_views_admin.py b/sponsors/tests/test_views_admin.py index 1b260187a..d842d60d9 100644 --- a/sponsors/tests/test_views_admin.py +++ b/sponsors/tests/test_views_admin.py @@ -1,30 +1,58 @@ import io -import json -import tempfile import zipfile +from datetime import ( + date, + timedelta, +) +from unittest.mock import ( + Mock, + PropertyMock, + patch, +) from uuid import uuid4 -from django.core.files.uploadedfile import SimpleUploadedFile -from model_bakery import baker -from datetime import date, timedelta -from unittest.mock import patch, PropertyMock, Mock - from django.conf import settings from django.contrib import messages from django.contrib.messages import get_messages from django.core import mail +from django.core.files.uploadedfile import SimpleUploadedFile from django.core.handlers.wsgi import WSGIRequest from django.http import HttpResponse -from django.test import TestCase, RequestFactory +from django.test import ( + RequestFactory, + TestCase, +) from django.urls import reverse +from model_bakery import baker -from .utils import assertMessage, get_static_image_file_as_upload -from ..models import Sponsorship, Contract, SponsorshipBenefit, SponsorBenefit, SponsorEmailNotificationTemplate, \ - GenericAsset, ImgAsset, TextAsset, SponsorshipCurrentYear, SponsorshipPackage -from ..forms import SponsorshipReviewAdminForm, SponsorshipsListForm, SignedSponsorshipReviewAdminForm, \ - SendSponsorshipNotificationForm, CloneApplicationConfigForm -from sponsors.views_admin import send_sponsorship_notifications_action, export_assets_as_zipfile from sponsors.use_cases import SendSponsorshipNotificationUseCase +from sponsors.views_admin import ( + export_assets_as_zipfile, + send_sponsorship_notifications_action, +) +from .utils import ( + assertMessage, + get_static_image_file_as_upload, +) +from ..forms import ( + CloneApplicationConfigForm, + SendSponsorshipNotificationForm, + SignedSponsorshipReviewAdminForm, + SponsorshipReviewAdminForm, + SponsorshipsListForm, +) +from ..models import ( + Contract, + GenericAsset, + ImgAsset, + SponsorBenefit, + SponsorEmailNotificationTemplate, + Sponsorship, + SponsorshipBenefit, + SponsorshipCurrentYear, + SponsorshipPackage, + TextAsset, +) class RollbackSponsorshipToEditingAdminViewTests(TestCase): @@ -836,7 +864,7 @@ def test_update_selected_sponsorships_only(self): self.benefit.description = 'New description' self.benefit.save() - response = self.client.post(self.url, data=self.data) + self.client.post(self.url, data=self.data) self.sponsor_benefit.refresh_from_db() self.assertEqual(self.sponsor_benefit.name, "New name") @@ -1047,7 +1075,7 @@ def test_render_form_error_if_invalid(self, mocked_render): request = self.request_factory.post("/", data={"confirm": "yes"}) request.user = self.user - resp = send_sponsorship_notifications_action(Mock(), request, self.queryset) + send_sponsorship_notifications_action(Mock(), request, self.queryset) context = mocked_render.call_args[1]["context"] form = context["form"] diff --git a/sponsors/urls.py b/sponsors/urls.py index d658dffe6..a0d6553fa 100644 --- a/sponsors/urls.py +++ b/sponsors/urls.py @@ -2,12 +2,11 @@ from . import views - urlpatterns = [ path('application/new/', views.NewSponsorshipApplicationView.as_view(), - name="new_sponsorship_application", - ), + name="new_sponsorship_application", + ), path('application/', views.SelectSponsorshipApplicationBenefitsView.as_view(), - name="select_sponsorship_application_benefits", - ), + name="select_sponsorship_application_benefits", + ), ] diff --git a/sponsors/use_cases.py b/sponsors/use_cases.py index 95b2d267e..4a76dba12 100644 --- a/sponsors/use_cases.py +++ b/sponsors/use_cases.py @@ -1,9 +1,18 @@ from django.db import transaction from sponsors import notifications -from sponsors.models import Sponsorship, Contract, SponsorContact, SponsorEmailNotificationTemplate, SponsorshipBenefit, \ - SponsorshipPackage -from sponsors.pdf import render_contract_to_pdf_file, render_contract_to_docx_file +from sponsors.models import ( + Contract, + SponsorContact, + SponsorEmailNotificationTemplate, + Sponsorship, + SponsorshipBenefit, + SponsorshipPackage, +) +from sponsors.pdf import ( + render_contract_to_docx_file, + render_contract_to_pdf_file, +) class BaseUseCaseWithNotifications: @@ -80,7 +89,7 @@ class SendContractUseCase(BaseUseCaseWithNotifications): # the generate contract file gets approved by PSF Board. # After that, the line bellow can be uncommented to enable # the desired behavior. - #notifications.ContractNotificationToSponsors(), + # notifications.ContractNotificationToSponsors(), notifications.SentContractLogger(), ] @@ -143,7 +152,13 @@ class SendSponsorshipNotificationUseCase(BaseUseCaseWithNotifications): notifications.SendSponsorNotificationLogger(), ] - def execute(self, notification: SponsorEmailNotificationTemplate, sponsorships, contact_types, **kwargs): + def execute( + self, + notification: SponsorEmailNotificationTemplate, + sponsorships, + contact_types, + **kwargs + ): msg_kwargs = { "to_primary": SponsorContact.PRIMARY_CONTACT in contact_types, "to_administrative": SponsorContact.ADMINISTRATIVE_CONTACT in contact_types, diff --git a/sponsors/views.py b/sponsors/views.py index dccd8446d..a130528fa 100644 --- a/sponsors/views.py +++ b/sponsors/views.py @@ -1,24 +1,36 @@ from itertools import chain + from django.conf import settings from django.contrib import messages from django.contrib.auth.decorators import login_required from django.db import transaction from django.forms.utils import ErrorList -from django.shortcuts import redirect, render -from django.urls import reverse_lazy, reverse +from django.shortcuts import ( + redirect, + render, +) +from django.urls import ( + reverse, + reverse_lazy, +) from django.utils.decorators import method_decorator -from django.views.generic import FormView, DetailView, RedirectView +from django.views.generic import FormView +from sponsors import ( + cookies, + use_cases, +) +from sponsors.forms import ( + SponsorshipApplicationForm, + SponsorshipsBenefitsForm, +) from .models import ( SponsorshipBenefit, + SponsorshipCurrentYear, SponsorshipPackage, - SponsorshipProgram, SponsorshipCurrentYear, + SponsorshipProgram, ) -from sponsors import cookies -from sponsors import use_cases -from sponsors.forms import SponsorshipsBenefitsForm, SponsorshipApplicationForm - class SelectSponsorshipApplicationBenefitsView(FormView): form_class = SponsorshipsBenefitsForm diff --git a/sponsors/views_admin.py b/sponsors/views_admin.py index f68025bf9..a4dc490ec 100644 --- a/sponsors/views_admin.py +++ b/sponsors/views_admin.py @@ -1,22 +1,35 @@ -import io, zipfile +import io +import zipfile from tempfile import NamedTemporaryFile -from django import forms from django.contrib import messages +from django.db import transaction from django.http import HttpResponse -from django.shortcuts import get_object_or_404, render, redirect +from django.shortcuts import ( + get_object_or_404, + redirect, + render, +) from django.urls import reverse -from django.utils import timezone -from django.db.models import Q -from django.db import transaction from sponsors import use_cases -from sponsors.forms import SponsorshipReviewAdminForm, SponsorshipsListForm, SignedSponsorshipReviewAdminForm, \ - SendSponsorshipNotificationForm, CloneApplicationConfigForm from sponsors.exceptions import InvalidStatusException -from sponsors.pdf import render_contract_to_pdf_response, render_contract_to_docx_response -from sponsors.models import Sponsorship, SponsorBenefit, EmailTargetable, SponsorContact, BenefitFeature, \ - SponsorshipCurrentYear, SponsorshipBenefit, SponsorshipPackage +from sponsors.forms import ( + CloneApplicationConfigForm, + SendSponsorshipNotificationForm, + SignedSponsorshipReviewAdminForm, + SponsorshipReviewAdminForm, + SponsorshipsListForm, +) +from sponsors.models import ( + BenefitFeature, + EmailTargetable, + SponsorshipCurrentYear, +) +from sponsors.pdf import ( + render_contract_to_docx_response, + render_contract_to_pdf_response, +) def preview_contract_view(ModelAdmin, request, pk): @@ -368,16 +381,20 @@ def export_assets_as_zipfile(ModelAdmin, request, queryset): if not queryset.exists(): ModelAdmin.message_user( request, - f"You have to select at least one asset to export.", + "You have to select at least one asset to export.", messages.WARNING ) return redirect(request.path) assets_without_values = [asset for asset in queryset if not asset.has_value] if any(assets_without_values): + _message = ( + f"{len(assets_without_values)} assets from the " + "selection doesn't have data to export. Please review your selection!" + ) ModelAdmin.message_user( request, - f"{len(assets_without_values)} assets from the selection doesn't have data to export. Please review your selection!", + _message, messages.WARNING ) return redirect(request.path) diff --git a/successstories/admin.py b/successstories/admin.py index fdde8878c..c60672510 100644 --- a/successstories/admin.py +++ b/successstories/admin.py @@ -1,8 +1,14 @@ from django.contrib import admin from django.utils.html import format_html -from .models import Story, StoryCategory -from cms.admin import ContentManageableModelAdmin, NameSlugAdmin +from cms.admin import ( + ContentManageableModelAdmin, + NameSlugAdmin, +) +from .models import ( + Story, + StoryCategory, +) @admin.register(StoryCategory) diff --git a/successstories/factories.py b/successstories/factories.py index 8d3d9d85e..58197f721 100644 --- a/successstories/factories.py +++ b/successstories/factories.py @@ -1,9 +1,11 @@ import factory from factory.django import DjangoModelFactory - from faker.providers import BaseProvider -from .models import StoryCategory, Story +from .models import ( + Story, + StoryCategory, +) class StoryProvider(BaseProvider): @@ -21,6 +23,7 @@ class StoryProvider(BaseProvider): def story_category(self): return self.random_element(self.story_categories) + factory.Faker.add_provider(StoryProvider) diff --git a/successstories/forms.py b/successstories/forms.py index f623001b0..0d067c810 100644 --- a/successstories/forms.py +++ b/successstories/forms.py @@ -2,8 +2,8 @@ from django.db.models import Q from django.utils.text import slugify -from .models import Story from cms.forms import ContentManageableModelForm +from .models import Story class StoryForm(ContentManageableModelForm): diff --git a/successstories/managers.py b/successstories/managers.py index 400609e63..7614a6ce5 100644 --- a/successstories/managers.py +++ b/successstories/managers.py @@ -1,8 +1,8 @@ import random from django.db.models import Manager -from django.db.models.query import QuerySet from django.db.models.aggregates import Count +from django.db.models.query import QuerySet class StoryQuerySet(QuerySet): diff --git a/successstories/models.py b/successstories/models.py index e5345b435..3c907df80 100644 --- a/successstories/models.py +++ b/successstories/models.py @@ -1,21 +1,21 @@ from django.conf import settings from django.contrib.sites.models import Site -from django.core.exceptions import ValidationError from django.core.mail import EmailMessage -from django.urls import reverse from django.db import models from django.db.models.signals import post_save from django.dispatch import receiver from django.template.loader import render_to_string - +from django.urls import reverse from markupfield.fields import MarkupField -from .managers import StoryManager from boxes.models import Box -from cms.models import ContentManageable, NameSlugModel +from cms.models import ( + ContentManageable, + NameSlugModel, +) from companies.models import Company from fastly.utils import purge_url - +from .managers import StoryManager PSF_TO_EMAILS = ['psf-staff@python.org'] DEFAULT_MARKUP_TYPE = getattr(settings, 'DEFAULT_MARKUP_TYPE', 'restructuredtext') diff --git a/successstories/templatetags/successstories.py b/successstories/templatetags/successstories.py index df9736796..142c79c8e 100644 --- a/successstories/templatetags/successstories.py +++ b/successstories/templatetags/successstories.py @@ -1,7 +1,9 @@ from django import template -from ..models import Story, StoryCategory - +from ..models import ( + Story, + StoryCategory, +) register = template.Library() diff --git a/successstories/tests/test_forms.py b/successstories/tests/test_forms.py index d4bb535cc..68eb6fe00 100644 --- a/successstories/tests/test_forms.py +++ b/successstories/tests/test_forms.py @@ -1,6 +1,6 @@ from django.test import TestCase -from ..factories import StoryFactory, StoryCategoryFactory +from ..factories import StoryCategoryFactory from ..forms import StoryForm diff --git a/successstories/tests/test_models.py b/successstories/tests/test_models.py index de5c0d577..8f874b79d 100644 --- a/successstories/tests/test_models.py +++ b/successstories/tests/test_models.py @@ -1,6 +1,9 @@ from django.test import TestCase -from ..factories import StoryFactory, StoryCategoryFactory +from ..factories import ( + StoryCategoryFactory, + StoryFactory, +) from ..models import Story diff --git a/successstories/tests/test_templatetags.py b/successstories/tests/test_templatetags.py index 88d89c85d..2f02c4dc9 100644 --- a/successstories/tests/test_templatetags.py +++ b/successstories/tests/test_templatetags.py @@ -1,7 +1,10 @@ from django import template from django.test import TestCase -from ..factories import StoryFactory, StoryCategoryFactory +from ..factories import ( + StoryCategoryFactory, + StoryFactory, +) class StoryTemplateTagTests(TestCase): diff --git a/successstories/tests/test_utils.py b/successstories/tests/test_utils.py index f2b659ddd..d881588c4 100644 --- a/successstories/tests/test_utils.py +++ b/successstories/tests/test_utils.py @@ -2,7 +2,10 @@ from django.test import SimpleTestCase -from ..utils import convert_to_datetime, get_field_list +from ..utils import ( + convert_to_datetime, + get_field_list, +) class UtilsTestCase(SimpleTestCase): diff --git a/successstories/tests/test_views.py b/successstories/tests/test_views.py index 62f478ccc..2cb6db5e1 100644 --- a/successstories/tests/test_views.py +++ b/successstories/tests/test_views.py @@ -1,13 +1,16 @@ import re from django.conf import settings -from django.core import mail -from django.urls import reverse from django.contrib.auth import get_user_model +from django.core import mail from django.test import TestCase +from django.urls import reverse from users.factories import UserFactory -from ..factories import StoryFactory, StoryCategoryFactory +from ..factories import ( + StoryCategoryFactory, + StoryFactory, +) from ..models import Story User = get_user_model() diff --git a/successstories/urls.py b/successstories/urls.py index eb9a5a454..f58f979e1 100644 --- a/successstories/urls.py +++ b/successstories/urls.py @@ -1,6 +1,6 @@ -from . import views from django.urls import path +from . import views urlpatterns = [ path('', views.StoryList.as_view(), name='success_story_list'), diff --git a/successstories/utils.py b/successstories/utils.py index 25c80574c..7ce490b0e 100644 --- a/successstories/utils.py +++ b/successstories/utils.py @@ -8,11 +8,13 @@ """ import datetime - from xml.etree.ElementTree import fromstring +from django.utils.timezone import ( + get_current_timezone, + make_aware, +) from docutils.core import publish_doctree -from django.utils.timezone import make_aware, get_current_timezone def convert_to_datetime(string): diff --git a/successstories/views.py b/successstories/views.py index 3a8c3542a..7a65422cc 100644 --- a/successstories/views.py +++ b/successstories/views.py @@ -2,12 +2,18 @@ from django.contrib.auth.mixins import LoginRequiredMixin from django.urls import reverse from django.utils.decorators import method_decorator -from django.views.generic import CreateView, DetailView, ListView - +from django.views.generic import ( + CreateView, + DetailView, + ListView, +) from honeypot.decorators import check_honeypot from .forms import StoryForm -from .models import Story, StoryCategory +from .models import ( + Story, + StoryCategory, +) class ContextMixin: @@ -40,6 +46,7 @@ def form_valid(self, form): messages.add_message(self.request, messages.SUCCESS, self.success_message) return super().form_valid(form) + class StoryDetail(ContextMixin, DetailView): template_name = 'successstories/story_detail.html' context_object_name = 'story' diff --git a/templates/sponsors/admin/update_related_sponsorships.html b/templates/sponsors/admin/update_related_sponsorships.html index 2f0c49053..573f64f75 100644 --- a/templates/sponsors/admin/update_related_sponsorships.html +++ b/templates/sponsors/admin/update_related_sponsorships.html @@ -1,5 +1,5 @@ {% extends 'admin/base_site.html' %} -{% load i18n admin_static %} +{% load i18n static %} {% block extrastyle %}{{ block.super }}{% endblock %} diff --git a/users/actions.py b/users/actions.py index 12313f5c5..8afd9fba4 100644 --- a/users/actions.py +++ b/users/actions.py @@ -26,4 +26,5 @@ def export_csv(modeladmin, request, queryset): }) return response + export_csv.short_description = 'Export CSV' diff --git a/users/admin.py b/users/admin.py index 1c003655c..28b18e90c 100644 --- a/users/admin.py +++ b/users/admin.py @@ -1,14 +1,14 @@ from django.contrib import admin from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from django.utils.translation import gettext_lazy as _ - from rest_framework.authtoken.admin import TokenAdmin - from tastypie.admin import ApiKeyInline as TastypieApiKeyInline -from tastypie.models import ApiKey from .actions import export_csv -from .models import User, Membership +from .models import ( + Membership, + User, +) TokenAdmin.search_fields = ('user__username',) TokenAdmin.raw_id_fields = ('user',) diff --git a/users/apps.py b/users/apps.py index ed64f2093..0b4434d46 100644 --- a/users/apps.py +++ b/users/apps.py @@ -7,4 +7,4 @@ class UsersAppConfig(AppConfig): verbose_name = 'Users' def ready(self): - import users.listeners + import users.listeners # noqa: F401 diff --git a/users/factories.py b/users/factories.py index 3ba8ddae7..6793f3957 100644 --- a/users/factories.py +++ b/users/factories.py @@ -1,7 +1,10 @@ import factory from factory.django import DjangoModelFactory -from .models import User, Membership +from .models import ( + Membership, + User, +) class UserFactory(DjangoModelFactory): diff --git a/users/forms.py b/users/forms.py index 89045bab1..84a9cbaed 100644 --- a/users/forms.py +++ b/users/forms.py @@ -1,7 +1,10 @@ from django import forms from django.forms import ModelForm -from .models import User, Membership +from .models import ( + Membership, + User, +) class UserProfileForm(ModelForm): @@ -99,4 +102,4 @@ class MembershipUpdateForm(MembershipForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - del(self.fields['psf_code_of_conduct']) + del (self.fields['psf_code_of_conduct']) diff --git a/users/listeners.py b/users/listeners.py index 85c8c8cdb..0c727591e 100644 --- a/users/listeners.py +++ b/users/listeners.py @@ -1,6 +1,5 @@ from django.db.models.signals import post_save from django.dispatch import receiver - from rest_framework.authtoken.models import Token from .models import User diff --git a/users/managers.py b/users/managers.py index 2f01dd550..de2a1a8a9 100644 --- a/users/managers.py +++ b/users/managers.py @@ -1,5 +1,5 @@ -from django.db.models.query import QuerySet from django.contrib.auth.models import UserManager as DjangoUserManager +from django.db.models.query import QuerySet class UserQuerySet(QuerySet): diff --git a/users/models.py b/users/models.py index d80f5ceef..221987877 100644 --- a/users/models.py +++ b/users/models.py @@ -1,14 +1,13 @@ import datetime from django.conf import settings -from django.contrib.auth.models import AbstractUser, UserManager -from django.urls import reverse +from django.contrib.auth.models import AbstractUser from django.db import models +from django.urls import reverse from django.utils import timezone - from markupfield.fields import MarkupField -from tastypie.models import create_api_key from rest_framework.authtoken.models import Token +from tastypie.models import create_api_key from .managers import UserManager @@ -17,7 +16,7 @@ class CustomUserManager(UserManager): def get_by_natural_key(self, username): - case_insensitive_username_field = '{}__iexact'.format(self.model.USERNAME_FIELD) + case_insensitive_username_field = f'{self.model.USERNAME_FIELD}__iexact' return self.get(**{case_insensitive_username_field: username}) @@ -40,7 +39,11 @@ class User(AbstractUser): (EMAIL_PRIVATE, 'Only logged-in users can see my e-mail address'), (EMAIL_NEVER, 'No one can ever see my e-mail address'), ) - email_privacy = models.IntegerField('E-mail privacy', choices=EMAIL_CHOICES, default=EMAIL_NEVER) + email_privacy = models.IntegerField( + 'E-mail privacy', + choices=EMAIL_CHOICES, + default=EMAIL_NEVER + ) public_profile = models.BooleanField('Make my profile public', default=True) @@ -100,8 +103,16 @@ class Membership(models.Model): postal_code = models.CharField(max_length=20, blank=True) # PSF fields - psf_code_of_conduct = models.BooleanField('I agree to the PSF Code of Conduct', blank=True, null=True) - psf_announcements = models.BooleanField('I would like to receive occasional PSF email announcements', blank=True, null=True) + psf_code_of_conduct = models.BooleanField( + 'I agree to the PSF Code of Conduct', + blank=True, + null=True + ) + psf_announcements = models.BooleanField( + 'I would like to receive occasional PSF email announcements', + blank=True, + null=True + ) # Voting votes = models.BooleanField("I would like to be a PSF Voting Member", default=False) diff --git a/users/tests/test_forms.py b/users/tests/test_forms.py index 10ab95e32..2c83cf349 100644 --- a/users/tests/test_forms.py +++ b/users/tests/test_forms.py @@ -1,9 +1,11 @@ +from allauth.account.forms import SignupForm from django.contrib.auth import get_user_model from django.test import TestCase -from allauth.account.forms import SignupForm - -from users.forms import UserProfileForm, MembershipForm +from users.forms import ( + MembershipForm, + UserProfileForm, +) User = get_user_model() diff --git a/users/tests/test_models.py b/users/tests/test_models.py index 5dda50111..72bdb4845 100644 --- a/users/tests/test_models.py +++ b/users/tests/test_models.py @@ -4,8 +4,14 @@ from django.test import TestCase from django.utils import timezone -from ..factories import UserFactory, MembershipFactory -from ..models import Membership, UserGroup +from ..factories import ( + MembershipFactory, + UserFactory, +) +from ..models import ( + Membership, + UserGroup, +) User = get_user_model() diff --git a/users/tests/test_views.py b/users/tests/test_views.py index 952425c98..8f69c3ad5 100644 --- a/users/tests/test_views.py +++ b/users/tests/test_views.py @@ -1,18 +1,23 @@ -from model_bakery import baker 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.urls import reverse +from model_bakery import baker -from sponsors.forms import SponsorUpdateForm, SponsorRequiredAssetsForm -from sponsors.models import Sponsorship, RequiredTextAssetConfiguration, SponsorBenefit +from sponsors.forms import ( + SponsorRequiredAssetsForm, + SponsorUpdateForm, +) +from sponsors.models import ( + RequiredTextAssetConfiguration, + SponsorBenefit, + Sponsorship, +) from sponsors.models.enums import AssetsRelatedTo from sponsors.tests.utils import get_static_image_file_as_upload from users.factories import UserFactory from users.models import Membership -from ..factories import MembershipFactory - User = get_user_model() @@ -567,7 +572,6 @@ def test_render_form_for_specific_asset_if_informed_via_querystring(self): def test_update_assets_information_with_valid_post(self): response = self.client.post(self.url, data={"text_input": "information"}) - context = response.context self.assertRedirects(response, reverse("users:sponsorship_application_detail", args=[self.sponsorship.pk])) self.assertEqual(self.required_asset.value, "information") diff --git a/users/urls.py b/users/urls.py index e925838b5..0db338580 100644 --- a/users/urls.py +++ b/users/urls.py @@ -1,18 +1,32 @@ -from . import views -from django.urls import path, re_path +from django.urls import ( + path, + re_path, +) +from . import views app_name = 'users' urlpatterns = [ path('edit/', views.UserUpdate.as_view(), name='user_profile_edit'), path('membership/', views.MembershipCreate.as_view(), name='user_membership_create'), path('membership/edit/', views.MembershipUpdate.as_view(), name='user_membership_edit'), - re_path(r'^membership/delete/(?P[-a-zA-Z0-9_\@\.+]+)/$', views.MembershipDeleteView.as_view(), name='user_membership_delete'), + re_path( + r'^membership/delete/(?P[-a-zA-Z0-9_\@\.+]+)/$', + views.MembershipDeleteView.as_view(), + name='user_membership_delete' + ), path('membership/thanks/', views.MembershipThanks.as_view(), name='user_membership_thanks'), path('membership/affirm/', views.MembershipVoteAffirm.as_view(), name='membership_affirm_vote'), - path('membership/affirm/done/', views.MembershipVoteAffirmDone.as_view(), name='membership_affirm_vote_done'), + path( + 'membership/affirm/done/', + views.MembershipVoteAffirmDone.as_view(), name='membership_affirm_vote_done' + ), path('nominations/', views.UserNominationsView.as_view(), name='user_nominations_view'), - path('sponsorships/', views.UserSponsorshipsDashboard.as_view(), name='user_sponsorships_dashboard'), + path( + 'sponsorships/', + views.UserSponsorshipsDashboard.as_view(), + name='user_sponsorships_dashboard' + ), path( "sponsorships/sponsor//", views.UpdateSponsorInfoView.as_view(), @@ -33,6 +47,10 @@ views.SponsorshipDetailView.as_view(), name="sponsorship_application_detail", ), - re_path(r'^(?P[-a-zA-Z0-9_\@\.+]+)/delete/$', views.UserDeleteView.as_view(), name='user_delete'), + re_path( + r'^(?P[-a-zA-Z0-9_\@\.+]+)/delete/$', + views.UserDeleteView.as_view(), + name='user_delete' + ), re_path(r'^(?P[-a-zA-Z0-9_\@\.+]+)/$', views.UserDetail.as_view(), name='user_detail'), ] diff --git a/users/views.py b/users/views.py index 517c1419a..df6babeb5 100644 --- a/users/views.py +++ b/users/views.py @@ -1,33 +1,50 @@ from collections import defaultdict +from allauth.account.views import ( + PasswordChangeView, + SignupView, +) +from django.conf import settings from django.contrib import messages from django.contrib.auth import get_user_model +from django.contrib.auth.decorators import login_required from django.contrib.auth.mixins import UserPassesTestMixin -from django.conf import settings from django.core.mail import send_mail from django.db.models import Subquery -from django.urls import reverse, reverse_lazy from django.http import Http404 -from django.shortcuts import render, redirect +from django.shortcuts import redirect +from django.urls import ( + reverse, + reverse_lazy, +) from django.utils import timezone from django.utils.decorators import method_decorator -from django.contrib.auth.decorators import login_required from django.views.generic import ( - CreateView, DetailView, TemplateView, UpdateView, DeleteView, ListView, FormView + CreateView, + DeleteView, + DetailView, + ListView, + TemplateView, + UpdateView, ) - -from allauth.account.views import SignupView, PasswordChangeView from honeypot.decorators import check_honeypot from pydotorg.mixins import LoginRequiredMixin -from sponsors.forms import SponsorUpdateForm, SponsorRequiredAssetsForm -from sponsors.models import Sponsor, BenefitFeature - +from sponsors.forms import ( + SponsorRequiredAssetsForm, + SponsorUpdateForm, +) +from sponsors.models import ( + BenefitFeature, + Sponsor, + Sponsorship, +) from .forms import ( - UserProfileForm, MembershipForm, MembershipUpdateForm, + MembershipForm, + MembershipUpdateForm, + UserProfileForm, ) from .models import Membership -from sponsors.models import Sponsorship User = get_user_model() diff --git a/work_groups/admin.py b/work_groups/admin.py index e17e300b3..258aa7609 100644 --- a/work_groups/admin.py +++ b/work_groups/admin.py @@ -1,7 +1,6 @@ from django.contrib import admin from cms.admin import ContentManageableModelAdmin - from .models import WorkGroup diff --git a/work_groups/models.py b/work_groups/models.py index 85a2e0a6e..f0a47e928 100644 --- a/work_groups/models.py +++ b/work_groups/models.py @@ -1,9 +1,11 @@ -from django.db import models from django.conf import settings - +from django.db import models from markupfield.fields import MarkupField -from cms.models import ContentManageable, NameSlugModel +from cms.models import ( + ContentManageable, + NameSlugModel, +) DEFAULT_MARKUP_TYPE = getattr(settings, 'DEFAULT_MARKUP_TYPE', 'restructuredtext') @@ -22,19 +24,32 @@ class WorkGroup(ContentManageable, NameSlugModel): purpose = MarkupField( default_markup_type=DEFAULT_MARKUP_TYPE, - help_text="State what the mission of the group is. List all (if any) common goals that will be shared amongst the workgroup.", + help_text=( + "State what the mission of the group is. List all (if any) " + "common goals that will be shared amongst the workgroup." + ), ) active_time = MarkupField( default_markup_type=DEFAULT_MARKUP_TYPE, - help_text="How long will this workgroup exist? If the mission is not complete by the stated time, is it extendable? Is so, for how long?", + help_text=( + "How long will this workgroup exist? If the mission is " + "not complete by the stated time, is it extendable? Is so, for how long?" + ), ) core_values = MarkupField( default_markup_type=DEFAULT_MARKUP_TYPE, - help_text="List the core values that the workgroup will adhere to throughout its existence. Will the workgroup adopt any statements? If so, which statement?", + help_text=( + "List the core values that the workgroup will adhere to " + "throughout its existence. Will the workgroup adopt any " + "statements? If so, which statement?" + ), ) rules = MarkupField( default_markup_type=DEFAULT_MARKUP_TYPE, - help_text="Give a comprehensive explanation of how the decision making will work within the workgroup and list the rules that accompany these procedures.", + help_text=( + "Give a comprehensive explanation of how the decision making " + "will work within the workgroup and list the rules that accompany these procedures." + ), ) communication = MarkupField( default_markup_type=DEFAULT_MARKUP_TYPE, @@ -43,7 +58,10 @@ class WorkGroup(ContentManageable, NameSlugModel): support = MarkupField( blank=True, default_markup_type=DEFAULT_MARKUP_TYPE, - help_text="What resources will you need from the PSF in order to have a functional and effective workgroup?", + help_text=( + "What resources will you need from the PSF " + "in order to have a functional and effective workgroup?" + ), ) url = models.URLField('URL', blank=True, help_text="Main URL for Group")