From d97f60a02035a0e24b0b65ecea0dc2cf40e4ecdc Mon Sep 17 00:00:00 2001 From: Matt Tuchfarber Date: Fri, 10 Sep 2021 14:19:36 -0400 Subject: [PATCH] style: add black to quality and format code (#6) --- .gitignore | 3 + Makefile | 7 +++ docs/conf.py | 92 ++++++++++++++++-------------- manage.py | 4 +- notices/__init__.py | 4 +- notices/apps.py | 2 +- notices/data.py | 1 + notices/models.py | 5 +- notices/rest_api/v1/serializers.py | 6 +- notices/rest_api/v1/urls.py | 2 +- notices/rest_api/v1/views.py | 26 +++++---- notices/settings/common.py | 2 +- notices/settings/production.py | 4 +- notices/urls.py | 4 +- pyproject.toml | 3 + requirements/base.txt | 2 +- requirements/dev.txt | 24 +++++++- requirements/doc.txt | 2 +- requirements/quality.in | 1 + requirements/quality.txt | 19 +++++- requirements/test.txt | 2 +- setup.py | 50 ++++++++-------- test_settings.py | 26 ++++----- test_utils/factories.py | 3 + tests/test_models.py | 5 +- tests/test_serializers.py | 22 +++---- tests/test_views.py | 31 +++++----- tox.ini | 3 +- 28 files changed, 208 insertions(+), 147 deletions(-) create mode 100644 pyproject.toml diff --git a/.gitignore b/.gitignore index c8d42af..766c827 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,6 @@ docs/notices.*.rst # Private requirements requirements/private.in requirements/private.txt + +# Virtual environments +venv/ diff --git a/Makefile b/Makefile index fecf045..17a6ca3 100644 --- a/Makefile +++ b/Makefile @@ -50,6 +50,13 @@ upgrade: ## update the requirements/*.txt files with the latest packages satisfy quality: ## check coding style with pycodestyle and pylint tox -e quality +quality-fix: ## Fix quality failures that are automatable + isort tests notices manage.py setup.py test_settings.py + black . + +quality-requirements: requirements ## Installs quality-related requirements + pip-sync requirements/quality.txt requirements/private.* + pii_check: ## check for PII annotations on all Django models tox -e pii_check diff --git a/docs/conf.py b/docs/conf.py index 75a9b15..3a78367 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -32,13 +32,13 @@ def get_version(*file_paths): version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M) if version_match: return version_match.group(1) - raise RuntimeError('Unable to find version string.') + raise RuntimeError("Unable to find version string.") REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.append(REPO_ROOT) -VERSION = get_version('../notices', '__init__.py') +VERSION = get_version("../notices", "__init__.py") # Configure Django for autodoc usage settings.configure() @@ -62,40 +62,40 @@ def get_version(*file_paths): # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'edx_theme', - 'sphinx.ext.autodoc', - 'sphinx.ext.doctest', - 'sphinx.ext.intersphinx', - 'sphinx.ext.ifconfig', - 'sphinx.ext.napoleon' + "edx_theme", + "sphinx.ext.autodoc", + "sphinx.ext.doctest", + "sphinx.ext.intersphinx", + "sphinx.ext.ifconfig", + "sphinx.ext.napoleon", ] # A list of warning types to suppress arbitrary warning messages. suppress_warnings = [ - 'image.nonlocal_uri', + "image.nonlocal_uri", ] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. # # source_encoding = 'utf-8-sig' # The top level toctree document. -top_level_doc = 'index' +top_level_doc = "index" # General information about the project. -project = 'platform-plugin-notices' +project = "platform-plugin-notices" copyright = edx_theme.COPYRIGHT # pylint: disable=redefined-builtin author = edx_theme.AUTHOR -project_title = 'platform-plugin-notices' +project_title = "platform-plugin-notices" documentation_title = "{project_title}".format(project_title=project_title) # The version info for the project you're documenting, acts as replacement for @@ -126,7 +126,7 @@ def get_version(*file_paths): # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # The reST default role (used for this markup: `text`) to use for all # documents. @@ -148,7 +148,7 @@ def get_version(*file_paths): # show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] @@ -165,7 +165,7 @@ def get_version(*file_paths): # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'edx_theme' +html_theme = "edx_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -199,7 +199,7 @@ def get_version(*file_paths): # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied @@ -279,7 +279,7 @@ def get_version(*file_paths): # html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. -htmlhelp_basename = '{project_name}doc'.format(project_name=project) +htmlhelp_basename = "{project_name}doc".format(project_name=project) # -- Options for LaTeX output --------------------------------------------- @@ -287,15 +287,12 @@ def get_version(*file_paths): # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. # # 'preamble': '', - # Latex figure (float) alignment # # 'figure_align': 'htbp', @@ -304,10 +301,9 @@ def get_version(*file_paths): # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). -latex_target = '{project}.tex'.format(project=project) +latex_target = "{project}.tex".format(project=project) latex_documents = [ - (top_level_doc, latex_target, documentation_title, - author, 'manual'), + (top_level_doc, latex_target, documentation_title, author, "manual"), ] # The name of an image file (relative to this directory) to place at the top of @@ -347,10 +343,7 @@ def get_version(*file_paths): # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - (top_level_doc, project_title, documentation_title, - [author], 1) -] +man_pages = [(top_level_doc, project_title, documentation_title, [author], 1)] # If true, show URL addresses after external links. # @@ -363,9 +356,15 @@ def get_version(*file_paths): # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (top_level_doc, project_title, documentation_title, - author, project_title, 'An edx-platform plugin which manages notices that must be acknowledged', - 'Miscellaneous'), + ( + top_level_doc, + project_title, + documentation_title, + author, + project_title, + "An edx-platform plugin which manages notices that must be acknowledged", + "Miscellaneous", + ), ] # Documents to append as an appendix to all manuals. @@ -439,7 +438,7 @@ def get_version(*file_paths): # epub_post_files = [] # A list of files that should not be packed into the epub file. -epub_exclude_files = ['search.html'] +epub_exclude_files = ["search.html"] # The depth of the table of contents in toc.ncx. # @@ -472,9 +471,9 @@ def get_version(*file_paths): # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { - 'python': ('https://docs.python.org/3.6', None), - 'django': ('https://docs.djangoproject.com/en/1.11/', 'https://docs.djangoproject.com/en/1.11/_objects/'), - 'model_utils': ('https://django-model-utils.readthedocs.io/en/latest/', None), + "python": ("https://docs.python.org/3.6", None), + "django": ("https://docs.djangoproject.com/en/1.11/", "https://docs.djangoproject.com/en/1.11/_objects/"), + "model_utils": ("https://django-model-utils.readthedocs.io/en/latest/", None), } @@ -486,17 +485,24 @@ def on_init(app): # pylint: disable=unused-argument avoid checking in the generated reStructuredText files. """ docs_path = os.path.abspath(os.path.dirname(__file__)) - root_path = os.path.abspath(os.path.join(docs_path, '..')) - apidoc_path = 'sphinx-apidoc' - if hasattr(sys, 'real_prefix'): # Check to see if we are in a virtualenv + root_path = os.path.abspath(os.path.join(docs_path, "..")) + apidoc_path = "sphinx-apidoc" + if hasattr(sys, "real_prefix"): # Check to see if we are in a virtualenv # If we are, assemble the path manually - bin_path = os.path.abspath(os.path.join(sys.prefix, 'bin')) + bin_path = os.path.abspath(os.path.join(sys.prefix, "bin")) apidoc_path = os.path.join(bin_path, apidoc_path) - check_call([apidoc_path, '-o', docs_path, os.path.join(root_path, 'notices'), - os.path.join(root_path, 'notices/migrations')]) + check_call( + [ + apidoc_path, + "-o", + docs_path, + os.path.join(root_path, "notices"), + os.path.join(root_path, "notices/migrations"), + ] + ) def setup(app): """Sphinx extension: run sphinx-apidoc.""" - event = 'builder-inited' + event = "builder-inited" app.connect(event, on_init) diff --git a/manage.py b/manage.py index 62a73c6..c27b912 100755 --- a/manage.py +++ b/manage.py @@ -9,8 +9,8 @@ PWD = os.path.abspath(os.path.dirname(__file__)) -if __name__ == '__main__': - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'test_settings') +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_settings") sys.path.append(PWD) try: from django.core.management import execute_from_command_line diff --git a/notices/__init__.py b/notices/__init__.py index d50fb2d..b44f371 100644 --- a/notices/__init__.py +++ b/notices/__init__.py @@ -2,6 +2,6 @@ An edx-platform plugin which manages notices that must be acknowledged. """ -__version__ = '0.1.0' +__version__ = "0.1.0" -default_app_config = 'notices.apps.NoticesConfig' # pylint: disable=invalid-name +default_app_config = "notices.apps.NoticesConfig" # pylint: disable=invalid-name diff --git a/notices/apps.py b/notices/apps.py index 0391df4..1a26440 100644 --- a/notices/apps.py +++ b/notices/apps.py @@ -25,5 +25,5 @@ class NoticesConfig(AppConfig): "production": {"relative_path": "settings.production"}, "common": {"relative_path": "settings.common"}, } - } + }, } diff --git a/notices/data.py b/notices/data.py index 17e1edf..8dca1ad 100644 --- a/notices/data.py +++ b/notices/data.py @@ -10,6 +10,7 @@ class AcknowledgmentResponseTypes(str, Enum): """ Options for the response_type field of a AcknowledgedNotice. """ + CONFIRMED = "confirmed" DISMISSED = "dismissed" diff --git a/notices/models.py b/notices/models.py index cc3a58e..a3dccad 100644 --- a/notices/models.py +++ b/notices/models.py @@ -45,6 +45,7 @@ class TranslatedNoticeContent(TimeStampedModel): .. no_pii: """ + notice = models.ForeignKey(Notice, on_delete=models.CASCADE, related_name="translated_notice_content") language_code = models.CharField(max_length=10, help_text="The IETF BCP 47 language code for this translation") html_content = models.TextField( @@ -58,7 +59,7 @@ class Meta: """Model metadata.""" app_label = "notices" - unique_together = ['notice', 'language_code'] + unique_together = ["notice", "language_code"] def save(self, *args, **kwargs): """Save method override to remove unsafe tags from html_content first.""" @@ -74,6 +75,7 @@ class AcknowledgedNotice(TimeStampedModel): .. no_pii: """ + RESPONSE_TYPE_CHOICES = [ (AcknowledgmentResponseTypes.CONFIRMED.value, "Confirmed"), (AcknowledgmentResponseTypes.DISMISSED.value, "Dismissed"), @@ -85,6 +87,7 @@ class AcknowledgedNotice(TimeStampedModel): class Meta: """Model metadata.""" + app_label = "notices" def __str__(self): diff --git a/notices/rest_api/v1/serializers.py b/notices/rest_api/v1/serializers.py index e1bdc1f..bc9cc59 100644 --- a/notices/rest_api/v1/serializers.py +++ b/notices/rest_api/v1/serializers.py @@ -6,15 +6,17 @@ class TranslateNoticeContentSerializer(serializers.ModelSerializer): """Serializer for translated notice content""" + class Meta: model = TranslatedNoticeContent - fields = ('language_code', 'html_content') + fields = ("language_code", "html_content") class NoticeSerializer(serializers.ModelSerializer): """Serializer for notice""" + translated_notice_content = TranslateNoticeContentSerializer(many=True, read_only=True) class Meta: model = Notice - fields = ('id', 'name', 'translated_notice_content') + fields = ("id", "name", "translated_notice_content") diff --git a/notices/rest_api/v1/urls.py b/notices/rest_api/v1/urls.py index d144479..a33a170 100644 --- a/notices/rest_api/v1/urls.py +++ b/notices/rest_api/v1/urls.py @@ -6,5 +6,5 @@ urlpatterns = [ url(r"unacknowledged", views.ListUnacknowledgedNotices.as_view(), name="unacknowledged_notices"), - url(r"acknowledge", views.AcknowledgeNotice.as_view(), name="acknowledge_notice") + url(r"acknowledge", views.AcknowledgeNotice.as_view(), name="acknowledge_notice"), ] diff --git a/notices/rest_api/v1/views.py b/notices/rest_api/v1/views.py index ce84e3d..cb25802 100644 --- a/notices/rest_api/v1/views.py +++ b/notices/rest_api/v1/views.py @@ -44,7 +44,11 @@ class ListUnacknowledgedNotices(APIView): ] } """ - authentication_classes = (JwtAuthentication, SessionAuthentication,) + + authentication_classes = ( + JwtAuthentication, + SessionAuthentication, + ) permission_classes = (permissions.IsAuthenticated,) def get(self, request): @@ -75,7 +79,11 @@ class AcknowledgeNotice(APIView): POST /api/notices/v1/acknowledge post data: {"notice_id": 10, "acknowledgment_type": "confirmed"} """ - authentication_classes = (JwtAuthentication, SessionAuthentication,) + + authentication_classes = ( + JwtAuthentication, + SessionAuthentication, + ) permission_classes = (permissions.IsAuthenticated,) def post(self, request): @@ -86,23 +94,21 @@ def post(self, request): acknowledgment_type = request.data.get("acknowledgment_type") if not notice_id: - raise ValidationError({'notice_id': "notice_id field required"}) + raise ValidationError({"notice_id": "notice_id field required"}) if not AcknowledgmentResponseTypes.includes_value(acknowledgment_type): valid_types = [e.value for e in AcknowledgmentResponseTypes] - raise ValidationError({ - 'acknowledgment_type': f"acknowledgment_type must be one of the following: {valid_types}" - }) + raise ValidationError( + {"acknowledgment_type": f"acknowledgment_type must be one of the following: {valid_types}"} + ) try: notice = Notice.objects.get(id=notice_id, active=True) except Notice.DoesNotExist as exc: - raise ValidationError({'notice_id': "notice_id field does not match an existing active notice"}) from exc + raise ValidationError({"notice_id": "notice_id field does not match an existing active notice"}) from exc AcknowledgedNotice.objects.update_or_create( - user=request.user, - notice=notice, - defaults={"response_type": acknowledgment_type} + user=request.user, notice=notice, defaults={"response_type": acknowledgment_type} ) # Since this is just an acknowledgment API, we can just return a 204 without any response data. return Response(status=HTTP_204_NO_CONTENT) diff --git a/notices/settings/common.py b/notices/settings/common.py index 3fdd841..4a8ef53 100644 --- a/notices/settings/common.py +++ b/notices/settings/common.py @@ -9,4 +9,4 @@ def plugin_settings(settings): # .. toggle_description: If True, users will be prompted to acknowledge the notice in a client # .. toggle_use_cases: open_edx # .. toggle_creation_date: 2021-08-19 - settings.FEATURES['ENABLE_NOTICES'] = False + settings.FEATURES["ENABLE_NOTICES"] = False diff --git a/notices/settings/production.py b/notices/settings/production.py index f6b3d83..6769098 100644 --- a/notices/settings/production.py +++ b/notices/settings/production.py @@ -4,6 +4,4 @@ def plugin_settings(settings): """Settings for the notices app""" # Whether to enable the feature or not - settings.FEATURES["ENABLE_NOTICES"] = settings.ENV_TOKENS.get( - "ENABLE_NOTICES", settings.FEATURES["ENABLE_NOTICES"] - ) + settings.FEATURES["ENABLE_NOTICES"] = settings.ENV_TOKENS.get("ENABLE_NOTICES", settings.FEATURES["ENABLE_NOTICES"]) diff --git a/notices/urls.py b/notices/urls.py index 582bbd8..4655fc5 100644 --- a/notices/urls.py +++ b/notices/urls.py @@ -4,6 +4,4 @@ from django.conf.urls import include, url -urlpatterns = [ - url(r"", include(("notices.rest_api.urls", "rest_api"), namespace="rest_api")) -] +urlpatterns = [url(r"", include(("notices.rest_api.urls", "rest_api"), namespace="rest_api"))] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..fae78d5 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[tool.black] +line-length = 120 +extend-exclude = '(node_modules|private.py|migrations)' diff --git a/requirements/base.txt b/requirements/base.txt index 144e561..7a7d57e 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -99,7 +99,7 @@ six==1.16.0 # edx-drf-extensions # pyjwkest # python-dateutil -sqlparse==0.4.1 +sqlparse==0.4.2 # via django stevedore==3.4.0 # via diff --git a/requirements/dev.txt b/requirements/dev.txt index 375545e..3b7ca7c 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -17,6 +17,8 @@ backports.entry-points-selectable==1.1.0 # via # -r requirements/ci.txt # virtualenv +black==21.8b0 + # via -r requirements/quality.txt bleach==4.1.0 # via # -r requirements/quality.txt @@ -41,6 +43,7 @@ click==8.0.1 # via # -r requirements/pip-tools.txt # -r requirements/quality.txt + # black # click-log # code-annotations # edx-lint @@ -199,6 +202,10 @@ mccabe==0.6.1 # via # -r requirements/quality.txt # pylint +mypy-extensions==0.4.3 + # via + # -r requirements/quality.txt + # black newrelic==6.8.1.164 # via # -r requirements/quality.txt @@ -212,6 +219,10 @@ packaging==21.0 # tox path==16.2.0 # via edx-i18n-tools +pathspec==0.9.0 + # via + # -r requirements/quality.txt + # black pbr==5.6.0 # via # -r requirements/quality.txt @@ -230,6 +241,7 @@ platformdirs==2.3.0 # via # -r requirements/ci.txt # -r requirements/quality.txt + # black # pylint # virtualenv pluggy==0.13.1 @@ -338,6 +350,10 @@ readme-renderer==29.0 # via # -r requirements/quality.txt # twine +regex==2021.8.28 + # via + # -r requirements/quality.txt + # black requests==2.26.0 # via # -r requirements/ci.txt @@ -384,7 +400,7 @@ snowballstemmer==2.1.0 # via # -r requirements/quality.txt # pydocstyle -sqlparse==0.4.1 +sqlparse==0.4.2 # via # -r requirements/quality.txt # django @@ -410,6 +426,8 @@ toml==0.10.2 tomli==1.2.1 # via # -r requirements/pip-tools.txt + # -r requirements/quality.txt + # black # pep517 tox==3.24.3 # via @@ -423,6 +441,10 @@ tqdm==4.62.2 # twine twine==3.4.2 # via -r requirements/quality.txt +typing-extensions==3.10.0.2 + # via + # -r requirements/quality.txt + # black urllib3==1.26.6 # via # -r requirements/ci.txt diff --git a/requirements/doc.txt b/requirements/doc.txt index 6dfcc55..af73fae 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -260,7 +260,7 @@ sphinxcontrib-qthelp==1.0.3 # via sphinx sphinxcontrib-serializinghtml==1.1.5 # via sphinx -sqlparse==0.4.1 +sqlparse==0.4.2 # via # -r requirements/test.txt # django diff --git a/requirements/quality.in b/requirements/quality.in index d477386..dc2f2ce 100644 --- a/requirements/quality.in +++ b/requirements/quality.in @@ -3,6 +3,7 @@ -r test.txt # Core and testing dependencies for this package +black # for autoformatting code edx-lint # edX pylint rules and plugins isort # to standardize order of imports pycodestyle # PEP 8 compliance validation diff --git a/requirements/quality.txt b/requirements/quality.txt index cabd90c..7a899d5 100644 --- a/requirements/quality.txt +++ b/requirements/quality.txt @@ -12,6 +12,8 @@ attrs==21.2.0 # via # -r requirements/test.txt # pytest +black==21.8b0 + # via -r requirements/quality.in bleach==4.1.0 # via # -r requirements/test.txt @@ -31,6 +33,7 @@ charset-normalizer==2.0.4 click==8.0.1 # via # -r requirements/test.txt + # black # click-log # code-annotations # edx-lint @@ -149,6 +152,8 @@ markupsafe==2.0.1 # jinja2 mccabe==0.6.1 # via pylint +mypy-extensions==0.4.3 + # via black newrelic==6.8.1.164 # via # -r requirements/test.txt @@ -158,6 +163,8 @@ packaging==21.0 # -r requirements/test.txt # bleach # pytest +pathspec==0.9.0 + # via black pbr==5.6.0 # via # -r requirements/test.txt @@ -165,7 +172,9 @@ pbr==5.6.0 pkginfo==1.7.1 # via twine platformdirs==2.3.0 - # via pylint + # via + # black + # pylint pluggy==0.13.1 # via # -c requirements/constraints.txt @@ -252,6 +261,8 @@ pyyaml==5.4.1 # code-annotations readme-renderer==29.0 # via twine +regex==2021.8.28 + # via black requests==2.26.0 # via # -r requirements/test.txt @@ -284,7 +295,7 @@ six==1.16.0 # readme-renderer snowballstemmer==2.1.0 # via pydocstyle -sqlparse==0.4.1 +sqlparse==0.4.2 # via # -r requirements/test.txt # django @@ -305,10 +316,14 @@ toml==0.10.2 # pylint # pytest # pytest-cov +tomli==1.2.1 + # via black tqdm==4.62.2 # via twine twine==3.4.2 # via -r requirements/quality.in +typing-extensions==3.10.0.2 + # via black urllib3==1.26.6 # via # -r requirements/test.txt diff --git a/requirements/test.txt b/requirements/test.txt index 74956b4..473070c 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -186,7 +186,7 @@ six==1.16.0 # edx-drf-extensions # pyjwkest # python-dateutil -sqlparse==0.4.1 +sqlparse==0.4.2 # via # -r requirements/base.txt # django diff --git a/setup.py b/setup.py index 37bf6fb..85f05c0 100755 --- a/setup.py +++ b/setup.py @@ -20,11 +20,10 @@ def get_version(*file_paths): filename = os.path.join(os.path.dirname(__file__), *file_paths) with open(filename, encoding="utf8") as version_file: version_file_contents = version_file.read() - version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", - version_file_contents, re.M) + version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file_contents, re.M) if version_match: return version_match.group(1) - raise RuntimeError('Unable to find version string.') + raise RuntimeError("Unable to find version string.") def load_requirements(*requirements_paths): @@ -38,8 +37,7 @@ def load_requirements(*requirements_paths): for path in requirements_paths: with open(path, encoding="utf8") as req_file: requirements.update( - line.split('#')[0].strip() for line in req_file.readlines() - if is_requirement(line.strip()) + line.split("#")[0].strip() for line in req_file.readlines() if is_requirement(line.strip()) ) return list(requirements) @@ -52,48 +50,48 @@ def is_requirement(line): bool: True if the line is not blank, a comment, a URL, or an included file """ - return line and not line.startswith(('-r', '#', '-e', 'git+', '-c')) + return line and not line.startswith(("-r", "#", "-e", "git+", "-c")) -VERSION = get_version('notices', '__init__.py') +VERSION = get_version("notices", "__init__.py") -if sys.argv[-1] == 'tag': +if sys.argv[-1] == "tag": print("Tagging the version on github:") os.system("git tag -a %s -m 'version %s'" % (VERSION, VERSION)) os.system("git push --tags") sys.exit() -with open(os.path.join(os.path.dirname(__file__), 'README.rst'), encoding="utf8") as readme_file: +with open(os.path.join(os.path.dirname(__file__), "README.rst"), encoding="utf8") as readme_file: README = readme_file.read() -with open(os.path.join(os.path.dirname(__file__), 'CHANGELOG.rst'), encoding="utf8") as changelog_file: +with open(os.path.join(os.path.dirname(__file__), "CHANGELOG.rst"), encoding="utf8") as changelog_file: CHANGELOG = changelog_file.read() setup( - name='notices', + name="notices", version=VERSION, description="""An edx-platform plugin which manages notices that must be acknowledged""", - long_description=README + '\n\n' + CHANGELOG, - author='edX', - author_email='mtuchfarber@edx.org', - url='https://github.com/edx/platform-plugin-notices', + long_description=README + "\n\n" + CHANGELOG, + author="edX", + author_email="mtuchfarber@edx.org", + url="https://github.com/edx/platform-plugin-notices", packages=[ - 'notices', + "notices", ], include_package_data=True, - install_requires=load_requirements('requirements/base.in'), + install_requires=load_requirements("requirements/base.in"), python_requires=">=3.8", license="AGPL 3.0", zip_safe=False, - keywords='Python edx', + keywords="Python edx", classifiers=[ - 'Development Status :: 3 - Alpha', - 'Framework :: Django', - 'Framework :: Django :: 2.2', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)', - 'Natural Language :: English', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.8', + "Development Status :: 3 - Alpha", + "Framework :: Django", + "Framework :: Django :: 2.2", + "Intended Audience :: Developers", + "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)", + "Natural Language :: English", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", ], entry_points={ "lms.djangoapp": [ diff --git a/test_settings.py b/test_settings.py index 3b0c29d..ae8c2a1 100644 --- a/test_settings.py +++ b/test_settings.py @@ -16,26 +16,26 @@ def root(*args): DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': 'default.db', - 'USER': '', - 'PASSWORD': '', - 'HOST': '', - 'PORT': '', + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": "default.db", + "USER": "", + "PASSWORD": "", + "HOST": "", + "PORT": "", } } INSTALLED_APPS = ( - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'notices', + "django.contrib.auth", + "django.contrib.contenttypes", + "notices", ) LOCALE_PATHS = [ - root('notices', 'conf', 'locale'), + root("notices", "conf", "locale"), ] -ROOT_URLCONF = 'notices.urls' +ROOT_URLCONF = "notices.urls" -SECRET_KEY = 'insecure-secret-key' +SECRET_KEY = "insecure-secret-key" diff --git a/test_utils/factories.py b/test_utils/factories.py index 188d5ad..8c6a50b 100644 --- a/test_utils/factories.py +++ b/test_utils/factories.py @@ -23,6 +23,7 @@ class Meta: is_superuser = False is_active = True + class NoticeFactory(DjangoModelFactory): """ Notice factory @@ -34,6 +35,7 @@ class Meta: name = factory.Sequence(u"Notice number {0}".format) active = True + class TranslatedNoticeContentFactory(DjangoModelFactory): """ TranslatedNoticeContent factory @@ -44,6 +46,7 @@ class Meta: notice = factory.SubFactory(NoticeFactory) + class AcknowledgedNoticeFactory(DjangoModelFactory): """ AcknowledgedNotice factory diff --git a/tests/test_models.py b/tests/test_models.py index 912ecf9..ce938eb 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -11,6 +11,7 @@ class TestTranslatedNoticeContent(TestCase): """ Tests of the TranslatedNoticeContent model. """ + def setUp(self): super().setUp() self.notice = NoticeFactory() @@ -30,8 +31,6 @@ def test_save_clean_tags(self): Link """ translated_content = TranslatedNoticeContentFactory( - notice=self.notice, - language_code="en-US", - html_content=unclean_content + notice=self.notice, language_code="en-US", html_content=unclean_content ) assert translated_content.html_content == expected_content diff --git a/tests/test_serializers.py b/tests/test_serializers.py index ef74ca5..19799a7 100644 --- a/tests/test_serializers.py +++ b/tests/test_serializers.py @@ -20,20 +20,16 @@ def testResponseFormat(self): first_notice = NoticeFactory() second_notice = NoticeFactory() first_content_for_first_notice = TranslatedNoticeContentFactory( - notice=first_notice, - language_code="en-US", - html_content="This is a test notice." + notice=first_notice, language_code="en-US", html_content="This is a test notice." ) second_content_for_first_notice = TranslatedNoticeContentFactory( notice=first_notice, language_code="es-ES", # This was an auto translation, sorry if it's wrong - html_content="Este es un aviso de prueba." + html_content="Este es un aviso de prueba.", ) first_content_for_second_notice = TranslatedNoticeContentFactory( - notice=second_notice, - language_code="en-US", - html_content="This is a second test notice." + notice=second_notice, language_code="en-US", html_content="This is a second test notice." ) notices = [first_notice, second_notice] @@ -44,13 +40,13 @@ def testResponseFormat(self): "translated_notice_content": [ { "language_code": first_content_for_first_notice.language_code, - "html_content": first_content_for_first_notice.html_content + "html_content": first_content_for_first_notice.html_content, }, { "language_code": second_content_for_first_notice.language_code, - "html_content": second_content_for_first_notice.html_content + "html_content": second_content_for_first_notice.html_content, }, - ] + ], }, { "id": second_notice.id, @@ -58,10 +54,10 @@ def testResponseFormat(self): "translated_notice_content": [ { "language_code": first_content_for_second_notice.language_code, - "html_content": first_content_for_second_notice.html_content + "html_content": first_content_for_second_notice.html_content, }, - ] - } + ], + }, ] serializer = NoticeSerializer(notices, many=True) # json required because serializer.data is an OrderedDict and this is diff --git a/tests/test_views.py b/tests/test_views.py index 40ec55b..debc2dd 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -20,7 +20,7 @@ def setUp(self): self.view = ListUnacknowledgedNotices.as_view() def test_no_notices(self): - request = self.request_factory.get('/api/v1/unacknowledged/') + request = self.request_factory.get("/api/v1/unacknowledged/") force_authenticate(request, user=self.user) response = self.view(request) # reformat response from list of OrderedDicts to list of Dict @@ -29,7 +29,7 @@ def test_no_notices(self): def test_single_notice(self): notice_1 = NoticeFactory(active=True) - request = self.request_factory.get('/api/v1/unacknowledged/') + request = self.request_factory.get("/api/v1/unacknowledged/") force_authenticate(request, user=self.user) response = self.view(request) assert len(response.data) == 1 @@ -39,7 +39,7 @@ def test_multiple_notices(self): notice_1 = NoticeFactory(active=True) notice_2 = NoticeFactory(active=True) notice_3 = NoticeFactory(active=True) - request = self.request_factory.get('/api/v1/unacknowledged/') + request = self.request_factory.get("/api/v1/unacknowledged/") force_authenticate(request, user=self.user) response = self.view(request) assert len(response.data) == 3 @@ -57,7 +57,7 @@ def test_some_acknowledged(self): # acknowledge the middle notice AcknowledgedNoticeFactory(user=self.user, notice=notice_2, response_type=AcknowledgmentResponseTypes.CONFIRMED) AcknowledgedNoticeFactory(user=self.user, notice=notice_1, response_type=AcknowledgmentResponseTypes.DISMISSED) - request = self.request_factory.get('/api/v1/unacknowledged/') + request = self.request_factory.get("/api/v1/unacknowledged/") force_authenticate(request, user=self.user) response = self.view(request) assert len(response.data) == 1 @@ -76,8 +76,8 @@ def setUp(self): def test_valid_acknowledgement(self): notice_1 = NoticeFactory(active=True) request = self.request_factory.post( - '/api/v1/acknowledge/', - {'notice_id': notice_1.id, "acknowledgment_type": AcknowledgmentResponseTypes.CONFIRMED.value} + "/api/v1/acknowledge/", + {"notice_id": notice_1.id, "acknowledgment_type": AcknowledgmentResponseTypes.CONFIRMED.value}, ) force_authenticate(request, user=self.user) response = self.view(request) @@ -88,33 +88,32 @@ def test_valid_acknowledgement(self): def test_no_notice_data(self): NoticeFactory(active=True) - request = self.request_factory.post('/api/v1/acknowledge/') + request = self.request_factory.post("/api/v1/acknowledge/") force_authenticate(request, user=self.user) response = self.view(request) assert response.status_code == 400 json_response_data = json.loads(json.dumps(response.data)) - assert json_response_data == {'notice_id': "notice_id field required"} + assert json_response_data == {"notice_id": "notice_id field required"} def test_invalid_notice_data(self): notice_1 = NoticeFactory(active=True) request = self.request_factory.post( - '/api/v1/acknowledge/', - {'notice_id': notice_1.id + 1, "acknowledgment_type": AcknowledgmentResponseTypes.CONFIRMED.value} + "/api/v1/acknowledge/", + {"notice_id": notice_1.id + 1, "acknowledgment_type": AcknowledgmentResponseTypes.CONFIRMED.value}, ) force_authenticate(request, user=self.user) response = self.view(request) assert response.status_code == 400 json_response_data = json.loads(json.dumps(response.data)) - assert json_response_data == {'notice_id': "notice_id field does not match an existing active notice"} + assert json_response_data == {"notice_id": "notice_id field does not match an existing active notice"} def test_invalid_response_type(self): INVALID_CHOICE = "invalid_CHOICE" notice_1 = NoticeFactory(active=True) request = self.request_factory.post( - '/api/v1/acknowledge/', - {'notice_id': notice_1.id, "acknowledgment_type": INVALID_CHOICE} + "/api/v1/acknowledge/", {"notice_id": notice_1.id, "acknowledgment_type": INVALID_CHOICE} ) force_authenticate(request, user=self.user) response = self.view(request) @@ -122,14 +121,14 @@ def test_invalid_response_type(self): json_response_data = json.loads(json.dumps(response.data)) acknowledgment_type_values = [e.value for e in AcknowledgmentResponseTypes] assert json_response_data == { - 'acknowledgment_type': f"acknowledgment_type must be one of the following: {acknowledgment_type_values}" + "acknowledgment_type": f"acknowledgment_type must be one of the following: {acknowledgment_type_values}" } def test_unauthenticated_call(self): notice_1 = NoticeFactory(active=True) request = self.request_factory.post( - '/api/v1/acknowledge/', - {'notice_id': notice_1.id + 1, "acknowledgment_type": AcknowledgmentResponseTypes.CONFIRMED} + "/api/v1/acknowledge/", + {"notice_id": notice_1.id + 1, "acknowledgment_type": AcknowledgmentResponseTypes.CONFIRMED}, ) response = self.view(request) assert response.status_code == 401 diff --git a/tox.ini b/tox.ini index 785a595..976d8eb 100644 --- a/tox.ini +++ b/tox.ini @@ -75,7 +75,8 @@ commands = rm tests/__init__.py pycodestyle notices tests manage.py setup.py pydocstyle notices tests manage.py setup.py - isort --check-only --diff --recursive tests notices manage.py setup.py test_settings.py + isort --check-only --diff tests notices manage.py setup.py test_settings.py + black --check . make selfcheck [testenv:pii_check]