Skip to content

Commit

Permalink
Merge pull request #2455 from dotkom/feat/application-periods
Browse files Browse the repository at this point in the history
Add application periods for committee applications
  • Loading branch information
oleast authored Mar 28, 2020
2 parents 361031e + b77cb57 commit 66a4c9e
Show file tree
Hide file tree
Showing 18 changed files with 981 additions and 75 deletions.
37 changes: 23 additions & 14 deletions apps/api/permissions.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,35 @@
from oauth2_provider.contrib.rest_framework import TokenHasScope
from rest_framework.permissions import DjangoObjectPermissions
from rest_framework.permissions import DjangoModelPermissions, DjangoObjectPermissions

_perms_map_with_view_perms = {
"GET": ["%(app_label)s.view_%(model_name)s"],
"OPTIONS": [],
"HEAD": [],
"POST": ["%(app_label)s.add_%(model_name)s"],
"PUT": ["%(app_label)s.change_%(model_name)s"],
"PATCH": ["%(app_label)s.change_%(model_name)s"],
"DELETE": ["%(app_label)s.delete_%(model_name)s"],
}


class TokenHasScopeOrUserHasObjectPermissionsOrWriteOnly(
DjangoObjectPermissions, TokenHasScope
):
"""
Allow anyone to write to this endpoint, but only the ones with the required scope to read.
"""

perms_map = {
"GET": ["%(app_label)s.view_%(model_name)s"],
"OPTIONS": [],
"HEAD": [],
"POST": ["%(app_label)s.add_%(model_name)s"],
"PUT": ["%(app_label)s.change_%(model_name)s"],
"PATCH": ["%(app_label)s.change_%(model_name)s"],
"DELETE": ["%(app_label)s.delete_%(model_name)s"],
}
perms_map = _perms_map_with_view_perms

def has_permission(self, request, view):
if request.method == "POST":
return True

return super().has_permission(request, view)


class TokenHasScopeOrUserHasModelPermissionsOrWriteOnly(
DjangoModelPermissions, TokenHasScope
):
perms_map = _perms_map_with_view_perms

def has_permission(self, request, view):
if request.method == "POST":
return True

Expand Down
18 changes: 12 additions & 6 deletions apps/approval/admin.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,33 @@
# -*- coding: utf-8 -*-

from django.contrib import admin
from django.contrib.admin import register

from apps.approval.models import (
CommitteeApplication,
CommitteeApplicationPeriod,
CommitteePriority,
MembershipApproval,
)


@admin.register(CommitteeApplicationPeriod)
class CommitteeApplicationPeriodAdmin(admin.ModelAdmin):
model = CommitteeApplicationPeriod
list_display = ["title", "year", "start", "deadline"]
search_fields = ["title", "start", "deadline"]
list_filter = ["committees"]


class CommitteePriorityInline(admin.TabularInline):
model = CommitteePriority


@admin.register(CommitteeApplication)
class CommitteeApplicationAdmin(admin.ModelAdmin):
model = CommitteeApplication
inlines = [CommitteePriorityInline]
list_display = ["__str__", "get_name", "created", "prioritized"]
list_filter = ["prioritized", "committees"]
list_filter = ["application_period", "prioritized", "committees"]
search_fields = [
"name",
"email",
Expand All @@ -28,7 +37,7 @@ class CommitteeApplicationAdmin(admin.ModelAdmin):
]


@register(MembershipApproval)
@admin.register(MembershipApproval)
class MembershipApprovalAdmin(admin.ModelAdmin):
model = MembershipApproval
list_display = (
Expand All @@ -46,6 +55,3 @@ class MembershipApprovalAdmin(admin.ModelAdmin):
"applicant__username",
"applicant__ntnu_username",
)


admin.site.register(CommitteeApplication, CommitteeApplicationAdmin)
23 changes: 23 additions & 0 deletions apps/approval/api/filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import django_filters as filters

from ..models import CommitteeApplicationPeriod


class CommitteeApplicationPeriodFilter(filters.FilterSet):
accepting_applications = filters.BooleanFilter(field_name="accepting_applications")
actual_deadline__lte = filters.DateTimeFilter(
field_name="actual_deadline", lookup_expr="lte"
)
actual_deadline__gte = filters.DateTimeFilter(
field_name="actual_deadline", lookup_expr="lte"
)

