diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/customPermissions.py b/src/customPermissions.py new file mode 100644 index 00000000..4ef8a001 --- /dev/null +++ b/src/customPermissions.py @@ -0,0 +1,7 @@ +from rest_framework.authentication import SessionAuthentication + +class CsrfExemptSessionAuthentication(SessionAuthentication): + + def enforce_csrf(self, request): + return + diff --git a/src/events/customPermissions.py b/src/events/customPermissions.py new file mode 100644 index 00000000..29cf3f62 --- /dev/null +++ b/src/events/customPermissions.py @@ -0,0 +1,24 @@ +from rest_framework.permissions import BasePermission +from rest_framework.authentication import SessionAuthentication +from datetime import datetime +class CsrfExemptSessionAuthentication(SessionAuthentication): + + def enforce_csrf(self, request): + return + +class OwnApplicationPermission(BasePermission): + """ + Object-level permission to only allow updating his own profile + """ + def has_object_permission(self, request, view, obj): + if obj.event.end_of_application < datetime.now(): + return False + return obj.event_applicant == request.user + +class ParticipantAllowancePermission(BasePermission): + + def has_object_permission(self, request, view, obj): + if request.method not in ["GET", "HEAD", "OPTIONS"]: + return not obj.ticket.locked + else: + return True diff --git a/src/events/serializers/serializers.py b/src/events/serializers/serializers.py new file mode 100644 index 00000000..450585b7 --- /dev/null +++ b/src/events/serializers/serializers.py @@ -0,0 +1,33 @@ +from events.models.costs import Costs +from events.models.event import Event +from events.models.participant import Participant +from events.models.application import EventApplication +from events.models.ticket import Ticket +from rest_framework import serializers + +class CostsSerializer(serializers.ModelSerializer): + class Meta: + model = Costs + fields = '__all__' + +class EventSerializer(serializers.ModelSerializer): + class Meta: + model = Event + fields = '__all__' + depth = 1 + +class ParticipantSerializer(serializers.ModelSerializer): + class Meta: + model = Participant + fields = '__all__' + +class EventApplicationSerializer(serializers.ModelSerializer): + class Meta: + model = EventApplication + fields = '__all__' + depth = 1 + +class TicketSerializer(serializers.ModelSerializer): + class Meta: + model = Ticket + fields = '__all__' diff --git a/src/events/urls.py b/src/events/urls.py index b730b2c6..be806b41 100644 --- a/src/events/urls.py +++ b/src/events/urls.py @@ -1,6 +1,15 @@ from django.urls import path, re_path +from rest_framework import routers from events import views +router = routers.SimpleRouter() +router.register(r'^api/costs', views.api.CostsViewSet, basename="CostsView") +router.register(r'^api/event', views.api.EventViewSet, basename="EventView") +router.register(r'^api/eventapplication', views.api.EventApplicationViewSet, basename="EventApplicationView") +router.register(r'^api/ticket', views.api.TicketViewSet, basename="TicketView") +router.register(r'^api/participant', views.api.ParticipantViewSet, basename="ParticipantView") + + urlpatterns = [ path('event/', views.EventView.as_view(), @@ -31,3 +40,5 @@ name='events_event_modeladmin_export_participants' ), ] + +urlpatterns += router.urls diff --git a/src/events/views/__init__.py b/src/events/views/__init__.py index 0bbf7678..d8b99b36 100644 --- a/src/events/views/__init__.py +++ b/src/events/views/__init__.py @@ -5,3 +5,4 @@ from .admin_unassign_unpaid import * from .admin_remove_applications import * from .admin_export_participants import * +from .api import * diff --git a/src/events/views/api.py b/src/events/views/api.py new file mode 100644 index 00000000..2d71e887 --- /dev/null +++ b/src/events/views/api.py @@ -0,0 +1,58 @@ +from rest_framework import viewsets, authentication, mixins +from events.serializers.serializers import * +from rest_framework.permissions import IsAuthenticatedOrReadOnly, AllowAny, IsAuthenticated +from events.models.costs import Costs +from events.models.event import Event +from events.models.participant import Participant +from events.models.application import EventApplication +from events.models.ticket import Ticket +from events.customPermissions import OwnApplicationPermission, CsrfExemptSessionAuthentication, \ + ParticipantAllowancePermission + +class CostsViewSet(viewsets.ReadOnlyModelViewSet): + serializer_class = CostsSerializer + authentication_classes = (CsrfExemptSessionAuthentication, authentication.BasicAuthentication) + permission_classes = [AllowAny] + queryset = Costs.objects.all() + +class EventViewSet(viewsets.ReadOnlyModelViewSet): + serializer_class = EventSerializer + authentication_classes = (CsrfExemptSessionAuthentication, authentication.BasicAuthentication) + permission_classes = [IsAuthenticatedOrReadOnly] + queryset = Event.objects.all() + +class ParticipantViewSet(viewsets.ModelViewSet): + serializer_class = ParticipantSerializer + authentication_classes = (CsrfExemptSessionAuthentication, authentication.BasicAuthentication) + permission_classes = [IsAuthenticatedOrReadOnly, ParticipantAllowancePermission] + + def get_queryset(self): + user = self.request.user + tickets = list(Ticket.objects.filter.values_list(owner=user)) + queryset = Participant.objects.filter(ticket__in=tickets) + return queryset + +class EventApplicationViewSet(viewsets.GenericViewSet, mixins.ListModelMixin, mixins.CreateModelMixin, mixins.RetrieveModelMixin, \ + mixins.DestroyModelMixin): + serializer_class = EventApplicationSerializer + authentication_classes = (CsrfExemptSessionAuthentication, authentication.BasicAuthentication) + permission_classes = [IsAuthenticated, OwnApplicationPermission] + + def get_queryset(self): + user = self.request.user + if self.action in ['retrieve', 'list']: + queryset = EventApplication.objects.filter(event_applicant=user) + else: + queryset = EventApplication.objects.all() + return queryset + +class TicketViewSet(viewsets.ReadOnlyModelViewSet): + serializer_class = TicketSerializer + authentication_classes = (CsrfExemptSessionAuthentication, authentication.BasicAuthentication) + permission_classes = [IsAuthenticated] + + def get_queryset(self): + user = self.request.user + queryset = Ticket.objects.filter(owner=user) + return queryset + diff --git a/src/involvement/customPermissions.py b/src/involvement/customPermissions.py new file mode 100644 index 00000000..6504233f --- /dev/null +++ b/src/involvement/customPermissions.py @@ -0,0 +1,42 @@ +from rest_framework.permissions import BasePermission +from rest_framework.authentication import SessionAuthentication +from datetime import date +class CsrfExemptSessionAuthentication(SessionAuthentication): + + def enforce_csrf(self, request): + return +class ReadAndCreate(BasePermission): + """ + Authenticated user can create but not delete or update. + """ + def has_permission(self, request, view): + return True if request.method in ["GET", "HEAD", "OPTIONS", "POST"] else False + +class ReadCreateUpdate(BasePermission): + """ + Authenticated user can create and update but not delete. + """ + def has_permission(self, request, view): + return True if request.method not in ["DELETE"] else False + +class OwnApplicationPermission(BasePermission): + """ + Object-level permission to only allow updating his own profile + """ + def has_object_permission(self, request, view, obj): + return obj.applicant == request.user + +class DeleteApplicationPermission(BasePermission): + def has_object_permission(self, request, view, obj): + if obj.status in ["Draft", "Submitted"]: + return True + return False + + +class EditApplicationPermission(BasePermission): + def has_object_permission(self, request, view, obj): + if obj.status in ["Draft"]: + if obj.position.recruitment_end > date.today(): + return True + return False + return False diff --git a/src/involvement/serializers/application_serializer.py b/src/involvement/serializers/application_serializer.py new file mode 100644 index 00000000..916186aa --- /dev/null +++ b/src/involvement/serializers/application_serializer.py @@ -0,0 +1,15 @@ +from involvement.models.application import Application +from rest_framework import serializers + +#Role serializer +class ApplicationSerializer(serializers.ModelSerializer): + class Meta: + model = Application + fields = '__all__' + depth = 1 + +class ApplicationEditSerializer(serializers.ModelSerializer): + class Meta: + model = Application + exclude = ["status"] + depth = 1 diff --git a/src/involvement/serializers/position_serializer.py b/src/involvement/serializers/position_serializer.py new file mode 100644 index 00000000..bc5d9d92 --- /dev/null +++ b/src/involvement/serializers/position_serializer.py @@ -0,0 +1,13 @@ +from involvement.models.position import Position +from rest_framework import serializers + +class PositionSerializer(serializers.ModelSerializer): + class Meta: + model = Position + fields = '__all__' + +class PositionDepthSerializer(serializers.ModelSerializer): + class Meta: + model = Position + fields = '__all__' + depth = 1 diff --git a/src/involvement/serializers/role_serializer.py b/src/involvement/serializers/role_serializer.py new file mode 100644 index 00000000..1c4b3676 --- /dev/null +++ b/src/involvement/serializers/role_serializer.py @@ -0,0 +1,8 @@ +from involvement.models.role import Role +from rest_framework import serializers + +#Role serializer +class RoleSerializer(serializers.ModelSerializer): + class Meta: + model = Role + fields = '__all__' diff --git a/src/involvement/serializers/team_serializer.py b/src/involvement/serializers/team_serializer.py new file mode 100644 index 00000000..f92541a7 --- /dev/null +++ b/src/involvement/serializers/team_serializer.py @@ -0,0 +1,9 @@ +from involvement.models.team import Team +from rest_framework import serializers + +#Serializer for team +class TeamSerializer(serializers.ModelSerializer): + class Meta: + model = Team + fields = '__all__' + diff --git a/src/involvement/urls.py b/src/involvement/urls.py index 7de575a5..2f64db08 100644 --- a/src/involvement/urls.py +++ b/src/involvement/urls.py @@ -1,7 +1,17 @@ from django.conf.urls import re_path - +from rest_framework import routers from involvement import views +#API URLs +router = routers.SimpleRouter() +router.register(r'^position', views.position_api.PositionViewSet, basename="PositionView") +router.register(r'^position2', views.position_api.Position2ViewSet, basename="Position2View") +router.register(r'^teams', views.team_read_api.TeamViewSet, basename="TeamsView") +router.register(r'^roles', views.role_read_api.RoleViewSet, basename="RolesView") +router.register(r'^application', views.application_api.ApplicationViewSet, basename="ApplicationView") +router.register(r'^application-update', views.application_api.ApplicationEditViewSet, basename="ApplicationEditView") +router.register(r'^application-delete', views.application_api.ApplicationDeleteViewSet, basename="ApplicationDeleteView") + urlpatterns = [ re_path( r'^admin/involvement/position/elect/(\d+)/$', @@ -19,3 +29,5 @@ name='involvement_position_extend' ), ] + +urlpatterns += router.urls diff --git a/src/involvement/views/__init__.py b/src/involvement/views/__init__.py index 6614ab97..bce2a547 100644 --- a/src/involvement/views/__init__.py +++ b/src/involvement/views/__init__.py @@ -9,9 +9,13 @@ from .position_create_view import PositionCreateView from .position_edit_view import PositionEditView from .position_inspect_view import PositionInspectView +from .position_api import PositionViewSet from .role_create_view import RoleCreateView from .role_edit_view import RoleEditView from .application_create_view import ApplicationCreateView from .application_edit_view import ApplicationEditView from .application_inspect_view import ApplicationInspectView from .role_inspect_view import RoleInspectView +from .team_read_api import TeamViewSet +from .role_read_api import RoleViewSet +from .application_api import ApplicationViewSet diff --git a/src/involvement/views/application_api.py b/src/involvement/views/application_api.py new file mode 100644 index 00000000..82a02991 --- /dev/null +++ b/src/involvement/views/application_api.py @@ -0,0 +1,39 @@ +from rest_framework import viewsets, mixins, authentication +from involvement.serializers.application_serializer import ApplicationSerializer, ApplicationEditSerializer +from rest_framework.permissions import IsAuthenticated +from involvement.models.application import Application +from involvement.customPermissions import OwnApplicationPermission, DeleteApplicationPermission, \ + EditApplicationPermission +from involvement.customPermissions import CsrfExemptSessionAuthentication + +#Role view +class ApplicationViewSet(viewsets.GenericViewSet, mixins.CreateModelMixin, mixins.ListModelMixin, \ + mixins.RetrieveModelMixin): + serializer_class = ApplicationSerializer + authentication_classes = (CsrfExemptSessionAuthentication, authentication.BasicAuthentication) + permission_classes = [IsAuthenticated, OwnApplicationPermission] + + def get_queryset(self): + user = self.request.user + queryset = Application.objects.filter(applicant=user) + return queryset + +class ApplicationDeleteViewSet(viewsets.GenericViewSet, mixins.DestroyModelMixin): + serializer_class = ApplicationSerializer + authentication_classes = (CsrfExemptSessionAuthentication, authentication.BasicAuthentication) + permission_classes = [IsAuthenticated, OwnApplicationPermission, DeleteApplicationPermission] + + def get_queryset(self): + user = self.request.user + queryset = Application.objects.filter(applicant=user) + return queryset + +class ApplicationEditViewSet(viewsets.GenericViewSet, mixins.UpdateModelMixin): + serializer_class = ApplicationEditSerializer + authentication_classes = (CsrfExemptSessionAuthentication, authentication.BasicAuthentication) + permission_classes = [IsAuthenticated, OwnApplicationPermission, EditApplicationPermission] + + def get_queryset(self): + user = self.request.user + queryset = Application.objects.filter(applicant=user) + return queryset diff --git a/src/involvement/views/position_api.py b/src/involvement/views/position_api.py new file mode 100644 index 00000000..7f27122b --- /dev/null +++ b/src/involvement/views/position_api.py @@ -0,0 +1,17 @@ +from rest_framework import viewsets, authentication, mixins +from involvement.serializers.position_serializer import PositionSerializer, PositionDepthSerializer +from rest_framework.permissions import IsAuthenticatedOrReadOnly, IsAuthenticated +from involvement.models.position import Position +from involvement.customPermissions import CsrfExemptSessionAuthentication + +class PositionViewSet(viewsets.ReadOnlyModelViewSet): + serializer_class = PositionSerializer + authentication_classes = (CsrfExemptSessionAuthentication, authentication.BasicAuthentication) + permission_classes = [IsAuthenticatedOrReadOnly] + queryset = Position.objects.all() + +class Position2ViewSet(viewsets.ReadOnlyModelViewSet): + serializer_class = PositionDepthSerializer + authentication_classes = (CsrfExemptSessionAuthentication, authentication.BasicAuthentication) + permission_classes = [IsAuthenticatedOrReadOnly] + queryset = Position.objects.all() diff --git a/src/involvement/views/role_read_api.py b/src/involvement/views/role_read_api.py new file mode 100644 index 00000000..7add581d --- /dev/null +++ b/src/involvement/views/role_read_api.py @@ -0,0 +1,11 @@ +from rest_framework import viewsets, authentication +from involvement.serializers.role_serializer import RoleSerializer +from rest_framework.permissions import AllowAny +from involvement.models.role import Role +from involvement.customPermissions import CsrfExemptSessionAuthentication +#Role view +class RoleViewSet(viewsets.ReadOnlyModelViewSet): + serializer_class = RoleSerializer + authentication_classes = (CsrfExemptSessionAuthentication, authentication.BasicAuthentication) + permission_classes = [AllowAny] + queryset = Role.objects.all() diff --git a/src/involvement/views/team_read_api.py b/src/involvement/views/team_read_api.py new file mode 100644 index 00000000..4ac1ab1f --- /dev/null +++ b/src/involvement/views/team_read_api.py @@ -0,0 +1,12 @@ +from rest_framework import viewsets, authentication +from involvement.serializers.team_serializer import TeamSerializer +from rest_framework.permissions import AllowAny +from involvement.models.team import Team +from involvement.customPermissions import CsrfExemptSessionAuthentication +#Read Teams API +class TeamViewSet(viewsets.ReadOnlyModelViewSet): + serializer_class = TeamSerializer + authentication_classes = (CsrfExemptSessionAuthentication, authentication.BasicAuthentication) + permission_classes = [AllowAny] + queryset = Team.objects.all() + diff --git a/src/members/customPermissions.py b/src/members/customPermissions.py new file mode 100644 index 00000000..eb9892a4 --- /dev/null +++ b/src/members/customPermissions.py @@ -0,0 +1,5 @@ +from rest_framework.authentication import SessionAuthentication +class CsrfExemptSessionAuthentication(SessionAuthentication): + + def enforce_csrf(self, request): + return diff --git a/src/members/serializers.py b/src/members/serializers.py index 4a05fb44..19b514a7 100644 --- a/src/members/serializers.py +++ b/src/members/serializers.py @@ -1,6 +1,12 @@ from rest_framework import serializers from utils.validators import SSNValidator +from members.models.member import Member +class MemberSerializer(serializers.ModelSerializer): + class Meta: + model = Member + fields = '__all__' + depth = 1 class MemberCheckSerializer(serializers.Serializer): ssn = serializers.CharField() diff --git a/src/members/urls.py b/src/members/urls.py index 3f266a08..2ce7f98d 100644 --- a/src/members/urls.py +++ b/src/members/urls.py @@ -1,10 +1,14 @@ from django.conf.urls import re_path, include from django.urls import reverse_lazy from django.views.generic import CreateView +from rest_framework import routers from members import views from members.forms import RegistrationForm +router = routers.SimpleRouter() +router.register(r'^me', views.MemberViewSet, basename="MemberViewSet") + urlpatterns = [ re_path(r'^profile/$', views.ProfileView.as_view(), name='profile'), re_path( @@ -25,3 +29,5 @@ views.CustomPasswordResetView.as_view(), name='password_reset_custom'), ] + +urlpatterns += router.urls diff --git a/src/members/views.py b/src/members/views.py index 49a6471c..913eb01f 100644 --- a/src/members/views.py +++ b/src/members/views.py @@ -11,13 +11,15 @@ from django.views.decorators.csrf import csrf_protect from django.utils.decorators import method_decorator from members.forms import MemberForm, CustomPasswordResetForm -from members.models import Section, StudyProgram +from members.models import Section, StudyProgram, Member from rest_framework.decorators import api_view from rest_framework.response import Response from members.serializers import MemberCheckSerializer from utils.melos_client import MelosClient -from rest_framework import status - +from rest_framework import status, viewsets, authentication +from members.serializers import MemberSerializer +from rest_framework.permissions import IsAuthenticated +from members.customPermissions import CsrfExemptSessionAuthentication class ProfileView(LoginRequiredMixin, UpdateView): template_name = 'members/profile.html' @@ -134,3 +136,13 @@ def member_check_api(request): status=status_code, headers={"Access-Control-Allow-Origin": "*"} ) + +class MemberViewSet(viewsets.ReadOnlyModelViewSet): + serializer_class = MemberSerializer + authentication_classes = (CsrfExemptSessionAuthentication, authentication.BasicAuthentication) + permission_classes = [IsAuthenticated] + + def get_queryset(self): + user = self.request.user.id + queryset = Member.objects.filter(id=user) + return queryset diff --git a/src/moore/settings/dev.py b/src/moore/settings/dev.py index d6e2ea99..81c29ed4 100644 --- a/src/moore/settings/dev.py +++ b/src/moore/settings/dev.py @@ -21,31 +21,31 @@ # https://docs.djangoproject.com/en/1.10/ref/settings/#databases -if IS_RUNNING_TEST: - DATABASES = { +# if IS_RUNNING_TEST: +DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), } } - # Override search backend to not use postgres - WAGTAILSEARCH_BACKENDS = { - 'default': { - 'BACKEND': 'wagtail.search.backends.database', - } - } - -elif 'DOCKER' in os.environ: - DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': 'moore', - 'USER': 'moore', - 'HOST': 'moore-db', - 'PASSWORD': 'moore', - 'PORT': 5432, - } - } + # # Override search backend to not use postgres + # WAGTAILSEARCH_BACKENDS = { + # 'default': { + # 'BACKEND': 'wagtail.search.backends.database', + # } + # } + +# elif 'DOCKER' in os.environ: +# DATABASES = { +# 'default': { +# 'ENGINE': 'django.db.backends.postgresql', +# 'NAME': 'moore', +# 'USER': 'moore', +# 'HOST': 'moore-db', +# 'PASSWORD': 'moore', +# 'PORT': 5432, +# } +# } # Base URL to use when referring to full URLs within the Wagtail admin # backend - e.g. in notification emails. Don't include '/admin' or a