From 6f758c3f545d82bf0f8696ba69dc5f120bd97f54 Mon Sep 17 00:00:00 2001 From: florimondmanca <florimond.manca@gmail.com> Date: Thu, 8 Feb 2018 17:18:14 +0100 Subject: [PATCH] fix clean_media command, add REST API, start Angular front example --- requirements.txt | 4 ++ uploadexample/api/__init__.py | 0 uploadexample/api/apps.py | 5 ++ uploadexample/{upload => api}/serializers.py | 8 ++- uploadexample/api/urls.py | 13 ++++ uploadexample/{upload => api}/validators.py | 0 uploadexample/api/views.py | 13 ++++ uploadexample/upload/admin.py | 3 - .../upload/management/commands/clean_media.py | 2 +- uploadexample/upload/models.py | 3 + .../upload/static/css/registration.css | 21 +++++- .../upload/templates/upload/base.html | 16 ++--- .../templates/upload/registration_list.html | 30 -------- .../studentregistration_confirm_delete.html | 10 +++ .../upload/studentregistration_form.html | 13 ++++ .../upload/studentregistration_list.html | 27 ++++++++ uploadexample/upload/tests.py | 3 - uploadexample/upload/urls.py | 2 +- uploadexample/upload/views.py | 68 ++++++++++--------- uploadexample/uploadexample/settings.py | 6 ++ uploadexample/uploadexample/urls.py | 1 + uploadfront | 1 + 22 files changed, 166 insertions(+), 83 deletions(-) create mode 100644 requirements.txt create mode 100644 uploadexample/api/__init__.py create mode 100644 uploadexample/api/apps.py rename uploadexample/{upload => api}/serializers.py (62%) create mode 100644 uploadexample/api/urls.py rename uploadexample/{upload => api}/validators.py (100%) create mode 100644 uploadexample/api/views.py delete mode 100644 uploadexample/upload/admin.py delete mode 100644 uploadexample/upload/templates/upload/registration_list.html create mode 100644 uploadexample/upload/templates/upload/studentregistration_confirm_delete.html create mode 100644 uploadexample/upload/templates/upload/studentregistration_form.html create mode 100644 uploadexample/upload/templates/upload/studentregistration_list.html delete mode 100644 uploadexample/upload/tests.py create mode 160000 uploadfront diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a2ceb15 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +django +djangorestframework +django-cors-headers +coreapi diff --git a/uploadexample/api/__init__.py b/uploadexample/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/uploadexample/api/apps.py b/uploadexample/api/apps.py new file mode 100644 index 0000000..d87006d --- /dev/null +++ b/uploadexample/api/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ApiConfig(AppConfig): + name = 'api' diff --git a/uploadexample/upload/serializers.py b/uploadexample/api/serializers.py similarity index 62% rename from uploadexample/upload/serializers.py rename to uploadexample/api/serializers.py index d4fd0a7..379cfe2 100644 --- a/uploadexample/upload/serializers.py +++ b/uploadexample/api/serializers.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from .models import StudentRegistration +from upload.models import StudentRegistration from .validators import FileValidator @@ -7,11 +7,15 @@ class StudentRegistrationSerializer(serializers.ModelSerializer): class Meta: model = StudentRegistration - fields = ('first_name', 'last_name', 'image_agreement') + fields = ('id', 'first_name', 'last_name', 'image_agreement', + 'submission_date',) extra_kwargs = { 'image_agreement': { 'validators': [ FileValidator(allowed_mimetypes=('application/pdf',)), ], + }, + 'submission_date': { + 'format': '%Y-%m-%d', } } diff --git a/uploadexample/api/urls.py b/uploadexample/api/urls.py new file mode 100644 index 0000000..6bb5f8e --- /dev/null +++ b/uploadexample/api/urls.py @@ -0,0 +1,13 @@ +from django.urls import path +from rest_framework.routers import DefaultRouter +from rest_framework.documentation import include_docs_urls +from . import views + + +urlpatterns = [ + path('docs', include_docs_urls(title='API')) +] + +router = DefaultRouter() +router.register(r'registrations', views.RegistrationViewSet) +urlpatterns += router.urls diff --git a/uploadexample/upload/validators.py b/uploadexample/api/validators.py similarity index 100% rename from uploadexample/upload/validators.py rename to uploadexample/api/validators.py diff --git a/uploadexample/api/views.py b/uploadexample/api/views.py new file mode 100644 index 0000000..6680aac --- /dev/null +++ b/uploadexample/api/views.py @@ -0,0 +1,13 @@ +from rest_framework import viewsets +from rest_framework.mixins import ( + CreateModelMixin, DestroyModelMixin, ListModelMixin) +from upload.models import StudentRegistration +from .serializers import StudentRegistrationSerializer + + +class RegistrationViewSet(ListModelMixin, + CreateModelMixin, + DestroyModelMixin, + viewsets.GenericViewSet): + queryset = StudentRegistration.objects.all() + serializer_class = StudentRegistrationSerializer diff --git a/uploadexample/upload/admin.py b/uploadexample/upload/admin.py deleted file mode 100644 index 8c38f3f..0000000 --- a/uploadexample/upload/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/uploadexample/upload/management/commands/clean_media.py b/uploadexample/upload/management/commands/clean_media.py index 654f1a3..4173b59 100644 --- a/uploadexample/upload/management/commands/clean_media.py +++ b/uploadexample/upload/management/commands/clean_media.py @@ -77,7 +77,7 @@ class Command(BaseCommand): if deletables: unused = len(deletables) self.stdout.write('Detected unused media files:') - self.stdout.write(*('\t' + f_ for f_ in deletables)) + self.stdout.write('\n'.join(f_ for f_ in deletables)) for file_ in deletables: os.remove(os.path.join(self.media_root, file_)) diff --git a/uploadexample/upload/models.py b/uploadexample/upload/models.py index 432286d..5b1f81f 100644 --- a/uploadexample/upload/models.py +++ b/uploadexample/upload/models.py @@ -25,3 +25,6 @@ class StudentRegistration(models.Model): verbose_name = "dossier d'inscription" verbose_name_plural = "dossiers d'inscription" ordering = ('-submission_date',) + + def __str__(self): + return '{} {}'.format(self.first_name, self.last_name) diff --git a/uploadexample/upload/static/css/registration.css b/uploadexample/upload/static/css/registration.css index 77ca424..6bc7f69 100644 --- a/uploadexample/upload/static/css/registration.css +++ b/uploadexample/upload/static/css/registration.css @@ -1,9 +1,19 @@ +@import url('https://fonts.googleapis.com/css?family=Nunito:400,700,900'); +@import url('https://fonts.googleapis.com/css?family=Raleway:400,700,900'); + body { - margin: 2em; + padding: 2em; + font-family: "Nunito", sans-serif; +} + +h1, h2, h3, h4, h5, h6 { + font-family: "Raleway", sans-serif; + font-weight: bolder; } + .content { - margin: 0 auto; + margin: 2em auto; max-width: 500px; } @@ -14,7 +24,12 @@ body { } .list li { - padding: .5em 1em; + padding: 1em; margin: 1em 0; background: #eee; + display: flex; +} + +.list li .right { + margin-left: auto; } diff --git a/uploadexample/upload/templates/upload/base.html b/uploadexample/upload/templates/upload/base.html index 65cbb90..324a7b8 100644 --- a/uploadexample/upload/templates/upload/base.html +++ b/uploadexample/upload/templates/upload/base.html @@ -6,19 +6,19 @@ <title>OSER</title> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> - <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css"> + <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css" integrity="sha384-PsH8R72JQ3SOdhVi3uxftmaW6Vc51MKb0q5P2rRUpPvrszuE4W1povHYgTpBfshb" crossorigin="anonymous"> <link rel="stylesheet" href="{% static 'css/registration.css' %}"> </head> <body> <nav> - <ul class="nav nav-tabs"> - <li> - <a href="{% url 'registration-list' %}">Liste des inscriptions</a> + <ul class="nav nav-tabs justify-content-center"> + <li class="nav-item"> + <a class="nav-link" href="{% url 'registration-list' %}">Liste des inscriptions</a> </li> - <li> - <a href="{% url 'registration-create' %}">Nouvelle inscription</a> + <li class="nav-item"> + <a class="nav-link" href="{% url 'registration-create' %}">Nouvelle inscription</a> </li> </ul> </nav> @@ -28,8 +28,8 @@ <div class="content"> {% block content %}{% endblock %} </div> - <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script> - <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script> + <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js" integrity="sha384-vFJXuSJphROIrBnz7yo7oB41mKfc8JzQZiCq4NCceLEaO4IHwicKwpJf9c9IpFgh" crossorigin="anonymous"></script> + <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/js/bootstrap.min.js" integrity="sha384-alpBpkh1PFOepccYVYDB4do5UnbKysX5WZXm3XxPqe5iKTfUKjNkCk9SaVuEZflJ" crossorigin="anonymous"></script> </body> </html> diff --git a/uploadexample/upload/templates/upload/registration_list.html b/uploadexample/upload/templates/upload/registration_list.html deleted file mode 100644 index 48edf49..0000000 --- a/uploadexample/upload/templates/upload/registration_list.html +++ /dev/null @@ -1,30 +0,0 @@ -{% extends "upload/base.html" %} - -{% block content %} - <h1>Dossiers d'inscriptions</h1> - - <a href="{% url 'registration-create' %}" class="btn btn-primary">Nouvelle inscription</a> - - <ul class="list"> - {% for registration in registrations %} - <li> - <div class="row"> - <div class="col-xs-10"> - <h4>{{ registration.first_name }} {{ registration.last_name }}</h4> - <p class="text-muted">Soumis le {{ registration.submission_date }}</p> - <br/> - {% if registration.image_agreement %} - <a href="{{ registration.image_agreement.url }}">Droit à l'image</a> - {% endif %} - </div> - <div class="col-xs-2"> - <span class="pull-right"> - <a class="btn btn-danger" href="{% url 'registration-delete' pk=registration.pk %}">Supprimer</a> - </span> - </div> - </div> - <br /> - </li> - {% endfor %} - </ul> -{% endblock %} diff --git a/uploadexample/upload/templates/upload/studentregistration_confirm_delete.html b/uploadexample/upload/templates/upload/studentregistration_confirm_delete.html new file mode 100644 index 0000000..28c3088 --- /dev/null +++ b/uploadexample/upload/templates/upload/studentregistration_confirm_delete.html @@ -0,0 +1,10 @@ +{% extends "upload/base.html" %} + +{% block content %} +<h2>Confirmation</h2> +<form action="" method="post"> + {% csrf_token %} + <p>Êtes-vous sûrs de vouloir supprimer le dossier de {{ object }}?</p> + <input type="submit" class="btn btn-danger" value="Oui, supprimer" /> +</form> +{% endblock %} diff --git a/uploadexample/upload/templates/upload/studentregistration_form.html b/uploadexample/upload/templates/upload/studentregistration_form.html new file mode 100644 index 0000000..431b7c3 --- /dev/null +++ b/uploadexample/upload/templates/upload/studentregistration_form.html @@ -0,0 +1,13 @@ +{% extends "upload/base.html" %} +{% load rest_framework %} + +{% block content %} +<h1>Inscription au tutorat</h1> + +<!-- Use multipart/form-data encoding to allow sending files --> +<form action="{% url 'registration-create' %}" method="post" enctype="multipart/form-data"> + {% csrf_token %} + {{ form.as_p }} + <input type="submit" class="btn btn-success" value="Envoyer"> +</form> +{% endblock %} diff --git a/uploadexample/upload/templates/upload/studentregistration_list.html b/uploadexample/upload/templates/upload/studentregistration_list.html new file mode 100644 index 0000000..2642a48 --- /dev/null +++ b/uploadexample/upload/templates/upload/studentregistration_list.html @@ -0,0 +1,27 @@ +{% extends "upload/base.html" %} + +{% block content %} + <h1>Dossiers d'inscriptions</h1> + + <a href="{% url 'registration-create' %}" class="btn btn-primary">Nouvelle inscription</a> + + <ul class="list"> + {% for registration in registrations %} + <li> + <div> + <h4>{{ registration.first_name }} {{ registration.last_name }}</h4> + <p class="text-muted">Soumis le {{ registration.submission_date }}</p> + <br/> + {% if registration.image_agreement %} + <a href="{{ registration.image_agreement.url }}">Droit à l'image</a> + {% endif %} + </div> + <div class="right"> + <span class="pull-right"> + <a class="btn btn-danger" href="{% url 'registration-delete' pk=registration.pk %}">Supprimer</a> + </span> + </div> + </li> + {% endfor %} + </ul> +{% endblock %} diff --git a/uploadexample/upload/tests.py b/uploadexample/upload/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/uploadexample/upload/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/uploadexample/upload/urls.py b/uploadexample/upload/urls.py index fdfcad6..b5b3e70 100644 --- a/uploadexample/upload/urls.py +++ b/uploadexample/upload/urls.py @@ -7,5 +7,5 @@ urlpatterns = [ path('inscription', RegistrationCreate.as_view(), name='registration-create'), path('inscription/<int:pk>', RegistrationDelete.as_view(), - name='registration-delete') + name='registration-delete'), ] diff --git a/uploadexample/upload/views.py b/uploadexample/upload/views.py index 559bd91..12d6dde 100644 --- a/uploadexample/upload/views.py +++ b/uploadexample/upload/views.py @@ -1,48 +1,52 @@ -from django.shortcuts import redirect, get_object_or_404 +"""Regular Django views for a template-based frontend.""" +from django.urls import reverse_lazy from django.contrib import messages -from rest_framework.response import Response -from rest_framework.views import APIView -from rest_framework.renderers import TemplateHTMLRenderer +from django.views.generic import ListView, CreateView, DeleteView from .models import StudentRegistration -from .serializers import StudentRegistrationSerializer # Create your views here. -class RegistrationCreate(APIView): - renderer_classes = [TemplateHTMLRenderer] - template_name = 'upload/registration_create.html' +class RegistrationList(ListView): + """List of the registrations.""" - def get(self, request): - registration = StudentRegistration() - serializer = StudentRegistrationSerializer(registration) - return Response({'serializer': serializer}) + model = StudentRegistration + context_object_name = 'registrations' - def post(self, request): - registration = StudentRegistration() - serializer = StudentRegistrationSerializer( - registration, data=request.data) - if not serializer.is_valid(): - return Response({'serializer': serializer}) - serializer.save() - messages.success(request, 'Inscription réussie') - return redirect('registration-list') +class RegistrationCreate(CreateView): + """Create a new registration.""" -class RegistrationList(APIView): - renderer_classes = [TemplateHTMLRenderer] - template_name = 'upload/registration_list.html' + model = StudentRegistration + fields = ('first_name', 'last_name', 'image_agreement') + success_url = reverse_lazy('registration-list') - def get(self, request): - queryset = StudentRegistration.objects.all() - return Response({'registrations': queryset}) + def get_form(self, **kwargs): + """Customize style by adding Bootstrap classes.""" + form = super().get_form(**kwargs) + classes = { + 'first_name': 'form-control', + 'last_name': 'form-control', + 'image_agreement': 'form-control-file', + } + for field in form.fields: + form.fields[field].widget.attrs.update({'class': classes[field]}) + return form + def form_valid(self, form): + response = super().form_valid(form) + messages.success(self.request, 'Inscription réussie') + return response -class RegistrationDelete(APIView): - def get(self, request, pk): - registration = get_object_or_404(StudentRegistration, pk=pk) - registration.delete() +class RegistrationDelete(DeleteView): + """Delete a registration after confirmation.""" + + model = StudentRegistration + success_url = reverse_lazy('registration-list') + + def delete(self, request, *args, **kwargs): + response = super().delete(request, *args, **kwargs) messages.success(request, 'Inscription supprimée') - return redirect('registration-list') + return response diff --git a/uploadexample/uploadexample/settings.py b/uploadexample/uploadexample/settings.py index 02abfe5..27c7166 100644 --- a/uploadexample/uploadexample/settings.py +++ b/uploadexample/uploadexample/settings.py @@ -47,12 +47,14 @@ INSTALLED_APPS = [ 'django.contrib.messages', 'django.contrib.staticfiles', 'rest_framework', + 'corsheaders', 'upload', ] MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', + 'corsheaders.middleware.CorsMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', @@ -60,6 +62,10 @@ MIDDLEWARE = [ 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] +CORS_ORIGIN_WHITELIST = ( + 'localhost:4200', +) + ROOT_URLCONF = 'uploadexample.urls' TEMPLATES = [ diff --git a/uploadexample/uploadexample/urls.py b/uploadexample/uploadexample/urls.py index 8f2e314..6b137d5 100644 --- a/uploadexample/uploadexample/urls.py +++ b/uploadexample/uploadexample/urls.py @@ -21,6 +21,7 @@ from django.conf.urls.static import static urlpatterns = [ path('admin/', admin.site.urls), path('', include('upload.urls')), + path('api/', include('api.urls')), ] if settings.DEBUG: diff --git a/uploadfront b/uploadfront new file mode 160000 index 0000000..7d63863 --- /dev/null +++ b/uploadfront @@ -0,0 +1 @@ +Subproject commit 7d638632964e91a9c1667431fd7071aa4d142b2e -- GitLab