Repository: zestedesavoir/zds-site

Issue:
Gestion des Unes - dates cassées
* Il semble impossible de modifier la date d'une Une
* Ajouter une Une avec une date provoque parfois une 500: https://sentry.sandhose.fr/zeste-de-savoir/backend-l7/issues/988/


Discussion:

Resolving patch:
diff --git a/zds/featured/forms.py b/zds/featured/forms.py
--- a/zds/featured/forms.py
+++ b/zds/featured/forms.py
@@ -13,7 +13,7 @@ class FeaturedResourceForm(forms.ModelForm):
     class Meta:
         model = FeaturedResource
 
-        fields = ['title', 'type', 'authors', 'image_url', 'url', 'pubdate']
+        fields = ['title', 'type', 'authors', 'image_url', 'url']
 
         widgets = {
             'title': forms.TextInput(
@@ -44,12 +44,6 @@ class Meta:
                 attrs={
                     'placeholder': _(u'Lien vers la ressource.')
                 }
-            ),
-
-            'pubdate': forms.DateTimeInput(
-                attrs={
-                    'placeholder': _(u'Exemple : 2016-12-25 00:00:00')
-                }
             )
         }
 
@@ -59,25 +53,48 @@ class Meta:
         required=False
     )
 
+    pubdate = forms.DateTimeField(
+        label=_(u'Date de publication (exemple: 25/12/2015 15:00 ou 2015-12-25T15:00)'),
+        input_formats=[
+            '%d/%m/%Y %H:%M:%S', '%Y-%m-%d %H:%M:%S',  # full format with second
+            '%Y-%m-%dT%H:%M',  # datetime field format
+            '%Y-%m-%d %H:%M', '%d/%m/%Y %H:%M',  # without second
+            '%Y-%m-%d', '%d/%m/%Y'  # day only
+        ],
+        widget=forms.DateTimeInput(
+            attrs={'placeholder': _(u'Exemple : 25/12/2016 10:00'), 'type': 'datetime-local'},
+            format='%Y-%m-%dT%H:%M'  # datetime field format
+        )
+    )
+
     def __init__(self, *args, **kwargs):
+        hide_major_update_field = kwargs.pop('hide_major_update_field', False)
+
         super(FeaturedResourceForm, self).__init__(*args, **kwargs)
         self.helper = FormHelper()
         self.helper.form_class = 'content-wrapper'
         self.helper.form_method = 'post'
         self.helper.form_action = reverse('featured-resource-create')
 
-        self.helper.layout = Layout(
+        fields = [
             Field('title'),
             Field('type'),
             Field('authors'),
             Field('image_url'),
-            Field('url'),
-            Field('major_update'),
+            Field('url')
+        ]
+
+        if not hide_major_update_field:
+            fields.append(Field('major_update'))
+
+        fields.extend([
             Field('pubdate'),
             ButtonHolder(
                 StrictButton(_(u'Enregistrer'), type='submit'),
-            ),
-        )
+            )
+        ])
+
+        self.helper.layout = Layout(*fields)
 
 
 class FeaturedMessageForm(forms.ModelForm):
diff --git a/zds/featured/views.py b/zds/featured/views.py
--- a/zds/featured/views.py
+++ b/zds/featured/views.py
@@ -5,11 +5,10 @@
 from django.contrib.auth.decorators import login_required, permission_required
 from django.core.urlresolvers import reverse
 from django.db import transaction
-from django.shortcuts import render, redirect
+from django.shortcuts import redirect
 from django.utils.decorators import method_decorator
 from django.utils.translation import ugettext as _
-from django.views.generic import CreateView, RedirectView, UpdateView
-from django.views.generic.detail import SingleObjectMixin
+from django.views.generic import CreateView, RedirectView, UpdateView, FormView, DeleteView
 from django.views.generic.list import MultipleObjectMixin
 
 from django.conf import settings
@@ -41,31 +40,34 @@ class FeaturedResourceCreate(CreateView):
 
     form_class = FeaturedResourceForm
     template_name = 'featured/resource/create.html'
+    context_object_name = 'featured_resource'
 
     @method_decorator(login_required)
     @method_decorator(permission_required('featured.change_featuredresource', raise_exception=True))
     def dispatch(self, request, *args, **kwargs):
         return super(FeaturedResourceCreate, self).dispatch(request, *args, **kwargs)
 
-    def post(self, request, *args, **kwargs):
-        form = self.form_class(request.POST)
-
-        if form.is_valid():
-            return self.form_valid(form)
-
-        return render(request, self.template_name, {'form': form})
+    def get_form_kwargs(self):
+        kw = super(FeaturedResourceCreate, self).get_form_kwargs()
+        kw['hide_major_update_field'] = True
+        return kw
 
     def form_valid(self, form):
