diff --git a/apps/api/__init__.py b/apps/api/__init__.py new file mode 100644 index 0000000..1b17aec --- /dev/null +++ b/apps/api/__init__.py @@ -0,0 +1,4 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +default_app_config = 'api.apps.APIConfig' diff --git a/apps/api/apps.py b/apps/api/apps.py new file mode 100644 index 0000000..11d7865 --- /dev/null +++ b/apps/api/apps.py @@ -0,0 +1,10 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from django.apps import AppConfig +from django.utils.translation import gettext_lazy as _ + + +class APIConfig(AppConfig): + name = 'api' + verbose_name = _('API') diff --git a/apps/api/urls.py b/apps/api/urls.py new file mode 100644 index 0000000..d08ac33 --- /dev/null +++ b/apps/api/urls.py @@ -0,0 +1,134 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from django.conf.urls import url, include +from django.contrib.auth.models import User +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework import routers, serializers +from rest_framework.filters import SearchFilter +from rest_framework.viewsets import ModelViewSet + +from member.models import TFJMUser, Authorization, Solution, Synthesis, MotivationLetter +from tournament.models import Team, Tournament + + +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = TFJMUser + exclude = ( + 'email', + 'password', + 'groups', + 'user_permissions', + ) + + +class TeamSerializer(serializers.ModelSerializer): + class Meta: + model = Team + fields = "__all__" + + +class TournamentSerializer(serializers.ModelSerializer): + class Meta: + model = Tournament + fields = "__all__" + + +class AuthorizationSerializer(serializers.ModelSerializer): + class Meta: + model = Authorization + fields = "__all__" + + +class MotivationLetterSerializer(serializers.ModelSerializer): + class Meta: + model = MotivationLetter + fields = "__all__" + + +class SolutionSerializer(serializers.ModelSerializer): + class Meta: + model = Solution + fields = "__all__" + + +class SynthesisSerializer(serializers.ModelSerializer): + class Meta: + model = Synthesis + fields = "__all__" + + +class UserViewSet(ModelViewSet): + queryset = TFJMUser.objects.all() + serializer_class = UserSerializer + filter_backends = [DjangoFilterBackend, SearchFilter] + filterset_fields = ['id', 'first_name', 'last_name', 'email', 'gender', 'student_class', 'role', 'year', 'team', + 'team__trigram', 'is_superuser', 'is_staff', 'is_active', ] + search_fields = ['$first_name', '$last_name', ] + + +class TeamViewSet(ModelViewSet): + queryset = Team.objects.all() + serializer_class = TeamSerializer + filter_backends = [DjangoFilterBackend, SearchFilter] + filterset_fields = ['name', 'trigram', 'validation_status', 'selected_for_final', 'access_code', 'tournament', + 'year', ] + search_fields = ['$name', 'trigram', ] + + +class TournamentViewSet(ModelViewSet): + queryset = Tournament.objects.all() + serializer_class = TournamentSerializer + filter_backends = [DjangoFilterBackend, SearchFilter] + filterset_fields = ['name', 'size', 'price', 'date_start', 'date_end', 'final', 'organizers', 'year', ] + search_fields = ['$name', ] + + +class AuthorizationViewSet(ModelViewSet): + queryset = Authorization.objects.all() + serializer_class = AuthorizationSerializer + filter_backends = [DjangoFilterBackend] + filterset_fields = ['user', 'type', ] + + +class MotivationLetterViewSet(ModelViewSet): + queryset = MotivationLetter.objects.all() + serializer_class = MotivationLetterSerializer + filter_backends = [DjangoFilterBackend] + filterset_fields = ['team', 'team__trigram', ] + + +class SolutionViewSet(ModelViewSet): + queryset = Solution.objects.all() + serializer_class = SolutionSerializer + filter_backends = [DjangoFilterBackend] + filterset_fields = ['team', 'team__trigram', 'problem', ] + + +class SynthesisViewSet(ModelViewSet): + queryset = Synthesis.objects.all() + serializer_class = SynthesisSerializer + filter_backends = [DjangoFilterBackend] + filterset_fields = ['team', 'team__trigram', 'dest', 'round', ] + + +# Routers provide an easy way of automatically determining the URL conf. +# Register each app API router and user viewset +router = routers.DefaultRouter() +router.register('user', UserViewSet) +router.register('team', TeamViewSet) +router.register('tournament', TournamentViewSet) +router.register('authorization', AuthorizationViewSet) +router.register('motivation_letter', MotivationLetterViewSet) +router.register('solution', SolutionViewSet) +router.register('synthesis', SynthesisViewSet) + +app_name = 'api' + +# Wire up our API using automatic URL routing. +# Additionally, we include login URLs for the browsable API. +urlpatterns = [ + url('^', include(router.urls)), + url('^api-auth/', include('rest_framework.urls', namespace='rest_framework')), +] diff --git a/apps/member/urls.py b/apps/member/urls.py index ca881bd..5fcadcd 100644 --- a/apps/member/urls.py +++ b/apps/member/urls.py @@ -7,7 +7,6 @@ app_name = "member" urlpatterns = [ path('signup/', CreateUserView.as_view(), name="signup"), - path("file//", DocumentView.as_view(), name="document"), path("my-account/", RedirectView.as_view(pattern_name="index"), name="my_account"), path("add-team/", RedirectView.as_view(pattern_name="index"), name="add_team"), path("join-team/", RedirectView.as_view(pattern_name="index"), name="join_team"), diff --git a/templates/django_filters/rest_framework/crispy_form.html b/templates/django_filters/rest_framework/crispy_form.html new file mode 100644 index 0000000..171767c --- /dev/null +++ b/templates/django_filters/rest_framework/crispy_form.html @@ -0,0 +1,5 @@ +{% load crispy_forms_tags %} +{% load i18n %} + +

