Parcourir la source

Add contribution management page for end-user

Protected by signed token
Jocelyn Delalande il y a 7 ans
Parent
commit
57e7c047f3

+ 1 - 0
requirements/dev.txt

@@ -1,2 +1,3 @@
 -r base.txt
 django-debug-toolbar
+freezegun

+ 12 - 0
wifiwithme/apps/contribmap/forms.py

@@ -94,3 +94,15 @@ class PublicContribForm(forms.ModelForm):
 
         for f in ['latitude', 'longitude']:
             self.fields[f].error_messages['required'] = "Veuillez déplacer le curseur à l'endroit où vous voulez partager/accéder au service"
+
+
+class ManageActionForm(forms.Form):
+    ACTION_DELETE = 'delete'
+    ACTION_RENEW = 'renew'
+
+    action = forms.ChoiceField(
+        widget=forms.HiddenInput(),
+        choices=(
+            (ACTION_DELETE, ACTION_DELETE),
+            (ACTION_RENEW, ACTION_RENEW),
+    ))

+ 6 - 0
wifiwithme/apps/contribmap/models.py

@@ -189,3 +189,9 @@ class Contrib(models.Model):
             return request.build_absolute_uri(url)
         else:
             return url
+
+    def make_management_url(self, token, request):
+        return request.build_absolute_uri(
+            '{}?token={}'.format(
+                reverse('manage_contrib', kwargs={'pk': self.pk}),
+                token))

+ 66 - 0
wifiwithme/apps/contribmap/templates/contribmap/manage_contrib.html

