Repository: zestedesavoir/zds-site

Issue:
Accès aux tutoriels en béta impossible
Via [Étoile filante](http://zestedesavoir.com/forums/sujet/321/tutoriels-en-beta-test/) : 

il est impossible d'accéder aux tutoriels en béta lorsqu'on est membre ou staff, y'a que les admins qui ont le droit (en tout cas en local, et confirmé par l'interventon d'Ariusi). Puisque j'ai identifié le bug et sa solution, je me propose, si vous me le permettez, de le régler (en plus, c'est pas très compliqué, en  lignes c'est fait, et faut bien que je débute par quelque part)



Discussion:
J'ai rajouté les tags qui vont bien. Tu sembles avoir une bonne piste expliquée sur les forums, ça devrait être assez simple. Par contre, assigne toi la tache (bizarrement, je ne peux pas le faire moi). Comme ça, on sait en un coup d'oeil qu'elle est prise par quelqu'un.

Je n'arrive pas à me l'assigner non plus. À mon avis, c'est parce que j'ai oublié de demander une autorisation à Sandhose/Alex-D/quelqu'un d'autre pour faire ce que je fait, je règle ça de suite.

Merci pour les tags (ça non plus je ne pouvais pas le faire) :)

> Je n'arrive pas à me l'assigner non plus

C'est des contraintes sur l'administration du GH. Je vais me l'assigner pour dire qu'elle est prise, je met le label "En cours" et je te laisse travaille :)

Merci bien :)

Bon, par contre, je me suis rendu compte que le problème était un peu plus important que ce que je ne pensais à la base. En effet, ce que propose actuellement le site, c'est un accès direct au tuto "offline", ce qui inclut tout le reste ("éditer", "ajouter" et ainsi de suite). Évidement, c'est pas la bonne solution.

Du coup, deux idées me viennent : 
1. Rattacher des nouvelles fonctions et des nouvelles URL (genre "/tutoriels/beta/%pk%/%slug%/" et ainsi de suite). À ce moment là, ce qui peut servir, c'est les templates de base de visualisation des tutos, avec limite un bandeau "ceci est une béta". Défaut, ça signifie aller chipoter des templates (entre autre la template de visualisation des tutos et la template qui donne les liens des tutos en béta pour donner les liens corrects) et j'ai cru comprendre que j'avais pas le droit de toucher à ça sans l'accord au moins d'@Alex-D
2. Modifier la template de visualisation de tutos "offline" pour qu'elle n'affiche pas les options "éditer" et ainsi de suite si le membre n'est ni auteur, ni staff. Si la première solution ne me plait pas parce que je doit chipoter des trucs auquel j'ai pas le droit de toucher, celle-ci me plait encore moins parce que c'est du bricolage qui n'as pas de sens.

Conclusion, je demande votre avis.

Je réagi sur un truc seulement : si tu peux toucher a tout sans l'avis de personne. Il y a une phase de review puis de tests pour s'assurer que le code est propre et fonctionnel. Mais non il n'y a personne en particulier a prévenir. Si c'est Alex qui fait la review il fera des remarques. Mais il y en a d'autres qui peuvent s'en occuper, comme sandhose

Comme tu dis, la seconde solution est un peu du bricolage. Ca serait dommage d'en arriver là. Et comme le dit Christophe, tu as tout à fait le droit de toucher aux templates (projet open source toussa). Tu passeras juste par une review d'Alex-D.

Ou par une review de quelqu'un d'autre.
Le 15 juin 2014 13:02, "Gerard" notifications@github.com a écrit :

> Comme tu dis, la seconde solution est un peu du bricolage. Ca serait
> dommage d'en arriver là. Et comme le dit Christophe, tu as tout à fait le
> droit de toucher aux templates (projet open source toussa). Tu passeras
> juste par une review d'Alex-D.
> 
> —
> Reply to this email directly or view it on GitHub
> https://github.com/zestedesavoir/zds-site/issues/926#issuecomment-46112851
> .