{% trans "Field filters" %}

+{% crispy filter.form %} diff --git a/templates/django_filters/rest_framework/form.html b/templates/django_filters/rest_framework/form.html new file mode 100644 index 0000000..b116e35 --- /dev/null +++ b/templates/django_filters/rest_framework/form.html @@ -0,0 +1,6 @@ +{% load i18n %} +

{% trans "Field filters" %}

+
+ {{ filter.form.as_p }} + +
diff --git a/templates/django_filters/widgets/multiwidget.html b/templates/django_filters/widgets/multiwidget.html new file mode 100644 index 0000000..089ddb2 --- /dev/null +++ b/templates/django_filters/widgets/multiwidget.html @@ -0,0 +1 @@ +{% for widget in widget.subwidgets %}{% include widget.template_name %}{% if forloop.first %}-{% endif %}{% endfor %} diff --git a/tfjm/middlewares.py b/tfjm/middlewares.py new file mode 100644 index 0000000..fff824c --- /dev/null +++ b/tfjm/middlewares.py @@ -0,0 +1,93 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from django.conf import settings +from django.contrib.auth.models import AnonymousUser, User + +from threading import local + +from django.contrib.sessions.backends.db import SessionStore + +USER_ATTR_NAME = getattr(settings, 'LOCAL_USER_ATTR_NAME', '_current_user') +SESSION_ATTR_NAME = getattr(settings, 'LOCAL_SESSION_ATTR_NAME', '_current_session') +IP_ATTR_NAME = getattr(settings, 'LOCAL_IP_ATTR_NAME', '_current_ip') + +_thread_locals = local() + + +def _set_current_user_and_ip(user=None, session=None, ip=None): + setattr(_thread_locals, USER_ATTR_NAME, user) + setattr(_thread_locals, SESSION_ATTR_NAME, session) + setattr(_thread_locals, IP_ATTR_NAME, ip) + + +def get_current_user() -> User: + return getattr(_thread_locals, USER_ATTR_NAME, None) + + +def get_current_session() -> SessionStore: + return getattr(_thread_locals, SESSION_ATTR_NAME, None) + + +def get_current_ip() -> str: + return getattr(_thread_locals, IP_ATTR_NAME, None) + + +def get_current_authenticated_user(): + current_user = get_current_user() + if isinstance(current_user, AnonymousUser): + return None + return current_user + + +class SessionMiddleware(object): + """ + This middleware get the current user with his or her IP address on each request. + """ + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + user = request.user + if 'HTTP_X_FORWARDED_FOR' in request.META: + ip = request.META.get('HTTP_X_FORWARDED_FOR') + else: + ip = request.META.get('REMOTE_ADDR') + + _set_current_user_and_ip(user, request.session, ip) + response = self.get_response(request) + _set_current_user_and_ip(None, None, None) + + return response + + +class TurbolinksMiddleware(object): + """ + Send the `Turbolinks-Location` header in response to a visit that was redirected, + and Turbolinks will replace the browser's topmost history entry. + """ + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + response = self.get_response(request) + + is_turbolinks = request.META.get('HTTP_TURBOLINKS_REFERRER') + is_response_redirect = response.has_header('Location') + + if is_turbolinks: + if is_response_redirect: + location = response['Location'] + prev_location = request.session.pop('_turbolinks_redirect_to', None) + if prev_location is not None: + # relative subsequent redirect + if location.startswith('.'): + location = prev_location.split('?')[0] + location + request.session['_turbolinks_redirect_to'] = location + else: + if request.session.get('_turbolinks_redirect_to'): + location = request.session.pop('_turbolinks_redirect_to') + response['Turbolinks-Location'] = location + return response diff --git a/tfjm/settings.py b/tfjm/settings.py index faa2437..7b855e9 100644 --- a/tfjm/settings.py +++ b/tfjm/settings.py @@ -65,6 +65,7 @@ MIDDLEWARE = [ 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.locale.LocaleMiddleware', 'django.contrib.sites.middleware.CurrentSiteMiddleware', + 'tfjm.middlewares.TurbolinksMiddleware', ] ROOT_URLCONF = 'tfjm.urls' @@ -127,6 +128,17 @@ PASSWORD_HASHERS = [ 'django.contrib.auth.hashers.BCryptPasswordHasher', ] +REST_FRAMEWORK = { + 'DEFAULT_PERMISSION_CLASSES': [ + 'rest_framework.permissions.IsAdminUser' + ], + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'rest_framework.authentication.SessionAuthentication', + 'rest_framework.authentication.TokenAuthentication', + ], + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', + 'PAGE_SIZE': 50, +} # Internationalization # https://docs.djangoproject.com/en/3.0/topics/i18n/ diff --git a/tfjm/urls.py b/tfjm/urls.py index b86d5b2..7e6622a 100644 --- a/tfjm/urls.py +++ b/tfjm/urls.py @@ -19,6 +19,8 @@ from django.contrib import admin from django.urls import path, include from django.views.generic import TemplateView +from member.views import DocumentView + urlpatterns = [ path('', TemplateView.as_view(template_name="index.html"), name="index"), path('i18n/', include('django.conf.urls.i18n')), @@ -28,7 +30,11 @@ urlpatterns = [ path('member/', include('member.urls')), path('tournament/', include('tournament.urls')), + + path("media//", DocumentView.as_view(), name="document"), + + path('api/', include('api.urls')), ] -urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) +# urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)