@@ -0,0 +1,66 @@
+{% extends "base.html" %}
+
+{% load staticfiles %}
+
+{% block content %}
+
+
+<style>
+.jumbotron h2 {
+  margin-top: 0px;
+}
+
+section.jumbotron {
+  margin-bottom: 15px;
+}
+
+#map {
+  margin-left: -15px;
+  margin-right: -15px;
+  margin-bottom: 15px;
+//  margin-top: 30px;
+}
+</style>
+<script src="{% static 'minimap.js' %}" type="text/javascript"></script>
+<script src="{% static 'confirmation.js' %}" type="text/javascript"></script>
+<h1>Gérer ma demande</h1>
+
+<div id="map" data-lat="{{ contrib.latitude|stringformat:"f" }}" data-lon="{{ contrib.longitude|stringformat:"f" }}"></div>
+
+<section class="jumbotron">
+
+<h2>Prolonger ma demande</h2>
+
+<p>
+Sans intervention de votre part, votre demande, ainsi que les informations
+personelles associées seront supprimés de nos serveurs le
+<strong>{{ contrib.expiration_date|date:"j F o" }}</strong> (au bout d'un an), conformément à <a
+href="#">notre déclaration CNIL</a>, il vous faut donc la prolonger si vous
+souhaitez que nous la conservions.
+</p>
+
+<form method="post" action="">
+{% csrf_token %}
+{{ renew_form.as_p }}
+<button type="submit" class="btn btn-primary btn-lg"><span
+class="glyphicon glyphicon-repeat"></span> Maintenir ma demande jusqu'au {{ wanabe_expiration_date|date:"j F o" }}</button>
+</form>
+</section>
+
+<div class="jumbotron"">
+<h2>Effacer ma demande</h2>
+<p>
+    Si vous supprimez votre demande, elle sera retirée de nos serveurs
+immédiatement, ainsi que les informations personelles associées.
+</p>
+<form method="post" action="">
+{% csrf_token %}
+{{ delete_form.as_p }}
+<button type="submit" class="btn btn-danger btn-lg confirmation-protected">
+    <span class="glyphicon glyphicon-trash"></span>
+    Effacer ma demande
+</button>
+</form>
+
+</div>
+{% endblock %}

+ 75 - 0
wifiwithme/apps/contribmap/tests.py

@@ -1,3 +1,4 @@
+import datetime
 import json
 import warnings
 
@@ -5,6 +6,8 @@ from django.core import mail
 from django.core.signing import BadSignature
 from django.contrib.auth.models import User
 from django.test import TestCase, Client, override_settings
+from freezegun import freeze_time
+import pytz
 
 from contribmap.models import Contrib
 from contribmap.forms import PublicContribForm
@@ -149,6 +152,78 @@ class TestViews(APITestCase):
         self.assertIn('JohnCleese', mail.outbox[0].subject)
         self.assertIn('JohnCleese', mail.outbox[0].body)
 
+
+class TestManageView(APITestCase):
+    def setUp(self):
+        self.contrib = Contrib.objects.create(
+            name='John',
+            phone='010101010101',
+            contrib_type=Contrib.CONTRIB_CONNECT,
+            privacy_coordinates=True,
+            latitude=0.5,
+            longitude=0.5,
+        )
+        self.token = ContribTokenManager().mk_token(self.contrib)
+
+    def test_manage_with_token(self):
+        # No token
+        response = self.client.get('/map/manage/{}'.format(self.contrib.pk))
+        self.assertEqual(response.status_code, 403)
+
+        # Garbage token
+        response = self.client.get(
+            '/map/manage/{}?token=burp'.format(self.contrib.pk))
+        self.assertEqual(response.status_code, 403)
+
+        # Valid token, but for another contrib
+        contrib2 = Contrib.objects.create(
+            name='John2',
+            phone='010101010101',
+            contrib_type=Contrib.CONTRIB_CONNECT,
+            privacy_coordinates=True,
+            latitude=0.5,
+            longitude=0.5,
+        )
+        token2 = ContribTokenManager().mk_token(contrib2)
+
+        response = self.client.get('/map/manage/{}?token={}'.format(
+            self.contrib.pk, token2))
+        self.assertEqual(response.status_code, 403)
+
+        # Normal legitimate access case
+        response = self.client.get(
+            '/map/manage/{}?token={}'.format(self.contrib.pk, self.token))
+        self.assertEqual(response.status_code, 200)
+
+        # Deleted contrib
+        Contrib.objects.all().delete()
+        response = self.client.get(
+            '/map/manage/{}?token={}'.format(self.contrib.pk, self.token))
+        self.assertEqual(response.status_code, 404)
+
+    def test_delete(self):
+        response = self.client.post(
+            '/map/manage/{}?token={}'.format(self.contrib.pk, self.token),
+            {'action': 'delete'})
+        self.assertEqual(response.status_code, 302)
+        self.assertFalse(Contrib.objects.filter(pk=self.contrib.pk).exists())
+
+    def test_renew(self):
+        self.contrib.date = datetime.datetime(2009, 10, 10, tzinfo=pytz.utc)
+        self.contrib.expiration_date = datetime.datetime(2010, 10, 10, tzinfo=pytz.utc)
+        self.contrib.save()
+
+        with freeze_time('12-12-2100', tz_offset=0):
+            response = self.client.post(
+                '/map/manage/{}?token={}'.format(self.contrib.pk, self.token),
+                {'action': 'renew'})
+        self.assertEqual(response.status_code, 200)
+        self.contrib = Contrib.objects.get(pk=self.contrib.pk)  # refresh
+        self.assertEqual(
+            self.contrib.expiration_date.date(),
+            datetime.date(2101, 12, 12))
+
+
 class TestForms(TestCase):
     valid_data = {
         'roof': True,

+ 4 - 1
wifiwithme/apps/contribmap/urls.py

@@ -1,12 +1,15 @@
 from django.conf.urls import url
 
-from .views import PublicJSON, PrivateJSON, display_map, legal, add_contrib, thanks
+from .views import (
+    PublicJSON, PrivateJSON,
+    add_contrib, manage_contrib, display_map, legal,  thanks)
 
 urlpatterns = [
     url(r'^$', display_map, name='display_map'),
     url(r'^legal$', legal, name='legal'),
     url(r'^contribute/thanks', thanks, name='thanks'),
     url(r'^contribute', add_contrib, name='add_contrib'),
+    url(r'^manage/(?P<pk>\d+)', manage_contrib, name='manage_contrib'),
     url(r'^public.json$', PublicJSON.as_view(), name='public_json'),
     url(r'^private.json$', PrivateJSON.as_view(), name='private_json'),
 ]

+ 74 - 4
wifiwithme/apps/contribmap/views.py

@@ -1,15 +1,23 @@
-from django.conf import settings
+import json
+import datetime
 
+from django.conf import settings
+from django.contrib import messages
 from django.core.urlresolvers import reverse
 from django.core.mail import send_mail
-from django.http import JsonResponse, HttpResponseForbidden
-from django.shortcuts import render, redirect
+from django.core.signing import BadSignature
+from django.http import (
+    JsonResponse, HttpResponseBadRequest, HttpResponseForbidden,
+    HttpResponseNotFound)
+from django.shortcuts import render, redirect, get_object_or_404
 from django.template.loader import get_template
 from django.views.generic import View
+import pytz
 
-from .forms import PublicContribForm
+from .forms import ManageActionForm, PublicContribForm
 from .models import Contrib
 from .decorators import prevent_robots
+from .tokens import ContribTokenManager, URLTokenManager, TokenError
 
 
 @prevent_robots()
@@ -46,6 +54,68 @@ def add_contrib(request):
     })
 
 