PS, hors sujet: Si vous avez besoin d'assigner un ticket à quelqu'un qui n'est pas encore dans la liste, prévenez-moi et je m'occupe de l'ajouter. ;)

À la réflexion, je classifie ce bug en Bloquant bêta publique, puisqu'il rend une feature (la bêta zone des tutos) inutilisable.

Pour te répondre Pierre, les PR passent par une phase de QA (validation qualité) qu'on met en place depuis hier, pour valider qu'elles ne pètent rien fonctionnellement. Les reviews sur le code sont un peu plus informelles : je propose qu'on se débrouille/s'organise pour les faire sur le chan zds-dev sur (irc.smoothirc.net).

Hello,

J'ai avancé dans le machin, à mon avis je devrais plus en avoir pour trop longtemps. Juste quelques points qui mérite réflexion, selon moi : 
1. Est ce qu'on a le droit de commenter un tuto en béta ? Si oui, est ce que ces commentaires doivent disparaitre avec la publication ? Avec chaque nouvelle béta ? Autre ?
2. Dans le processus, on se promène avec un SHA qui correspond à une certaine version du tuto. L'auteur (ou les auteurs) peut (peuvent) avoir envie de mettre la béta à jour, pour ce faire, il doivent désactiver et réactiver la béta. Un bouton "mettre à niveau la béta" (ou un terme plus joli) ne serait-il pas plus pertinent ?
3. Comme ça a été discuté sur le forum, peut-être que les tutos en béta doivent passer dans une catégorie à part. Plusieurs manière de faire ça : à l'aide d'un tag, ou en dur dans le code. A voir, donc.

(pour le reste, [voilà ou j'en suis](https://github.com/pierre-24/zds-site/commit/cff9bb1b9d5db1ad0c8b1482a78e72d1cc13f1b7))

> 1. Est ce qu'on a le droit de commenter un tuto en béta ? Si oui, est ce que ces commentaires doivent disparaitre avec la publication ? Avec chaque nouvelle béta ? Autre ?

Très bonnes questions !

Il me semblait qu'il avait été décidé que seuls les staff + les auteurs auraient le droit de commenter un tutoriel en beta.

Pour moi, oui les commentaires en beta doivent être différents de ceux de la version en ligne. Faut bien garder en tête que les deux peuvent cohabiter : beta et publiée peuvent être activés en même temps ; ça pointe vers deux sha différents. A chaque beta : je ne sais pas trop, d'un côté on a envie de dire oui car certains commentaires seront dépassés, mais d'autres ne le seront pas, du coup c'est délicat. En conclusion mon avis serait de les laisser au cours de toutes les beta ; en tous cas  ne pas les retirer à la MàJ du sha de la beta.
1. J'aime l'idée ! Beaucoup plus pratique.
2. Mon avis est qu'il faudrait une page dédiée à lister les tutoriels en beta. Les tags et autres sont déjà utilisés et ont un sens.

> Il me semblait qu'il avait été décidé que seuls les staff + les auteurs auraient le droit de commenter un tutoriel en beta.

Ça me semble pas trop complexe à implémenter

> Pour moi, oui les commentaires en beta doivent être différents de ceux de la version en ligne. Faut bien garder en tête que les deux peuvent cohabiter : beta et publiée peuvent être activés en même temps ; ça pointe vers deux sha différents. A chaque beta : je ne sais pas trop, d'un côté on a envie de dire oui car certains commentaires seront dépassés, mais d'autres ne le seront pas, du coup c'est délicat. En conclusion mon avis serait de les laisser au cours de toutes les beta ; en tous cas ne pas les retirer à la MàJ du sha de la beta.

Ok. J'ai pas encore tout compris au système de commentaires, mais je vais voir ce que je peux faire à ce niveau là.

> J'aime l'idée ! Beaucoup plus pratique.

J'ajoute ça

> Mon avis est qu'il faudrait une page dédiée à lister les tutoriels en beta. Les tags et autres sont déjà utilisés et ont un sens.

Je préfère alors ouvrir une issue externe pour discuter de ça, l'histoire de pas faire n'importe quoi tout seul dans mon coin.

A noter : les auteurs = membres ayant au moins un tutoriel en ligne, pas en beta.

En résumé pour les commentaires, il doit y avoir deux flux : publié, beta. Le flux publié c'est pour la version publique du tuto. Le flux beta c'est pour que les auteurs puissent commenter le tutoriel et donner leur avis : ça n'a rien à faire en ligne vois-tu ?

Ah, non, t'inquiète, quand je disais que j'avais pas encore tout compris au principe des commentaires, je parlais du code, mais je suis en train de regarder à ça ;)


Resolving patch:
diff --git a/zds/tutorial/factories.py b/zds/tutorial/factories.py
--- a/zds/tutorial/factories.py
+++ b/zds/tutorial/factories.py
@@ -55,6 +55,7 @@ def _prepare(cls, create, **kwargs):
         cm = repo.index.commit("Init Tuto")
 
         tuto.sha_draft = cm.hexsha
+        tuto.sha_beta = None
         return tuto
 
 
diff --git a/zds/tutorial/urls.py b/zds/tutorial/urls.py
--- a/zds/tutorial/urls.py
+++ b/zds/tutorial/urls.py
@@ -80,11 +80,6 @@
                            'zds.tutorial.views.diff'),
 
                        # user actions