class Meta:
model = CommitteeApplicationPeriod
fields = {
"start": ["gte", "lte"],
"deadline": ["gte", "lte"],
"accepting_applications": "exact",
"actual_deadline__lte": "exact",
"actual_deadline__gte": "exact",
}
120 changes: 110 additions & 10 deletions apps/approval/api/serializers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
from django.core.exceptions import ValidationError as DjangoValidationError
from django.core.exceptions import ValidationError
from django.utils import timezone
from rest_framework import serializers

from apps.approval.models import CommitteeApplication, CommitteePriority
from apps.approval.models import (
CommitteeApplication,
CommitteeApplicationPeriod,
CommitteePriority,
)
from apps.authentication.serializers import UserReadOnlySerializer


Expand All @@ -13,12 +18,61 @@ class Meta:
fields = ("group", "group_name", "priority")

def get_group_name(self, instance):
return instance.group.name
return instance.group.name_long


class CommitteeApplicationPeriodSerializer(serializers.ModelSerializer):
# Define annotated fields manually since they can't be inferred from the class
actual_deadline = serializers.DateTimeField(read_only=True)
accepting_applications = serializers.BooleanField(read_only=True)

def validate(self, attrs: dict):
# Validate on a class instance instead of on the attrs themselves.
# This lets us use instance properties and default values correctly.
# Use request data without the 'many'-related properties, as the can't be added directly in the class.
data = attrs.copy()
data.pop("committees")
period = CommitteeApplicationPeriod(**data)

minimum_duration = timezone.timedelta(days=1)
if period.start + minimum_duration >= period.deadline:
raise serializers.ValidationError(
"En opptaksperiode må vare i minst én dag"
)

actual_deadline = period.deadline + period.deadline_delta
overlapping_periods = CommitteeApplicationPeriod.objects.filter_overlapping(
period.start, actual_deadline
)

if overlapping_periods.exists():
raise serializers.ValidationError(
"Opptaksperioder kan ikke overlappe med hverandre"
)

return attrs

class Meta:
model = CommitteeApplicationPeriod
fields = (
"id",
"title",
"start",
"deadline",
"deadline_delta",
"actual_deadline",
"committees",
"accepting_applications",
"year",
)


class CommitteeApplicationSerializer(serializers.ModelSerializer):
committees = CommitteeSerializer(many=True, source="committeepriority_set")
applicant = UserReadOnlySerializer(read_only=True)
application_period = serializers.PrimaryKeyRelatedField(
required=True, queryset=CommitteeApplicationPeriod.objects.all()
)

class Meta:
model = CommitteeApplication
Expand All @@ -29,20 +83,66 @@ class Meta:
"application_text",
"prioritized",
"committees",
"application_period",
)

def _is_authenticated(self):
request = self.context.get("request")
return request.user.is_authenticated

def validate_application_period(
self, application_period: CommitteeApplicationPeriod
):
if not application_period.accepting_applications:
raise ValidationError(
f"Opptaksperioden {application_period} tar ikke lenger imot søknader. "
f"Opptaket stengte {application_period.deadline}"
)
return application_period

def validate_committees(self, committees):
committees.sort(key=lambda c: c.get("priority"))
for i in range(len(committees)):
committee = committees[i]
if i + 1 != committee.get("priority"):
raise serializers.ValidationError(
"Prioriteringer er feilformatert. "
"Prioriteringer må være en rekke med tall mellom 1 og 3. "
"Alle gruper som sendes på ha forkjsellig prioritering."
)

return committees

def validate(self, attrs: dict):
email = attrs.get("email")
name = attrs.get("name")

if not self._is_authenticated() and not (email and name):
raise ValidationError(
"Enten en brukerkonto (søker) eller navn og e-postadresse er påkrevd."
)

committees_priorities = attrs.get("committeepriority_set")
application_period = attrs.get("application_period")

committees = [c.get("group") for c in committees_priorities]
allowed_committees = application_period.committees.all()

for committee in committees:
if committee not in allowed_committees:
raise serializers.ValidationError(
f"En av de valgte komiteene er ikke del av dette opptaket, {allowed_committees}, {committees}"
)