-
         featured_resource = FeaturedResource()
-        featured_resource.title = form.data.get('title')
-        featured_resource.type = form.data.get('type')
-        featured_resource.authors = form.data.get('authors')
-        featured_resource.image_url = form.data.get('image_url')
-        featured_resource.url = form.data.get('url')
-        featured_resource.pubdate = form.data.get('pubdate')
+        featured_resource.title = form.cleaned_data.get('title')
+        featured_resource.type = form.cleaned_data.get('type')
+        featured_resource.authors = form.cleaned_data.get('authors')
+        featured_resource.image_url = form.cleaned_data.get('image_url')
+        featured_resource.url = form.cleaned_data.get('url')
+
+        if form.cleaned_data.get('major_update', False):
+            featured_resource.pubdate = datetime.now()
+        else:
+            featured_resource.pubdate = form.cleaned_data.get('pubdate')
+
         featured_resource.save()
 
+        messages.success(self.request, _(u'La une a été créée.'))
         return redirect(reverse('featured-resource-list'))
 
 
@@ -77,74 +79,66 @@ class FeaturedResourceUpdate(UpdateView):
     form_class = FeaturedResourceForm
     template_name = 'featured/resource/update.html'
     queryset = FeaturedResource.objects.all()
-    featured_resource = None
+    context_object_name = 'featured_resource'
 
     @method_decorator(login_required)
     @method_decorator(permission_required('featured.change_featuredresource', raise_exception=True))
     def dispatch(self, request, *args, **kwargs):
         return super(FeaturedResourceUpdate, self).dispatch(request, *args, **kwargs)
 