-                       url(r'^activation/beta/(?P<tutorial_pk>\d+)/(?P<version>.+)/$',
-                           'zds.tutorial.views.activ_beta'),
-                       url(
-                           r'^desactivation/beta/(?P<tutorial_pk>\d+)/(?P<version>.+)/$',
-                           'zds.tutorial.views.desactiv_beta'),
                        url(r'^suppression/(?P<tutorial_pk>\d+)/$',
                            'zds.tutorial.views.delete_tutorial'),
                        url(r'^validation/tutoriel/$',
diff --git a/zds/tutorial/views.py b/zds/tutorial/views.py
--- a/zds/tutorial/views.py
+++ b/zds/tutorial/views.py
@@ -418,32 +418,6 @@ def invalid_tutorial(request, tutorial_pk):
 
 # User actions on tutorial.
 
-@can_write_and_read_now
-@login_required
-def activ_beta(request, tutorial_pk, version):
-    tutorial = get_object_or_404(Tutorial, pk=tutorial_pk)
-    if request.user not in tutorial.authors.all():
-        if not request.user.has_perm("tutorial.change_tutorial"):
-            raise PermissionDenied
-    tutorial.sha_beta = version
-    tutorial.save()
-    messages.success(request, u"La BETA sur ce tutoriel est bien activée.")
-    return redirect(tutorial.get_absolute_url_beta())
-
-
-@can_write_and_read_now
-@login_required
-def desactiv_beta(request, tutorial_pk, version):
-    tutorial = get_object_or_404(Tutorial, pk=tutorial_pk)
-    if request.user not in tutorial.authors.all():
-        if not request.user.has_perm("tutorial.change_tutorial"):
-            raise PermissionDenied
-    tutorial.sha_beta = None
-    tutorial.save()
-    messages.info(request, u"La BETA sur ce tutoriel a été désactivée.")
-    return redirect(tutorial.get_absolute_url_beta())
-
-
 @can_write_and_read_now
 @login_required
 @require_POST
@@ -600,8 +574,29 @@ def modify_tutorial(request):
                              u"L'auteur {0} a bien été retiré du tutoriel."
                              .format(author.username))
             return redirect(redirect_url)
