diff --git a/Ernestophone/settings/common.py b/Ernestophone/settings/common.py index 4c2539d..7ee1c70 100644 --- a/Ernestophone/settings/common.py +++ b/Ernestophone/settings/common.py @@ -48,12 +48,12 @@ INSTALLED_APPS = [ 'pads', ] -MIDDLEWARE_CLASSES = [ +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] @@ -62,9 +62,7 @@ ROOT_URLCONF = "Ernestophone.urls" TEMPLATES = [{ 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [ - os.path.join(BASE_DIR, 'templates/'), - ], + 'DIRS': [], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ @@ -72,16 +70,15 @@ TEMPLATES = [{ 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', - 'django.template.context_processors.i18n', - 'django.template.context_processors.media', - 'django.template.context_processors.static', ], } }] +WSGI_APPLICATION = "Ernestophone.wsgi.application" + DATABASES = { "default": { - "ENGINE": "django.db.backends.postgresql_psycopg2", + "ENGINE": "django.db.backends.postgresql", "NAME": DBNAME, "USER": DBUSER, "PASSWORD": DBPASSWD, @@ -89,6 +86,21 @@ DATABASES = { } } +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + # I18n LANGUAGE_CODE = 'fr-fr' diff --git a/Ernestophone/settings/local.py b/Ernestophone/settings/local.py index 08c7016..ff331d4 100644 --- a/Ernestophone/settings/local.py +++ b/Ernestophone/settings/local.py @@ -2,7 +2,7 @@ import os from .common import * # noqa -from .common import BASE_DIR, INSTALLED_APPS, MIDDLEWARE_CLASSES +from .common import BASE_DIR, INSTALLED_APPS, MIDDLEWARE EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" @@ -10,13 +10,14 @@ EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" DEBUG = True INSTALLED_APPS += ["debug_toolbar"] -MIDDLEWARE_CLASSES = ( +MIDDLEWARE = ( ["debug_toolbar.middleware.DebugToolbarMiddleware"] - + MIDDLEWARE_CLASSES + + MIDDLEWARE ) STATIC_URL = "/static/" MEDIA_URL = "/media/" +MEDIA_ROOT = "media" DATABASES = { "default": { diff --git a/Ernestophone/urls.py b/Ernestophone/urls.py index 8313307..f6fb3c5 100644 --- a/Ernestophone/urls.py +++ b/Ernestophone/urls.py @@ -1,36 +1,40 @@ -from django.conf.urls import include, url from django.contrib import admin from django.contrib.auth import views as auth_views from django.conf.urls.static import static from django.conf import settings +from django.urls import include, path from gestion import views as gestion_views + urlpatterns = [ - url(r'^$', gestion_views.home, name='home'), - url(r'^login/?$', gestion_views.login, name='login'), - url(r'^logout/?$', auth_views.logout, {'next_page': '/'}, name='logout'), - url(r'^registration/?$', gestion_views.inscription_membre, - name='registration'), - url(r'^change/?', gestion_views.change_membre, name='change_membre'), - url(r'^password/?', gestion_views.change_password, name='change_password'), - url(r'^user/password/reset/$', auth_views.password_reset, - {'post_reset_redirect': '/user/password/reset/done/'}, - name="password_reset"), - url(r'^user/password/reset/done/$', - auth_views.password_reset_done), - url(r'^user/password/reset/(?P[0-9A-Za-z]+)-(?P.+)/$', - auth_views.password_reset_confirm, - {'post_reset_redirect': '/user/password/done/'}, - name="password_reset_confirm"), - url(r'^user/password/done/$', - auth_views.password_reset_complete), - url(r'^admin/', include(admin.site.urls)), - url(r'^partitions/', include('partitions.urls')), - url(r'^pads/', include('pads.urls')), - url(r'^calendar/', include('calendrier.urls')), - url(r'^propositions/', include('propositions.urls')), - url(r'^divers/', gestion_views.divers), + path("", gestion_views.home, name="home"), + path("login", gestion_views.login, name="login"), + path("logout", auth_views.logout, {"next_page": "/"}, name="logout"), + path("registration", gestion_views.inscription_membre, name="registration"), + path("change", gestion_views.change_membre, name="change_membre"), + path("password", gestion_views.change_password, name="change_password"), + path("user/password/reset", auth_views.password_reset, + {"post_reset_redirect": "/user/password/reset/done/"}, + name="password_reset"), + path("user/password/reset/done", auth_views.password_reset_done), + path("user/password/reset///", auth_views.password_reset_confirm, + {'post_reset_redirect': '/user/password/done/'}, + name="password_reset_confirm"), + path("user/password/done", auth_views.password_reset_complete), + path("admin/", admin.site.urls), + path("partitions/", include('partitions.urls')), + path("pads/", include('pads.urls')), + path("calendar/", include('calendrier.urls')), + path("propositions/", include('propositions.urls')), + path("divers/", gestion_views.divers), ] +if "debug_toolbar" in settings.INSTALLED_APPS: + import debug_toolbar + from django.conf.urls import include, url + urlpatterns = [ + url(r"^__debug__/", include(debug_toolbar.urls)), + ] + urlpatterns + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/calendrier/migrations/0001_initial.py b/calendrier/migrations/0001_initial.py index c0eb8f8..42840b3 100644 --- a/calendrier/migrations/0001_initial.py +++ b/calendrier/migrations/0001_initial.py @@ -36,8 +36,8 @@ class Migration(migrations.Migration): ('id', models.AutoField(auto_created=True, primary_key=True, verbose_name='ID', serialize=False)), ('reponse', models.CharField(max_length=20, choices=[('oui', 'Oui'), ('non', 'Non'), ('pe', 'Peut-être')], default='non', verbose_name='Réponse')), ('details', models.CharField(blank=True, max_length=50)), - ('event', models.ForeignKey(to='calendrier.Event')), - ('participant', models.ForeignKey(to='gestion.ErnestoUser')), + ('event', models.ForeignKey(to='calendrier.Event', on_delete=models.CASCADE)), + ('participant', models.ForeignKey(to='gestion.ErnestoUser', on_delete=models.CASCADE)), ], ), ] diff --git a/calendrier/models.py b/calendrier/models.py index 96eb583..2a6cdda 100644 --- a/calendrier/models.py +++ b/calendrier/models.py @@ -33,10 +33,8 @@ class Event(models.Model): class Participants(models.Model): - event = models.ForeignKey(Event) - participant = models.ForeignKey(ErnestoUser) + event = models.ForeignKey(Event, on_delete=models.CASCADE) + participant = models.ForeignKey(ErnestoUser, on_delete=models.CASCADE) reponse = models.CharField("Réponse", max_length=20, default="non", choices=ANSWERS) details = models.CharField(max_length=50, blank=True) - -# Create your models here. diff --git a/calendrier/templates/calendrier/changename.html b/calendrier/templates/calendrier/changename.html index 24a6da6..12fbb47 100644 --- a/calendrier/templates/calendrier/changename.html +++ b/calendrier/templates/calendrier/changename.html @@ -9,7 +9,7 @@ {% if success %}

Changement enregistré !

{% endif %} -
+ {% csrf_token %} {{ form.as_p }} diff --git a/calendrier/templates/calendrier/create.html b/calendrier/templates/calendrier/create.html index f21e009..d5cbacc 100644 --- a/calendrier/templates/calendrier/create.html +++ b/calendrier/templates/calendrier/create.html @@ -8,7 +8,7 @@ {% if erreur %} {{ erreur }} {% endif %} - + {% csrf_token %} {{ form.as_p }} diff --git a/calendrier/templates/calendrier/home.html b/calendrier/templates/calendrier/home.html index 3a0f089..856005c 100644 --- a/calendrier/templates/calendrier/home.html +++ b/calendrier/templates/calendrier/home.html @@ -34,7 +34,7 @@ Fanfaron de passage, musicien intrigué, Ernestophoniste en quête de sensations {{Calendar|translate}} {% if user.profile.is_chef %} -Ajouter un évènement +Ajouter un évènement {% endif %} {% endblock %} diff --git a/calendrier/templates/calendrier/reponse.html b/calendrier/templates/calendrier/reponse.html index 715cdb5..bf679de 100644 --- a/calendrier/templates/calendrier/reponse.html +++ b/calendrier/templates/calendrier/reponse.html @@ -6,10 +6,10 @@ {% if envoi %}

Votre réponse a été enregistrée !

{% endif %} -

Retour à l'événement

+

Retour à l'événement

Voulez vous participer à l'événement {{ ev.nom }}, le {{ ev.date }} à {{ ev.debut|time:"H:i" }} ?
- + {% csrf_token %} {{ form.as_p }} diff --git a/calendrier/templates/calendrier/resend.html b/calendrier/templates/calendrier/resend.html index c6a3fb6..7b5bd6e 100644 --- a/calendrier/templates/calendrier/resend.html +++ b/calendrier/templates/calendrier/resend.html @@ -6,7 +6,7 @@ {% if erreur %} {{ erreur }} {% endif %} - + {% csrf_token %} {{ form.as_p }} diff --git a/calendrier/templates/calendrier/view_event.html b/calendrier/templates/calendrier/view_event.html index 42829bc..c81d7d7 100644 --- a/calendrier/templates/calendrier/view_event.html +++ b/calendrier/templates/calendrier/view_event.html @@ -20,7 +20,7 @@

Supprimer l'événement

Renvoyer les mails

{% endif %} -

Changer mon nom pour le doodle

+

Changer mon nom pour le doodle

{% endif %} {% if user.is_authenticated %} @@ -51,7 +51,7 @@ Pas de réponse pour l'instant {% endfor %} -

Répondre à l'événement

+

Répondre à l'événement

{% endif %} {% endblock %} diff --git a/calendrier/tests/__init__.py b/calendrier/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/calendrier/tests/test_views.py b/calendrier/tests/test_views.py new file mode 100644 index 0000000..6b200e0 --- /dev/null +++ b/calendrier/tests/test_views.py @@ -0,0 +1,159 @@ +from datetime import timedelta + +from django.contrib.auth import get_user_model +from django.template.defaultfilters import urlencode +from django.test import Client, TestCase +from django.utils import timezone + +from gestion.models import ErnestoUser +from ..models import Event + +User = get_user_model() + + +def new_user(username, ernesto=False, chef=False): + u = User.objects.create_user(username=username) + ErnestoUser.objects.create(user=u, slug=username, is_chef=chef, is_ernesto=ernesto) + return u + + +class TestViews(TestCase): + # TODO: test forms + + def setUp(self): + # User with different access level and their clients + chef = new_user("chef", ernesto=True, chef=True) + chef_c = Client() + chef_c.force_login(chef) + ernesto = new_user("ernesto", ernesto=True) + ernesto_c = Client() + ernesto_c.force_login(ernesto) + self.client_matrix = [ + (chef, chef_c), + (ernesto, ernesto_c), + (None, Client()) + ] + # A private and a public event + now = timezone.now() + later = now + timedelta(seconds=3600) + self.priv_event = Event.objects.create( + nom="private event", + nomcourt="privevt", + date=now.date(), + debut=now.time(), + fin=later.time(), + slug="privevt", + lieu="somewhere", + calendrier=False + ) + self.pub_event = Event.objects.create( + nom="public event", + nomcourt="pubevt", + date=now.date(), + debut=now.time(), + fin=later.time(), + slug="pubevt", + lieu="somewhere", + calendrier=True + ) + + # Everyone can see theses pages + + def _everyone_can_get(self, url, redirect_url=None): + """Shorthand for checking that every kind of user can get a page""" + for _, client in self.client_matrix: + resp = client.get(url) + if redirect_url: + self.assertRedirects(resp, redirect_url) + else: + self.assertEqual(200, resp.status_code) + + def test_get_home(self): + url = "/calendar/" + self._everyone_can_get(url) + + def test_get_calendar(self): + year, month = 2017, 5 + url = "/calendar/{}/{}".format(year, month) + self._everyone_can_get(url) + + def test_get_public_event(self): + """Public event, everyone can see""" + evt = self.pub_event + url = "/calendar/{}/{}/{}".format(evt.date.year, evt.date.month, evt.id) + self._everyone_can_get(url) + + def test_get_public_event_bis(self): + """Public event, everyone can see""" + url = "/calendar/{}".format(self.pub_event.id) + self._everyone_can_get(url) + + def test_get_reponse_event(self): + # XXX: this view sucks + chef, _ = self.client_matrix[0] + codereps = ["oui", "non", "pe"] + for coderep in codereps: + url = "/calendar/rep/{}/{}/{}".format(chef.profile.slug, self.priv_event.slug, coderep) + self._everyone_can_get(url, redirect_url="/calendar/") + + # Only ernesto members can get theses pages + + def _get_restricted_page(self, url, chef_only=False, redirect_url=None): + """Shorthand for testing wether a page in only accessible by ernesto members""" + def user_allowed(user): + if user is None: + return False + if chef_only: + return user.profile.is_chef + return True + + for user, client in self.client_matrix: + # If user is not None, it is an ernesto member + resp = client.get(url) + if user_allowed(user): + self.assertEqual(200, resp.status_code) + else: + if redirect_url is None: + redirect_url = "/login?next={}".format(urlencode(urlencode(url))) + self.assertRedirects(resp, redirect_url) + + def test_get_private_event(self): + """Private event, restricted access""" + evt = self.priv_event + url = "/calendar/{}/{}/{}".format(evt.date.year, evt.date.month, evt.id) + self._get_restricted_page(url, redirect_url="/calendar/") + + def test_get_private_event_bis(self): + """Private event, restricted access""" + url = "/calendar/{}".format(self.priv_event.id) + self._get_restricted_page(url, redirect_url="/calendar/") + + def test_get_new(self): + """Only chef can create an event""" + url = "/calendar/new" + self._get_restricted_page(url, chef_only=True) + + def test_get_edit(self): + """Only chef can edit an event""" + url = "/calendar/edition/{}".format(self.pub_event.id) + self._get_restricted_page(url, chef_only=True) + + def test_get_delete(self): + """Only chef can delete an event""" + url = "/calendar/supprimer/{}".format(self.pub_event.id) + self._get_restricted_page(url, chef_only=True) + + def test_get_resend(self): + """Only chef can send event emails""" + url = "/calendar/resend/{}".format(self.pub_event.id) + self._get_restricted_page(url, chef_only=True) + + def test_get_changename(self): + """Only authenticated users can have a doodle name""" + url = "/calendar/changename" + self._get_restricted_page(url) + + def test_get_answers(self): + """Only ernesto members can see who attends an event""" + url = "/calendar/{}/reponse".format(self.priv_event.id) + self._get_restricted_page(url) diff --git a/calendrier/urls.py b/calendrier/urls.py index b37e1fb..9b65a15 100644 --- a/calendrier/urls.py +++ b/calendrier/urls.py @@ -1,20 +1,20 @@ -from django.conf.urls import url +from django.urls import path -from calendrier import views -from calendrier.views import EventUpdate, EventDelete +from . import views +from .views import EventUpdate, EventDelete + +app_name = "calendrier" urlpatterns = [ - url(r'^new$', views.create_event, name='calendrier-create_event'), - url(r'^$', views.home), - url(r'^edition/(?P\d+)$', EventUpdate.as_view()), - url(r'^supprimer/(?P\d+)$', EventDelete.as_view()), - url(r'^resend/(?P\d+)$', views.resend, name='calendrier-resend'), - url(r'^changename/?$', views.changename, name='change-doodle-name'), - url(r'(?P\d+)/reponse/?', views.reponse, name='calendrier-reponse'), - url(r'(?P\w{6})/(?P\w{6})/(?P\w+)/?', - views.reponse_event, - name='calendrier.reponse_event'), - url(r'(?P\d+)/(?P\d+)/(?P\d+)/?', views.view_event), - url(r'(?P\d+)/(?P\d+)/?$', views.calendar), - url(r'(?P\d+)/?', views.view_eventbis, name='view-event'), + path("", views.home, name="home"), + path("new", views.create_event, name="create_event"), + path("edition/", EventUpdate.as_view()), + path("supprimer/", EventDelete.as_view()), + path("resend/", views.resend, name="resend"), + path("changename", views.changename, name="change-doodle-name"), + path("/reponse", views.reponse, name="reponse"), + path("rep///", views.reponse_event, name="reponse_event"), + path("//", views.view_event), + path("/", views.calendar), + path("", views.view_eventbis, name="view-event"), ] diff --git a/calendrier/views.py b/calendrier/views.py index 5515a7f..8f9eb14 100644 --- a/calendrier/views.py +++ b/calendrier/views.py @@ -4,7 +4,7 @@ from django.shortcuts import render, redirect, get_object_or_404 from django.contrib.sites.shortcuts import get_current_site from django.utils.safestring import mark_safe from django.views.generic import UpdateView, DeleteView -from django.core.urlresolvers import reverse, reverse_lazy +from django.urls import reverse, reverse_lazy from django.contrib.auth.decorators import login_required from django.utils.decorators import method_decorator from django.core.mail import send_mail @@ -37,7 +37,7 @@ def calendar(request, pYear, pMonth): lEvents = Event.objects.filter(date__gte=lCalendarFromMonth, date__lte=lCalendarToMonth, calendrier=True) - if request.user.is_authenticated(): + if request.user.is_authenticated: lEvents = Event.objects.filter(date__gte=lCalendarFromMonth, date__lte=lCalendarToMonth) lCalendar = EventCalendar(lEvents).formatmonth(lYear, lMonth) @@ -71,8 +71,8 @@ def calendar(request, pYear, pMonth): def view_event(request, pYear, pMonth, id): ev = get_object_or_404(Event, id=id) - if not request.user.is_authenticated() and not ev.calendrier: - return redirect('calendrier.views.home') + if not request.user.is_authenticated and not ev.calendrier: + return redirect(reverse('calendrier:home')) nom = ev.nom.capitalize fin = False desc = False @@ -89,8 +89,8 @@ def view_event(request, pYear, pMonth, id): def view_eventbis(request, id): ev = get_object_or_404(Event, id=id) - if not request.user.is_authenticated() and not ev.calendrier: - return redirect('calendrier.views.home') + if not request.user.is_authenticated and not ev.calendrier: + return redirect(reverse('calendrier:home')) part = ev.participants_set.all() nom = ev.nom.capitalize fin = False @@ -241,6 +241,7 @@ def reponse(request, id): return render(request, "calendrier/reponse.html", locals()) +# XXX: UNSAFE! def reponse_event(request, codeus, codeev, coderep): """ Inscriptions aux événements via les liens envoyés par mail. @@ -254,7 +255,7 @@ def reponse_event(request, codeus, codeev, coderep): Participants.objects.filter(event=ev, participant=part).delete() # Et on écrit la nouvelle inscription Participants.objects.create(participant=part, event=ev, reponse=coderep) - return redirect('calendrier.views.home') + return redirect(reverse('calendrier:home')) class EventUpdate(UpdateView): diff --git a/gestion/admin.py b/gestion/admin.py index 476447a..40d5d75 100644 --- a/gestion/admin.py +++ b/gestion/admin.py @@ -84,7 +84,7 @@ class UserProfileAdmin(UserAdmin): perm = Permission.objects.get_by_natural_key(*nat_key) chef_group.permissions.add(perm) # On met tous les chef dans le groupe - chef_group.user_set = User.objects.filter(profile__is_chef=True) + chef_group.user_set.set(User.objects.filter(profile__is_chef=True)) # Les chefs sont dans le groupe Chef if user.profile.is_chef: print("J'aime la choucroute") diff --git a/gestion/migrations/0001_initial.py b/gestion/migrations/0001_initial.py index f95ea63..ff61978 100644 --- a/gestion/migrations/0001_initial.py +++ b/gestion/migrations/0001_initial.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import migrations, models from django.conf import settings @@ -23,7 +20,14 @@ class Migration(migrations.Migration): ('slug', models.CharField(max_length=7, editable=False, unique=True)), ('doodlename', models.CharField(blank=True, max_length=30, verbose_name='Nom pour le doodle')), ('mails', models.BooleanField(verbose_name='Recevoir les mails', default=True)), - ('user', models.OneToOneField(to=settings.AUTH_USER_MODEL, related_name='profile')), + ( + 'user', + models.OneToOneField( + to=settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name='profile' + ) + ), ], options={ 'verbose_name': 'Profil Ernestophoniste', diff --git a/gestion/models.py b/gestion/models.py index 851fcba..df21027 100644 --- a/gestion/models.py +++ b/gestion/models.py @@ -5,7 +5,7 @@ from django.contrib.auth.models import User class ErnestoUser(models.Model): - user = models.OneToOneField(User, related_name="profile") + user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile") is_ernesto = models.BooleanField("Membre de l'Ernestophone", default=True) is_chef = models.BooleanField("Chef Fanfare", default=False) phone = models.CharField("Téléphone", max_length=20, blank=True) @@ -19,9 +19,6 @@ class ErnestoUser(models.Model): verbose_name = "Profil Ernestophoniste" verbose_name_plural = "Profil Ernestophoniste" - def __unicode__(self): - return unicode(self.user.username) - def __str__(self): return self.user.username # Create your models here. diff --git a/gestion/templates/gestion/base.html b/gestion/templates/gestion/base.html index fd532e0..02e2b99 100644 --- a/gestion/templates/gestion/base.html +++ b/gestion/templates/gestion/base.html @@ -19,7 +19,7 @@