diff --git a/evap/evaluation/migrations/0019_merge.py b/evap/evaluation/migrations/0019_merge.py index 573b79cf8bd190e49e1b6418c484edceb63019ab..4e737b6399fa8771aedbefafca9aff6c592da6d8 100644 --- a/evap/evaluation/migrations/0019_merge.py +++ b/evap/evaluation/migrations/0019_merge.py @@ -7,6 +7,3 @@ class Migration(migrations.Migration): ('evaluation', '0018_unique_degrees'), ('evaluation', '0014_rename_lecturer'), ] - - operations = [ - ] diff --git a/evap/evaluation/models.py b/evap/evaluation/models.py index 60cb31e9a4367d054e71630745fecbba72e07404..ddb9248272981aba5649479cce1a8f6b9b9cca32 100644 --- a/evap/evaluation/models.py +++ b/evap/evaluation/models.py @@ -1,9 +1,11 @@ import logging import secrets import uuid -from collections import defaultdict, namedtuple +from collections import defaultdict from datetime import date, datetime, timedelta from enum import Enum, auto +from numbers import Number +from typing import Dict, List, NamedTuple, Tuple, Union from django.conf import settings from django.contrib import messages @@ -17,7 +19,7 @@ from django.db.models import Count, Manager, OuterRef, Q, Subquery from django.db.models.functions import Coalesce from django.dispatch import Signal, receiver from django.template import Context, Template -from django.template.base import TemplateSyntaxError +from django.template.exceptions import TemplateSyntaxError from django.template.loader import render_to_string from django.urls import reverse from django.utils.functional import cached_property @@ -1178,8 +1180,30 @@ class Question(models.Model): return self.is_text_question or self.is_rating_question and self.allows_additional_textanswers -Choices = namedtuple("Choices", ("cssClass", "values", "colors", "grades", "names")) -BipolarChoices = namedtuple("BipolarChoices", Choices._fields + ("plus_name", "minus_name")) +# Let's deduplicate the fields here once mypy is smart enough to keep up with us :) +Choices = NamedTuple( + "Choices", + [ + ("cssClass", str), + ("values", Tuple[Number]), + ("colors", Tuple[str]), + ("grades", Tuple[Number]), + ("names", List[str]), + ], +) +BipolarChoices = NamedTuple( + "BipolarChoices", + [ + ("cssClass", str), + ("values", Tuple[Number]), + ("colors", Tuple[str]), + ("grades", Tuple[Number]), + ("names", List[str]), + ("plus_name", str), + ("minus_name", str), + ], +) + NO_ANSWER = 6 BASE_UNIPOLAR_CHOICES = { @@ -1203,7 +1227,7 @@ BASE_YES_NO_CHOICES = { "grades": (1, 5), } -CHOICES = { +CHOICES: Dict[int, Union[Choices, BipolarChoices]] = { Question.LIKERT: Choices( names=[ _("Strongly\nagree"), @@ -1213,7 +1237,7 @@ CHOICES = { _("Strongly\ndisagree"), _("No answer"), ], - **BASE_UNIPOLAR_CHOICES, + **BASE_UNIPOLAR_CHOICES, # type: ignore ), Question.GRADE: Choices( names=[ @@ -1224,7 +1248,7 @@ CHOICES = { "5", _("No answer"), ], - **BASE_UNIPOLAR_CHOICES, + **BASE_UNIPOLAR_CHOICES, # type: ignore ), Question.EASY_DIFFICULT: BipolarChoices( minus_name=_("Easy"), @@ -1239,7 +1263,7 @@ CHOICES = { _("Way too\ndifficult"), _("No answer"), ], - **BASE_BIPOLAR_CHOICES, + **BASE_BIPOLAR_CHOICES, # type: ignore ), Question.FEW_MANY: BipolarChoices( minus_name=_("Few"), @@ -1254,7 +1278,7 @@ CHOICES = { _("Way too\nmany"), _("No answer"), ], - **BASE_BIPOLAR_CHOICES, + **BASE_BIPOLAR_CHOICES, # type: ignore ), Question.LITTLE_MUCH: BipolarChoices( minus_name=_("Little"), @@ -1269,7 +1293,7 @@ CHOICES = { _("Way too\nmuch"), _("No answer"), ], - **BASE_BIPOLAR_CHOICES, + **BASE_BIPOLAR_CHOICES, # type: ignore ), Question.SMALL_LARGE: BipolarChoices( minus_name=_("Small"), @@ -1284,7 +1308,7 @@ CHOICES = { _("Way too\nlarge"), _("No answer"), ], - **BASE_BIPOLAR_CHOICES, + **BASE_BIPOLAR_CHOICES, # type: ignore ), Question.SLOW_FAST: BipolarChoices( minus_name=_("Slow"), @@ -1299,7 +1323,7 @@ CHOICES = { _("Way too\nfast"), _("No answer"), ], - **BASE_BIPOLAR_CHOICES, + **BASE_BIPOLAR_CHOICES, # type: ignore ), Question.SHORT_LONG: BipolarChoices( minus_name=_("Short"), @@ -1314,7 +1338,7 @@ CHOICES = { _("Way too\nlong"), _("No answer"), ], - **BASE_BIPOLAR_CHOICES, + **BASE_BIPOLAR_CHOICES, # type: ignore ), Question.POSITIVE_YES_NO: Choices( names=[ @@ -1322,7 +1346,7 @@ CHOICES = { _("No"), _("No answer"), ], - **BASE_YES_NO_CHOICES, + **BASE_YES_NO_CHOICES, # type: ignore ), Question.NEGATIVE_YES_NO: Choices( names=[ @@ -1330,7 +1354,7 @@ CHOICES = { _("Yes"), _("No answer"), ], - **BASE_YES_NO_CHOICES, + **BASE_YES_NO_CHOICES, # type: ignore ), } @@ -1517,7 +1541,7 @@ class UserProfile(AbstractBaseUser, PermissionsMixin): verbose_name_plural = _("users") USERNAME_FIELD = "email" - REQUIRED_FIELDS = [] + REQUIRED_FIELDS: List[str] = [] objects = UserProfileManager() diff --git a/evap/evaluation/templatetags/navbar_templatetags.py b/evap/evaluation/templatetags/navbar_templatetags.py index 332c6d337395572137252a3e7d7798b30140ecf6..ece4b251aafcb6c07f9e8bbe9032c96f4541ed82 100644 --- a/evap/evaluation/templatetags/navbar_templatetags.py +++ b/evap/evaluation/templatetags/navbar_templatetags.py @@ -1,7 +1,7 @@ from django.template import Library from evap.evaluation.models import Semester -from evap.settings import DEBUG, LANGUAGES +from evap.settings import DEBUG, LANGUAGES # type: ignore register = Library() diff --git a/evap/evaluation/tests/tools.py b/evap/evaluation/tests/tools.py index 38a8dadb63e48514077241056494d6606767213e..4466b48cd7f90fd5dc0b02de6aa25c1decba9f01 100644 --- a/evap/evaluation/tests/tools.py +++ b/evap/evaluation/tests/tools.py @@ -1,6 +1,7 @@ import functools import os from datetime import timedelta +from typing import List, Union from django.conf import settings from django.contrib.auth.models import Group @@ -84,7 +85,7 @@ def render_pages(test_item): class WebTestWith200Check(WebTest): url = "/" - test_users = [] + test_users: List[Union[UserProfile, str]] = [] def test_check_response_code_200(self): for user in self.test_users: diff --git a/evap/evaluation/tools.py b/evap/evaluation/tools.py index 41bdd1a73f291e100414e6dd6a833ab86b0181f4..4699b06da831fc2967a51f35e47aad161eaee43a 100644 --- a/evap/evaluation/tools.py +++ b/evap/evaluation/tools.py @@ -1,5 +1,6 @@ import datetime from abc import ABC, abstractmethod +from typing import Optional from urllib.parse import quote import xlwt @@ -96,7 +97,7 @@ class ExcelExporter(ABC): # Derived classes can set this to # have a sheet added at initialization. - default_sheet_name = None + default_sheet_name: Optional[str] = None def __init__(self): self.workbook = xlwt.Workbook() diff --git a/evap/results/exporters.py b/evap/results/exporters.py index 59ca75c463c691918ccf81f9f2c2c34642a1e393..027676ab06dbea8b99ced878acfa3e8c3e54b7fb 100644 --- a/evap/results/exporters.py +++ b/evap/results/exporters.py @@ -1,6 +1,7 @@ import warnings from collections import OrderedDict, defaultdict from itertools import chain, repeat +from typing import Dict, Tuple import xlwt from django.db.models import Q @@ -24,7 +25,7 @@ class ResultsExporter(ExcelExporter): STEP = 0.2 # we only have a limited number of custom colors # Filled in ResultsExporter.init_grade_styles - COLOR_MAPPINGS = {} + COLOR_MAPPINGS: Dict[int, Tuple[int, int, int]] = {} styles = { "evaluation": xlwt.easyxf( diff --git a/evap/results/tools.py b/evap/results/tools.py index 681bb50c9a0a1d8445d7a560016f7505ecfdfef8..4cd3633defe89cf7a9244dabb881935da9529930 100644 --- a/evap/results/tools.py +++ b/evap/results/tools.py @@ -1,5 +1,6 @@ from collections import OrderedDict, defaultdict, namedtuple from math import ceil, modf +from typing import Tuple, cast from django.conf import settings from django.core.cache import caches @@ -375,7 +376,9 @@ def distribution_to_grade(distribution): def color_mix(color1, color2, fraction): - return tuple(int(round(color1[i] * (1 - fraction) + color2[i] * fraction)) for i in range(3)) + return cast( + Tuple[int, int, int], tuple(int(round(color1[i] * (1 - fraction) + color2[i] * fraction)) for i in range(3)) + ) def get_grade_color(grade): diff --git a/evap/results/views.py b/evap/results/views.py index 84a03d67cd84f1b969942f76308f2ba5e9f1abdb..a4f57beb283ee8a69a158e808d0b6200ae164d0b 100644 --- a/evap/results/views.py +++ b/evap/results/views.py @@ -112,7 +112,7 @@ def update_template_cache_of_published_evaluations_in_course(course): def get_evaluations_with_prefetched_data(evaluations): - if isinstance(evaluations, QuerySet): + if isinstance(evaluations, QuerySet): # type: ignore evaluations = evaluations.select_related("course__type").prefetch_related( "course__degrees", "course__semester", diff --git a/evap/settings.py b/evap/settings.py index 37ce6a5b6d51dea43d48e863366d618e400886cf..fea0244ceb91e90272efabc79704e88d2c0f17ef 100644 --- a/evap/settings.py +++ b/evap/settings.py @@ -1,3 +1,5 @@ +# type: ignore + """ Django settings for EvaP project. diff --git a/evap/staff/forms.py b/evap/staff/forms.py index b30553706db30bf0c213b9c857b6e9ef93203f93..8dae2b034ce4d210976f120f86390644bb3d356e 100644 --- a/evap/staff/forms.py +++ b/evap/staff/forms.py @@ -250,7 +250,7 @@ class CourseFormMixin: self.add_error(name_field, e) -class CourseForm(CourseFormMixin, forms.ModelForm): +class CourseForm(CourseFormMixin, forms.ModelForm): # type: ignore semester = forms.ModelChoiceField(Semester.objects.all(), disabled=True, required=False, widget=forms.HiddenInput()) def __init__(self, *args, **kwargs): @@ -260,17 +260,17 @@ class CourseForm(CourseFormMixin, forms.ModelForm): disable_all_fields(self) -class CourseCopyForm(CourseFormMixin, forms.ModelForm): +class CourseCopyForm(CourseFormMixin, forms.ModelForm): # type: ignore semester = forms.ModelChoiceField(Semester.objects.all()) vote_start_datetime = forms.DateTimeField(label=_("Start of evaluations"), localize=True) vote_end_date = forms.DateField(label=_("Last day of evaluations"), localize=True) field_order = ["semester"] - def __init__(self, data=None, instance: Course = None): + def __init__(self, data=None, *, instance: Course): self.old_course = instance - opts = self._meta - initial = forms.models.model_to_dict(instance, opts.fields, opts.exclude) + opts = self._meta # type: ignore + initial = forms.models.model_to_dict(instance, opts.fields, opts.exclude) # type: ignore super().__init__(data=data, initial=initial) self._set_responsibles_queryset(instance) diff --git a/evap/staff/staff_mode.py b/evap/staff/staff_mode.py index f51c935a4187652985856c6f7a3ef21ece877151..3ac3d50e60b3a8012849600909a5cadc81973340 100644 --- a/evap/staff/staff_mode.py +++ b/evap/staff/staff_mode.py @@ -3,7 +3,7 @@ import time from django.contrib import messages from django.utils.translation import ugettext as _ -from evap.settings import STAFF_MODE_INFO_TIMEOUT, STAFF_MODE_TIMEOUT +from evap.settings import STAFF_MODE_INFO_TIMEOUT, STAFF_MODE_TIMEOUT # type: ignore def staff_mode_middleware(get_response): diff --git a/evap/staff/views.py b/evap/staff/views.py index c205af0edc5cf503830f632f2008b5f1d1f17231..e635405e2beb43439df42e46e04ae2024cba31f3 100644 --- a/evap/staff/views.py +++ b/evap/staff/views.py @@ -3,7 +3,7 @@ import itertools from collections import OrderedDict, defaultdict, namedtuple from dataclasses import dataclass from datetime import date, datetime -from typing import Any, Container, Dict +from typing import Any, Container, Dict, Optional from django.conf import settings from django.contrib import messages @@ -226,10 +226,10 @@ def semester_view(request, semester_id): class EvaluationOperation: - email_template_name = None - email_template_contributor_name = None - email_template_participant_name = None - confirmation_message = None + email_template_name: Optional[str] = None + email_template_contributor_name: Optional[str] = None + email_template_participant_name: Optional[str] = None + confirmation_message: Optional[str] = None @staticmethod def applicable_to(evaluation): diff --git a/pyproject.toml b/pyproject.toml index 92902f4b8bf25fe1d6ea5f72d48c5775d7361606..d033c206065922024937f44adad20c8d44b97ca8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,8 @@ extend-exclude = """\ .*/migrations/.*\ """ +############################################## + [tool.isort] profile = "black" src_paths = ["evap"] @@ -15,6 +17,8 @@ line_length = 120 skip_gitignore = true extend_skip_glob = ["**/migrations/*"] +############################################## + [tool.pylint.master] jobs = 0 @@ -58,7 +62,7 @@ disable = [ "duplicate-code", # Can not be suppressed for the 4 false positives we have: https://github.com/PyCQA/pylint/issues/214 ] - +############################################## [tool.coverage.run] branch = true @@ -67,3 +71,28 @@ source = ["evap"] [tool.coverage.report] omit = ["*/migrations/*", "*__init__.py"] + +############################################## + +[tool.mypy] +plugins = ["mypy_django_plugin.main"] +exclude = 'evap/.*/migrations/.*\.py$' + +[tool.django-stubs] +django_settings_module = "evap.settings" + +[[tool.mypy.overrides]] +module = [ + "django_fsm.*", + "django_sendfile.*", + "django_webtest.*", + "debug_toolbar.*", + "mozilla_django_oidc.*", + "model_bakery.*", + "xlrd.*", + "xlwt.*", + "xlutils.*", + + "evap.staff.fixtures.*", +] +ignore_missing_imports = true diff --git a/requirements-dev.txt b/requirements-dev.txt index 1daaa81a31c7b29c06a31f5dd3aa0f7e09418e60..908bef7b459466a67fd047fb25250dfbaf121929 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,3 +6,5 @@ pylint==2.9.5 pylint-django==2.4.4 black==21.7b0 isort==5.9.3 +mypy +django-stubs