+        elif "activ_beta" in request.POST:
+            if "version" in request.POST and tutorial.sha_draft == request.POST['version'] :
+                tutorial.sha_beta = tutorial.sha_draft
+                tutorial.save()
+                messages.success(request, u"La BETA sur ce tutoriel est bien activée.")
+            else:
+                messages.error(request, u"Impossible d'activer la BETA sur ce tutoriel.")
+            return redirect(tutorial.get_absolute_url_beta())
+        elif "update_beta" in request.POST:
+            if "version" in request.POST and tutorial.sha_draft == request.POST['version'] :
+                tutorial.sha_beta = tutorial.sha_draft
+                tutorial.save()
+                messages.success(request, u"La BETA sur ce tutoriel a bien été mise à jour.")
+            else:
+                messages.error(request, u"Impossible de mettre à jour la BETA sur ce tutoriel.")
+            return redirect(tutorial.get_absolute_url_beta())
+        elif "desactiv_beta" in request.POST:
+            tutorial.sha_beta = None
+            tutorial.save()
+            messages.info(request, u"La BETA sur ce tutoriel a été désactivée.")
+            return redirect(tutorial.get_absolute_url())
 
-    # No action performed, raise 404
+    # No action performed, raise 403
 
     raise PermissionDenied
 


Test patch:
diff --git a/zds/tutorial/tests.py b/zds/tutorial/tests.py
--- a/zds/tutorial/tests.py
+++ b/zds/tutorial/tests.py
@@ -789,6 +789,172 @@ def test_delete_image_tutorial(self):
         self.assertEqual(1, Tutorial.objects.filter(pk=self.bigtuto.pk).count())
         self.assertTrue(Tutorial.objects.get(pk=self.bigtuto.pk).image == None)
 
+    def test_workflow_beta_tuto(self) :
+        "Ensure the behavior of the beta version of tutorials"
+
+        # logout before
+        self.client.logout()
+        # first, login with author :
+        self.assertEqual(
+            self.client.login(
+                username=self.user_author.username,
+                password='hostel77'),
+            True)
+        # then active the beta on tutorial :
+        sha_draft = Tutorial.objects.get(pk=self.bigtuto.pk).sha_draft
+        response = self.client.post(
+                reverse('zds.tutorial.views.modify_tutorial'),
+                {
+                    'tutorial': self.bigtuto.pk,
+                    'activ_beta': True,
+                    'version': sha_draft
+                },
+                follow=False
+        )
+        self.assertEqual(302, response.status_code)
+        # test beta :
+        self.assertEqual(
+            Tutorial.objects.get(pk=self.bigtuto.pk).sha_beta, 
+            sha_draft)
+        url = Tutorial.objects.get(pk=self.bigtuto.pk).get_absolute_url_beta()
+        sha_beta = sha_draft
+        # Test access for author (get 200)
+        self.assertEqual(
+            self.client.get(url).status_code,
+            200)
+        # test access from outside (get 302, connexion form)
+        self.client.logout()
+        self.assertEqual(
+            self.client.get(url).status_code,
+            302)
+        # test access for random user (get 200)
+        self.assertEqual(
+            self.client.login(
+                username=self.user.username,
+                password='hostel77'),
+            True)
+        self.assertEqual(
+            self.client.get(url).status_code,
+            200)
+        
+        # then modify tutorial
+        self.assertEqual(
+            self.client.login(
+                username=self.user_author.username,
+                password='hostel77'),
+            True)
+        result = self.client.post(
+            reverse( 'zds.tutorial.views.add_extract') +
+            '?chapitre={0}'.format(
+                self.chapter2_1.pk),
+
+            {
+            'title' : "Introduction",
+            'text' : u"Le contenu de l'extrait"
+            })
+        self.assertEqual(result.status_code, 302)
+        self.assertEqual(
+            Tutorial.objects.get(pk=self.bigtuto.pk).sha_beta, 
+            sha_beta)
+        self.assertNotEqual(
+            Tutorial.objects.get(pk=self.bigtuto.pk).sha_draft, 
+            sha_beta)
+        # update beta
+        sha_draft = Tutorial.objects.get(pk=self.bigtuto.pk).sha_draft
+        response = self.client.post(
+                reverse('zds.tutorial.views.modify_tutorial'),
+                {
+                    'tutorial': self.bigtuto.pk,
+                    'update_beta': True,
+                    'version': sha_draft
+                },
+                follow=False
+        )
+        self.assertEqual(302, response.status_code)
+        old_url = url
+        url = Tutorial.objects.get(pk=self.bigtuto.pk).get_absolute_url_beta()
+        # test access to new beta url (get 200) :
+        self.assertEqual(
+            self.client.get(old_url).status_code,
+            200)
+        # test access for random user to new url (get 200) and old (get 403)
+        self.assertEqual(
+            self.client.login(
+                username=self.user.username,
+                password='hostel77'),
+            True)
+        self.assertEqual(
+            self.client.get(url).status_code,
+            200)
+        self.assertEqual(
+            self.client.get(old_url).status_code,
+            403)
+
+        # then desactive beta :
+        self.assertEqual(
+            self.client.login(
+                username=self.user_author.username,
+                password='hostel77'),
+            True)
+        response = self.client.post(
+                reverse('zds.tutorial.views.modify_tutorial'),
+                {
+                    'tutorial': self.bigtuto.pk,
+                    'desactiv_beta': True,
+                    'version': sha_draft
+                },
+                follow=False
+        )
+        self.assertEqual(302, response.status_code)
+        self.assertEqual(
+            Tutorial.objects.get(pk=self.bigtuto.pk).sha_beta, 
+            None)
+        # test access from outside (get 302, connexion form)
+        self.client.logout()
+        self.assertEqual(
+            self.client.get(url).status_code,
+            302)
+        # test access for random user (get 403)
+        self.assertEqual(
+            self.client.login(
+                username=self.user.username,
+                password='hostel77'),
+            True)
+        self.assertEqual(
+            self.client.get(url).status_code,
+            403)
+
+        # ensure staff behavior (staff can also active/update/desactive beta)
+        self.assertEqual(
+            self.client.login(
+                username=self.staff.username,
+                password='hostel77'),
+            True)
+        response = self.client.post(
+                reverse('zds.tutorial.views.modify_tutorial'),
+                {
+                    'tutorial': self.bigtuto.pk,
+                    'activ_beta': True,
+                    'version': sha_draft
+                },
+                follow=False
+        )
+        self.assertEqual(302, response.status_code)
+        # test access from outside (get 302, connexion form)
+        self.client.logout()
+        self.assertEqual(
+            self.client.get(url).status_code,
+            302)
+        # test access for random user (get 200)
+        self.assertEqual(
+            self.client.login(
+                username=self.user.username,
+                password='hostel77'),
+            True)
+        self.assertEqual(
+            self.client.get(url).status_code,
+            200)
+
     def tearDown(self):
         if os.path.isdir(settings.REPO_PATH):
             shutil.rmtree(settings.REPO_PATH)
