Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion evap/cms/json_importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ def get_log(self) -> str:

return log

def send_mail(self):
def send_mail(self) -> None:
subject = "[EvaP] JSON importer log"

recipients = settings.JSON_IMPORTER_LOG_RECIPIENTS
Expand Down
2 changes: 1 addition & 1 deletion evap/cms/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class Meta:
verbose_name = _("ignored evaluation")
verbose_name_plural = _("ignored evaluations")

def __str__(self):
def __str__(self) -> str:
if self.name:
return f"{self.course.name} – {self.name}"
return self.course.name
3 changes: 2 additions & 1 deletion evap/cms/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
from evap.cms.models import IgnoredEvaluation
from evap.evaluation.auth import manager_required
from evap.evaluation.tools import (
HttpRequest,
HttpResponseNoContent,
get_object_from_dict_pk_entry_or_logged_40x,
)


@require_POST
@manager_required
def ignored_evaluation_delete(request):
def ignored_evaluation_delete(request: HttpRequest) -> HttpResponseNoContent:
ignored_evaluation = get_object_from_dict_pk_entry_or_logged_40x(
IgnoredEvaluation, request.POST, "ignored_evaluation_id"
)
Expand Down
9 changes: 5 additions & 4 deletions evap/context_processors.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
import random

from django.conf import settings
from django.http import HttpRequest
from django.utils.translation import get_language

from evap.evaluation.forms import NotebookForm


def slogan(request):
def slogan(request: HttpRequest):
if get_language() == "de":
return {"slogan": random.choice(settings.SLOGANS_DE)} # nosec
return {"slogan": random.choice(settings.SLOGANS_EN)} # nosec


def debug(request):
def debug(request: HttpRequest):
return {"debug": settings.DEBUG}


def notebook_form(request):
def notebook_form(request: HttpRequest):
if request.user.is_authenticated:
return {"notebook_form": NotebookForm(instance=request.user)}
return {}


def allow_anonymous_feedback_messages(request):
def allow_anonymous_feedback_messages(request: HttpRequest):
return {"allow_anonymous_feedback_messages": settings.ALLOW_ANONYMOUS_FEEDBACK_MESSAGES}
6 changes: 3 additions & 3 deletions evap/development/views.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from django.conf import settings
from django.core.exceptions import BadRequest
from django.http import HttpResponse
from django.http import HttpRequest, HttpResponse
from django.shortcuts import render