-    def get(self, request, *args, **kwargs):
-        self.featured_resource = self.get_object()
-        form = self.form_class(initial={
-            'title': self.featured_resource.title,
-            'type': self.featured_resource.type,
-            'authors': self.featured_resource.authors,
-            'image_url': self.featured_resource.image_url,
-            'url': self.featured_resource.url,
-            'pubdate': self.featured_resource.pubdate,
+    def get_initial(self):
+        initial = super(FeaturedResourceUpdate, self).get_initial()
+        initial.update({
+            'title': self.object.title,
+            'type': self.object.type,
+            'authors': self.object.authors,
+            'image_url': self.object.image_url,
+            'url': self.object.url,
+            'pubdate': self.object.pubdate,
         })
-        form.helper.form_action = reverse('featured-resource-update', args=[self.featured_resource.pk])
-        return render(request, self.template_name, {'form': form, 'featured_resource': self.featured_resource})
 
-    def post(self, request, *args, **kwargs):
-        self.featured_resource = self.get_object()
-        form = self.form_class(request.POST)
-
-        if form.is_valid():
-            return self.form_valid(form)
-
-        return render(request, self.template_name, {'form': form, 'featured_resource': self.featured_resource})
+        return initial
 
     def form_valid(self, form):
 
-        self.featured_resource.title = form.data.get('title')
-        self.featured_resource.type = form.data.get('type')
-        self.featured_resource.authors = form.data.get('authors')
-        self.featured_resource.image_url = form.data.get('image_url')
-        self.featured_resource.url = form.data.get('url')
-        if form.data.get('major_update') is not None:
-            self.featured_resource.pubdate = datetime.now()
-
-        self.featured_resource.save()
-        return redirect(reverse('homepage'))
-
-    def get_form(self, form_class=FeaturedResourceForm):
-        form = self.form_class(self.request.POST)
-        form.helper.form_action = reverse('featured-resource-update', args=[self.featured_resource.pk])
+        self.object.title = form.cleaned_data.get('title')
+        self.object.type = form.cleaned_data.get('type')
+        self.object.authors = form.cleaned_data.get('authors')
+        self.object.image_url = form.cleaned_data.get('image_url')
+        self.object.url = form.cleaned_data.get('url')
+        if form.cleaned_data.get('major_update', False):
+            self.object.pubdate = datetime.now()
+        else:
+            self.object.pubdate = form.cleaned_data.get('pubdate')
+
+        messages.success(self.request, _(u'La une a été mise à jour.'))
+        self.success_url = reverse('featured-resource-list')
+        return super(FeaturedResourceUpdate, self).form_valid(form)
+
+    def get_form(self, form_class=None):
+        form = super(FeaturedResourceUpdate, self).get_form(form_class)
+        form.helper.form_action = reverse('featured-resource-update', args=[self.object.pk])
         return form
 
 
-class FeaturedResourceDeleteDetail(SingleObjectMixin, RedirectView):
+class FeaturedResourceDeleteDetail(DeleteView):
     """
     Deletes a featured resource.
     """
-    queryset = FeaturedResource.objects.all()
-    permanent = False
+
+    model = FeaturedResource
 
     @method_decorator(login_required)
     @method_decorator(transaction.atomic)
     @method_decorator(permission_required('featured.change_featuredresource', raise_exception=True))
     def dispatch(self, request, *args, **kwargs):
+        self.success_url = reverse('featured-resource-list')
         return super(FeaturedResourceDeleteDetail, self).dispatch(request, *args, **kwargs)
 
     def post(self, request, *args, **kwargs):
-        featured_resource = self.get_object()
-        featured_resource.delete()
-
+        r = super(FeaturedResourceDeleteDetail, self).post(request, *args, **kwargs)
         messages.success(request, _(u'La une a été supprimée avec succès.'))
-
-        return redirect(reverse('featured-resource-list'))
+        return r
 
 
 class FeaturedResourceDeleteList(MultipleObjectMixin, RedirectView):
@@ -171,48 +165,42 @@ def post(self, request, *args, **kwargs):
         return redirect(reverse('featured-resource-list'))
 
 
-class FeaturedMessageCreateUpdate(CreateView):
+class FeaturedMessageCreateUpdate(FormView):
     """
     Creates or updates the featured message.
     """
 
     form_class = FeaturedMessageForm
     template_name = 'featured/message/create.html'
+    last_message = None
 
     @method_decorator(login_required)
     @method_decorator(permission_required('featured.change_featuredmessage', raise_exception=True))
     def dispatch(self, request, *args, **kwargs):
+        self.last_message = FeaturedMessage.objects.get_last_message()
         return super(FeaturedMessageCreateUpdate, self).dispatch(request, *args, **kwargs)
 
-    def get(self, request, *args, **kwargs):
-        last_message = FeaturedMessage.objects.get_last_message()
-        init = {}
-        if last_message is not None:
-            init = {
-                'hook': last_message.hook,
-                'message': last_message.message,
-                'url': last_message.url,
-            }
+    def get_initial(self):
+        init = super(FeaturedMessageCreateUpdate, self).get_initial()
 
-        form = self.form_class(initial=init)
-        form.helper.form_action = reverse('featured-message-create')
-        return render(request, self.template_name, {'form': form})
+        if self.last_message is not None:
+            init.update({
+                'hook': self.last_message.hook,
+                'message': self.last_message.message,
+                'url': self.last_message.url,
+            })
 
-    def post(self, request, *args, **kwargs):
-        form = self.form_class(request.POST)
-
-        if form.is_valid():
-            return self.form_valid(form)
-
-        return render(request, self.template_name, {'form': form})
+        return init
 
     def form_valid(self, form):
-        last_message = FeaturedMessage.objects.get_last_message()
-        if last_message:
-            last_message.delete()
+        if self.last_message:
+            self.last_message.delete()
+
         featured_message = FeaturedMessage()
         featured_message.hook = form.data.get('hook')
         featured_message.message = form.data.get('message')
         featured_message.url = form.data.get('url')
         featured_message.save()
-        return redirect(reverse('homepage'))
+
+        messages.success(self.request, _(u'Le message a été changé'))
+        return redirect(reverse('featured-resource-list'))


Test patch:
diff --git a/zds/featured/tests.py b/zds/featured/tests.py
--- a/zds/featured/tests.py
+++ b/zds/featured/tests.py
@@ -6,6 +6,7 @@
 from zds.member.factories import StaffProfileFactory, ProfileFactory
 from zds.featured.factories import FeaturedResourceFactory
 from zds.featured.models import FeaturedResource, FeaturedMessage
+from datetime import datetime, date
 
 
 stringof2001chars = 'http://url.com/'
@@ -52,25 +53,44 @@ def test_success_create_featured(self):
             username=staff.user.username,
             password='hostel77'
         )
-        self.assertTrue(login_check)
 
+        self.assertTrue(login_check)
         self.assertEqual(0, FeaturedResource.objects.all().count())
-        response = self.client.post(
-            reverse('featured-resource-create'),
-            {
-                'title': 'title',
-                'type': 'type',
-                'image_url': 'image_url',
-                'url': 'url',
-                'authors': staff.user.username,
-                'pubdate': '2016-12-25 00:00:00'
-            },
-            follow=True
-        )
+
+        pubdate = date(2016, 1, 1).strftime('%d/%m/%Y %H:%M:%S')
+
+        fields = {
+            'title': 'title',
+            'type': 'type',
+            'image_url': 'http://test.com/image.png',
+            'url': 'http://test.com',
+            'authors': staff.user.username,
+            'pubdate': pubdate
+        }
+
+        response = self.client.post(reverse('featured-resource-create'), fields, follow=True)
 
         self.assertEqual(200, response.status_code)
         self.assertEqual(1, FeaturedResource.objects.all().count())
 
+        featured = FeaturedResource.objects.first()
+
+        for field, value in fields.items():
+            if field != 'pubdate':
+                self.assertEqual(value, getattr(featured, field), msg='Error on {}'.format(field))
+            else:
+                self.assertEqual(value, featured.pubdate.strftime('%d/%m/%Y %H:%M:%S'))
+
+        # now with major_update
+        fields['major_update'] = 'on'
+
+        response = self.client.post(reverse('featured-resource-create'), fields, follow=True)
+        self.assertEqual(200, response.status_code)
+        self.assertEqual(2, FeaturedResource.objects.all().count())
+
+        featured = FeaturedResource.objects.last()
+        self.assertTrue((datetime.now() - featured.pubdate).total_seconds() < 10)
+
     def test_failure_create_featured_with_unauthenticated_user(self):
         response = self.client.get(reverse('featured-resource-create'))
 
@@ -103,7 +123,7 @@ def test_failure_too_long_url(self):
                 'title': 'title',
                 'type': 'type',
                 'image_url': stringof2001chars,
-                'url': 'url',
+                'url': 'http://test.com',
                 'authors': staff.user.username
             },
             follow=True