@@ -1443,6 +1609,172 @@ def test_delete_image_tutorial(self):
         self.assertEqual(1, Tutorial.objects.filter(pk=self.minituto.pk).count())
         self.assertTrue(Tutorial.objects.get(pk=self.minituto.pk).image == None)
 
+    def test_workflow_beta_tuto(self) :
+        "Ensure the behavior of the beta version of tutorials"
+
+        # logout before
+        self.client.logout()
+        # first, login with author :
+        self.assertEqual(
+            self.client.login(
+                username=self.user_author.username,
+                password='hostel77'),
+            True)
+        # then active the beta on tutorial :
+        sha_draft = Tutorial.objects.get(pk=self.minituto.pk).sha_draft
+        response = self.client.post(
+                reverse('zds.tutorial.views.modify_tutorial'),
+                {
+                    'tutorial': self.minituto.pk,
+                    'activ_beta': True,
+                    'version': sha_draft
+                },
+                follow=False
+        )
+        self.assertEqual(302, response.status_code)
+        # test beta :
+        self.assertEqual(
+            Tutorial.objects.get(pk=self.minituto.pk).sha_beta, 
+            sha_draft)
+        url = Tutorial.objects.get(pk=self.minituto.pk).get_absolute_url_beta()
+        sha_beta = sha_draft
+        # Test access for author (get 200)
+        self.assertEqual(
+            self.client.get(url).status_code,
+            200)
+        # test access from outside (get 302, connexion form)
+        self.client.logout()
+        self.assertEqual(
+            self.client.get(url).status_code,
+            302)
+        # test access for random user (get 200)
+        self.assertEqual(
+            self.client.login(
+                username=self.user.username,
+                password='hostel77'),
+            True)
+        self.assertEqual(
+            self.client.get(url).status_code,
+            200)
+        
+        # then modify tutorial
+        self.assertEqual(
+            self.client.login(
+                username=self.user_author.username,
+                password='hostel77'),
+            True)
+        result = self.client.post(
+            reverse( 'zds.tutorial.views.add_extract') +
+            '?chapitre={0}'.format(
+                self.chapter.pk),
+
+            {
+            'title' : "Introduction",
+            'text' : u"Le contenu de l'extrait"
+            })
+        self.assertEqual(result.status_code, 302)
+        self.assertEqual(
+            Tutorial.objects.get(pk=self.minituto.pk).sha_beta, 
+            sha_beta)
+        self.assertNotEqual(
+            Tutorial.objects.get(pk=self.minituto.pk).sha_draft, 
+            sha_beta)
+        # update beta
+        sha_draft = Tutorial.objects.get(pk=self.minituto.pk).sha_draft
+        response = self.client.post(
+                reverse('zds.tutorial.views.modify_tutorial'),
+                {
+                    'tutorial': self.minituto.pk,
+                    'update_beta': True,
+                    'version': sha_draft
+                },
+                follow=False
+        )
+        self.assertEqual(302, response.status_code)
+        old_url = url
+        url = Tutorial.objects.get(pk=self.minituto.pk).get_absolute_url_beta()
+        # test access to new beta url (get 200) :
+        self.assertEqual(
+            self.client.get(old_url).status_code,
+            200)
+        # test access for random user to new url (get 200) and old (get 403)
+        self.assertEqual(
+            self.client.login(
+                username=self.user.username,
+                password='hostel77'),
+            True)
+        self.assertEqual(
+            self.client.get(url).status_code,
+            200)
+        self.assertEqual(
+            self.client.get(old_url).status_code,
+            403)
+
+        # then desactive beta :
+        self.assertEqual(
+            self.client.login(
+                username=self.user_author.username,
+                password='hostel77'),
+            True)
+        response = self.client.post(
+                reverse('zds.tutorial.views.modify_tutorial'),
+                {
+                    'tutorial': self.minituto.pk,
+                    'desactiv_beta': True,
+                    'version': sha_draft
+                },
+                follow=False
+        )
+        self.assertEqual(302, response.status_code)
+        self.assertEqual(
+            Tutorial.objects.get(pk=self.minituto.pk).sha_beta, 
+            None)
+        # test access from outside (get 302, connexion form)
+        self.client.logout()
+        self.assertEqual(
+            self.client.get(url).status_code,
+            302)
+        # test access for random user (get 403)
+        self.assertEqual(
+            self.client.login(
+                username=self.user.username,
+                password='hostel77'),
+            True)
+        self.assertEqual(
+            self.client.get(url).status_code,
+            403)
+
+        # ensure staff behavior (staff can also active/update/desactive beta)
+        self.assertEqual(
+            self.client.login(
+                username=self.staff.username,
+                password='hostel77'),
+            True)
+        response = self.client.post(
+                reverse('zds.tutorial.views.modify_tutorial'),
+                {
+                    'tutorial': self.minituto.pk,
+                    'activ_beta': True,
+                    'version': sha_draft
+                },
+                follow=False
+        )
+        self.assertEqual(302, response.status_code)
+        # test access from outside (get 302, connexion form)
+        self.client.logout()
+        self.assertEqual(
+            self.client.get(url).status_code,
+            302)
+        # test access for random user (get 200)
+        self.assertEqual(
+            self.client.login(
+                username=self.user.username,
+                password='hostel77'),
+            True)
+        self.assertEqual(
+            self.client.get(url).status_code,
+            200)
+
     def tearDown(self):
         if os.path.isdir(settings.REPO_PATH):
             shutil.rmtree(settings.REPO_PATH)