+def manage_contrib(request, pk):
+    """ Contribution management by the user itself
+
+    Auth is done by signed token
+    """
+    try:
+        token = request.GET['token']
+    except KeyError:
+        return HttpResponseForbidden(
+            'Missing authorization token')
+    pk = int(pk)
+
+    try:
+        contrib = ContribTokenManager().get_instance_if_allowed(token, pk)
+
+    except TokenError:
+        return HttpResponseForbidden(
+            'Bad signature, or expired token')
+
+    else:
+        if not contrib:
+            return HttpResponseNotFound("Inexistant Contrib")
+
+        wanabe_expiration_date = contrib.get_postponed_expiration_date(
+            datetime.datetime.now(pytz.utc))
+        if request.POST:
+            action_form = ManageActionForm(request.POST)
+            if not action_form.is_valid():
+                return HttpResponseBadRequest('Action invalide')
+
+            action = action_form.cleaned_data['action']
+            if action == action_form.ACTION_DELETE:
+                contrib.delete()
+                messages.add_message(
+                    request, messages.INFO,
+                    'Votre demande a bien été supprimée.')
+                return redirect(reverse('display_map'))
+
+            elif action == action_form.ACTION_RENEW:
+                contrib.expiration_date = wanabe_expiration_date
+                contrib.save()
+                messages.add_message(
+                    request, messages.INFO,
+                    "Votre demande a été prolongée jusqu'au {:%d/%m/%Y}".format(
+                        contrib.expiration_date))
+            else:
+                return HttpResponseBadRequest('Action invalide')
+
+        return render(request, 'contribmap/manage_contrib.html', {
+            'contrib': contrib,
+            'wanabe_expiration_date': wanabe_expiration_date,
+            'delete_form': ManageActionForm({
+                'action': ManageActionForm.ACTION_DELETE
+            }),
+            'renew_form': ManageActionForm({
+                'action': ManageActionForm.ACTION_RENEW
+            }),
+            'messages': messages.api.get_messages(request),
+            'isp': settings.ISP,
+        })
+
+
 def display_map(request):
     private_mode = request.user.is_authenticated()
     if private_mode:

+ 14 - 0
wifiwithme/static/confirmation.js

@@ -0,0 +1,14 @@
+$(document).ready(function() {
+  /* If JS is enabled, add a confirmation box.
+   * If not, delete without confirmation.
+   */
+  $('.confirmation-protected')
+  .attr('type', 'button')
+  .click(function(evt) {
+    var confirmed = confirm(
+      'Êtes-vous certain·e de vouloir supprimer votre contribution ?')
+    if (confirmed) {
+      $(evt.target).parent('form').submit();
+    }
+  });
+});

+ 14 - 0
wifiwithme/static/minimap.js

@@ -0,0 +1,14 @@
+$( document ).ready(function() {
+    // Create map
+    var lat = parseFloat($('#map').data("lat"));
+    var lon = parseFloat($('#map').data("lon"));
+
+    var map = L.map('map', {scrollWheelZoom: false}).setView([lat, lon], 15);
+;
+    L.tileLayer('//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
+        attribution: 'Map data &copy; <a href="//openstreetmap.org">OpenStreetMap</a> contributors, <a href="//creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, Imagery © <a href="//mapbox.com">Mapbox</a>',
+        maxZoom: 18
+    }).addTo(map);
+
+  var marker = L.marker([lat, lon]).addTo(map);
+});