def development_components(request):
def development_components(request: HttpRequest) -> HttpResponse:
theme_colors = ["primary", "secondary", "success", "info", "warning", "danger", "light", "dark"]
template_data = {
"theme_colors": theme_colors,
Expand All @@ -13,7 +13,7 @@ def development_components(request):
return render(request, "development_components.html", template_data)


def development_rendered(request, filename):
def development_rendered(request: HttpRequest, filename: str) -> HttpResponse:
fixtures_directory = settings.STATICFILES_DIRS[0] / "ts" / "rendered"
try:
with open(fixtures_directory / filename, encoding="utf-8") as fixture:
Expand Down
41 changes: 23 additions & 18 deletions evap/evaluation/auth.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import inspect
from collections.abc import Callable, Iterable
from functools import wraps
from typing import Any

from django.conf import settings
from django.contrib.auth.backends import ModelBackend
from django.core.exceptions import PermissionDenied
from django.core.mail import EmailMessage
from django.http import HttpRequest
from django.utils.decorators import method_decorator
from mozilla_django_oidc.auth import OIDCAuthenticationBackend

Expand All @@ -25,7 +27,7 @@ class RequestAuthUserBackend(ModelBackend):

# Having a different method signature is okay according to django documentation:
# https://docs.djangoproject.com/en/3.0/topics/auth/customizing/#writing-an-authentication-backend
def authenticate(self, request, key): # pylint: disable=arguments-differ
def authenticate(self, request: HttpRequest, key: int) -> UserProfile | None: # type: ignore[override] # pylint: disable=arguments-differ
if not key:
return None

Expand All @@ -37,7 +39,9 @@ def authenticate(self, request, key): # pylint: disable=arguments-differ

class EmailAuthenticationBackend(ModelBackend):
# https://docs.djangoproject.com/en/3.1/topics/auth/customizing/#writing-an-authentication-backend
def authenticate(self, request, email=None, password=None): # pylint: disable=arguments-differ,arguments-renamed
def authenticate( # type: ignore[override] # pylint: disable=arguments-differ,arguments-renamed
self, request: HttpRequest | None, email: str | None = None, password: str | None = None
) -> UserProfile | None:
assert password_login_is_active()
try:
user = UserProfile.objects.get(email=email)
Expand All @@ -60,7 +64,8 @@ def class_or_function_check_decorator(test_func: Callable[[UserProfile], bool]):

def function_decorator(func):
@wraps(func)
def wrapped(request, *args, **kwargs):
def wrapped(request: HttpRequest, *args, **kwargs) -> Any:
assert isinstance(request.user, UserProfile)
if not test_func(request.user):
raise PermissionDenied
return func(request, *args, **kwargs)
Expand All @@ -79,57 +84,57 @@ def decorator(class_or_function):


@class_or_function_check_decorator
def internal_required(user):
def internal_required(user: UserProfile) -> bool:
return not user.is_external


@class_or_function_check_decorator
def staff_permission_required(user):
def staff_permission_required(user: UserProfile) -> bool:
return user.has_staff_permission


@class_or_function_check_decorator
def manager_required(user):
def manager_required(user: UserProfile) -> bool:
return user.is_manager


@class_or_function_check_decorator
def reviewer_required(user):
def reviewer_required(user: UserProfile) -> bool:
return user.is_reviewer


@class_or_function_check_decorator
def grade_publisher_required(user):
def grade_publisher_required(user: UserProfile) -> bool:
return user.is_grade_publisher


@class_or_function_check_decorator
def grade_publisher_or_manager_required(user):
def grade_publisher_or_manager_required(user: UserProfile) -> bool:
return user.is_grade_publisher or user.is_manager


@class_or_function_check_decorator
def grade_downloader_required(user):
def grade_downloader_required(user: UserProfile) -> bool:
return user.can_download_grades


@class_or_function_check_decorator
def responsible_or_contributor_or_delegate_required(user):
def responsible_or_contributor_or_delegate_required(user: UserProfile) -> bool:
return user.is_responsible_or_contributor_or_delegate


@class_or_function_check_decorator
def editor_or_delegate_required(user):
def editor_or_delegate_required(user: UserProfile) -> bool:
return user.is_editor_or_delegate


@class_or_function_check_decorator
def participant_required(user):
def participant_required(user: UserProfile) -> bool:
return user.is_participant


@class_or_function_check_decorator
def reward_user_required(user):
def reward_user_required(user: UserProfile) -> bool:
return can_reward_points_be_used_by(user)


Expand All @@ -142,13 +147,13 @@ def possible_existing_account_emails(email: str) -> Iterable[str]:
if previous_domain := settings.OIDC_EMAIL_TRANSITIONS.get(domain):
yield f"{name}@{previous_domain}"

def get_userinfo(self, *args, **kwargs):
def get_userinfo(self, *args, **kwargs) -> dict[str, Any]:
claims = super().get_userinfo(*args, **kwargs)
if "email" in claims:
claims["email"] = clean_email(claims["email"])
return claims

def filter_users_by_claims(self, claims):
def filter_users_by_claims(self, claims: dict[str, Any]) -> list[UserProfile]:
assert openid_login_is_active()
email = claims.get("email")
if not email:
Expand All @@ -162,15 +167,15 @@ def filter_users_by_claims(self, claims):

return []

def create_user(self, claims):
def create_user(self, claims: dict[str, Any]) -> UserProfile:
assert openid_login_is_active()
return self.UserModel.objects.create(
email=claims.get("email"),
first_name_given=claims.get("given_name", ""),
last_name=claims.get("family_name", ""),
)

def update_user(self, user, claims):
def update_user(self, user: UserProfile, claims: dict[str, Any]) -> UserProfile:
assert openid_login_is_active()
if not user.first_name_given:
user.first_name_given = claims.get("given_name", "")
Expand Down
40 changes: 21 additions & 19 deletions evap/evaluation/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
from django.conf import settings
from django.contrib.auth import authenticate
from django.core.exceptions import ValidationError
from django.http import HttpRequest
from django.utils.translation import gettext_lazy as _
from django.views.decorators.debug import sensitive_variables

from evap.evaluation.models import Evaluation, UserProfile
from evap.results.tools import STATES_WITH_RESULTS_CACHING, cache_results
from evap.tools import assert_not_none

logger = logging.getLogger(__name__)

Expand All @@ -19,21 +21,21 @@ class LoginEmailForm(forms.Form):
email = forms.CharField(label=_("Email"), max_length=254, widget=forms.EmailInput(attrs={"autofocus": True}))
password = forms.CharField(label=_("Password"), widget=forms.PasswordInput)

def __init__(self, request, *args, **kwargs):
def __init__(self, request: HttpRequest, *args, **kwargs) -> None:
"""
If request is passed in, the form will validate that cookies are
enabled. Note that the request (a HttpRequest object) must have set a
cookie with the key TEST_COOKIE_NAME and value TEST_COOKIE_VALUE before
running this validation.
"""
self.request = request
self.user_cache = None
self.user_cache: UserProfile | None = None
super().__init__(*args, **kwargs)

@sensitive_variables("password")
def clean_password(self):
email = self.cleaned_data.get("email")
password = self.cleaned_data.get("password")
def clean_password(self) -> str | None:
Comment thread
richardebeling marked this conversation as resolved.
email: str = self.cleaned_data["email"]
password: str | None = self.cleaned_data.get("password")

email = email.lower()

Expand All @@ -44,31 +46,31 @@ def clean_password(self):
self.check_for_test_cookie()
return password

def check_for_test_cookie(self):
def check_for_test_cookie(self) -> None:
if self.request and not self.request.session.test_cookie_worked():
raise forms.ValidationError(
_("Your Web browser doesn't appear to have cookies enabled. Cookies are required for logging in.")
)

def get_user_id(self):
def get_user_id(self) -> int | None:
if self.user_cache:
return self.user_cache.id
return None

def get_user(self):
def get_user(self) -> UserProfile | None:
return self.user_cache


class NewKeyForm(forms.Form):
email = forms.EmailField(label=_("Email address"))

def __init__(self, *args, **kwargs):
self.user_cache = None
def __init__(self, *args, **kwargs) -> None:
self.user_cache: UserProfile | None = None

super().__init__(*args, **kwargs)

def clean_email(self):
email = self.cleaned_data.get("email")
def clean_email(self) -> str:
email: str = assert_not_none(self.cleaned_data.get("email"))

if not UserProfile.email_needs_login_key(email):
raise forms.ValidationError(
Expand All @@ -90,19 +92,19 @@ def clean_email(self):

return email

def get_user(self):
def get_user(self) -> UserProfile | None:
return self.user_cache


class UserModelChoiceField(forms.ModelChoiceField):
def label_from_instance(self, obj):
def label_from_instance(self, obj: UserProfile) -> str:
return obj.full_name_with_additional_info


class UserModelMultipleChoiceField(forms.ModelMultipleChoiceField):
widget = forms.SelectMultiple(attrs={"data-tomselect-fullwidth": ""})

def label_from_instance(self, obj):
def label_from_instance(self, obj: UserProfile) -> str:
return obj.full_name_with_additional_info


Expand All @@ -118,12 +120,12 @@ class Meta:
"delegates": UserModelMultipleChoiceField,
}

def __init__(self, *args, **kwargs):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
for field in ("title", "first_name_given", "last_name", "email"):
self.fields[field].disabled = True

def save(self, *args, **kwargs):
def save(self, *args, **kwargs) -> None:
super().save(*args, **kwargs)

if "first_name_chosen" in self.changed_data:
Expand All @@ -138,7 +140,7 @@ def save(self, *args, **kwargs):

logger.info('User "%s" edited the settings.', self.instance.email)

def clean_first_name_chosen(self):
def clean_first_name_chosen(self) -> str:
name = self.cleaned_data["first_name_chosen"]

for character in name:
Expand All @@ -149,7 +151,7 @@ def clean_first_name_chosen(self):


class NotebookForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.fields["notes"].widget.attrs.update({"class": "notebook-textarea"})

Expand Down
Loading