return super().validate(attrs)

def create(self, validated_data):
committees = validated_data.pop("committeepriority_set")
application = CommitteeApplication(**validated_data)
try:
application.clean()
except DjangoValidationError as django_error:
raise serializers.ValidationError(django_error.message)
application.save()
application = super().create(validated_data)

for committee in committees:
CommitteePriority.objects.create(
committee_application=application, **committee
)

return CommitteeApplication.objects.get(pk=application.pk)
return application
7 changes: 6 additions & 1 deletion apps/approval/api/urls.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from apps.api.utils import SharedAPIRootRouter

from .views import CommitteeApplicationViewSet
from .views import CommitteeApplicationPeriodViewSet, CommitteeApplicationViewSet

urlpatterns = []

Expand All @@ -11,3 +11,8 @@
CommitteeApplicationViewSet,
basename="committeeapplications",
)
router.register(
"committee-application-periods",
CommitteeApplicationPeriodViewSet,
basename="committee-application-periods",
)
25 changes: 17 additions & 8 deletions apps/approval/api/views.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
from oauth2_provider.contrib.rest_framework import OAuth2Authentication
from rest_framework.authentication import SessionAuthentication
from rest_framework.permissions import DjangoModelPermissionsOrAnonReadOnly
from rest_framework.viewsets import ModelViewSet

from apps.api.permissions import TokenHasScopeOrUserHasObjectPermissionsOrWriteOnly
from apps.approval.api.serializers import CommitteeApplicationSerializer
from apps.approval.models import CommitteeApplication
from apps.api.permissions import TokenHasScopeOrUserHasModelPermissionsOrWriteOnly
from apps.approval.models import CommitteeApplication, CommitteeApplicationPeriod

from .filters import CommitteeApplicationPeriodFilter
from .serializers import (
CommitteeApplicationPeriodSerializer,
CommitteeApplicationSerializer,
)


class CommitteeApplicationPeriodViewSet(ModelViewSet):
serializer_class = CommitteeApplicationPeriodSerializer
queryset = CommitteeApplicationPeriod.objects.all()
filter_class = CommitteeApplicationPeriodFilter
permission_classes = [DjangoModelPermissionsOrAnonReadOnly]


class CommitteeApplicationViewSet(ModelViewSet):
Expand All @@ -15,9 +26,7 @@ class CommitteeApplicationViewSet(ModelViewSet):

serializer_class = CommitteeApplicationSerializer
queryset = CommitteeApplication.objects.all()
authentication_classes = [OAuth2Authentication, SessionAuthentication]
permission_classes = [TokenHasScopeOrUserHasObjectPermissionsOrWriteOnly]
required_scopes = ["approval"]
permission_classes = [TokenHasScopeOrUserHasModelPermissionsOrWriteOnly]

def perform_create(self, serializer):
if self.request.user and self.request.user.is_authenticated:
Expand Down
38 changes: 38 additions & 0 deletions apps/approval/dashboard/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from django import forms
from django.core.exceptions import ValidationError
from django.utils import timezone

from apps.dashboard.widgets import DatetimePickerInput, multiple_widget_generator

from ..models import CommitteeApplicationPeriod


class CommitteeApplicationPeriodForm(forms.ModelForm):
def clean(self):
cleaned_data = super().clean()
data = cleaned_data.copy()
data.pop("committees")
period = CommitteeApplicationPeriod(**data)

minimum_duration = timezone.timedelta(days=1)
if period.start + minimum_duration >= period.deadline:
raise ValidationError("En opptaksperiode må vare i minst én dag")

actual_deadline = period.deadline + period.deadline_delta
overlapping_periods = CommitteeApplicationPeriod.objects.filter_overlapping(
period.start, actual_deadline
)

if overlapping_periods.exists():
raise ValidationError("Opptaksperioder kan ikke overlappe med hverandre")

return cleaned_data

class Meta:
model = CommitteeApplicationPeriod
fields = ("title", "start", "deadline", "deadline_delta", "committees")

dtp_fields = (("start", {}), ("deadline", {}))
widgetlist = [(DatetimePickerInput, dtp_fields)]

widgets = multiple_widget_generator(widgetlist)
Loading

0 comments on commit 66a4c9e

Please sign in to comment.