@@ -117,7 +137,7 @@ def test_failure_too_long_url(self):
             {
                 'title': 'title',
                 'type': 'type',
-                'image_url': 'url',
+                'image_url': 'http://test.com/image.png',
                 'url': stringof2001chars,
                 'authors': staff.user.username
             },
@@ -139,21 +159,45 @@ def test_success_update_featured(self):
 
         news = FeaturedResourceFactory()
         self.assertEqual(1, FeaturedResource.objects.all().count())
-        response = self.client.post(
-            reverse('featured-resource-update', args=[news.pk]),
-            {
-                'title': 'title',
-                'type': 'type',
-                'image_url': 'image_url',
-                'url': 'url',
-                'authors': staff.user.username
-            },
-            follow=True
-        )
+
+        old_featured = FeaturedResource.objects.first()
+
+        pubdate = date(2016, 1, 1).strftime('%d/%m/%Y %H:%M:%S')
+
+        fields = {
+            'title': 'title',
+            'type': 'type',
+            'image_url': 'http://test.com/image.png',
+            'url': 'http://test.com',
+            'authors': staff.user.username,
+            'pubdate': pubdate
+        }
+
+        response = self.client.post(reverse('featured-resource-update', args=[news.pk]), fields, follow=True)
 
         self.assertEqual(200, response.status_code)
         self.assertEqual(1, FeaturedResource.objects.all().count())
 
+        featured = FeaturedResource.objects.first()
+
+        for field, value in fields.items():
+            self.assertNotEqual(getattr(featured, field), getattr(old_featured, field))
+
+            if field != 'pubdate':
+                self.assertEqual(value, getattr(featured, field), msg='Error on {}'.format(field))
+            else:
+                self.assertEqual(value, featured.pubdate.strftime('%d/%m/%Y %H:%M:%S'))
+
+        # now with major_update
+        self.assertFalse((datetime.now() - featured.pubdate).total_seconds() < 10)
+
+        fields['major_update'] = 'on'
+
+        response = self.client.post(reverse('featured-resource-update', args=[news.pk]), fields, follow=True)
+        self.assertEqual(200, response.status_code)
+        featured = FeaturedResource.objects.first()
+        self.assertTrue((datetime.now() - featured.pubdate).total_seconds() < 10)
+
     def test_failure_create_featured_with_unauthenticated_user(self):
         response = self.client.get(reverse('featured-resource-update', args=[42]))
 
@@ -266,7 +310,7 @@ def test_success_list_create_message(self):
             reverse('featured-message-create'),
             {
                 'message': 'message',
-                'url': 'url',
+                'url': 'http://test.com',
             },
             follow=True
         )
@@ -286,7 +330,7 @@ def test_create_only_one_message_in_system(self):
             reverse('featured-message-create'),
             {
                 'message': 'message',
-                'url': 'url',
+                'url': 'http://test.com',
             },
             follow=True
         )
@@ -298,7 +342,7 @@ def test_create_only_one_message_in_system(self):
             reverse('featured-message-create'),
             {
                 'message': 'message',
-                'url': 'url',
+                'url': 'http://test.com',
             },
             follow=True
         )