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