From 9f7dc619d2604715d7f8cc2420aee97442f12dfa Mon Sep 17 00:00:00 2001 From: Arnaud-D <35631001+Arnaud-D@users.noreply.github.com> Date: Sat, 30 Oct 2021 11:43:22 +0200 Subject: [PATCH 01/53] =?UTF-8?q?Corrige=20deux=20tests=20mal=20nomm=C3=A9?= =?UTF-8?q?s=20et=20donc=20non=20d=C3=A9couverts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/tutorialv2/tests/tests_views/tests_content.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/zds/tutorialv2/tests/tests_views/tests_content.py b/zds/tutorialv2/tests/tests_views/tests_content.py index 32e0d8fb55..411ba5cd8b 100644 --- a/zds/tutorialv2/tests/tests_views/tests_content.py +++ b/zds/tutorialv2/tests/tests_views/tests_content.py @@ -1648,7 +1648,7 @@ def test_import_in_existing_content(self): # clean up os.remove(draft_zip_path) - def import_with_bad_title(self): + def test_import_with_bad_title(self): """Tests an error case that happen when someone sends an archive that modify the content title with a string that cannont be properly slugified""" new_article = PublishableContentFactory(type="ARTICLE", title="extension", authors=[self.user_author]) @@ -3270,7 +3270,7 @@ def test_publication_make_extra_contents(self): if extra == "md": continue result = self.client.get(published.get_absolute_url_to_extra_content(extra)) - self.assertEqual(result.status_code, 200, msg="Could not read {} export".format(extra)) + self.assertEqual(result.status_code, 200, msg=f"Could not read {extra} export") def test_publication_give_pubdate_if_no_major(self): """if a content has never been published and `is_major` is not checked, still gives a publication date""" @@ -3352,7 +3352,7 @@ def test_publication_give_pubdate_if_no_major(self): tuto = PublishableContent.objects.get(pk=tuto.pk) self.assertEqual(tuto.pubdate, current_pubdate) # `is_major` in False → no update of the publication date - def no_form_not_allowed(self): + def test_no_form_not_allowed(self): """Check that author cannot access to form that he is not allowed to in the creation process, because - The container already have child container and author ask to add a child extract ; - The container already contains child extract and author ask to add a container.""" From 9caed20b4116d1de7ff63c9a7efa386c1339d768 Mon Sep 17 00:00:00 2001 From: Arnaud-D <35631001+Arnaud-D@users.noreply.github.com> Date: Sun, 31 Oct 2021 09:43:07 +0100 Subject: [PATCH 02/53] =?UTF-8?q?Corrige=20un=20test=20qui=20n'=C3=A9tait?= =?UTF-8?q?=20pas=20jou=C3=A9=20pr=C3=A9c=C3=A9demment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/tutorialv2/tests/tests_views/tests_content.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/zds/tutorialv2/tests/tests_views/tests_content.py b/zds/tutorialv2/tests/tests_views/tests_content.py index 411ba5cd8b..1aa28ac807 100644 --- a/zds/tutorialv2/tests/tests_views/tests_content.py +++ b/zds/tutorialv2/tests/tests_views/tests_content.py @@ -1651,14 +1651,13 @@ def test_import_in_existing_content(self): def test_import_with_bad_title(self): """Tests an error case that happen when someone sends an archive that modify the content title with a string that cannont be properly slugified""" - new_article = PublishableContentFactory(type="ARTICLE", title="extension", authors=[self.user_author]) + new_article = PublishableContentFactory(type="ARTICLE", title="extension", author_list=[self.user_author]) self.client.force_login(self.user_author) archive_path = settings.BASE_DIR / "fixtures" / "tuto" / "BadArchive.zip" answer = self.client.post( reverse("content:import", args=[new_article.pk, new_article.slug]), { "archive": archive_path.open("rb"), - "image_archive": None, "msg_commit": "let it go, let it goooooooo ! can't hold it back anymoooooore!", }, ) From f3080ee475122fdd14fd502fbd2aff964f6a356c Mon Sep 17 00:00:00 2001 From: Arnaud-D <35631001+Arnaud-D@users.noreply.github.com> Date: Sun, 31 Oct 2021 20:48:16 +0100 Subject: [PATCH 03/53] Retire le champ de recherche (inactif) sur les profils (#6201) --- templates/member/profile.html | 8 -------- 1 file changed, 8 deletions(-) diff --git a/templates/member/profile.html b/templates/member/profile.html index f31bffab95..ad8ced65e3 100644 --- a/templates/member/profile.html +++ b/templates/member/profile.html @@ -119,14 +119,6 @@

{% endfor %}
- -
+ + {# Checks if it's an IPV6 to show the members from the same IPV6 network #} + {% if ":" in ip %} +

+ {% blocktrans %} + Liste des membres dont la dernière IP connue fait partie du bloc {{ network_ip }} + {% endblocktrans %} +

+ +
+ +
+ +

+ En IPv6, les adresses sont attribuées par bloc d'IP. Un bot de spam peut donc facilement changer d'adresse IP au sein de ce bloc. Sont affichés ici tous les membres dont l'IPv6 fait partie du même bloc que l'IP demandée. +

+ {% endif %} {% endblock %} diff --git a/zds/member/tests/tests_views.py b/zds/member/tests/tests_views.py index 0cafd05363..d2d61071f3 100644 --- a/zds/member/tests/tests_views.py +++ b/zds/member/tests/tests_views.py @@ -14,6 +14,7 @@ from django.utils.html import escape from django.utils.translation import gettext_lazy as _ +from zds.member.views import member_from_ip from zds.notification.models import TopicAnswerSubscription from zds.member.factories import ( ProfileFactory, @@ -1723,3 +1724,97 @@ def send_messages(messages): ) self.assertEqual(result.status_code, 200) self.assertIn(escape("Impossible d'envoyer l'email."), result.content.decode("utf-8")) + + +class IpListingsTests(TestCase): + """Test the member_from_ip function : listing users from a same IPV4/IPV6 address or same IPV6 network.""" + + def setUp(self) -> None: + self.staff = StaffProfileFactory().user + self.regular_user = ProfileFactory() + + self.user_ipv4_same_ip_1 = ProfileFactory(last_ip_address="155.128.92.54") + self.user_ipv4_same_ip_1.user.username = "user_ipv4_same_ip_1" + self.user_ipv4_same_ip_1.user.save() + + self.user_ipv4_same_ip_2 = ProfileFactory(last_ip_address="155.128.92.54") + self.user_ipv4_same_ip_2.user.username = "user_ipv4_same_ip_2" + self.user_ipv4_same_ip_2.user.save() + + self.user_ipv4_different_ip = ProfileFactory(last_ip_address="155.128.92.55") + self.user_ipv4_different_ip.user.username = "user_ipv4_different_ip" + self.user_ipv4_different_ip.user.save() + + self.user_ipv6_same_ip_1 = ProfileFactory(last_ip_address="2001:8f8:1425:60a0:7981:9852:1493:3721") + self.user_ipv6_same_ip_1.user.username = "user_ipv6_same_ip_1" + self.user_ipv6_same_ip_1.user.save() + + self.user_ipv6_same_ip_2 = ProfileFactory(last_ip_address="2001:8f8:1425:60a0:7981:9852:1493:3721") + self.user_ipv6_same_ip_2.user.username = "user_ipv6_same_ip_2" + self.user_ipv6_same_ip_2.user.save() + + self.user_ipv6_same_network = ProfileFactory(last_ip_address="2001:8f8:1425:60a0:9852:7981:3721:1493") + self.user_ipv6_same_network.user.username = "user_ipv6_same_network" + self.user_ipv6_same_network.user.save() + + self.user_ipv6_different_network = ProfileFactory(last_ip_address="8f8:60a0:3721:1425:7981:1493:2001:9852") + self.user_ipv6_different_network.user.username = "user_ipv6_different_network" + self.user_ipv6_different_network.user.save() + + def test_same_ipv4(self) -> None: + self.client.force_login(self.staff) + response = self.client.get(reverse(member_from_ip, args=[self.user_ipv4_same_ip_1.last_ip_address])) + self.assertContains(response, self.user_ipv4_same_ip_1.user.username) + self.assertContains(response, self.user_ipv4_same_ip_2.user.username) + self.assertContains(response, self.user_ipv4_same_ip_1.last_ip_address) + self.assertNotContains(response, self.user_ipv4_different_ip.user.username) + + def test_different_ipv4(self) -> None: + self.client.force_login(self.staff) + response = self.client.get(reverse(member_from_ip, args=[self.user_ipv4_different_ip.last_ip_address])) + self.assertContains(response, self.user_ipv4_different_ip.user.username) + self.assertContains(response, self.user_ipv4_different_ip.last_ip_address) + self.assertNotContains(response, self.user_ipv6_same_ip_1.user.username) + + def test_same_ipv6_and_same_ipv6_network(self) -> None: + self.client.force_login(self.staff) + response = self.client.get(reverse(member_from_ip, args=[self.user_ipv6_same_ip_1.last_ip_address])) + self.assertContains(response, self.user_ipv6_same_ip_1.user.username) + self.assertContains(response, self.user_ipv6_same_ip_2.user.username) + self.assertContains(response, self.user_ipv6_same_network.user.username) + self.assertNotContains(response, self.user_ipv6_different_network.user.username) + + def test_same_ipv6_network_but_different_ip(self) -> None: + self.client.force_login(self.staff) + response = self.client.get(reverse(member_from_ip, args=[self.user_ipv6_same_network.last_ip_address])) + self.assertContains(response, self.user_ipv6_same_network.user.username) + self.assertContains(response, self.user_ipv6_same_ip_1.user.username) + self.assertContains(response, self.user_ipv6_same_ip_2.user.username) + self.assertNotContains(response, self.user_ipv6_different_network.user.username) + + def test_different_ipv6_network(self) -> None: + self.client.force_login(self.staff) + response = self.client.get(reverse(member_from_ip, args=[self.user_ipv6_different_network.last_ip_address])) + self.assertContains(response, self.user_ipv6_different_network.user.username) + self.assertNotContains(response, self.user_ipv6_same_ip_1.user.username) + self.assertNotContains(response, self.user_ipv6_same_ip_2.user.username) + self.assertNotContains(response, self.user_ipv6_same_network.user.username) + + def test_access_rights_to_ip_page_as_regular_user(self) -> None: + self.client.force_login(self.regular_user.user) + response = self.client.get(reverse(member_from_ip, args=["0.0.0.0"])) + self.assertEqual(response.status_code, 403) + + def test_access_rights_to_ip_page_as_anonymous(self) -> None: + response = self.client.get(reverse(member_from_ip, args=["0.0.0.0"])) + self.assertEqual(response.status_code, 302) + + def test_access_rights_to_ip_page_as_staff(self) -> None: + self.client.force_login(self.staff) + response = self.client.get(reverse(member_from_ip, args=["0.0.0.0"])) + self.assertEqual(response.status_code, 200) + + def test_template_used_by_ip_page(self) -> None: + self.client.force_login(self.staff) + response = self.client.get(reverse(member_from_ip, args=["0.0.0.0"])) + self.assertTemplateUsed(response, "member/admin/memberip.html") diff --git a/zds/member/views.py b/zds/member/views.py index aff1c6cf02..bd237803fe 100644 --- a/zds/member/views.py +++ b/zds/member/views.py @@ -1,3 +1,4 @@ +import ipaddress import uuid from datetime import datetime, timedelta from urllib.parse import unquote @@ -1457,10 +1458,20 @@ def settings_promote(request, user_pk): @login_required @permission_required("member.change_profile", raise_exception=True) def member_from_ip(request, ip_address): - """List users connected from a particular IP.""" + """List users connected from a particular IP, and an IPV6 subnetwork.""" members = Profile.objects.filter(last_ip_address=ip_address).order_by("-last_visit") - return render(request, "member/admin/memberip.html", {"members": members, "ip": ip_address}) + members_and_ip = {"members": members, "ip": ip_address} + + if ":" in ip_address: # Check if it's an IPV6 + network_ip = ipaddress.ip_network(ip_address + "/64", strict=False).network_address # Get the network / block + # Remove the additional ":" at the end of the network adresse, so we can filter the IP adresses on this network + network_ip = str(network_ip)[:-1] + network_members = Profile.objects.filter(last_ip_address__startswith=network_ip).order_by("-last_visit") + members_and_ip["network_members"] = network_members + members_and_ip["network_ip"] = network_ip + + return render(request, "member/admin/memberip.html", members_and_ip) @login_required From 188e5bc668422d9b40cd4ee0bd29167d0cf1648c Mon Sep 17 00:00:00 2001 From: "Ph. SW" <5911232+philippemilink@users.noreply.github.com> Date: Sun, 28 Nov 2021 17:09:57 +0100 Subject: [PATCH 14/53] =?UTF-8?q?Pr=C3=A9cise=20que=20changer=20une=20imag?= =?UTF-8?q?e=20ne=20la=20changera=20pas=20l=C3=A0=20o=C3=B9=20elle=20est?= =?UTF-8?q?=20d=C3=A9j=C3=A0=20utilis=C3=A9e=20(#6209)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/source/back-end/gallery.rst | 12 ++++++------ zds/gallery/forms.py | 3 +++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/doc/source/back-end/gallery.rst b/doc/source/back-end/gallery.rst index 4ec385d7a1..3a93d1b236 100644 --- a/doc/source/back-end/gallery.rst +++ b/doc/source/back-end/gallery.rst @@ -24,7 +24,7 @@ Il est ensuite possible d'uploader des images via le menu de gauche : Liens permettant d'uploader des images -Via celui-ci, on peut importer des archives contenant des images (au format ZIP) ou des images seules. Dans ce dernier cas, le formulaire d'*upload* est le suivant : +Via celui-ci, on peut importer des archives (au format ZIP) contenant des images ou des images seules. Dans ce dernier cas, le formulaire d'*upload* est le suivant : .. figure:: ../images/gallery/nouvelle-image.png :align: center @@ -33,6 +33,9 @@ Via celui-ci, on peut importer des archives contenant des images (au format ZIP) Comme on peut le voir, chaque image doit posséder au minimum un titre et peut posséder une légende, qui sera employée par la suite. Il est donc conseillé de remplir également ce second champ, bien que ce ne soit pas obligatoire. Quant à l'image elle-même, sa taille ne peut pas excéder 1024 Kio. +.. attention:: + Le titre de l'image n'entre pas en compte dans le nommage de l'image une fois cette dernière téléchargée. Afin de réduire le risque de rencontrer des conflits de noms de fichiers, ces derniers sont hashés. + Une fois l'image uploadée, il est possible d'y effectuer différentes actions sur la page qui lui est spécifique : .. figure:: ../images/gallery/gestion-image.png @@ -40,14 +43,11 @@ Une fois l'image uploadée, il est possible d'y effectuer différentes actions s Gestion d'une image -Autrement dit, +Autrement dit : -+ En modifier le titre, la légende ou encore l'image en elle-même. À noter que le titre et la légende peuvent être modifiés **sans qu'il ne soit nécessaire** d'uploader une nouvelle image. ++ En modifier le titre, la légende ou encore l'image en elle-même. À noter que le titre et la légende peuvent être modifiés **sans qu'il ne soit nécessaire** d'uploader une nouvelle image. Si une nouvelle version de l'image est uploadée, l'ancienne version de l'image n'est pas supprimée du serveur et reste accessible depuis son URL ; un nouvel identifiant (et donc une nouvelle URL) sera attribué à la nouvelle version de l'image. Cela signifie notamment que mettre à jour une image ne changera pas l'image là où elle a déjà été utilisée (tutoriel, article, message, ...). Ce comportement permet d'éviter que les images utilisées dans des contenus validés soient changées sans repasser par une validation. + Obtenir le code à insérer dans un champ de texte acceptant le Markdown pour l'image en elle-même, sa miniature ou encore la miniature accompagnée du lien vers l'image en taille réelle. -.. attention:: - Le titre de l'image n'entre pas en compte dans le nommage de l'image une fois cette dernière téléchargée. Afin de réduire le risque de rencontrer des conflits de noms de fichiers, ces derniers sont hashés. - Les utilisateurs et leurs droits -------------------------------- diff --git a/zds/gallery/forms.py b/zds/gallery/forms.py index f7f5512c18..6e6c6728dd 100644 --- a/zds/gallery/forms.py +++ b/zds/gallery/forms.py @@ -159,6 +159,9 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields["physical"].required = False + self.fields["physical"].label = _( + "Changer l'image (attention : cela ne changera pas l'image là où vous l'avez déjà utilisée ; un nouvel identifiant sera attribué à la nouvelle image)" + ) self.helper = FormHelper() self.helper.form_class = "clearfix" From 9e28a6281cbb171eb5a1c4b452c99342151eea95 Mon Sep 17 00:00:00 2001 From: "Ph. SW" <5911232+philippemilink@users.noreply.github.com> Date: Sun, 28 Nov 2021 17:21:10 +0100 Subject: [PATCH 15/53] =?UTF-8?q?Utilise=20les=20commandes=20make=20dans?= =?UTF-8?q?=20la=20documentation=20pour=20contribuer=20=C3=A0=20ZdS=20(#62?= =?UTF-8?q?14)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Et aussi : - supprime l'étape des tests front (il n'y en a pas/plus) - corrige quelques problèmes de formatage Co-authored-by: Arnaud-D <35631001+Arnaud-D@users.noreply.github.com> --- doc/source/contributing.rst | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/doc/source/contributing.rst b/doc/source/contributing.rst index 05aea6f295..1d19067107 100644 --- a/doc/source/contributing.rst +++ b/doc/source/contributing.rst @@ -18,11 +18,10 @@ Contribuer à Zeste De Savoir 1. Créez une branche pour contenir votre travail. 2. Faites vos modifications. 3. Ajoutez un test pour votre modification. Seules les modifications de documentation et les réusinages n'ont pas besoin de nouveaux tests. -4. Assurez-vous que vos tests passent en utilisant la commande ``python manage.py test`` (`voir la documentation `_). Lancer la commande sur tous les tests du site risque de prendre un certain temps et n'est pas nécessaire : les tests seront de toute manière lancés de manière automatisée sur votre *pull request*. -5. Si vous avez fait des modifications du _frontend_, jouez les tests associés : ``yarn test``. -6. Si vous modifiez les modèles (les fichiers ``models.py``), n'oubliez pas de créer les fichiers de migration : ``python manage.py makemigrations``. -7. Poussez votre travail et faites une *pull request*. -8. Si votre travail nécessite des actions spécifiques lors du déploiement, précisez-les dans le corps de votre *pull request*. Elles seront ajoutées au *changelog* par le mainteneur qui effectuera le *merge*. +4. Assurez-vous que vos tests passent en utilisant la commande ``make test-back`` (voyez la `page dédiée <./guides/backend-tests.html>`_ pour plus de détails). Lancer la commande sur tous les tests du site risque de prendre un certain temps et n'est pas nécessaire : les tests seront de toute manière lancés de manière automatisée sur votre *pull request*. +5. Si vous avez modifié les modèles (les fichiers ``models.py``), n'oubliez pas de créer les fichiers de migration : ``python manage.py makemigrations``. +6. Poussez votre travail et faites une *pull request*. +7. Si votre travail nécessite des actions spécifiques lors du déploiement, précisez-les dans le corps de votre *pull request*. Elles seront ajoutées au *changelog* par le mainteneur qui effectuera le *merge*. Quelques bonnes pratiques ------------------------- From a7858d810ac8c7ee22c2a2bd687b9a5d974039ed Mon Sep 17 00:00:00 2001 From: "Ph. SW" <5911232+philippemilink@users.noreply.github.com> Date: Sun, 28 Nov 2021 17:34:46 +0100 Subject: [PATCH 16/53] Grise les boutons de comparaisons de versions si elles sont identiques (#6181) Fix #6105 Co-authored-by: Arnaud-D <35631001+Arnaud-D@users.noreply.github.com> --- templates/tutorialv2/view/content.html | 57 +++++++++++++++++++------- zds/tutorialv2/mixins.py | 2 +- 2 files changed, 43 insertions(+), 16 deletions(-) diff --git a/templates/tutorialv2/view/content.html b/templates/tutorialv2/view/content.html index 707b006f6c..5c981f7e28 100644 --- a/templates/tutorialv2/view/content.html +++ b/templates/tutorialv2/view/content.html @@ -498,27 +498,54 @@

{# END ONLINE VERSION #} {% if content.sha_public %} -
  • - - {% trans "Comparer avec la version en ligne" %} - -
  • + {# interpreted as: (version and (content.sha_public != version)) or ((not version) and (content.sha_public != content.sha_draft)) #} + {% if version and content.sha_public != version or not version and content.sha_public != content.sha_draft %} +
  • + + {% trans "Comparer avec la version en ligne" %} + +
  • + {% else %} +
  • + + {% trans "Comparer avec la version en ligne (identique)" %} + +
  • + {% endif %} {% endif %} {% if content.sha_beta %} -
  • - - {% trans "Comparer avec la bêta" %} - -
  • + {# interpreted as: (version and (content.sha_beta != version)) or ((not version) and (content.sha_beta != content.sha_draft)) #} + {% if version and content.sha_beta != version or not version and content.sha_beta != content.sha_draft %} +
  • + + {% trans "Comparer avec la bêta" %} + +
  • + {% else %} +
  • + + {% trans "Comparer avec la bêta (identique)" %} + +
  • + {% endif %} {% endif %} {% if content.in_validation %} -
  • - - {% trans "Comparer avec la version en validation" %} - -
  • + {# interpreted as: (version and (validation.version != version)) or ((not version) and (validation.version != content.sha_draft)) #} + {% if version and validation.version != version or not version and validation.version != content.sha_draft %} +
  • + + {% trans "Comparer avec la version en validation" %} + +
  • + {% else %} +
  • + + {% trans "Comparer avec la version en validation (identique)" %} + +
  • + {% endif %} {% endif %} {# HISTORY #} diff --git a/zds/tutorialv2/mixins.py b/zds/tutorialv2/mixins.py index 8b40e99929..83c0ac3c34 100644 --- a/zds/tutorialv2/mixins.py +++ b/zds/tutorialv2/mixins.py @@ -221,7 +221,7 @@ def get_context_data(self, **kwargs): class SingleContentDetailViewMixin(SingleContentViewMixin, DetailView): """ - This enhanced DetailView ensure, + This enhanced DetailView ensures, - by rewriting `get()`, that: * `self.object` contains the result of `get_object()` (as it must be if `get()` is not rewritten) From f061d1b716931824dc6c8fa19f5048ec79067cbe Mon Sep 17 00:00:00 2001 From: "Ph. SW" <5911232+philippemilink@users.noreply.github.com> Date: Sun, 28 Nov 2021 17:47:58 +0100 Subject: [PATCH 17/53] Petites corrections pour l'installation de l'env de dev (#6152) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Met à jour pip dans l'environnement virtuel lors de l'installation Est nécessaire pour certaines dépendances en Rust, lorsque le pip utilisé est un peu trop vieux. Fix #6082 * Facilite et documente (légèrement) l'utilisation dans Docker Fix #6118 --- Makefile | 4 +-- doc/source/install/install-docker.rst | 51 +++++++++++++++++++++++++++ doc/source/install/install-linux.rst | 5 ++- scripts/install_zds.sh | 11 +++++- zds/settings/abstract_base/django.py | 7 ++-- zds/settings/dev.py | 3 +- 6 files changed, 73 insertions(+), 8 deletions(-) create mode 100644 doc/source/install/install-docker.rst diff --git a/Makefile b/Makefile index 6cc7aefcb3..ac95851e19 100644 --- a/Makefile +++ b/Makefile @@ -33,10 +33,10 @@ install-back-with-prod: pip install --upgrade -r requirements-dev.txt -r requirements-prod.txt run-back: zmd-check ## Run the backend server - python manage.py runserver --nostatic + python manage.py runserver --nostatic 0.0.0.0:8000 run-back-fast: zmd-check ## Run the backend server in fast mode (no debug toolbar & full browser cache) - python manage.py runserver --settings zds.settings.dev_fast + python manage.py runserver --settings zds.settings.dev_fast 0.0.0.0:8000 lint-back: ## Lint Python code black . --check diff --git a/doc/source/install/install-docker.rst b/doc/source/install/install-docker.rst new file mode 100644 index 0000000000..db01c5b0f6 --- /dev/null +++ b/doc/source/install/install-docker.rst @@ -0,0 +1,51 @@ +======================== +Installation dans Docker +======================== + + +.. note:: + + Par manque de développeurs utilisant Docker au sein de l'équipe de + développement de ZdS, cette section n'est guère fournie. Les instructions + données ici ne le sont qu'à titre indicatif. N'hésitez pas à signaler tout + problème ou proposer des améliorations ! + +L'installation de l'environnement de développement dans Docker se base sur `l'installation sous Linux `_. + +Lancez un shell interactif dans un conteneur basé sur Debian : + +.. sourcecode:: bash + + docker run -it -p 8000:8000 debian:buster + + +Une fois dans le conteneur, saisissez les commandes suivantes : + +.. sourcecode:: bash + + # On se place dans le $HOME + cd + + # Permet d'utiliser correctement apt + DEBIAN_FRONTEND=noninteractive + + # Installez les paquets minimaux requis + apt update + apt install sudo make vim git + + # Clonez le dépôt de ZdS + git clone https://github.com//zds-site.git + cd zds-site/ + + # Installez ZdS + make install-linux + + # Nécessaire pour avoir nvm dans le PATH + source ../.bashrc + + # À partir de maintenant, les commandes ne sont plus spécifiques à l'utilisation de Docker. + + # Lancement de ZdS + source zdsenv/bin/activate + make zmd-start + make run-back diff --git a/doc/source/install/install-linux.rst b/doc/source/install/install-linux.rst index 8efbf3367c..eb750ac80a 100644 --- a/doc/source/install/install-linux.rst +++ b/doc/source/install/install-linux.rst @@ -15,7 +15,10 @@ Pour installer une version locale de ZdS sur GNU/Linux, veuillez suivre les inst - Si malgré tout vous ne parvenez pas à installer ZdS, n'hésitez pas à ouvrir `un sujet sur le forum `_ -Après avoir cloné, installer ZdS sous Linux est relativement simple. En effet, il suffit de lancer la commande suivante (qui se chargera d'installer ce qui est nécessaire, plus d'infos ci-dessous): +Pour installer ZdS, vous aurez besoin d'abord des programmes ``make`` et ``sudo``. S'ils ne sont pas déjà installés sur votre système, ils sont généralement disponibles dans les gestionnaires de paquets sous le même nom. + + +Après avoir cloné le dépôt du code source, installer ZdS sous Linux est relativement simple. En effet, il suffit de lancer la commande suivante (qui se chargera d'installer ce qui est nécessaire, plus d'infos ci-dessous): .. sourcecode:: bash diff --git a/scripts/install_zds.sh b/scripts/install_zds.sh index a0f7d199c7..18c2f1a73f 100755 --- a/scripts/install_zds.sh +++ b/scripts/install_zds.sh @@ -222,7 +222,16 @@ if ! $(_in "--force-skip-activating" $@) && [[ ( $VIRTUAL_ENV == "" || $(realpat echo " - If you don't have other choice, use \`--force-skip-activating\`." exit 1 fi -else + + # Some dependencies (like rust ones) require a recent pip: + print_info "* upgrading pip" + pip install --upgrade pip; exVal=$? + + if [[ $exVal != 0 ]]; then + print_error "!! Failed to upgrade pip" + exit 1 + fi + print_info "!! Add \`$(realpath $ZDS_VENV)\` in your PATH." if [ ! -d $ZDS_VENV ]; then diff --git a/zds/settings/abstract_base/django.py b/zds/settings/abstract_base/django.py index ebbfd9e8bc..ba1f7eb4f9 100644 --- a/zds/settings/abstract_base/django.py +++ b/zds/settings/abstract_base/django.py @@ -7,8 +7,11 @@ from .config import config from .base_dir import BASE_DIR - -INTERNAL_IPS = ("127.0.0.1",) # debug toolbar +# especially for debug toolbar: +INTERNAL_IPS = ( + "127.0.0.1", + "172.17.0.1", # to enable debug toolbar when executed in a Docker container +) DATABASES = { "default": { diff --git a/zds/settings/dev.py b/zds/settings/dev.py index ddc63a6e1d..1f2c2e2239 100644 --- a/zds/settings/dev.py +++ b/zds/settings/dev.py @@ -4,8 +4,7 @@ DEBUG = True -# NOTE: Can be removed once Django 3 is used -ALLOWED_HOSTS = [".localhost", "127.0.0.1", "[::1]"] +ALLOWED_HOSTS = ["*"] # allow everything in case we are in a Docker container INSTALLED_APPS += ( "debug_toolbar", From e8c31db7e3314eb68bdbecc62c5afd1e875e2123 Mon Sep 17 00:00:00 2001 From: Arnaud-D <35631001+Arnaud-D@users.noreply.github.com> Date: Tue, 21 Dec 2021 11:40:43 +0100 Subject: [PATCH 18/53] =?UTF-8?q?Ajoute=20des=20tests=20=C3=A0=20RemoveCon?= =?UTF-8?q?tributor=20(#6193)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tests_views/tests_removecontributor.py | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 zds/tutorialv2/tests/tests_views/tests_removecontributor.py diff --git a/zds/tutorialv2/tests/tests_views/tests_removecontributor.py b/zds/tutorialv2/tests/tests_views/tests_removecontributor.py new file mode 100644 index 0000000000..27daec1dad --- /dev/null +++ b/zds/tutorialv2/tests/tests_views/tests_removecontributor.py @@ -0,0 +1,137 @@ +from django.test import TestCase +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ +from django.utils.html import escape + +from zds.member.factories import ProfileFactory, StaffProfileFactory +from zds.tutorialv2.factories import PublishableContentFactory +from zds.tutorialv2.models.database import ContentContribution, ContentContributionRole +from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents + + +def create_contribution(role, contributor, content): + contribution = ContentContribution(contribution_role=role, user=contributor, content=content) + contribution.save() + return contribution + + +def create_role(title): + role = ContentContributionRole(title=title) + role.save() + return role + + +@override_for_contents() +class RemoveContributorPermissionTests(TutorialTestMixin, TestCase): + """Test permissions and associated behaviors, such as redirections and status codes.""" + + def setUp(self): + # Create users + self.author = ProfileFactory().user + self.staff = StaffProfileFactory().user + self.outsider = ProfileFactory().user + self.contributor = ProfileFactory().user + + # Create a contribution role + self.role = create_role("Validateur") + + # Create content + self.content = PublishableContentFactory(author_list=[self.author]) + self.contribution = create_contribution(self.role, self.contributor, self.content) + + # Get information to be reused in tests + self.form_url = reverse("content:remove-contributor", kwargs={"pk": self.content.pk}) + self.login_url = reverse("member-login") + "?next=" + self.form_url + self.content_url = reverse("content:view", kwargs={"pk": self.content.pk, "slug": self.content.slug}) + self.form_data = {"pk_contribution": self.contribution.pk} + + def test_not_authenticated(self): + self.client.logout() + response = self.client.post(self.form_url, self.form_data) + self.assertRedirects(response, self.login_url) + + def test_authenticated_outsider(self): + self.client.force_login(self.outsider) + self.content.type = "TUTORIAL" + self.content.save() + response = self.client.post(self.form_url, self.form_data) + self.assertEqual(response.status_code, 403) + + def test_authenticated_author(self): + self.client.force_login(self.author) + self.content.type = "TUTORIAL" + self.content.save() + response = self.client.post(self.form_url, self.form_data) + self.assertRedirects(response, self.content_url) + + def test_authenticated_staff_tutorial(self): + self.client.force_login(self.staff) + self.content.type = "TUTORIAL" + self.content.save() + response = self.client.post(self.form_url, self.form_data) + self.assertRedirects(response, self.content_url) + + def test_authenticated_staff_article(self): + self.client.force_login(self.staff) + self.content.type = "ARTICLE" + self.content.save() + response = self.client.post(self.form_url, self.form_data) + self.assertRedirects(response, self.content_url) + + def test_authenticated_staff_opinion(self): + self.client.force_login(self.staff) + self.content.type = "OPINION" + self.content.save() + response = self.client.post(self.form_url, self.form_data) + self.assertEqual(response.status_code, 403) + + +class RemoveContributorWorkflowTests(TutorialTestMixin, TestCase): + """Test the workflow of the form, such as validity errors and success messages.""" + + def setUp(self): + # Create entities for the test + self.author = ProfileFactory().user + self.contributor = ProfileFactory().user + self.role = create_role("Validateur") + self.content = PublishableContentFactory(author_list=[self.author]) + self.contribution = create_contribution(self.role, self.contributor, self.content) + + # Get information to be reused in tests + self.form_url = reverse("content:remove-contributor", kwargs={"pk": self.content.pk}) + self.success_message_fragment = _("Vous avez enlevé ") + self.error_message_fragment = _("Les contributeurs sélectionnés n'existent pas.") + + # Log in with an authorized user to perform the tests + self.client.force_login(self.author) + + def test_existing(self): + response = self.client.post(self.form_url, {"pk_contribution": self.contribution.pk}, follow=True) + self.assertContains(response, escape(self.success_message_fragment)) + self.assertEqual(list(ContentContribution.objects.all()), []) + + def test_empty(self): + response = self.client.post(self.form_url, {"pk_contribution": ""}, follow=True) + self.assertContains(response, escape(self.error_message_fragment)) + self.assertEqual(list(ContentContribution.objects.all()), [self.contribution]) + + def test_invalid(self): + response = self.client.post(self.form_url, {"pk_contribution": "420"}, follow=True) # pk must not exist + self.assertEqual(response.status_code, 404) + self.assertEqual(list(ContentContribution.objects.all()), [self.contribution]) + + def test_not_integer(self): + with self.assertRaises(ValueError): + self.client.post(self.form_url, {"pk_contribution": "abcd"}, follow=True) + self.assertEqual(list(ContentContribution.objects.all()), [self.contribution]) + + def test_no_argument(self): + response = self.client.post(self.form_url, follow=True) + self.assertContains(response, escape(self.error_message_fragment)) + self.assertEqual(list(ContentContribution.objects.all()), [self.contribution]) + + def test_wrong_contribution(self): + form_url = reverse("content:remove-contributor", kwargs={"pk": 3023}) # pk must not exist + response = self.client.post(form_url, follow=True) + self.assertEqual(response.status_code, 404) + self.assertEqual(list(ContentContribution.objects.all()), [self.contribution]) From 85e6b60825da2b68072ce3792f875a47927763e1 Mon Sep 17 00:00:00 2001 From: Arnaud-D <35631001+Arnaud-D@users.noreply.github.com> Date: Tue, 21 Dec 2021 12:01:26 +0100 Subject: [PATCH 19/53] =?UTF-8?q?Ajoute=20des=20tests=20=C3=A0=20AddContri?= =?UTF-8?q?butor=20(#6194)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tests/tests_views/tests_addcontributor.py | 192 ++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 zds/tutorialv2/tests/tests_views/tests_addcontributor.py diff --git a/zds/tutorialv2/tests/tests_views/tests_addcontributor.py b/zds/tutorialv2/tests/tests_views/tests_addcontributor.py new file mode 100644 index 0000000000..f2790d1ffc --- /dev/null +++ b/zds/tutorialv2/tests/tests_views/tests_addcontributor.py @@ -0,0 +1,192 @@ +from unittest.mock import patch + +from django.conf import settings +from django.test import TestCase +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ +from django.utils.html import escape + + +from zds.member.factories import ProfileFactory, StaffProfileFactory +from zds.tutorialv2.factories import PublishableContentFactory +from zds.tutorialv2.forms import ContributionForm +from zds.tutorialv2.models.database import ContentContribution, ContentContributionRole +from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents + + +def create_contribution(role, contributor, content): + contribution = ContentContribution(contribution_role=role, user=contributor, content=content) + contribution.save() + return contribution + + +def create_role(title): + role = ContentContributionRole(title=title) + role.save() + return role + + +@override_for_contents() +class AddContributorPermissionTests(TutorialTestMixin, TestCase): + """Test permissions and associated behaviors, such as redirections and status codes.""" + + def setUp(self): + # Create users + self.author = ProfileFactory().user + self.staff = StaffProfileFactory().user + self.outsider = ProfileFactory().user + self.contributor = ProfileFactory().user + settings.ZDS_APP["member"]["bot_account"] = ProfileFactory().user.username + + # Create content + self.content = PublishableContentFactory(author_list=[self.author]) + self.role = create_role("Contributeur espiègle") + + # Get information to be reused in tests + self.form_url = reverse("content:add-contributor", kwargs={"pk": self.content.pk}) + self.login_url = reverse("member-login") + "?next=" + self.form_url + self.content_url = reverse("content:view", kwargs={"pk": self.content.pk, "slug": self.content.slug}) + self.form_data = {"username": self.contributor, "contribution_role": self.role.pk} + + def test_not_authenticated(self): + self.client.logout() + response = self.client.post(self.form_url, self.form_data) + self.assertRedirects(response, self.login_url) + + def test_authenticated_outsider(self): + self.client.force_login(self.outsider) + self.content.type = "TUTORIAL" + self.content.save() + response = self.client.post(self.form_url, self.form_data) + self.assertEqual(response.status_code, 403) + + def test_authenticated_author(self): + self.client.force_login(self.author) + self.content.type = "TUTORIAL" + self.content.save() + response = self.client.post(self.form_url, self.form_data) + self.assertRedirects(response, self.content_url) + + def test_authenticated_staff_tutorial(self): + self.client.force_login(self.staff) + self.content.type = "TUTORIAL" + self.content.save() + response = self.client.post(self.form_url, self.form_data) + self.assertRedirects(response, self.content_url) + + def test_authenticated_staff_article(self): + self.client.force_login(self.staff) + self.content.type = "ARTICLE" + self.content.save() + response = self.client.post(self.form_url, self.form_data) + self.assertRedirects(response, self.content_url) + + def test_authenticated_staff_opinion(self): + self.client.force_login(self.staff) + self.content.type = "OPINION" + self.content.save() + response = self.client.post(self.form_url, self.form_data) + self.assertEqual(response.status_code, 403) + + +class AddContributorWorkflowTests(TutorialTestMixin, TestCase): + """Test the workflow of the form, such as validity errors and success messages.""" + + def setUp(self): + # Create entities for the test + self.author = ProfileFactory().user + self.contributor = ProfileFactory().user + self.role = create_role("Validateur") + self.content = PublishableContentFactory(author_list=[self.author]) + settings.ZDS_APP["member"]["bot_account"] = ProfileFactory().user.username + + # Get information to be reused in tests + self.form_url = reverse("content:add-contributor", kwargs={"pk": self.content.pk}) + self.error_message_author_contributor = _("Un auteur ne peut pas être désigné comme contributeur") + self.error_message_empty_user = _("Veuillez renseigner l'utilisateur") + self.comment = "What an mischievious person!" + + # Log in with an authorized user to perform the tests + self.client.force_login(self.author) + + def test_correct(self): + form_data = { + "username": self.contributor, + "contribution_role": self.role.pk, + "comment": self.comment, + } + self.client.post(self.form_url, form_data, follow=True) + contribution = ContentContribution.objects.filter( + content=self.content.pk, + user=self.contributor, + contribution_role=self.role, + comment=self.comment, + ).first() + self.assertEqual(list(ContentContribution.objects.all()), [contribution]) + + def test_empty_user(self): + form_data = { + "username": "", + "contribution_role": self.role.pk, + } + response = self.client.post(self.form_url, form_data, follow=True) + self.assertContains(response, escape(ContributionForm.declared_fields["username"].error_messages["required"])) + self.assertEqual(list(ContentContribution.objects.all()), []) + + def test_no_user(self): + form_data = { + "contribution_role": self.role.pk, + } + response = self.client.post(self.form_url, form_data, follow=True) + self.assertContains(response, escape(ContributionForm.declared_fields["username"].error_messages["required"])) + self.assertEqual(list(ContentContribution.objects.all()), []) + + def test_invalid_user(self): + form_data = { + "username": "this pseudo does not exist", + "contribution_role": self.role.pk, + } + response = self.client.post(self.form_url, form_data, follow=True) + self.assertContains(response, escape(self.error_message_empty_user)) + self.assertEqual(list(ContentContribution.objects.all()), []) + + def test_author_contributor(self): + form_data = { + "username": self.author, + "contribution_role": self.role.pk, + } + response = self.client.post(self.form_url, form_data, follow=True) + self.assertContains(response, escape(self.error_message_author_contributor)) + self.assertEqual(list(ContentContribution.objects.all()), []) + + def test_empty_role(self): + form_data = { + "username": self.contributor, + "contribution_role": "", + } + response = self.client.post(self.form_url, form_data, follow=True) + self.assertContains( + response, escape(ContributionForm.declared_fields["contribution_role"].error_messages["required"]) + ) + self.assertEqual(list(ContentContribution.objects.all()), []) + + def test_no_role(self): + form_data = { + "username": self.contributor, + } + response = self.client.post(self.form_url, form_data, follow=True) + self.assertContains( + response, escape(ContributionForm.declared_fields["contribution_role"].error_messages["required"]) + ) + self.assertEqual(list(ContentContribution.objects.all()), []) + + def test_invalid_role(self): + form_data = { + "username": self.contributor, + "contribution_role": 3150, # must be an invalid pk, integer or not + } + response = self.client.post(self.form_url, form_data, follow=True) + self.assertContains( + response, escape(ContributionForm.declared_fields["contribution_role"].error_messages["invalid_choice"]) + ) + self.assertEqual(list(ContentContribution.objects.all()), []) From 480a9e19da8ab3f84c36e8ad29f8df3ec4fe0d98 Mon Sep 17 00:00:00 2001 From: Philippe MILINK Date: Sat, 25 Dec 2021 17:40:14 +0100 Subject: [PATCH 20/53] Corrige une typo dans la documentation --- doc/source/install.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/install.rst b/doc/source/install.rst index 77af624dc4..76762a90ca 100644 --- a/doc/source/install.rst +++ b/doc/source/install.rst @@ -27,7 +27,7 @@ Si vous souhaitez terminer d'installer puis démarrer une instance locale de ZdS install/install-* -Les détails concernant la contribution au code du site peuvent être trouvé `ici <./contributing.html>`_. +Les détails concernant la contribution au code du site peuvent être trouvés `ici <./contributing.html>`_. Quelques informations supplémentaires: From 5ea1809cf85dd96d228c73cf03e0ec977b2e8aa9 Mon Sep 17 00:00:00 2001 From: Situphen Date: Mon, 27 Dec 2021 12:20:39 +0100 Subject: [PATCH 21/53] =?UTF-8?q?Mise=20=C3=A0=20jour=20triviale=20des=20d?= =?UTF-8?q?=C3=A9pendances=20Python?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 2 +- .pre-commit-config.yaml | 2 +- requirements-dev.txt | 10 +++++----- requirements-prod.txt | 6 +++--- requirements.txt | 10 +++++----- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index def1f01fe2..136757b7ac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,7 @@ env: PYTHON_VERSION: "3.7" MARIADB_VERSION: "10.4.10" COVERALLS_VERSION: "2.2.0" - BLACK_VERSION: "21.10b0" # needs to be also updated in requirements-dev.txt and .pre-commit-config.yaml + BLACK_VERSION: "21.12b0" # needs to be also updated in requirements-dev.txt and .pre-commit-config.yaml # As GitHub Action does not allow environment variables # to be used in services definitions, these are only for diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c8adfd10c0..5f8a27b7b5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ repos: - id: pyupgrade args: [--py36-plus] - repo: https://github.com/psf/black - rev: 21.10b0 # needs to be also updated in requirements-dev.txt and .github/workflows/ci.yml + rev: 21.12b0 # needs to be also updated in requirements-dev.txt and .github/workflows/ci.yml hooks: - id: black language_version: python3 diff --git a/requirements-dev.txt b/requirements-dev.txt index 1724d81f90..94e66db865 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,12 +1,12 @@ -r requirements.txt -black==21.10b0 # needs to be also updated in .github/workflows/ci.yml and .pre-commit-config.yaml +black==21.12b0 # needs to be also updated in .github/workflows/ci.yml and .pre-commit-config.yaml colorlog==6.6.0 -django-debug-toolbar==3.2.2 +django-debug-toolbar==3.2.4 django-extensions==3.1.5 -Faker==9.8.1 -pre-commit==2.15.0 +Faker==10.0.0 +pre-commit==2.16.0 PyYAML==6.0 selenium==3.141.0 -Sphinx==4.3.0 +Sphinx==4.3.2 sphinx_rtd_theme==1.0.0 diff --git a/requirements-prod.txt b/requirements-prod.txt index 58a8ff8dbd..c0766fd28b 100644 --- a/requirements-prod.txt +++ b/requirements-prod.txt @@ -1,6 +1,6 @@ -r requirements.txt gunicorn==20.1.0 -mysqlclient==2.0.3 -sentry-sdk==1.4.3 -ujson==4.2.0 +mysqlclient==2.1.0 +sentry-sdk==1.5.1 +ujson==5.1.0 diff --git a/requirements.txt b/requirements.txt index f680f43b2b..a9e7cb0699 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,24 +10,24 @@ django-crispy-forms==1.13.0 django-model-utils==4.2.0 django-munin==0.2.1 django-recaptcha==2.0.6 -Django==2.2.24 +Django==2.2.25 easy-thumbnails==2.8.0 factory-boy==3.2.1 -geoip2==4.4.0 +geoip2==4.5.0 GitPython==3.1.24 homoglyphs==2.0.4 -lxml==4.6.4 +lxml==4.7.1 Pillow==8.4.0 python-memcached==1.59 requests==2.26.0 toml==0.10.2 # Api dependencies -django-cors-headers==3.10.0 +django-cors-headers==3.10.1 django-filter==21.1 django-oauth-toolkit==1.5.0 djangorestframework-xml==2.0.0 -djangorestframework==3.12.4 +djangorestframework==3.13.1 drf-extensions==0.7.1 dry-rest-permissions==0.1.10 drf_yasg==1.20.0 From 44a96f42a841f300c38152e51e9847d7d5668ffb Mon Sep 17 00:00:00 2001 From: Arnaud-D <35631001+Arnaud-D@users.noreply.github.com> Date: Mon, 27 Dec 2021 22:10:09 +0100 Subject: [PATCH 22/53] Importe l'information 'ready_to_publish' depuis les archives (#6203) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Prend en compte le champ "ready_to_publish" à l'import * Ajoute un test pour l'import de 'ready_to_publish' * Corrige une coquille dans un test des tutos --- .../tests/tests_views/tests_content.py | 55 ++++++++++++++++++- zds/tutorialv2/views/archives.py | 3 + 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/zds/tutorialv2/tests/tests_views/tests_content.py b/zds/tutorialv2/tests/tests_views/tests_content.py index 1aa28ac807..0b5decb0a8 100644 --- a/zds/tutorialv2/tests/tests_views/tests_content.py +++ b/zds/tutorialv2/tests/tests_views/tests_content.py @@ -1634,7 +1634,7 @@ def test_import_in_existing_content(self): # ensure the content self.assertEqual(versioned.get_introduction(), some_text) - self.assertEqual(versioned.get_introduction(), some_text) + self.assertEqual(versioned.get_conclusion(), some_text) self.assertEqual(len(versioned.children), 1) new_chapter = versioned.children[-1] @@ -1774,6 +1774,59 @@ def test_import_image_with_archive(self): os.remove(draft_zip_path) os.remove(image_zip_path) + def test_import_ready_to_publish(self): + """Test whether the 'ready_to_publish' info from the archive is correctly imported.""" + + # General principle of this test: + # * create an archive by creating a content and exporting it + # * change the 'ready_to_publish' toggles on the content + # * import the archive and check whether we get back to the initial state (i.e. correct import) + + self.client.force_login(self.user_author) + + # Create a content with parts + content = PublishableContentFactory(author_list=[self.user_author]) + versioned = content.load_version() + part1 = ContainerFactory(db_object=content, parent=versioned) + chapter1 = ContainerFactory(db_object=content, parent=part1) + chapter1.ready_to_publish = False + ContainerFactory(db_object=content, parent=part1) # chapter 2 + part2 = ContainerFactory(db_object=content, parent=versioned) + part2.ready_to_publish = False + sha = versioned.repo_update(content.title, content.slug, "introduction", "conclusion") + content.sha_draft = sha + content.save() + + # Download archive of initial state for content + result = self.client.get(reverse("content:download-zip", args=[content.pk, content.slug]), follow=False) + self.assertEqual(result.status_code, 200) + draft_zip_path = os.path.join(tempfile.gettempdir(), "__draft1.zip") + with open(draft_zip_path, "wb") as f: + f.write(result.content) + + # Update readiness of part 2 and part1/chapter1 + # Failure to import this information defaults also to True, this is to make sure. + versioned.children[0].children[0].ready_to_publish = True + versioned.children[1].ready_to_publish = True + sha = versioned.repo_update_top_container(content.title, content.slug, "introduction", "conclusion") + content.sha_draft = sha + content.save() + + # Import archive + result = self.client.post( + reverse("content:import", args=[content.pk, content.slug]), + {"archive": open(draft_zip_path, "rb")}, + ) + self.assertEqual(result.status_code, 302) + + # Check override of previous modifications through the import + content = PublishableContent.objects.get(pk=content.pk) # reload from database + versioned = content.load_version() + self.assertTrue(versioned.children[0].ready_to_publish) + self.assertTrue(versioned.children[0].children[1].ready_to_publish) + self.assertFalse(versioned.children[0].children[0].ready_to_publish) + self.assertFalse(versioned.children[1].ready_to_publish) + def test_display_history(self): """Test DisplayHistory view""" diff --git a/zds/tutorialv2/views/archives.py b/zds/tutorialv2/views/archives.py index 6f5c7600fc..91ded70442 100644 --- a/zds/tutorialv2/views/archives.py +++ b/zds/tutorialv2/views/archives.py @@ -198,6 +198,8 @@ def update_from_new_version_in_zip(copy_to, copy_from, zip_file): raise BadArchiveError(_(f"Le fichier « {child.conclusion} » n'est pas encodé en UTF-8")) copy_to.repo_add_container(child.title, introduction, conclusion, do_commit=False, slug=child.slug) + copy_to.children[-1].ready_to_publish = child.ready_to_publish + copy_to.repo_update(copy_to.title, introduction, conclusion, do_commit=False) UpdateContentWithArchive.update_from_new_version_in_zip(copy_to.children[-1], child, zip_file) elif isinstance(child, Extract): @@ -381,6 +383,7 @@ def form_valid(self, form): if new_version.conclusion: conclusion = str(zfile.read(new_version.conclusion), "utf-8") + versioned.ready_to_publish = new_version.ready_to_publish versioned.repo_update_top_container( new_version.title, new_version.slug, introduction, conclusion, do_commit=False ) From 915c7d177d5bd96d3ee869aacb58581f4df21fde Mon Sep 17 00:00:00 2001 From: ChantyTaguan Date: Tue, 28 Dec 2021 14:45:00 +0100 Subject: [PATCH 23/53] Supprime notifications forum inaccessible (#6196) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Supprime code inutile Redondant avec notification/receivers.py remove_group_subscription_on_quitting_groups * Complète les tests * Supprime les notifications caduques qui trainent * Ajout d'un forum réservé au staff dans les fixtures --- fixtures/forums.yaml | 8 ++ zds/member/views.py | 9 -- ...n_notifications_new_topic_forums_groups.py | 26 +++++ zds/notification/tests/tests_tricky.py | 102 +++++++++++++++++- 4 files changed, 132 insertions(+), 13 deletions(-) create mode 100644 zds/notification/migrations/0017_clean_notifications_new_topic_forums_groups.py diff --git a/fixtures/forums.yaml b/fixtures/forums.yaml index 69c5e589a1..a49fcf641e 100644 --- a/fixtures/forums.yaml +++ b/fixtures/forums.yaml @@ -106,3 +106,11 @@ subtitle: Discutez de tout ! category: 4 slug: discussions-generales +- model: forum.Forum + pk: 13 + fields: + title: Staff only + subtitle: Réservé à l'équipe ! + category: 4 + slug: staff-only + groups: [1] diff --git a/zds/member/views.py b/zds/member/views.py index bd237803fe..8f142e4dd0 100644 --- a/zds/member/views.py +++ b/zds/member/views.py @@ -1405,16 +1405,7 @@ def settings_promote(request, user_pk): request, _("{0} n'appartient maintenant plus au groupe {1}.").format(user.username, group.name), ) - topics_followed = TopicAnswerSubscription.objects.get_objects_followed_by(user) - for topic in topics_followed: - if isinstance(topic, Topic) and group in topic.forum.groups.all(): - TopicAnswerSubscription.objects.toggle_follow(topic, user) else: - for group in usergroups: - topics_followed = TopicAnswerSubscription.objects.get_objects_followed_by(user) - for topic in topics_followed: - if isinstance(topic, Topic) and group in topic.forum.groups.all(): - TopicAnswerSubscription.objects.toggle_follow(topic, user) user.groups.clear() messages.warning(request, _("{0} n'appartient (plus ?) à aucun groupe.").format(user.username)) diff --git a/zds/notification/migrations/0017_clean_notifications_new_topic_forums_groups.py b/zds/notification/migrations/0017_clean_notifications_new_topic_forums_groups.py new file mode 100644 index 0000000000..76267b56f4 --- /dev/null +++ b/zds/notification/migrations/0017_clean_notifications_new_topic_forums_groups.py @@ -0,0 +1,26 @@ +from django.db import migrations + +from zds.notification.models import NewTopicSubscription +from zds.forum.models import Topic, Forum + + +def cleanup(apps, *_): + for forum in Forum.objects.filter(groups__isnull=False).all(): + for subscription in NewTopicSubscription.objects.get_subscriptions(forum): + if not forum.can_read(subscription.user): + subscription.is_active = False + if subscription.last_notification: + subscription.last_notification.is_read = True + subscription.last_notification.save() + subscription.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ("notification", "0016_auto_20190114_1301"), + ] + + operations = [ + migrations.RunPython(cleanup), + ] diff --git a/zds/notification/tests/tests_tricky.py b/zds/notification/tests/tests_tricky.py index e07f2e4779..15642c246d 100644 --- a/zds/notification/tests/tests_tricky.py +++ b/zds/notification/tests/tests_tricky.py @@ -5,13 +5,15 @@ from django.core import mail from django.test import TestCase from django.test.utils import override_settings +from django.contrib.contenttypes.models import ContentType -from zds.forum.factories import ForumCategoryFactory, ForumFactory +from zds.forum.factories import ForumCategoryFactory, ForumFactory, TopicFactory, PostFactory from zds.forum.models import Topic from zds.gallery.factories import UserGalleryFactory from zds.member.factories import StaffProfileFactory, ProfileFactory from zds.notification.models import ( NewTopicSubscription, + TopicAnswerSubscription, Notification, NewPublicationSubscription, ContentReactionAnswerSubscription, @@ -287,7 +289,7 @@ def test_no_dead_ping_notif_on_moving_to_private_forum(self): self.assertEqual(subscription.last_notification, Notification.objects.filter(sender=self.user2).first()) self.assertTrue(subscription.last_notification.is_read, "As forum is not reachable, notification is read") - def test_no_more_notif_on_losing_all_groups(self): + def test_no_more_new_topic_notif_on_losing_all_groups(self): NewTopicSubscription.objects.get_or_create_active(self.to_be_changed_staff, self.forum12) self.client.force_login(self.staff) self.client.post( @@ -302,13 +304,18 @@ def test_no_more_notif_on_losing_all_groups(self): ) subscription = NewTopicSubscription.objects.get_existing(self.to_be_changed_staff, self.forum12, True) self.assertIsNotNone(subscription, "There must be an active subscription for now") + self.assertIsNotNone(subscription.last_notification, "There must be a notification.") + self.assertFalse(subscription.last_notification.is_read, "The notification has not been read yet") + self.to_be_changed_staff.groups.clear() self.to_be_changed_staff.save() + subscription = NewTopicSubscription.objects.get_existing(self.to_be_changed_staff, self.forum12, False) - self.assertIsNotNone(subscription, "There must be an active subscription for now") + self.assertIsNotNone(subscription, "The subscription should now be inactive") self.assertFalse(subscription.is_active) + self.assertTrue(subscription.last_notification.is_read, "As forum is not reachable, notification is read") - def test_no_more_notif_on_losing_one_group(self): + def test_no_more_new_topic_notif_on_losing_one_group(self): NewTopicSubscription.objects.get_or_create_active(self.to_be_changed_staff, self.forum12) self.client.force_login(self.staff) self.client.post( @@ -323,11 +330,98 @@ def test_no_more_notif_on_losing_one_group(self): ) subscription = NewTopicSubscription.objects.get_existing(self.to_be_changed_staff, self.forum12, True) self.assertIsNotNone(subscription, "There must be an active subscription for now") + self.assertIsNotNone(subscription.last_notification, "There must be a notification.") + self.assertFalse(subscription.last_notification.is_read, "The notification has not been read yet") + self.to_be_changed_staff.groups.remove(list(self.to_be_changed_staff.groups.all())[0]) self.to_be_changed_staff.save() + subscription = NewTopicSubscription.objects.get_existing(self.to_be_changed_staff, self.forum12, False) + self.assertIsNotNone(subscription, "There must be an inactive subscription now") + self.assertFalse(subscription.is_active) + self.assertTrue(subscription.last_notification.is_read, "As forum is not reachable, notification is read") + + def test_no_more_topic_answer_notif_on_losing_all_groups(self): + self.client.force_login(self.to_be_changed_staff) + self.client.post( + reverse("topic-new") + f"?forum={self.forum12.pk}", + { + "title": "Super sujet", + "subtitle": "Pour tester les notifs", + "text": "En tout cas l'un abonnement", + "tags": "", + }, + follow=False, + ) + topic = Topic.objects.filter(title="Super sujet").first() + + self.client.force_login(self.staff) + self.client.post( + reverse("post-new") + f"?sujet={topic.pk}", + { + "last_post": topic.last_message.pk, + "text": "C'est tout simplement l'histoire de la ville de Paris que je voudrais vous conter ", + }, + follow=False, + ) + + subscription = TopicAnswerSubscription.objects.get_existing( + content_object=topic, user=self.to_be_changed_staff, is_active=True + ) + self.assertIsNotNone(subscription, "There must be an active subscription for now") + self.assertIsNotNone(subscription.last_notification, "There must be a notification.") + self.assertFalse(subscription.last_notification.is_read, "The notification has not been read yet") + + self.to_be_changed_staff.groups.clear() + self.to_be_changed_staff.save() + + subscription = TopicAnswerSubscription.objects.get_existing( + content_object=topic, user=self.to_be_changed_staff, is_active=False + ) + self.assertIsNotNone(subscription, "The subscription must now be inactive") + self.assertFalse(subscription.is_active) + self.assertTrue(subscription.last_notification.is_read, "As forum is not reachable, notification is read") + + def test_no_more_topic_answer_notif_on_losing_one_group(self): + self.client.force_login(self.to_be_changed_staff) + self.client.post( + reverse("topic-new") + f"?forum={self.forum12.pk}", + { + "title": "Super sujet", + "subtitle": "Pour tester les notifs", + "text": "En tout cas l'un abonnement", + "tags": "", + }, + follow=False, + ) + topic = Topic.objects.filter(title="Super sujet").first() + + self.client.force_login(self.staff) + self.client.post( + reverse("post-new") + f"?sujet={topic.pk}", + { + "last_post": topic.last_message.pk, + "text": "C'est tout simplement l'histoire de la ville de Paris que je voudrais vous conter ", + }, + follow=False, + ) + + subscription = TopicAnswerSubscription.objects.get_existing( + content_object=topic, user=self.to_be_changed_staff, is_active=True + ) self.assertIsNotNone(subscription, "There must be an active subscription for now") + self.assertIsNotNone(subscription.last_notification, "There must be a notification.") + self.assertFalse(subscription.last_notification.is_read, "The notification has not been read yet") + + self.to_be_changed_staff.groups.remove(list(self.to_be_changed_staff.groups.all())[0]) + self.to_be_changed_staff.save() + + subscription = TopicAnswerSubscription.objects.get_existing( + content_object=topic, user=self.to_be_changed_staff, is_active=False + ) + self.assertIsNotNone(subscription, "The subscription must now be inactive") self.assertFalse(subscription.is_active) + self.assertTrue(subscription.last_notification.is_read, "As forum is not reachable, notification is read") @override_for_contents() From 2ba922223c859984a413cc6c108a8aa4023b113e Mon Sep 17 00:00:00 2001 From: Luuka Date: Tue, 4 Jan 2022 21:47:35 +0100 Subject: [PATCH 24/53] Corrige l'initialisation du menu mobile sur tablette --- assets/js/mobile-menu.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/mobile-menu.js b/assets/js/mobile-menu.js index 8ea47b98ca..75f39f69ac 100644 --- a/assets/js/mobile-menu.js +++ b/assets/js/mobile-menu.js @@ -46,7 +46,7 @@ * Manage mobile sidebar on resize */ $(window).on('resize', function() { - if (parseInt($('html').css('width')) < 960 && !disableMobileMenu) { + if (parseInt($('html').css('width')) < 1024 && !disableMobileMenu) { $('.page-container').css('width', $('html').css('width')) if (!$('#mobile-menu').hasClass('initialized')) { From 7be4440aba9e17b6795bbda2638063b45f7ff2f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Deuchnord?= Date: Mon, 17 Jan 2022 22:23:43 +0100 Subject: [PATCH 25/53] Activate GitHub link in the social footer (#6222) * Activate the GitHub link in the social footer --- zds/settings/abstract_base/zds.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zds/settings/abstract_base/zds.py b/zds/settings/abstract_base/zds.py index d4bb76136a..2f6d60a3a6 100644 --- a/zds/settings/abstract_base/zds.py +++ b/zds/settings/abstract_base/zds.py @@ -110,8 +110,8 @@ "mastodon": "https://framapiaf.org/@ZesteDeSavoir", "facebook": "https://www.facebook.com/ZesteDeSavoir", "twitter": "https://twitter.com/ZesteDeSavoir", + "github": "https://github.com/zestedesavoir/zds-site", # 'discord': 'https://discord.gg/ue5MTKq' - # 'github': 'https://github.com/zestedesavoir/zds-site' }, "cnil": "1771020", }, From 18a199132b736ae3a49dd1166d0f0867412f9b88 Mon Sep 17 00:00:00 2001 From: Situphen Date: Mon, 27 Dec 2021 12:38:24 +0100 Subject: [PATCH 26/53] =?UTF-8?q?Passage=20=C3=A0=20Django=203.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 4 ++-- zds/settings/abstract_base/django.py | 2 ++ zds/settings/prod.py | 2 +- zds/utils/models.py | 2 +- zds/utils/templatetags/form_categories.py | 2 +- 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/requirements.txt b/requirements.txt index a9e7cb0699..c5aa633f3a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ django-crispy-forms==1.13.0 django-model-utils==4.2.0 django-munin==0.2.1 django-recaptcha==2.0.6 -Django==2.2.25 +Django==3.2.11 easy-thumbnails==2.8.0 factory-boy==3.2.1 geoip2==4.5.0 @@ -18,7 +18,7 @@ GitPython==3.1.24 homoglyphs==2.0.4 lxml==4.7.1 Pillow==8.4.0 -python-memcached==1.59 +pymemcache==3.5.0 requests==2.26.0 toml==0.10.2 diff --git a/zds/settings/abstract_base/django.py b/zds/settings/abstract_base/django.py index ba1f7eb4f9..48134bb3e4 100644 --- a/zds/settings/abstract_base/django.py +++ b/zds/settings/abstract_base/django.py @@ -322,3 +322,5 @@ # Properly handle HTTPS vs HTTP SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") + +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" diff --git a/zds/settings/prod.py b/zds/settings/prod.py index 7c110b2853..ce96723775 100644 --- a/zds/settings/prod.py +++ b/zds/settings/prod.py @@ -52,7 +52,7 @@ CACHES = { "default": { - "BACKEND": "django.core.cache.backends.memcached.MemcachedCache", + "BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache", "LOCATION": "127.0.0.1:11211", } } diff --git a/zds/utils/models.py b/zds/utils/models.py index 3ac25cf5e7..08aec11959 100644 --- a/zds/utils/models.py +++ b/zds/utils/models.py @@ -210,7 +210,7 @@ class HatRequest(models.Model): date = models.DateTimeField( auto_now_add=True, db_index=True, verbose_name="Date de la demande", db_column="request_date" ) - is_granted = models.NullBooleanField("Est acceptée") + is_granted = models.BooleanField("Est acceptée", null=True) solved_at = models.DateTimeField("Date de résolution", blank=True, null=True) moderator = models.ForeignKey(User, verbose_name="Modérateur", blank=True, null=True, on_delete=models.SET_NULL) comment = models.TextField("Commentaire", max_length=1000, blank=True) diff --git a/zds/utils/templatetags/form_categories.py b/zds/utils/templatetags/form_categories.py index 21fa4e79d9..5bef472bbf 100644 --- a/zds/utils/templatetags/form_categories.py +++ b/zds/utils/templatetags/form_categories.py @@ -19,7 +19,7 @@ def order_categories(choices): for choice in choices: # many request but only used in "new content" page # if someone find a better solution, please create a Pull Request - subcat = SubCategory.objects.get(pk=choice[0]) + subcat = SubCategory.objects.get(pk=choice[0].value) parent = subcat.get_parent_category() if parent: ch = {"choice": choice, "parent": parent.title, "order": parent.pk} From 0cf61a969ac0518651b7dd9921f59a9a35a14788 Mon Sep 17 00:00:00 2001 From: Arnaud-D <35631001+Arnaud-D@users.noreply.github.com> Date: Sat, 22 Jan 2022 13:49:26 +0100 Subject: [PATCH 27/53] Ajoute la mention du scope minimal du token GitHub (#6185) Co-authored-by: Situphen --- templates/member/settings/github.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/templates/member/settings/github.html b/templates/member/settings/github.html index fb9235ac21..4e5d8c7ceb 100644 --- a/templates/member/settings/github.html +++ b/templates/member/settings/github.html @@ -47,7 +47,8 @@

    {% trans "Remplissez le formulaire suivant pour enregistrer votre token GitHub afin de pouvoir créer des tickets pour les sujets. Vous pouvez créer un token sur" %} - {% trans "cette page" %}. + {% trans "cette page" %}. Le token doit avoir au moins le scope + public_repo.

    From b41da5db8cd7829d78641f0031a56f95ae4bea9a Mon Sep 17 00:00:00 2001 From: SpaceFox Date: Sun, 23 Jan 2022 01:31:27 +0100 Subject: [PATCH 28/53] Corrige les marges des listes sur mobile (#6232) --- assets/scss/layout/_content.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/assets/scss/layout/_content.scss b/assets/scss/layout/_content.scss index f98d3b9db2..c665cc9bea 100644 --- a/assets/scss/layout/_content.scss +++ b/assets/scss/layout/_content.scss @@ -200,6 +200,11 @@ margin-right: $length-10; } + ul, + ol { + margin-right: $length-10; + } + figure { p, blockquote { From 854f1ec5e3647b7df48c26ddc395cddc6bb99967 Mon Sep 17 00:00:00 2001 From: Jean Lapostolle Date: Sun, 23 Jan 2022 15:18:21 +0100 Subject: [PATCH 29/53] fix #5684 --- templates/forum/category/forum.html | 8 +++++--- templates/forum/topic/index.html | 22 +++++++++++----------- templates/misc/message_form.html | 4 ++++ 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/templates/forum/category/forum.html b/templates/forum/category/forum.html index 999bc73fb0..ba54730e0f 100644 --- a/templates/forum/category/forum.html +++ b/templates/forum/category/forum.html @@ -66,9 +66,11 @@ {% block new_btn %} - - {% trans "Nouveau sujet" %} - + {% if user.can_write_now %} + + {% trans "Nouveau sujet" %} + + {% endif %} {% endblock %} diff --git a/templates/forum/topic/index.html b/templates/forum/topic/index.html index 91bd080b79..4c31327ecb 100644 --- a/templates/forum/topic/index.html +++ b/templates/forum/topic/index.html @@ -161,21 +161,21 @@ {% endblock %} - {% block new_btn %} - - {% trans "Nouveau sujet" %} - - - {% if topic.author.pk == user.pk and topic.first_post.is_visible or is_staff %} - - {% trans "Éditer le sujet" %} - - {% endif %} + {% if user.can_write_now %} + + {% trans "Nouveau sujet" %} + + + {% if topic.author.pk == user.pk and topic.first_post.is_visible or is_staff %} + + {% trans "Éditer le sujet" %} + + {% endif %} + {% endif %} {% endblock %} - {% block sidebar_actions %} {% if topic.author.pk == user.pk %}
  • diff --git a/templates/misc/message_form.html b/templates/misc/message_form.html index 0f59981a79..4cde75fa83 100644 --- a/templates/misc/message_form.html +++ b/templates/misc/message_form.html @@ -23,6 +23,10 @@

    Pas encore membre ?

    {% trans "Créer un compte" %}

  • +{% elif not user.can_write_now %} +
    + {% trans "Vous êtes en lecture seule. Vous ne pouvez pas poster de messages." %} +
    {% elif topic.antispam or is_antispam %}
    {% trans "Vous venez de poster. Merci de patienter au moins 15 minutes entre deux messages consécutifs afin de limiter le flood." %} From a91d572c8c749902225a5140af29869d637ab777 Mon Sep 17 00:00:00 2001 From: SpaceFox Date: Sun, 23 Jan 2022 16:40:54 +0100 Subject: [PATCH 30/53] Affichage en LS : utilisation du bon objet et tests (#5684) --- templates/forum/category/forum.html | 2 +- templates/forum/topic/index.html | 2 +- templates/misc/message_form.html | 8 +++--- zds/forum/tests/tests.py | 39 +++++++++++++++++++++++++++++ 4 files changed, 45 insertions(+), 6 deletions(-) diff --git a/templates/forum/category/forum.html b/templates/forum/category/forum.html index ba54730e0f..949ea8d7c2 100644 --- a/templates/forum/category/forum.html +++ b/templates/forum/category/forum.html @@ -66,7 +66,7 @@ {% block new_btn %} - {% if user.can_write_now %} + {% if user.profile.can_write_now %} {% trans "Nouveau sujet" %} diff --git a/templates/forum/topic/index.html b/templates/forum/topic/index.html index 4c31327ecb..1f2a656bb8 100644 --- a/templates/forum/topic/index.html +++ b/templates/forum/topic/index.html @@ -162,7 +162,7 @@ {% block new_btn %} - {% if user.can_write_now %} + {% if user.profile.can_write_now %} {% trans "Nouveau sujet" %} diff --git a/templates/misc/message_form.html b/templates/misc/message_form.html index 4cde75fa83..12f1e2783b 100644 --- a/templates/misc/message_form.html +++ b/templates/misc/message_form.html @@ -23,10 +23,10 @@

    Pas encore membre ?

    {% trans "Créer un compte" %}

    -{% elif not user.can_write_now %} -
    - {% trans "Vous êtes en lecture seule. Vous ne pouvez pas poster de messages." %} -
    +{% elif not user.profile.can_write_now %} +
    + {% trans "Vous êtes en lecture seule. Vous ne pouvez pas poster de messages." %} +
    {% elif topic.antispam or is_antispam %}
    {% trans "Vous venez de poster. Merci de patienter au moins 15 minutes entre deux messages consécutifs afin de limiter le flood." %} diff --git a/zds/forum/tests/tests.py b/zds/forum/tests/tests.py index 21c3351aef..717158541c 100644 --- a/zds/forum/tests/tests.py +++ b/zds/forum/tests/tests.py @@ -776,6 +776,45 @@ def test_frontend_alert_existence_other_pages(self): template_response = self.client.get(topic.get_absolute_url() + "?page=2") self.assertNotIn(expected, template_response.content.decode("utf-8")) + def test_frontend_topic_message_when_profile_readonly(self): + readonly_profile = ProfileFactory(can_write=False) + self.client.force_login(readonly_profile.user) + + topic = TopicFactory(forum=self.forum11, author=readonly_profile.user) + PostFactory(topic=topic, author=readonly_profile.user, position=1) + PostFactory(topic=topic, author=self.user, position=2) + + template_response = self.client.get(topic.get_absolute_url()) + decoded_content = template_response.content.decode("utf-8") + self.assertNotIn("Nouveau sujet", decoded_content) + self.assertNotIn("Éditer le sujet", decoded_content) + self.assertIn("Vous êtes en lecture seule. Vous ne pouvez pas poster de messages.", decoded_content) + + def test_frontend_topic_no_message_when_profile_can_write(self): + self.client.force_login(self.user2) + + topic = TopicFactory(forum=self.forum11, author=self.user2) + PostFactory(topic=topic, author=self.user2, position=1) + PostFactory(topic=topic, author=self.user, position=2) + + template_response = self.client.get(topic.get_absolute_url()) + decoded_content = template_response.content.decode("utf-8") + self.assertIn("Nouveau sujet", decoded_content) + self.assertIn("Éditer le sujet", decoded_content) + self.assertNotIn("Vous êtes en lecture seule. Vous ne pouvez pas poster de messages.", decoded_content) + + def test_frontend_no_create_topic_when_profile_readonly(self): + readonly_profile = ProfileFactory(can_write=False) + self.client.force_login(readonly_profile.user) + + template_response = self.client.get(self.forum22.get_absolute_url()) + self.assertNotIn("Nouveau sujet", template_response.content.decode("utf-8")) + + def test_frontend_can_create_topic_when_profile_can_write(self): + self.client.force_login(self.user) + template_response = self.client.get(self.forum22.get_absolute_url()) + self.assertIn("Nouveau sujet", template_response.content.decode("utf-8")) + class ForumGuestTests(TestCase): def setUp(self): From c6767d8c484bff090b8e13677ff9298ec425dab0 Mon Sep 17 00:00:00 2001 From: Philippe MILINK Date: Tue, 1 Feb 2022 21:25:57 +0100 Subject: [PATCH 31/53] =?UTF-8?q?Met=20=C3=A0=20jour=20Django?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pour intégrer les correctifs de sécurité. --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c5aa633f3a..548ffaf113 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ django-crispy-forms==1.13.0 django-model-utils==4.2.0 django-munin==0.2.1 django-recaptcha==2.0.6 -Django==3.2.11 +Django==3.2.12 easy-thumbnails==2.8.0 factory-boy==3.2.1 geoip2==4.5.0 From 155e700331e29daaedc9f064a63e33b5c268b680 Mon Sep 17 00:00:00 2001 From: SpaceFox Date: Sat, 5 Feb 2022 19:09:26 +0100 Subject: [PATCH 32/53] =?UTF-8?q?Passage=20=C3=A0=20NodeJS=2016=20LTS=20(#?= =?UTF-8?q?6229)=20(#6235)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .nvmrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.nvmrc b/.nvmrc index 48082f72f0..b6a7d89c68 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -12 +16 From 94cdffe2be03bc586bdc52fd93a374304cc19cd7 Mon Sep 17 00:00:00 2001 From: "Ph. SW" <5911232+philippemilink@users.noreply.github.com> Date: Sat, 5 Feb 2022 20:12:14 +0100 Subject: [PATCH 33/53] =?UTF-8?q?V=C3=A9rifie=20qu'il=20ne=20manque=20pas?= =?UTF-8?q?=20de=20migrations=20dans=20la=20CI=20(#6240)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Vérifie qu'il ne manque pas de migrations dans la CI * Update .github/workflows/ci.yml Co-authored-by: Situphen Co-authored-by: Situphen --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 136757b7ac..bf4243c36d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -223,6 +223,9 @@ jobs: - name: Install Python dependencies run: pip install -r requirements-ci.txt + - name: Check that no migration is missing + run: python manage.py makemigrations --check --dry-run + - name: Build and start zmarkdown run: | make zmd-install From d3fc65fff1b57b0ef566c7580b6be9f26397f8da Mon Sep 17 00:00:00 2001 From: Situphen Date: Sat, 5 Feb 2022 20:22:41 +0100 Subject: [PATCH 34/53] =?UTF-8?q?Oups,=20une=20migration=20oubli=C3=A9e=20?= =?UTF-8?q?(#6231)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../0024_alter_hatrequest_is_granted.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 zds/utils/migrations/0024_alter_hatrequest_is_granted.py diff --git a/zds/utils/migrations/0024_alter_hatrequest_is_granted.py b/zds/utils/migrations/0024_alter_hatrequest_is_granted.py new file mode 100644 index 0000000000..2f2b73fdc5 --- /dev/null +++ b/zds/utils/migrations/0024_alter_hatrequest_is_granted.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.11 on 2022-01-22 13:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("utils", "0023_move_potential_spam_to_comment_model"), + ] + + operations = [ + migrations.AlterField( + model_name="hatrequest", + name="is_granted", + field=models.BooleanField(null=True, verbose_name="Est acceptée"), + ), + ] From 7b389ba846e130e10ede52fe5770940894911fc4 Mon Sep 17 00:00:00 2001 From: Situphen Date: Sat, 5 Feb 2022 22:11:21 +0100 Subject: [PATCH 35/53] =?UTF-8?q?Mise=20=C3=A0=20jour=20des=20d=C3=A9penda?= =?UTF-8?q?nces=20Node.js=20(#6230)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 10 +- yarn.lock | 875 +++++++++++++++++++++++++-------------------------- 2 files changed, 427 insertions(+), 458 deletions(-) diff --git a/package.json b/package.json index 23e4f2ec1f..383694ef0a 100644 --- a/package.json +++ b/package.json @@ -25,10 +25,10 @@ }, "homepage": "https://github.com/zestedesavoir/zds-site", "dependencies": { - "autoprefixer": "10.2.5", - "chart.js": "3.5.1", + "autoprefixer": "10.4.2", + "chart.js": "3.7.0", "chartjs-adapter-moment": "1.0.0", - "cssnano": "5.0.4", + "cssnano": "5.0.15", "del": "6.0.0", "easymde": "2.10.2-360.0", "gulp": "4.0.2", @@ -37,13 +37,13 @@ "gulp-if": "3.0.0", "gulp-imagemin": "7.1.0", "gulp-options": "1.1.1", - "gulp-postcss": "9.0.0", + "gulp-postcss": "9.0.1", "gulp-terser-js": "5.1.2", "gulp.spritesmith": "6.12.1", "jquery": "3.6.0", "moment": "2.29.1", "normalize.css": "8.0.1", - "postcss": "8.3.0" + "postcss": "8.4.5" }, "devDependencies": { "eslint-config-standard": "14.1.1", diff --git a/yarn.lock b/yarn.lock index f732b7dfcc..ae51348aaf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -49,38 +49,33 @@ resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd" integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow== -"@trysound/sax@0.1.1": - version "0.1.1" - resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.1.1.tgz#3348564048e7a2d7398c935d466c0414ebb6a669" - integrity sha512-Z6DoceYb/1xSg5+e+ZlPZ9v0N16ZvZ+wYMraFue4HYrE4ttONKtsvruIRf6t9TBR0YvSOfi1hUU0fJfBLCDYow== +"@trysound/sax@0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" + integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== "@types/glob@^7.1.1": - version "7.1.3" - resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183" - integrity sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w== + version "7.2.0" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb" + integrity sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA== dependencies: "@types/minimatch" "*" "@types/node" "*" "@types/minimatch@*": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.4.tgz#f0ec25dbf2f0e4b18647313ac031134ca5b24b21" - integrity sha512-1z8k4wzFnNjVK/tlxvrWuK5WMt6mydWWP7+zvH5eFep4oj+UkrfiJTRtjCeBXNpwaA/FYqqtb4/QS4ianFpIRA== + version "3.0.5" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40" + integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ== "@types/node@*": - version "15.12.2" - resolved "https://registry.yarnpkg.com/@types/node/-/node-15.12.2.tgz#1f2b42c4be7156ff4a6f914b2fb03d05fa84e38d" - integrity sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww== - -"@types/parse-json@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" - integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== + version "17.0.10" + resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.10.tgz#616f16e9d3a2a3d618136b1be244315d95bd7cab" + integrity sha512-S/3xB4KzyFxYGCppyDt68yzBU9ysL88lSdIah4D6cptdcltc4NCPCAMc0+PCpg/lLIyC7IPvj2Z52OJWeIUkog== "@types/q@^1.5.1": - version "1.5.4" - resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24" - integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug== + version "1.5.5" + resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.5.tgz#75a2a8e7d8ab4b230414505d92335d1dcb53a6df" + integrity sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ== acorn-jsx@^5.2.0: version "5.3.1" @@ -391,17 +386,17 @@ atob@^2.1.2: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== -autoprefixer@10.2.5: - version "10.2.5" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.2.5.tgz#096a0337dbc96c0873526d7fef5de4428d05382d" - integrity sha512-7H4AJZXvSsn62SqZyJCP+1AWwOuoYpUfK6ot9vm0e87XD6mT8lDywc9D9OTJPMULyGcvmIxzTAMeG2Cc+YX+fA== +autoprefixer@10.4.2: + version "10.4.2" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.2.tgz#25e1df09a31a9fba5c40b578936b90d35c9d4d3b" + integrity sha512-9fOPpHKuDW1w/0EKfRmVnxTDt8166MAnLI3mgZ1JCnhNtYWxcJ6Ud5CO/AVOZi/AvFa8DY9RTy3h3+tFBlrrdQ== dependencies: - browserslist "^4.16.3" - caniuse-lite "^1.0.30001196" - colorette "^1.2.2" - fraction.js "^4.0.13" + browserslist "^4.19.1" + caniuse-lite "^1.0.30001297" + fraction.js "^4.1.2" normalize-range "^0.1.2" - postcss-value-parser "^4.1.0" + picocolors "^1.0.0" + postcss-value-parser "^4.2.0" aws-sign2@~0.7.0: version "0.7.0" @@ -572,7 +567,7 @@ braces@^3.0.1, braces@~3.0.2: dependencies: fill-range "^7.0.1" -browserslist@^4.0.0, browserslist@^4.16.0, browserslist@^4.16.3, browserslist@^4.16.6: +browserslist@^4.0.0, browserslist@^4.16.0, browserslist@^4.16.6: version "4.16.6" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.6.tgz#d7901277a5a88e554ed305b183ec9b0c08f66fa2" integrity sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ== @@ -583,6 +578,17 @@ browserslist@^4.0.0, browserslist@^4.16.0, browserslist@^4.16.3, browserslist@^4 escalade "^3.1.1" node-releases "^1.1.71" +browserslist@^4.19.1: + version "4.19.1" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.19.1.tgz#4ac0435b35ab655896c31d53018b6dd5e9e4c9a3" + integrity sha512-u2tbbG5PdKRTUoctO3NBD8FQ5HdPh1ZXPHzp1rwaa5jTc+RV9/+RlWiAIKmjRPQF+xbGM9Kklj5bZQFa2s/38A== + dependencies: + caniuse-lite "^1.0.30001286" + electron-to-chromium "^1.4.17" + escalade "^3.1.1" + node-releases "^2.0.1" + picocolors "^1.0.0" + buffer-alloc-unsafe@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0" @@ -660,25 +666,6 @@ call-bind@^1.0.0, call-bind@^1.0.2: function-bind "^1.1.1" get-intrinsic "^1.0.2" -caller-callsite@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134" - integrity sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ= - dependencies: - callsites "^2.0.0" - -caller-path@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-2.0.0.tgz#468f83044e369ab2010fac5f06ceee15bb2cb1f4" - integrity sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ= - dependencies: - caller-callsite "^2.0.0" - -callsites@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" - integrity sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA= - callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -712,10 +699,10 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001196, caniuse-lite@^1.0.30001219: - version "1.0.30001237" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001237.tgz#4b7783661515b8e7151fc6376cfd97f0e427b9e5" - integrity sha512-pDHgRndit6p1NR2GhzMbQ6CkRrp4VKuSsqbcLeOQppYPKOYkKT/6ZvZDvKJUqcmtyWIAHuZq3SVS2vc1egCZzw== +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001219, caniuse-lite@^1.0.30001286, caniuse-lite@^1.0.30001297: + version "1.0.30001301" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001301.tgz#ebc9086026534cab0dab99425d9c3b4425e5f450" + integrity sha512-csfD/GpHMqgEL3V3uIgosvh+SVIQvCh43SNu9HRbP1lnxkKm1kjDG4f32PP571JplkLjfS+mg2p1gxR7MYrrIA== caseless@~0.12.0: version "0.12.0" @@ -773,10 +760,10 @@ chardet@^0.7.0: resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== -chart.js@3.5.1: - version "3.5.1" - resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-3.5.1.tgz#73e24d23a4134a70ccdb5e79a917f156b6f3644a" - integrity sha512-m5kzt72I1WQ9LILwQC4syla/LD/N413RYv2Dx2nnTkRS9iv/ey1xLTt0DnPc/eWV4zI+BgEgDYBIzbQhZHc/PQ== +chart.js@3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-3.7.0.tgz#7a19c93035341df801d613993c2170a1fcf1d882" + integrity sha512-31gVuqqKp3lDIFmzpKIrBeum4OpZsQjSIAqlOpgjosHDJZlULtvwLEZKtEhIAZc7JMPaHlYMys40Qy9Mf+1AAg== chartjs-adapter-moment@1.0.0: version "1.0.0" @@ -966,10 +953,10 @@ color-support@^1.1.3: resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== -colord@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/colord/-/colord-2.0.1.tgz#1e7fb1f9fa1cf74f42c58cb9c20320bab8435aa0" - integrity sha512-vm5YpaWamD0Ov6TSG0GGmUIwstrWcfKQV/h2CmbR7PbNu41+qdB5PW9lpzhjedrpm08uuYvcXi0Oel1RLZIJuA== +colord@^2.9.1: + version "2.9.2" + resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.2.tgz#25e2bacbbaa65991422c07ea209e2089428effb1" + integrity sha512-Uqbg+J445nc1TKn4FoDPS6ZZqAvEDnwrH42yo8B40JSOgSLxMZ/gt3h4nmCtPLQeXhjJJkqBx7SCY35WnIixaQ== colorette@^1.2.2: version "1.2.2" @@ -988,7 +975,7 @@ commander@^2.20.0, commander@^2.8.1: resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== -commander@^7.1.0: +commander@^7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== @@ -1091,27 +1078,6 @@ core-util-is@1.0.2, core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= -cosmiconfig@^5.0.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a" - integrity sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA== - dependencies: - import-fresh "^2.0.0" - is-directory "^0.3.1" - js-yaml "^3.13.1" - parse-json "^4.0.0" - -cosmiconfig@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.0.tgz#ef9b44d773959cae63ddecd122de23853b60f8d3" - integrity sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA== - dependencies: - "@types/parse-json" "^4.0.0" - import-fresh "^3.2.1" - parse-json "^5.0.0" - path-type "^4.0.0" - yaml "^1.10.0" - cross-spawn@^5.0.1: version "5.1.0" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" @@ -1141,16 +1107,6 @@ cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" -css-color-names@^0.0.4: - version "0.0.4" - resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" - integrity sha1-gIrcLnnPhHOAabZGyyDsJ762KeA= - -css-color-names@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-1.0.1.tgz#6ff7ee81a823ad46e020fa2fd6ab40a887e2ba67" - integrity sha512-/loXYOch1qU1biStIFsHH8SxTmOseh1IJqFvy8IujXOm1h+QjUdDhkzOrR5HG8K8mlxREj0yfi8ewCHx0eMxzA== - css-declaration-sorter@^6.0.3: version "6.0.3" resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-6.0.3.tgz#9dfd8ea0df4cc7846827876fafb52314890c21a9" @@ -1173,16 +1129,16 @@ css-select@^2.0.0: domutils "^1.7.0" nth-check "^1.0.2" -css-select@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-3.1.2.tgz#d52cbdc6fee379fba97fb0d3925abbd18af2d9d8" - integrity sha512-qmss1EihSuBNWNNhHjxzxSfJoFBM/lERB/Q4EnsJQQC62R2evJDW481091oAdOr9uh46/0n4nrg0It5cAnj1RA== +css-select@^4.1.3: + version "4.2.1" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.2.1.tgz#9e665d6ae4c7f9d65dbe69d0316e3221fb274cdd" + integrity sha512-/aUslKhzkTNCQUB2qTX84lVmfia9NyjP3WpDGtj/WxhwBzWBYUV3DgUpurHTme8UTPcPlAD1DJ+b0nN/t50zDQ== dependencies: boolbase "^1.0.0" - css-what "^4.0.0" - domhandler "^4.0.0" - domutils "^2.4.3" - nth-check "^2.0.0" + css-what "^5.1.0" + domhandler "^4.3.0" + domutils "^2.8.0" + nth-check "^2.0.1" css-tree@1.0.0-alpha.37: version "1.0.0-alpha.37" @@ -1192,7 +1148,7 @@ css-tree@1.0.0-alpha.37: mdn-data "2.0.4" source-map "^0.6.1" -css-tree@^1.1.2: +css-tree@^1.1.2, css-tree@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== @@ -1205,64 +1161,64 @@ css-what@^3.2.1: resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.4.2.tgz#ea7026fcb01777edbde52124e21f327e7ae950e4" integrity sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ== -css-what@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-4.0.0.tgz#35e73761cab2eeb3d3661126b23d7aa0e8432233" - integrity sha512-teijzG7kwYfNVsUh2H/YN62xW3KK9YhXEgSlbxMlcyjPNvdKJqFx5lrwlJgoFP1ZHlB89iGDlo/JyshKeRhv5A== +css-what@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.1.0.tgz#3f7b707aadf633baf62c2ceb8579b545bb40f7fe" + integrity sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw== cssesc@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== -cssnano-preset-default@^5.1.1: - version "5.1.3" - resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-5.1.3.tgz#caa54183a8c8df03124a9e23f374ab89df5a9a99" - integrity sha512-qo9tX+t4yAAZ/yagVV3b+QBKeLklQbmgR3wI7mccrDcR+bEk9iHgZN1E7doX68y9ThznLya3RDmR+nc7l6/2WQ== +cssnano-preset-default@^5.1.10: + version "5.1.10" + resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-5.1.10.tgz#9350765fdf3c49bf78fac7673354fa58fa95daa4" + integrity sha512-BcpSzUVygHMOnp9uG5rfPzTOCb0GAHQkqtUQx8j1oMNF9A1Q8hziOOhiM4bdICpmrBIU85BE64RD5XGYsVQZNA== dependencies: css-declaration-sorter "^6.0.3" - cssnano-utils "^2.0.1" - postcss-calc "^8.0.0" - postcss-colormin "^5.2.0" - postcss-convert-values "^5.0.1" + cssnano-utils "^3.0.0" + postcss-calc "^8.2.0" + postcss-colormin "^5.2.3" + postcss-convert-values "^5.0.2" postcss-discard-comments "^5.0.1" postcss-discard-duplicates "^5.0.1" postcss-discard-empty "^5.0.1" - postcss-discard-overridden "^5.0.1" - postcss-merge-longhand "^5.0.2" - postcss-merge-rules "^5.0.2" - postcss-minify-font-values "^5.0.1" - postcss-minify-gradients "^5.0.1" - postcss-minify-params "^5.0.1" - postcss-minify-selectors "^5.1.0" + postcss-discard-overridden "^5.0.2" + postcss-merge-longhand "^5.0.4" + postcss-merge-rules "^5.0.4" + postcss-minify-font-values "^5.0.2" + postcss-minify-gradients "^5.0.4" + postcss-minify-params "^5.0.3" + postcss-minify-selectors "^5.1.1" postcss-normalize-charset "^5.0.1" - postcss-normalize-display-values "^5.0.1" - postcss-normalize-positions "^5.0.1" - postcss-normalize-repeat-style "^5.0.1" - postcss-normalize-string "^5.0.1" - postcss-normalize-timing-functions "^5.0.1" - postcss-normalize-unicode "^5.0.1" - postcss-normalize-url "^5.0.2" - postcss-normalize-whitespace "^5.0.1" - postcss-ordered-values "^5.0.2" - postcss-reduce-initial "^5.0.1" - postcss-reduce-transforms "^5.0.1" - postcss-svgo "^5.0.2" - postcss-unique-selectors "^5.0.1" - -cssnano-utils@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/cssnano-utils/-/cssnano-utils-2.0.1.tgz#8660aa2b37ed869d2e2f22918196a9a8b6498ce2" - integrity sha512-i8vLRZTnEH9ubIyfdZCAdIdgnHAUeQeByEeQ2I7oTilvP9oHO6RScpeq3GsFUVqeB8uZgOQ9pw8utofNn32hhQ== + postcss-normalize-display-values "^5.0.2" + postcss-normalize-positions "^5.0.2" + postcss-normalize-repeat-style "^5.0.2" + postcss-normalize-string "^5.0.2" + postcss-normalize-timing-functions "^5.0.2" + postcss-normalize-unicode "^5.0.2" + postcss-normalize-url "^5.0.4" + postcss-normalize-whitespace "^5.0.2" + postcss-ordered-values "^5.0.3" + postcss-reduce-initial "^5.0.2" + postcss-reduce-transforms "^5.0.2" + postcss-svgo "^5.0.3" + postcss-unique-selectors "^5.0.2" + +cssnano-utils@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cssnano-utils/-/cssnano-utils-3.0.0.tgz#c0b9fcd6e4f05c5155b07e9ab11bf94b97163057" + integrity sha512-Pzs7/BZ6OgT+tXXuF12DKR8SmSbzUeVYCtMBbS8lI0uAm3mrYmkyqCXXPsQESI6kmLfEVBppbdVY/el3hg3nAA== -cssnano@5.0.4: - version "5.0.4" - resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-5.0.4.tgz#5ca90729c94c71c4bc3d45abb543be10740bf381" - integrity sha512-I+fDW74CJ4yb31765ov9xXe70XLZvFTXjwhmA//VgAAuSAU34Oblbe94Q9zffiCX1VhcSfQWARQnwhz+Nqgb4Q== +cssnano@5.0.15: + version "5.0.15" + resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-5.0.15.tgz#8779eaf60e3665e6a12687c814d375cc9f78db76" + integrity sha512-ppZsS7oPpi2sfiyV5+i+NbB/3GtQ+ab2Vs1azrZaXWujUSN4o+WdTxlCZIMcT9yLW3VO/5yX3vpyDaQ1nIn8CQ== dependencies: - cosmiconfig "^7.0.0" - cssnano-preset-default "^5.1.1" - is-resolvable "^1.1.0" + cssnano-preset-default "^5.1.10" + lilconfig "^2.0.3" + yaml "^1.10.2" csso@^4.0.2, csso@^4.2.0: version "4.2.0" @@ -1515,13 +1471,20 @@ domelementtype@^2.0.1, domelementtype@^2.2.0: resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57" integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A== -domhandler@^4.0.0, domhandler@^4.2.0: +domhandler@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.2.0.tgz#f9768a5f034be60a89a27c2e4d0f74eba0d8b059" integrity sha512-zk7sgt970kzPks2Bf+dwT/PLzghLnsivb9CcxkvR8Mzr66Olr0Ofd8neSbglHJHaHa2MadfoSdNlKYAaafmWfA== dependencies: domelementtype "^2.2.0" +domhandler@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.0.tgz#16c658c626cf966967e306f966b431f77d4a5626" + integrity sha512-fC0aXNQXqKSFTr2wDNZDhsEYjCiYsDWl3D01kwt25hm1YIPyDGHvvi3rw+PLqHAl/m71MaiF7d5zvBr0p5UB2g== + dependencies: + domelementtype "^2.2.0" + domutils@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" @@ -1530,10 +1493,10 @@ domutils@^1.7.0: dom-serializer "0" domelementtype "1" -domutils@^2.4.3: - version "2.7.0" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.7.0.tgz#8ebaf0c41ebafcf55b0b72ec31c56323712c5442" - integrity sha512-8eaHa17IwJUPAiB+SoTYBo5mCdeMgdcAoXJ59m6DT1vw+5iLS3gNoqYaRowaBKtGVrOF1Jz4yDTgYKLK2kvfJg== +domutils@^2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" + integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== dependencies: dom-serializer "^1.0.1" domelementtype "^2.2.0" @@ -1629,6 +1592,11 @@ electron-to-chromium@^1.3.723: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.752.tgz#0728587f1b9b970ec9ffad932496429aef750d09" integrity sha512-2Tg+7jSl3oPxgsBsWKh5H83QazTkmWG/cnNwJplmyZc7KcN61+I10oUgaXSVk/NwfvN3BdkKDR4FYuRBQQ2v0A== +electron-to-chromium@^1.4.17: + version "1.4.51" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.51.tgz#a432f5a5d983ace79278a33057300cf949627e63" + integrity sha512-JNEmcYl3mk1tGQmy0EvL5eik/CKSBuzAyGP0QFdG6LIgxQe3II0BL1m2zKc2MZMf3uGqHWE1TFddJML0RpjSHQ== + emoji-regex@^7.0.1: version "7.0.3" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" @@ -1651,14 +1619,40 @@ entities@^2.0.0: resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== -error-ex@^1.2.0, error-ex@^1.3.1: +error-ex@^1.2.0: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== dependencies: is-arrayish "^0.2.1" -es-abstract@^1.17.2, es-abstract@^1.18.0-next.1, es-abstract@^1.18.0-next.2, es-abstract@^1.18.2: +es-abstract@^1.17.2, es-abstract@^1.19.1: + version "1.19.1" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.19.1.tgz#d4885796876916959de78edaa0df456627115ec3" + integrity sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w== + dependencies: + call-bind "^1.0.2" + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + get-intrinsic "^1.1.1" + get-symbol-description "^1.0.0" + has "^1.0.3" + has-symbols "^1.0.2" + internal-slot "^1.0.3" + is-callable "^1.2.4" + is-negative-zero "^2.0.1" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.1" + is-string "^1.0.7" + is-weakref "^1.0.1" + object-inspect "^1.11.0" + object-keys "^1.1.1" + object.assign "^4.1.2" + string.prototype.trimend "^1.0.4" + string.prototype.trimstart "^1.0.4" + unbox-primitive "^1.0.1" + +es-abstract@^1.18.0-next.1, es-abstract@^1.18.0-next.2, es-abstract@^1.18.2: version "1.18.3" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.3.tgz#25c4c3380a27aa203c44b2b685bba94da31b63e0" integrity sha512-nQIr12dxV7SSxE6r6f1l3DtAeEYdsGpps13dR0TwJg1S8gyp4ZPgy3FZcHBgbiQqnoqSTb+oC+kO4UQ0C/J8vw== @@ -2099,7 +2093,18 @@ fast-deep-equal@^3.1.1: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-glob@^3.0.3, fast-glob@^3.1.1: +fast-glob@^3.0.3: + version "3.2.11" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9" + integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fast-glob@^3.1.1: version "3.2.5" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.5.tgz#7939af2a656de79a4f1901903ee8adcaa7cb9661" integrity sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg== @@ -2354,10 +2359,10 @@ form-data@~2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" -fraction.js@^4.0.13: - version "4.1.1" - resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.1.1.tgz#ac4e520473dae67012d618aab91eda09bcb400ff" - integrity sha512-MHOhvvxHTfRFpF1geTK9czMIZ6xclsEor2wkIGYYq+PxcQqT7vStJqjhe6S1TenZrMZzo+wlqOufBDVepUEgPg== +fraction.js@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.1.2.tgz#13e420a92422b6cf244dff8690ed89401029fbe8" + integrity sha512-o2RiJQ6DZaR/5+Si0qJUIy637QMRudSi9kU/FFzx9EZazrIdnBgpU+3sEWCxAVhH2RtxW2Oz+T4p2o8uOPVcgA== fragment-cache@^0.2.1: version "0.2.1" @@ -2420,7 +2425,7 @@ get-caller-file@^1.0.1: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== -get-intrinsic@^1.0.2, get-intrinsic@^1.1.1: +get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== @@ -2483,6 +2488,14 @@ get-stream@^6.0.0: resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== +get-symbol-description@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" + integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.1" + get-value@^2.0.3, get-value@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" @@ -2520,7 +2533,7 @@ glob-parent@^3.1.0: is-glob "^3.1.0" path-dirname "^1.0.0" -glob-parent@^5.0.0, glob-parent@^5.1.0, glob-parent@~5.1.0: +glob-parent@^5.0.0, glob-parent@^5.1.0, glob-parent@^5.1.2, glob-parent@~5.1.0: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== @@ -2671,11 +2684,16 @@ got@^8.3.1: url-parse-lax "^3.0.0" url-to-options "^1.0.1" -graceful-fs@^4.0.0, graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.2, graceful-fs@^4.2.4: +graceful-fs@^4.0.0, graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.4: version "4.2.6" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== +graceful-fs@^4.2.2: + version "4.2.9" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96" + integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ== + gulp-cli@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/gulp-cli/-/gulp-cli-2.3.0.tgz#ec0d380e29e52aa45e47977f0d32e18fd161122f" @@ -2771,14 +2789,14 @@ gulp-options@1.1.1: resolved "https://registry.yarnpkg.com/gulp-options/-/gulp-options-1.1.1.tgz#589676e1adfaa48deef633b79b86c169f2dbbb99" integrity sha1-WJZ24a36pI3u9jO3m4bBafLbu5k= -gulp-postcss@9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/gulp-postcss/-/gulp-postcss-9.0.0.tgz#2ade18809ab475dae743a88bd6501af0b04ee54e" - integrity sha512-5mSQ9CK8salSagrXgrVyILfEMy6I5rUGPRiR9rVjgJV9m/rwdZYUhekMr+XxDlApfc5ZdEJ8gXNZrU/TsgT5dQ== +gulp-postcss@9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/gulp-postcss/-/gulp-postcss-9.0.1.tgz#d43caa2f2ce1018f889f7c1296faf82e9928b66f" + integrity sha512-9QUHam5JyXwGUxaaMvoFQVT44tohpEFpM8xBdPfdwTYGM0AItS1iTQz0MpsF8Jroh7GF5Jt2GVPaYgvy8qD2Fw== dependencies: fancy-log "^1.3.3" plugin-error "^1.0.1" - postcss-load-config "^2.1.1" + postcss-load-config "^3.0.0" vinyl-sourcemaps-apply "^0.2.1" gulp-terser-js@5.1.2: @@ -2891,6 +2909,13 @@ has-to-string-tag-x@^1.2.0: dependencies: has-symbol-support-x "^1.4.1" +has-tostringtag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" + integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== + dependencies: + has-symbols "^1.0.2" + has-value@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" @@ -2929,11 +2954,6 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" -hex-color-regex@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" - integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== - homedir-polyfill@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" @@ -2946,16 +2966,6 @@ hosted-git-info@^2.1.4: resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== -hsl-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/hsl-regex/-/hsl-regex-1.0.0.tgz#d49330c789ed819e276a4c0d272dffa30b18fe6e" - integrity sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4= - -hsla-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/hsla-regex/-/hsla-regex-1.0.0.tgz#c1ce7a3168c8c6614033a4b5f7877f3b225f9c38" - integrity sha1-wc56MWjIxmFAM6S194d/OyJfnDg= - http-cache-semantics@3.8.1: version "3.8.1" resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2" @@ -3045,22 +3055,7 @@ imagemin@^7.0.0: p-pipe "^3.0.0" replace-ext "^1.0.0" -import-cwd@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9" - integrity sha1-qmzzbnInYShcs3HsZRn1PiQ1sKk= - dependencies: - import-from "^2.1.0" - -import-fresh@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546" - integrity sha1-2BNVwVYS04bGH53dOSLUMEgipUY= - dependencies: - caller-path "^2.0.0" - resolve-from "^3.0.0" - -import-fresh@^3.0.0, import-fresh@^3.2.1: +import-fresh@^3.0.0: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== @@ -3068,13 +3063,6 @@ import-fresh@^3.0.0, import-fresh@^3.2.1: parent-module "^1.0.0" resolve-from "^4.0.0" -import-from@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/import-from/-/import-from-2.1.0.tgz#335db7f2a7affd53aaa471d4b8021dee36b7f3b1" - integrity sha1-M1238qev/VOqpHHUuAId7ja387E= - dependencies: - resolve-from "^3.0.0" - import-lazy@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-3.1.0.tgz#891279202c8a2280fdbd6674dbd8da1a1dfc67cc" @@ -3134,6 +3122,15 @@ inquirer@^7.0.0: strip-ansi "^6.0.0" through "^2.3.6" +internal-slot@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c" + integrity sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA== + dependencies: + get-intrinsic "^1.1.0" + has "^1.0.3" + side-channel "^1.0.4" + interpret@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" @@ -3162,11 +3159,6 @@ irregular-plurals@^2.0.0: resolved "https://registry.yarnpkg.com/irregular-plurals/-/irregular-plurals-2.0.0.tgz#39d40f05b00f656d0b7fa471230dd3b714af2872" integrity sha512-Y75zBYLkh0lJ9qxeHlMjQ7bSbyiSqNW/UOPWDmzC7cXskL1hekSITh1Oc6JV0XCWWZ9DE8VYSB71xocLk3gmGw== -is-absolute-url@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-3.0.3.tgz#96c6a22b6a23929b11ea0afb1836c36ad4a5d698" - integrity sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q== - is-absolute@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-absolute/-/is-absolute-1.0.0.tgz#395e1ae84b11f26ad1795e73c17378e48a301576" @@ -3230,17 +3222,10 @@ is-callable@^1.1.4, is-callable@^1.2.3: resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.3.tgz#8b1e0500b73a1d76c70487636f368e519de8db8e" integrity sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ== -is-color-stop@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-color-stop/-/is-color-stop-1.1.0.tgz#cfff471aee4dd5c9e158598fbe12967b5cdad345" - integrity sha1-z/9HGu5N1cnhWFmPvhKWe1za00U= - dependencies: - css-color-names "^0.0.4" - hex-color-regex "^1.1.0" - hsl-regex "^1.0.0" - hsla-regex "^1.0.0" - rgb-regex "^1.0.1" - rgba-regex "^1.0.0" +is-callable@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945" + integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w== is-core-module@^2.2.0: version "2.4.0" @@ -3286,11 +3271,6 @@ is-descriptor@^1.0.0, is-descriptor@^1.0.2: is-data-descriptor "^1.0.0" kind-of "^6.0.2" -is-directory@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" - integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE= - is-extendable@^0.1.0, is-extendable@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" @@ -3438,6 +3418,14 @@ is-regex@^1.1.3: call-bind "^1.0.2" has-symbols "^1.0.2" +is-regex@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" + integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + is-relative@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-relative/-/is-relative-1.0.0.tgz#a1bb6935ce8c5dba1e8b9754b9b2dcc020e2260d" @@ -3445,16 +3433,16 @@ is-relative@^1.0.0: dependencies: is-unc-path "^1.0.0" -is-resolvable@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88" - integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg== - is-retry-allowed@^1.0.0, is-retry-allowed@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz#d778488bd0a4666a3be8a1482b9f2baafedea8b4" integrity sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg== +is-shared-array-buffer@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz#97b0c85fbdacb59c9c446fe653b82cf2b5b7cfe6" + integrity sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA== + is-stream@^1.0.0, is-stream@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" @@ -3470,6 +3458,13 @@ is-string@^1.0.5, is-string@^1.0.6: resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.6.tgz#3fe5d5992fb0d93404f32584d4b0179a71b54a5f" integrity sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w== +is-string@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" + integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== + dependencies: + has-tostringtag "^1.0.0" + is-svg@^4.2.1: version "4.3.1" resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-4.3.1.tgz#8c63ec8c67c8c7f0a8de0a71c8c7d58eccf4406b" @@ -3506,6 +3501,13 @@ is-valid-glob@^1.0.0: resolved "https://registry.yarnpkg.com/is-valid-glob/-/is-valid-glob-1.0.0.tgz#29bf3eff701be2d4d315dbacc39bc39fe8f601aa" integrity sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao= +is-weakref@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" + integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== + dependencies: + call-bind "^1.0.2" + is-windows@^1.0.1, is-windows@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" @@ -3599,16 +3601,6 @@ json-content-demux@~0.1.2: resolved "https://registry.yarnpkg.com/json-content-demux/-/json-content-demux-0.1.4.tgz#6d573fbb5a2b224ae225736e287f32ea91a4273d" integrity sha512-3GqPH2O0+8qBMTa1YTuL+7L24YJYNDjdXfa798y9S6GetScZAY2iAOGCdFkEPZJZdafPKv8ZUnp18VCCPTs0Nw== -json-parse-better-errors@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" - integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== - -json-parse-even-better-errors@^2.3.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" - integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== - json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" @@ -3738,10 +3730,10 @@ liftoff@^3.1.0: rechoir "^0.6.2" resolve "^1.1.7" -lines-and-columns@^1.1.6: - version "1.1.6" - resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" - integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= +lilconfig@^2.0.3, lilconfig@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.4.tgz#f4507d043d7058b380b6a8f5cb7bcd4b34cee082" + integrity sha512-bfTIN7lEsiooCocSISTWXkiWJkRqtL9wYtYy+8EK3Y41qh3mpwPU0ycTOgjdY9ErwXCc8QyrQp82bdL0Xkm9yA== load-json-file@^1.0.0: version "1.1.0" @@ -3949,7 +3941,7 @@ micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4: snapdragon "^0.8.1" to-regex "^3.0.2" -micromatch@^4.0.2: +micromatch@^4.0.2, micromatch@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== @@ -4050,10 +4042,10 @@ nan@^2.12.1: resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19" integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ== -nanoid@^3.1.23: - version "3.1.23" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.23.tgz#f744086ce7c2bc47ee0a8472574d5c78e4183a81" - integrity sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw== +nanoid@^3.1.30: + version "3.2.0" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c" + integrity sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA== nanomatch@^1.2.9: version "1.2.13" @@ -4125,6 +4117,11 @@ node-releases@^1.1.71: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.73.tgz#dd4e81ddd5277ff846b80b52bb40c49edf7a7b20" integrity sha512-uW7fodD6pyW2FZNZnp/Z3hvWKeEW1Y8R1+1CnErE8cXFXzl5blBOoVB41CvMer6P6Q0S5FXDwcHgFd1Wj0U9zg== +node-releases@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.1.tgz#3d1d395f204f1f2f29a54358b9fb678765ad2fc5" + integrity sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA== + normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: version "2.5.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" @@ -4207,10 +4204,10 @@ nth-check@^1.0.2: dependencies: boolbase "~1.0.0" -nth-check@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.0.tgz#1bb4f6dac70072fc313e8c9cd1417b5074c0a125" - integrity sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q== +nth-check@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.1.tgz#2efe162f5c3da06a28959fbd3db75dbeea9f0fc2" + integrity sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w== dependencies: boolbase "^1.0.0" @@ -4248,6 +4245,11 @@ object-inspect@^1.10.3: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.10.3.tgz#c2aa7d2d09f50c99375704f7a0adf24c5782d369" integrity sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw== +object-inspect@^1.11.0, object-inspect@^1.9.0: + version "1.12.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0" + integrity sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g== + object-keys@^1.0.12, object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" @@ -4281,13 +4283,13 @@ object.defaults@^1.0.0, object.defaults@^1.1.0: isobject "^3.0.0" object.getownpropertydescriptors@^2.1.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.2.tgz#1bd63aeacf0d5d2d2f31b5e393b03a7c601a23f7" - integrity sha512-WtxeKSzfBjlzL+F9b7M7hewDzMwy+C8NRssHd1YrNlzHzIDrXcXiNOMrezdAEM4UXixgV+vvnyBeN7Rygl2ttQ== + version "2.1.3" + resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.3.tgz#b223cf38e17fefb97a63c10c91df72ccb386df9e" + integrity sha512-VdDoCwvJI4QdC6ndjpqFmoL3/+HxffFBbcJzKi5hwLLqqx3mdbedRpfZDdK0SrOSauj8X4GzBvnDZl4vTN7dOw== dependencies: call-bind "^1.0.2" define-properties "^1.1.3" - es-abstract "^1.18.0-next.2" + es-abstract "^1.19.1" object.map@^1.0.0: version "1.0.1" @@ -4508,24 +4510,6 @@ parse-json@^2.2.0: dependencies: error-ex "^1.2.0" -parse-json@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" - integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA= - dependencies: - error-ex "^1.3.1" - json-parse-better-errors "^1.0.1" - -parse-json@^5.0.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" - integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== - dependencies: - "@babel/code-frame" "^7.0.0" - error-ex "^1.3.1" - json-parse-even-better-errors "^2.3.0" - lines-and-columns "^1.1.6" - parse-node-version@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parse-node-version/-/parse-node-version-1.0.1.tgz#e2b5dbede00e7fa9bc363607f53327e8b073189b" @@ -4621,6 +4605,11 @@ performance-now@^2.1.0: resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3: version "2.3.0" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" @@ -4706,28 +4695,28 @@ posix-character-classes@^0.1.0: resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= -postcss-calc@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-8.0.0.tgz#a05b87aacd132740a5db09462a3612453e5df90a" - integrity sha512-5NglwDrcbiy8XXfPM11F3HeC6hoT9W7GUH/Zi5U/p7u3Irv4rHhdDcIZwG0llHXV4ftsBjpfWMXAnXNl4lnt8g== +postcss-calc@^8.2.0: + version "8.2.2" + resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-8.2.2.tgz#9706e7399e8ec8b61a47830dcf1f21391af23373" + integrity sha512-B5R0UeB4zLJvxNt1FVCaDZULdzsKLPc6FhjFJ+xwFiq7VG4i9cuaJLxVjNtExNK8ocm3n2o4unXXLiVX1SCqxA== dependencies: postcss-selector-parser "^6.0.2" postcss-value-parser "^4.0.2" -postcss-colormin@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-5.2.0.tgz#2b620b88c0ff19683f3349f4cf9e24ebdafb2c88" - integrity sha512-+HC6GfWU3upe5/mqmxuqYZ9B2Wl4lcoUUNkoaX59nEWV4EtADCMiBqui111Bu8R8IvaZTmqmxrqOAqjbHIwXPw== +postcss-colormin@^5.2.3: + version "5.2.3" + resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-5.2.3.tgz#da7fb80e81ad80d2867ea9e38672a892add5df15" + integrity sha512-dra4xoAjub2wha6RUXAgadHEn2lGxbj8drhFcIGLOMn914Eu7DkPUurugDXgstwttCYkJtZ/+PkWRWdp3UHRIA== dependencies: browserslist "^4.16.6" caniuse-api "^3.0.0" - colord "^2.0.1" - postcss-value-parser "^4.1.0" + colord "^2.9.1" + postcss-value-parser "^4.2.0" -postcss-convert-values@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-5.0.1.tgz#4ec19d6016534e30e3102fdf414e753398645232" - integrity sha512-C3zR1Do2BkKkCgC0g3sF8TS0koF2G+mN8xxayZx3f10cIRmTaAnpgpRQZjNekTZxM2ciSPoh2IWJm0VZx8NoQg== +postcss-convert-values@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-5.0.2.tgz#879b849dc3677c7d6bc94b6a2c1a3f0808798059" + integrity sha512-KQ04E2yadmfa1LqXm7UIDwW1ftxU/QWZmz6NKnHnUvJ3LEYbbcX6i329f/ig+WnEByHegulocXrECaZGLpL8Zg== dependencies: postcss-value-parser "^4.1.0" @@ -4746,70 +4735,67 @@ postcss-discard-empty@^5.0.1: resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-5.0.1.tgz#ee136c39e27d5d2ed4da0ee5ed02bc8a9f8bf6d8" integrity sha512-vfU8CxAQ6YpMxV2SvMcMIyF2LX1ZzWpy0lqHDsOdaKKLQVQGVP1pzhrI9JlsO65s66uQTfkQBKBD/A5gp9STFw== -postcss-discard-overridden@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-5.0.1.tgz#454b41f707300b98109a75005ca4ab0ff2743ac6" - integrity sha512-Y28H7y93L2BpJhrdUR2SR2fnSsT+3TVx1NmVQLbcnZWwIUpJ7mfcTC6Za9M2PG6w8j7UQRfzxqn8jU2VwFxo3Q== +postcss-discard-overridden@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-5.0.2.tgz#e6f51d83e66feffcf05ed94c4ad20b814d0aab5f" + integrity sha512-+56BLP6NSSUuWUXjRgAQuho1p5xs/hU5Sw7+xt9S3JSg+7R6+WMGnJW7Hre/6tTuZ2xiXMB42ObkiZJ2hy/Pew== -postcss-load-config@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-2.1.2.tgz#c5ea504f2c4aef33c7359a34de3573772ad7502a" - integrity sha512-/rDeGV6vMUo3mwJZmeHfEDvwnTKKqQ0S7OHUi/kJvvtx3aWtyWG2/0ZWnzCt2keEclwN6Tf0DST2v9kITdOKYw== +postcss-load-config@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-3.1.1.tgz#2f53a17f2f543d9e63864460af42efdac0d41f87" + integrity sha512-c/9XYboIbSEUZpiD1UQD0IKiUe8n9WHYV7YFe7X7J+ZwCsEKkUJSFWjS9hBU1RR9THR7jMXst8sxiqP0jjo2mg== dependencies: - cosmiconfig "^5.0.0" - import-cwd "^2.0.0" + lilconfig "^2.0.4" + yaml "^1.10.2" -postcss-merge-longhand@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-5.0.2.tgz#277ada51d9a7958e8ef8cf263103c9384b322a41" - integrity sha512-BMlg9AXSI5G9TBT0Lo/H3PfUy63P84rVz3BjCFE9e9Y9RXQZD3+h3YO1kgTNsNJy7bBc1YQp8DmSnwLIW5VPcw== +postcss-merge-longhand@^5.0.4: + version "5.0.4" + resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-5.0.4.tgz#41f4f3270282ea1a145ece078b7679f0cef21c32" + integrity sha512-2lZrOVD+d81aoYkZDpWu6+3dTAAGkCKbV5DoRhnIR7KOULVrI/R7bcMjhrH9KTRy6iiHKqmtG+n/MMj1WmqHFw== dependencies: - css-color-names "^1.0.1" postcss-value-parser "^4.1.0" stylehacks "^5.0.1" -postcss-merge-rules@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-5.0.2.tgz#d6e4d65018badbdb7dcc789c4f39b941305d410a" - integrity sha512-5K+Md7S3GwBewfB4rjDeol6V/RZ8S+v4B66Zk2gChRqLTCC8yjnHQ601omj9TKftS19OPGqZ/XzoqpzNQQLwbg== +postcss-merge-rules@^5.0.4: + version "5.0.4" + resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-5.0.4.tgz#a50640fd832380f322bd2861a9b33fbde4219f9b" + integrity sha512-yOj7bW3NxlQxaERBB0lEY1sH5y+RzevjbdH4DBJurjKERNpknRByFNdNe+V72i5pIZL12woM9uGdS5xbSB+kDQ== dependencies: browserslist "^4.16.6" caniuse-api "^3.0.0" - cssnano-utils "^2.0.1" + cssnano-utils "^3.0.0" postcss-selector-parser "^6.0.5" - vendors "^1.0.3" -postcss-minify-font-values@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-5.0.1.tgz#a90cefbfdaa075bd3dbaa1b33588bb4dc268addf" - integrity sha512-7JS4qIsnqaxk+FXY1E8dHBDmraYFWmuL6cgt0T1SWGRO5bzJf8sUoelwa4P88LEWJZweHevAiDKxHlofuvtIoA== +postcss-minify-font-values@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-5.0.2.tgz#4603e956d85cd0719156e2b3eb68e3cd2f917092" + integrity sha512-R6MJZryq28Cw0AmnyhXrM7naqJZZLoa1paBltIzh2wM7yb4D45TLur+eubTQ4jCmZU9SGeZdWsc5KcSoqTMeTg== dependencies: - postcss-value-parser "^4.1.0" + postcss-value-parser "^4.2.0" -postcss-minify-gradients@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-5.0.1.tgz#2dc79fd1a1afcb72a9e727bc549ce860f93565d2" - integrity sha512-odOwBFAIn2wIv+XYRpoN2hUV3pPQlgbJ10XeXPq8UY2N+9ZG42xu45lTn/g9zZ+d70NKSQD6EOi6UiCMu3FN7g== +postcss-minify-gradients@^5.0.4: + version "5.0.4" + resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-5.0.4.tgz#f13146950513f5a201015306914e3c76d10b591d" + integrity sha512-RVwZA7NC4R4J76u8X0Q0j+J7ItKUWAeBUJ8oEEZWmtv3Xoh19uNJaJwzNpsydQjk6PkuhRrK+YwwMf+c+68EYg== dependencies: - cssnano-utils "^2.0.1" - is-color-stop "^1.1.0" - postcss-value-parser "^4.1.0" + colord "^2.9.1" + cssnano-utils "^3.0.0" + postcss-value-parser "^4.2.0" -postcss-minify-params@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-5.0.1.tgz#371153ba164b9d8562842fdcd929c98abd9e5b6c" - integrity sha512-4RUC4k2A/Q9mGco1Z8ODc7h+A0z7L7X2ypO1B6V8057eVK6mZ6xwz6QN64nHuHLbqbclkX1wyzRnIrdZehTEHw== +postcss-minify-params@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-5.0.3.tgz#9f933d37098ef1dcf007e159a47bb2c1cf06989d" + integrity sha512-NY92FUikE+wralaiVexFd5gwb7oJTIDhgTNeIw89i1Ymsgt4RWiPXfz3bg7hDy4NL6gepcThJwOYNtZO/eNi7Q== dependencies: alphanum-sort "^1.0.2" - browserslist "^4.16.0" - cssnano-utils "^2.0.1" - postcss-value-parser "^4.1.0" - uniqs "^2.0.0" + browserslist "^4.16.6" + cssnano-utils "^3.0.0" + postcss-value-parser "^4.2.0" -postcss-minify-selectors@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-5.1.0.tgz#4385c845d3979ff160291774523ffa54eafd5a54" - integrity sha512-NzGBXDa7aPsAcijXZeagnJBKBPMYLaJJzB8CQh6ncvyl2sIndLVWfbcDi0SBjRWk5VqEjXvf8tYwzoKf4Z07og== +postcss-minify-selectors@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-5.1.1.tgz#20ae03b411f7fb397451e3d7d85b989f944b871c" + integrity sha512-TOzqOPXt91O2luJInaVPiivh90a2SIK5Nf1Ea7yEIM/5w+XA5BGrZGUSW8aEx9pJ/oNj7ZJBhjvigSiBV+bC1Q== dependencies: alphanum-sort "^1.0.2" postcss-selector-parser "^6.0.5" @@ -4819,91 +4805,86 @@ postcss-normalize-charset@^5.0.1: resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-5.0.1.tgz#121559d1bebc55ac8d24af37f67bd4da9efd91d0" integrity sha512-6J40l6LNYnBdPSk+BHZ8SF+HAkS4q2twe5jnocgd+xWpz/mx/5Sa32m3W1AA8uE8XaXN+eg8trIlfu8V9x61eg== -postcss-normalize-display-values@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-5.0.1.tgz#62650b965981a955dffee83363453db82f6ad1fd" - integrity sha512-uupdvWk88kLDXi5HEyI9IaAJTE3/Djbcrqq8YgjvAVuzgVuqIk3SuJWUisT2gaJbZm1H9g5k2w1xXilM3x8DjQ== +postcss-normalize-display-values@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-5.0.2.tgz#8b5273c6c7d0a445e6ef226b8a5bb3204a55fb99" + integrity sha512-RxXoJPUR0shSjkMMzgEZDjGPrgXUVYyWA/YwQRicb48H15OClPuaDR7tYokLAlGZ2tCSENEN5WxjgxSD5m4cUw== dependencies: - cssnano-utils "^2.0.1" - postcss-value-parser "^4.1.0" + postcss-value-parser "^4.2.0" -postcss-normalize-positions@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-5.0.1.tgz#868f6af1795fdfa86fbbe960dceb47e5f9492fe5" - integrity sha512-rvzWAJai5xej9yWqlCb1OWLd9JjW2Ex2BCPzUJrbaXmtKtgfL8dBMOOMTX6TnvQMtjk3ei1Lswcs78qKO1Skrg== +postcss-normalize-positions@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-5.0.2.tgz#799fa494b352a5da183be8f050024af6d92fa29c" + integrity sha512-tqghWFVDp2btqFg1gYob1etPNxXLNh3uVeWgZE2AQGh6b2F8AK2Gj36v5Vhyh+APwIzNjmt6jwZ9pTBP+/OM8g== dependencies: - postcss-value-parser "^4.1.0" + postcss-value-parser "^4.2.0" -postcss-normalize-repeat-style@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.0.1.tgz#cbc0de1383b57f5bb61ddd6a84653b5e8665b2b5" - integrity sha512-syZ2itq0HTQjj4QtXZOeefomckiV5TaUO6ReIEabCh3wgDs4Mr01pkif0MeVwKyU/LHEkPJnpwFKRxqWA/7O3w== +postcss-normalize-repeat-style@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.0.2.tgz#fd9bddba3e6fd5f5d95c18dfb42a09ecd563adea" + integrity sha512-/rIZn8X9bBzC7KvY4iKUhXUGW3MmbXwfPF23jC9wT9xTi7kAvgj8sEgwxjixBmoL6MVa4WOgxNz2hAR6wTK8tw== dependencies: - cssnano-utils "^2.0.1" - postcss-value-parser "^4.1.0" + postcss-value-parser "^4.2.0" -postcss-normalize-string@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-5.0.1.tgz#d9eafaa4df78c7a3b973ae346ef0e47c554985b0" - integrity sha512-Ic8GaQ3jPMVl1OEn2U//2pm93AXUcF3wz+OriskdZ1AOuYV25OdgS7w9Xu2LO5cGyhHCgn8dMXh9bO7vi3i9pA== +postcss-normalize-string@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-5.0.2.tgz#1b2bbf91526f61266f28abf7f773e4136b2c4bd2" + integrity sha512-zaI1yzwL+a/FkIzUWMQoH25YwCYxi917J4pYm1nRXtdgiCdnlTkx5eRzqWEC64HtRa06WCJ9TIutpb6GmW4gFw== dependencies: - postcss-value-parser "^4.1.0" + postcss-value-parser "^4.2.0" -postcss-normalize-timing-functions@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.0.1.tgz#8ee41103b9130429c6cbba736932b75c5e2cb08c" - integrity sha512-cPcBdVN5OsWCNEo5hiXfLUnXfTGtSFiBU9SK8k7ii8UD7OLuznzgNRYkLZow11BkQiiqMcgPyh4ZqXEEUrtQ1Q== +postcss-normalize-timing-functions@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.0.2.tgz#db4f4f49721f47667afd1fdc5edb032f8d9cdb2e" + integrity sha512-Ao0PP6MoYsRU1LxeVUW740ioknvdIUmfr6uAA3xWlQJ9s69/Tupy8qwhuKG3xWfl+KvLMAP9p2WXF9cwuk/7Bg== dependencies: - cssnano-utils "^2.0.1" - postcss-value-parser "^4.1.0" + postcss-value-parser "^4.2.0" -postcss-normalize-unicode@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-5.0.1.tgz#82d672d648a411814aa5bf3ae565379ccd9f5e37" - integrity sha512-kAtYD6V3pK0beqrU90gpCQB7g6AOfP/2KIPCVBKJM2EheVsBQmx/Iof+9zR9NFKLAx4Pr9mDhogB27pmn354nA== +postcss-normalize-unicode@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-5.0.2.tgz#c4db89a0116066716b9e9fcb6444ce63178f5ced" + integrity sha512-3y/V+vjZ19HNcTizeqwrbZSUsE69ZMRHfiiyLAJb7C7hJtYmM4Gsbajy7gKagu97E8q5rlS9k8FhojA8cpGhWw== dependencies: - browserslist "^4.16.0" - postcss-value-parser "^4.1.0" + browserslist "^4.16.6" + postcss-value-parser "^4.2.0" -postcss-normalize-url@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-5.0.2.tgz#ddcdfb7cede1270740cf3e4dfc6008bd96abc763" - integrity sha512-k4jLTPUxREQ5bpajFQZpx8bCF2UrlqOTzP9kEqcEnOfwsRshWs2+oAFIHfDQB8GO2PaUaSE0NlTAYtbluZTlHQ== +postcss-normalize-url@^5.0.4: + version "5.0.4" + resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-5.0.4.tgz#3b0322c425e31dd275174d0d5db0e466f50810fb" + integrity sha512-cNj3RzK2pgQQyNp7dzq0dqpUpQ/wYtdDZM3DepPmFjCmYIfceuD9VIAcOdvrNetjIU65g1B4uwdP/Krf6AFdXg== dependencies: - is-absolute-url "^3.0.3" normalize-url "^6.0.1" - postcss-value-parser "^4.1.0" + postcss-value-parser "^4.2.0" -postcss-normalize-whitespace@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.0.1.tgz#b0b40b5bcac83585ff07ead2daf2dcfbeeef8e9a" - integrity sha512-iPklmI5SBnRvwceb/XH568yyzK0qRVuAG+a1HFUsFRf11lEJTiQQa03a4RSCQvLKdcpX7XsI1Gen9LuLoqwiqA== +postcss-normalize-whitespace@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.0.2.tgz#92c5eaffe5255b5c43fca0baf19227e607c534db" + integrity sha512-CXBx+9fVlzSgbk0IXA/dcZn9lXixnQRndnsPC5ht3HxlQ1bVh77KQDL1GffJx1LTzzfae8ftMulsjYmO2yegxA== dependencies: - postcss-value-parser "^4.1.0" + postcss-value-parser "^4.2.0" -postcss-ordered-values@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-5.0.2.tgz#1f351426977be00e0f765b3164ad753dac8ed044" - integrity sha512-8AFYDSOYWebJYLyJi3fyjl6CqMEG/UVworjiyK1r573I56kb3e879sCJLGvR3merj+fAdPpVplXKQZv+ey6CgQ== +postcss-ordered-values@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-5.0.3.tgz#d80a8565f2e21efe8a06abacd60629a783bbcf54" + integrity sha512-T9pDS+P9bWeFvqivXd5ACzQmrCmHjv3ZP+djn8E1UZY7iK79pFSm7i3WbKw2VSmFmdbMm8sQ12OPcNpzBo3Z2w== dependencies: - cssnano-utils "^2.0.1" - postcss-value-parser "^4.1.0" + cssnano-utils "^3.0.0" + postcss-value-parser "^4.2.0" -postcss-reduce-initial@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-5.0.1.tgz#9d6369865b0f6f6f6b165a0ef5dc1a4856c7e946" - integrity sha512-zlCZPKLLTMAqA3ZWH57HlbCjkD55LX9dsRyxlls+wfuRfqCi5mSlZVan0heX5cHr154Dq9AfbH70LyhrSAezJw== +postcss-reduce-initial@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-5.0.2.tgz#fa424ce8aa88a89bc0b6d0f94871b24abe94c048" + integrity sha512-v/kbAAQ+S1V5v9TJvbGkV98V2ERPdU6XvMcKMjqAlYiJ2NtsHGlKYLPjWWcXlaTKNxooId7BGxeraK8qXvzKtw== dependencies: - browserslist "^4.16.0" + browserslist "^4.16.6" caniuse-api "^3.0.0" -postcss-reduce-transforms@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-5.0.1.tgz#93c12f6a159474aa711d5269923e2383cedcf640" - integrity sha512-a//FjoPeFkRuAguPscTVmRQUODP+f3ke2HqFNgGPwdYnpeC29RZdCBvGRGTsKpMURb/I3p6jdKoBQ2zI+9Q7kA== +postcss-reduce-transforms@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-5.0.2.tgz#9242758629f9ad4d90312eadbc921259d15bee4d" + integrity sha512-25HeDeFsgiPSUx69jJXZn8I06tMxLQJJNF5h7i9gsUg8iP4KOOJ8EX8fj3seeoLt3SLU2YDD6UPnDYVGUO7DEA== dependencies: - cssnano-utils "^2.0.1" - postcss-value-parser "^4.1.0" + postcss-value-parser "^4.2.0" postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5: version "6.0.6" @@ -4913,36 +4894,40 @@ postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector cssesc "^3.0.0" util-deprecate "^1.0.2" -postcss-svgo@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-5.0.2.tgz#bc73c4ea4c5a80fbd4b45e29042c34ceffb9257f" - integrity sha512-YzQuFLZu3U3aheizD+B1joQ94vzPfE6BNUcSYuceNxlVnKKsOtdo6hL9/zyC168Q8EwfLSgaDSalsUGa9f2C0A== +postcss-svgo@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-5.0.3.tgz#d945185756e5dfaae07f9edb0d3cae7ff79f9b30" + integrity sha512-41XZUA1wNDAZrQ3XgWREL/M2zSw8LJPvb5ZWivljBsUQAGoEKMYm6okHsTjJxKYI4M75RQEH4KYlEM52VwdXVA== dependencies: postcss-value-parser "^4.1.0" - svgo "^2.3.0" + svgo "^2.7.0" -postcss-unique-selectors@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-5.0.1.tgz#3be5c1d7363352eff838bd62b0b07a0abad43bfc" - integrity sha512-gwi1NhHV4FMmPn+qwBNuot1sG1t2OmacLQ/AX29lzyggnjd+MnVD5uqQmpXO3J17KGL2WAxQruj1qTd3H0gG/w== +postcss-unique-selectors@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-5.0.2.tgz#5d6893daf534ae52626708e0d62250890108c0c1" + integrity sha512-w3zBVlrtZm7loQWRPVC0yjUwwpty7OM6DnEHkxcSQXO1bMS3RJ+JUS5LFMSDZHJcvGsRwhZinCWVqn8Kej4EDA== dependencies: alphanum-sort "^1.0.2" postcss-selector-parser "^6.0.5" - uniqs "^2.0.0" postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb" integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ== -postcss@8.3.0: - version "8.3.0" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.0.tgz#b1a713f6172ca427e3f05ef1303de8b65683325f" - integrity sha512-+ogXpdAjWGa+fdYY5BQ96V/6tAo+TdSSIMP5huJBIygdWwKtVoB5JWZ7yUd4xZ8r+8Kvvx4nyg/PQ071H4UtcQ== +postcss-value-parser@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" + integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== + +postcss@8.4.5: + version "8.4.5" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.5.tgz#bae665764dfd4c6fcc24dc0fdf7e7aa00cc77f95" + integrity sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg== dependencies: - colorette "^1.2.2" - nanoid "^3.1.23" - source-map-js "^0.6.2" + nanoid "^3.1.30" + picocolors "^1.0.0" + source-map-js "^1.0.1" prelude-ls@~1.1.2: version "1.1.2" @@ -5302,11 +5287,6 @@ resolve-dir@^1.0.0, resolve-dir@^1.0.1: expand-tilde "^2.0.0" global-modules "^1.0.0" -resolve-from@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" - integrity sha1-six699nWiBvItuZTM17rywoYh0g= - resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" @@ -5357,16 +5337,6 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== -rgb-regex@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/rgb-regex/-/rgb-regex-1.0.1.tgz#c0e0d6882df0e23be254a475e8edd41915feaeb1" - integrity sha1-wODWiC3w4jviVKR16O3UGRX+rrE= - -rgba-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3" - integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM= - rimraf@2.6.3: version "2.6.3" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" @@ -5534,6 +5504,15 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" @@ -5604,10 +5583,10 @@ sort-keys@^2.0.0: dependencies: is-plain-obj "^1.0.0" -source-map-js@^0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-0.6.2.tgz#0bb5de631b41cfbda6cfba8bd05a80efdfd2385e" - integrity sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug== +source-map-js@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" + integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== source-map-resolve@^0.5.0: version "0.5.3" @@ -5970,17 +5949,17 @@ svgo@^1.3.2: unquote "~1.1.1" util.promisify "~1.0.0" -svgo@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.3.0.tgz#6b3af81d0cbd1e19c83f5f63cec2cb98c70b5373" - integrity sha512-fz4IKjNO6HDPgIQxu4IxwtubtbSfGEAJUq/IXyTPIkGhWck/faiiwfkvsB8LnBkKLvSoyNNIY6d13lZprJMc9Q== +svgo@^2.7.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.8.0.tgz#4ff80cce6710dc2795f0c7c74101e6764cfccd24" + integrity sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg== dependencies: - "@trysound/sax" "0.1.1" - chalk "^4.1.0" - commander "^7.1.0" - css-select "^3.1.2" - css-tree "^1.1.2" + "@trysound/sax" "0.2.0" + commander "^7.2.0" + css-select "^4.1.3" + css-tree "^1.1.3" csso "^4.2.0" + picocolors "^1.0.0" stable "^0.1.8" table@^5.2.3: @@ -6311,11 +6290,6 @@ uniq@^1.0.0: resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8= -uniqs@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02" - integrity sha1-/+3ks2slKQaW5uFl1KWe25mOawI= - unique-stream@^2.0.2: version "2.3.1" resolved "https://registry.yarnpkg.com/unique-stream/-/unique-stream-2.3.1.tgz#c65d110e9a4adf9a6c5948b28053d9a8d04cbeac" @@ -6437,11 +6411,6 @@ value-or-function@^3.0.0: resolved "https://registry.yarnpkg.com/value-or-function/-/value-or-function-3.0.0.tgz#1c243a50b595c1be54a754bfece8563b9ff8d813" integrity sha1-HCQ6ULWVwb5Up1S/7OhWO5/42BM= -vendors@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.4.tgz#e2b800a53e7a29b93506c3cf41100d16c4c4ad8e" - integrity sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w== - verror@1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" @@ -6612,7 +6581,7 @@ yallist@^2.1.2: resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= -yaml@^1.10.0: +yaml@^1.10.2: version "1.10.2" resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== From 337fb40c65b083e6efc89f8b4007a33290e18b2a Mon Sep 17 00:00:00 2001 From: Arnaud-D <35631001+Arnaud-D@users.noreply.github.com> Date: Fri, 1 Oct 2021 22:45:13 +0200 Subject: [PATCH 36/53] Ajoute des tests et refactorise AddSuggestion --- .../tests/tests_views/tests_addsuggestion.py | 151 ++++++++++++++++++ zds/tutorialv2/views/editorialization.py | 5 +- 2 files changed, 154 insertions(+), 2 deletions(-) create mode 100644 zds/tutorialv2/tests/tests_views/tests_addsuggestion.py diff --git a/zds/tutorialv2/tests/tests_views/tests_addsuggestion.py b/zds/tutorialv2/tests/tests_views/tests_addsuggestion.py new file mode 100644 index 0000000000..87d1df5358 --- /dev/null +++ b/zds/tutorialv2/tests/tests_views/tests_addsuggestion.py @@ -0,0 +1,151 @@ +from django.test import TestCase +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ +from django.utils.html import escape + +from zds.member.factories import ProfileFactory, StaffProfileFactory +from zds.tutorialv2.factories import PublishableContentFactory, PublishedContentFactory +from zds.tutorialv2.models.database import ContentSuggestion +from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents + + +@override_for_contents() +class AddSuggestionPermissionTests(TutorialTestMixin, TestCase): + """Test permissions and associated behaviors, such as redirections and status codes.""" + + def setUp(self): + # Create users + self.author = ProfileFactory().user + self.staff = StaffProfileFactory().user + self.outsider = ProfileFactory().user + + # Create contents and suggestion + self.content = PublishableContentFactory(author_list=[self.author]) + self.suggestable_content = PublishedContentFactory() + + # Get information to be reused in tests + self.form_url = reverse("content:add-suggestion", kwargs={"pk": self.content.pk}) + self.login_url = reverse("member-login") + "?next=" + self.form_url + self.content_url = reverse("content:view", kwargs={"pk": self.content.pk, "slug": self.content.slug}) + self.form_data = {"options": self.suggestable_content.pk} + + def test_not_authenticated(self): + self.client.logout() + self.content.type = "TUTORIAL" + self.content.save() + response = self.client.post(self.form_url, self.form_data) + self.assertRedirects(response, self.login_url) + + def test_authenticated_outsider(self): + self.client.force_login(self.outsider) + self.content.type = "TUTORIAL" + self.content.save() + response = self.client.post(self.form_url, self.form_data) + self.assertEqual(response.status_code, 403) + + def test_authenticated_author(self): + self.client.force_login(self.author) + self.content.type = "TUTORIAL" + self.content.save() + response = self.client.post(self.form_url, self.form_data) + self.assertEqual(response.status_code, 403) + + def test_authenticated_staff_tutorial(self): + self.client.force_login(self.staff) + self.content.type = "TUTORIAL" + self.content.save() + response = self.client.post(self.form_url, self.form_data) + self.assertRedirects(response, self.content_url) + + def test_authenticated_staff_article(self): + self.client.force_login(self.staff) + self.content.type = "ARTICLE" + self.content.save() + response = self.client.post(self.form_url, self.form_data) + self.assertRedirects(response, self.content_url) + + def test_authenticated_staff_opinion(self): + self.client.force_login(self.staff) + self.content.type = "OPINION" + self.content.save() + response = self.client.post(self.form_url, self.form_data) + self.assertEqual(response.status_code, 403) + + +class AddSuggestionWorkflowTests(TutorialTestMixin, TestCase): + """Test the workflow of the form, such as validity errors and success messages.""" + + def setUp(self): + # Create users + self.staff = StaffProfileFactory().user + self.author = ProfileFactory().user + + # Createcontents + self.content = PublishableContentFactory(author_list=[self.author]) + self.suggestable_content_1 = PublishedContentFactory() + self.suggestable_content_2 = PublishedContentFactory() + self.unpublished_content = PublishableContentFactory() + + self.not_picked_opinion = PublishedContentFactory() + self.not_picked_opinion.type = "OPINION" + self.not_picked_opinion.save() + + # Get information to be reused in tests + self.form_url = reverse("content:add-suggestion", kwargs={"pk": self.content.pk}) + self.success_message_fragment = _("a été ajouté dans les suggestions") + self.error_message_fragment_unpublished = _("un contenu qui n'a pas été publié") + self.error_message_fragment_already_suggested = _("fait déjà partie des suggestions de") + self.error_message_fragment_self = _("en tant que suggestion pour lui même") + self.error_messge_fragment_not_picked = _("un billet qui n'a pas été mis en avant") + + # Log in with an authorized user to perform the tests + self.client.force_login(self.staff) + + def test_published_simple(self): + response = self.client.post(self.form_url, {"options": self.suggestable_content_1.pk}, follow=True) + self.assertContains(response, escape(self.success_message_fragment)) + suggestion = ContentSuggestion.objects.get(publication=self.content, suggestion=self.suggestable_content_1) + self.assertEqual(list(ContentSuggestion.objects.all()), [suggestion]) + + def test_published_multiple(self): + response = self.client.post( + self.form_url, {"options": [self.suggestable_content_1.pk, self.suggestable_content_2.pk]}, follow=True + ) + self.assertContains(response, escape(self.success_message_fragment)) + suggestion_1 = ContentSuggestion.objects.get(publication=self.content, suggestion=self.suggestable_content_1) + suggestion_2 = ContentSuggestion.objects.get(publication=self.content, suggestion=self.suggestable_content_2) + self.assertEqual(list(ContentSuggestion.objects.all()), [suggestion_1, suggestion_2]) + + def test_already_suggested(self): + suggestion = ContentSuggestion(publication=self.content, suggestion=self.suggestable_content_1) + suggestion.save() + response = self.client.post(self.form_url, {"options": self.suggestable_content_1.pk}, follow=True) + self.assertContains(response, escape(self.error_message_fragment_already_suggested)) + self.assertEqual(list(ContentSuggestion.objects.all()), [suggestion]) + + def test_self(self): + response = self.client.post(self.form_url, {"options": self.content.pk}, follow=True) + self.assertContains(response, escape(self.error_message_fragment_self)) + self.assertQuerysetEqual(ContentSuggestion.objects.all(), []) + + def test_not_picked_opinion(self): + response = self.client.post(self.form_url, {"options": self.not_picked_opinion.pk}, follow=True) + self.assertContains(response, escape(self.error_messge_fragment_not_picked)) + self.assertQuerysetEqual(ContentSuggestion.objects.all(), []) + + def test_unpublished(self): + response = self.client.post(self.form_url, {"options": self.unpublished_content.pk}, follow=True) + self.assertContains(response, escape(self.error_message_fragment_unpublished)) + self.assertQuerysetEqual(ContentSuggestion.objects.all(), []) + + def test_invalid(self): + response = self.client.post(self.form_url, {"options": "420"}, follow=True) # pk must not exist + self.assertEqual(response.status_code, 404) + + def test_not_integer(self): + with self.assertRaises(ValueError): + self.client.post(self.form_url, {"options": "abcd"}, follow=True) + + def test_empty(self): + with self.assertRaises(ValueError): + self.client.post(self.form_url, {"options": ""}, follow=True) diff --git a/zds/tutorialv2/views/editorialization.py b/zds/tutorialv2/views/editorialization.py index 40e40f2925..3c94beb743 100644 --- a/zds/tutorialv2/views/editorialization.py +++ b/zds/tutorialv2/views/editorialization.py @@ -3,7 +3,7 @@ from django.shortcuts import get_object_or_404, redirect from django.utils.translation import gettext_lazy as _ -from zds.member.decorator import LoggedWithReadWriteHability +from zds.member.decorator import LoggedWithReadWriteHability, PermissionRequiredMixin from zds.tutorialv2.forms import RemoveSuggestionForm, EditContentTagsForm from zds.tutorialv2.mixins import SingleContentFormViewMixin from zds.tutorialv2.models.database import ContentSuggestion, PublishableContent @@ -48,9 +48,10 @@ def form_invalid(self, form): return super().form_valid(form) -class AddSuggestion(LoggedWithReadWriteHability, SingleContentFormViewMixin): +class AddSuggestion(LoggedWithReadWriteHability, PermissionRequiredMixin, SingleContentFormViewMixin): only_draft_version = True authorized_for_staff = True + permissions = ["tutorialv2.change_publishablecontent"] def post(self, request, *args, **kwargs): publication = get_object_or_404(PublishableContent, pk=kwargs["pk"]) From 53845c5980e894fb75cbe1e448da960caacb86b4 Mon Sep 17 00:00:00 2001 From: "Ph. SW" <5911232+philippemilink@users.noreply.github.com> Date: Sat, 19 Feb 2022 13:49:33 +0100 Subject: [PATCH 37/53] =?UTF-8?q?Mise=20=C3=A0=20jour=20de=20la=20version?= =?UTF-8?q?=20support=C3=A9e=20de=20Debian=20(#6239)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/source/install/install-docker.rst | 2 +- scripts/dependencies/debian.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/install/install-docker.rst b/doc/source/install/install-docker.rst index db01c5b0f6..2666e68e74 100644 --- a/doc/source/install/install-docker.rst +++ b/doc/source/install/install-docker.rst @@ -16,7 +16,7 @@ Lancez un shell interactif dans un conteneur basé sur Debian : .. sourcecode:: bash - docker run -it -p 8000:8000 debian:buster + docker run -it -p 8000:8000 debian:bullseye Une fois dans le conteneur, saisissez les commandes suivantes : diff --git a/scripts/dependencies/debian.txt b/scripts/dependencies/debian.txt index aaac3293bc..e56dd78a6c 100644 --- a/scripts/dependencies/debian.txt +++ b/scripts/dependencies/debian.txt @@ -1,5 +1,5 @@ #title=Debian -#desc=Utilisation de apt-get / Testé sur Debian Stretch (9), Buster (10) +#desc=Utilisation de apt-get / Testé sur Debian Stretch (9), Buster (10), Bullseye (11) #updatecmd=apt-get -y update #installcmd=apt-get -y install git From af471aca8914e7a50ad13b4f0f014b60d5d9fbdb Mon Sep 17 00:00:00 2001 From: Arnaud-D <35631001+Arnaud-D@users.noreply.github.com> Date: Sat, 19 Feb 2022 17:11:21 +0100 Subject: [PATCH 38/53] =?UTF-8?q?Ajoute=20Ubuntu=2020.04=20LTS=20dans=20la?= =?UTF-8?q?=20liste=20des=20versions=20Ubuntu=20test=C3=A9es=20du=20script?= =?UTF-8?q?=20d'installation=20(#6241)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/dependencies/ubuntu.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/dependencies/ubuntu.txt b/scripts/dependencies/ubuntu.txt index 969c3492d4..82b633729d 100644 --- a/scripts/dependencies/ubuntu.txt +++ b/scripts/dependencies/ubuntu.txt @@ -1,5 +1,5 @@ #title=Ubuntu -#desc=Utilisation de apt-get / Testé sur : Ubuntu 16.04 LTS, 18.04 LTS, 19.04 & Linux Mint 19 +#desc=Utilisation de apt-get / Testé sur : 18.04 LTS, 19.04, 20.04 LTS & Linux Mint 19 #updatecmd=apt-get -y update #installcmd=apt-get -y install git From 2b2c62d92e3e85f71de922da0b5c8b5bdd200fb3 Mon Sep 17 00:00:00 2001 From: Philippe MILINK Date: Sat, 19 Feb 2022 17:26:15 +0100 Subject: [PATCH 39/53] =?UTF-8?q?Met=20=C3=A0=20jour=20Pillow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Version qui corrige des failles de securité. --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 548ffaf113..ee575d81d0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,7 +17,7 @@ geoip2==4.5.0 GitPython==3.1.24 homoglyphs==2.0.4 lxml==4.7.1 -Pillow==8.4.0 +Pillow==9.0.1 pymemcache==3.5.0 requests==2.26.0 toml==0.10.2 From 576ac130ed583e7edce7414fef40d09418bdee1a Mon Sep 17 00:00:00 2001 From: Situphen Date: Sat, 19 Feb 2022 21:40:53 +0100 Subject: [PATCH 40/53] Supprime l'ancien module de recherche (#6248) --- zds/search/constant.py | 53 ------------------------------------------ zds/search/forms.py | 0 2 files changed, 53 deletions(-) delete mode 100644 zds/search/constant.py delete mode 100644 zds/search/forms.py diff --git a/zds/search/constant.py b/zds/search/constant.py deleted file mode 100644 index d4710dfb25..0000000000 --- a/zds/search/constant.py +++ /dev/null @@ -1,53 +0,0 @@ -from django.utils.translation import gettext_lazy as _ - -MODEL_TOPIC = "sujets" -MODEL_POST = "messages" -MODEL_ARTICLE = "article" -MODEL_TUTORIAL = "tutorial" -MODEL_OPINION = "opinion" -MODEL_PART = "parts" -MODEL_CHAPTER = "chapters" -MODEL_EXTRACT = "extracts" - -MODEL_NAMES = [ - (MODEL_TOPIC, _("Sujets du forum")), - (MODEL_POST, _("Messages du forum")), - (MODEL_ARTICLE, _("Articles")), - (MODEL_TUTORIAL, _("Tutoriels")), - (MODEL_OPINION, _("Billets")), - (MODEL_PART, _("Parties des tutoriels")), - (MODEL_CHAPTER, _("Chapitres des tutoriels")), - (MODEL_EXTRACT, _("Sections des tutoriels")), -] - - -def model_topic(): - return MODEL_TOPIC - - -def model_post(): - return MODEL_POST - - -def model_article(): - return MODEL_ARTICLE - - -def model_tutorial(): - return MODEL_TUTORIAL - - -def model_opinion(): - return MODEL_OPINION - - -def model_part(): - return MODEL_PART - - -def model_chapter(): - return MODEL_CHAPTER - - -def model_extract(): - return MODEL_EXTRACT diff --git a/zds/search/forms.py b/zds/search/forms.py deleted file mode 100644 index e69de29bb2..0000000000 From 9b7a6b52407846c255c361c089d446a69c8828f4 Mon Sep 17 00:00:00 2001 From: Arnaud-D <35631001+Arnaud-D@users.noreply.github.com> Date: Sun, 20 Feb 2022 08:03:01 +0100 Subject: [PATCH 41/53] =?UTF-8?q?D=C3=A9place=20les=20tests=20et=20factori?= =?UTF-8?q?es=20de=20featured=20dans=20un=20dossier=20d=C3=A9di=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/featured/tests/__init__.py | 0 zds/featured/{ => tests}/factories.py | 0 zds/featured/{ => tests}/tests.py | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 zds/featured/tests/__init__.py rename zds/featured/{ => tests}/factories.py (100%) rename zds/featured/{ => tests}/tests.py (99%) diff --git a/zds/featured/tests/__init__.py b/zds/featured/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/zds/featured/factories.py b/zds/featured/tests/factories.py similarity index 100% rename from zds/featured/factories.py rename to zds/featured/tests/factories.py diff --git a/zds/featured/tests.py b/zds/featured/tests/tests.py similarity index 99% rename from zds/featured/tests.py rename to zds/featured/tests/tests.py index f8ce3833ba..919a44c317 100644 --- a/zds/featured/tests.py +++ b/zds/featured/tests/tests.py @@ -5,7 +5,7 @@ from django.utils.translation import gettext as _ from zds.member.factories import StaffProfileFactory, ProfileFactory -from zds.featured.factories import FeaturedResourceFactory +from zds.featured.tests.factories import FeaturedResourceFactory from zds.featured.models import FeaturedResource, FeaturedMessage, FeaturedRequested from zds.forum.factories import ForumCategoryFactory, ForumFactory, TopicFactory from zds.gallery.factories import GalleryFactory, ImageFactory From 49e94919d3ebb48d3b0c4ce1ed50d9fc0ac44d15 Mon Sep 17 00:00:00 2001 From: Arnaud-D <35631001+Arnaud-D@users.noreply.github.com> Date: Sun, 20 Feb 2022 08:04:26 +0100 Subject: [PATCH 42/53] =?UTF-8?q?D=C3=A9place=20les=20factories=20de=20for?= =?UTF-8?q?um=20dans=20le=20dossier=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/featured/tests/tests.py | 2 +- zds/forum/api/tests.py | 2 +- zds/forum/{ => tests}/factories.py | 0 zds/forum/tests/tests.py | 2 +- zds/forum/tests/tests_feeds.py | 2 +- zds/forum/tests/tests_forms.py | 2 +- zds/forum/tests/tests_views.py | 10 ++++------ zds/member/tests/tests_models.py | 2 +- zds/member/tests/tests_views.py | 2 +- zds/notification/tests/tests_basics.py | 2 +- zds/notification/tests/tests_tricky.py | 3 +-- zds/pages/tests.py | 2 +- zds/searchv2/tests/tests_models.py | 4 ++-- zds/searchv2/tests/tests_utils.py | 4 ++-- zds/searchv2/tests/tests_views.py | 4 ++-- zds/tutorialv2/factories.py | 2 +- zds/tutorialv2/tests/tests_feeds.py | 2 +- zds/tutorialv2/tests/tests_lists.py | 2 +- zds/tutorialv2/tests/tests_move.py | 2 +- zds/tutorialv2/tests/tests_views/tests_content.py | 2 +- .../tests/tests_views/tests_editcontenttags.py | 2 +- zds/tutorialv2/tests/tests_views/tests_published.py | 2 +- zds/utils/management/commands/load_fixtures.py | 2 +- zds/utils/tests/tests_comments.py | 2 +- zds/utils/tests/tests_context_processors.py | 2 +- zds/utils/tests/tests_topbar_tags.py | 2 +- 26 files changed, 31 insertions(+), 34 deletions(-) rename zds/forum/{ => tests}/factories.py (100%) diff --git a/zds/featured/tests/tests.py b/zds/featured/tests/tests.py index 919a44c317..4d46101b82 100644 --- a/zds/featured/tests/tests.py +++ b/zds/featured/tests/tests.py @@ -7,7 +7,7 @@ from zds.member.factories import StaffProfileFactory, ProfileFactory from zds.featured.tests.factories import FeaturedResourceFactory from zds.featured.models import FeaturedResource, FeaturedMessage, FeaturedRequested -from zds.forum.factories import ForumCategoryFactory, ForumFactory, TopicFactory +from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory, TopicFactory from zds.gallery.factories import GalleryFactory, ImageFactory from zds.tutorialv2.factories import PublishedContentFactory from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents diff --git a/zds/forum/api/tests.py b/zds/forum/api/tests.py index 987dadc605..52a8c87f5a 100644 --- a/zds/forum/api/tests.py +++ b/zds/forum/api/tests.py @@ -7,7 +7,7 @@ from rest_framework.test import APITestCase from rest_framework_extensions.settings import extensions_api_settings -from zds.forum.factories import PostFactory, create_category_and_forum, create_topic_in_forum +from zds.forum.tests.factories import PostFactory, create_category_and_forum, create_topic_in_forum from zds.member.factories import ProfileFactory from zds.utils.models import CommentVote diff --git a/zds/forum/factories.py b/zds/forum/tests/factories.py similarity index 100% rename from zds/forum/factories.py rename to zds/forum/tests/factories.py diff --git a/zds/forum/tests/tests.py b/zds/forum/tests/tests.py index 717158541c..245d41752e 100644 --- a/zds/forum/tests/tests.py +++ b/zds/forum/tests/tests.py @@ -7,7 +7,7 @@ from django.test import TestCase from zds.forum.commons import PostEditMixin -from zds.forum.factories import ForumCategoryFactory, ForumFactory, TopicFactory, PostFactory, TagFactory +from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory, TopicFactory, PostFactory, TagFactory from zds.forum.models import Forum, TopicRead, Post, Topic from zds.member.factories import ProfileFactory, StaffProfileFactory from zds.notification.models import TopicAnswerSubscription diff --git a/zds/forum/tests/tests_feeds.py b/zds/forum/tests/tests_feeds.py index de2c8003c3..94899df660 100644 --- a/zds/forum/tests/tests_feeds.py +++ b/zds/forum/tests/tests_feeds.py @@ -3,7 +3,7 @@ from django.test import TestCase from django.test.client import RequestFactory -from zds.forum.factories import ForumCategoryFactory, ForumFactory, TopicFactory, PostFactory, TagFactory +from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory, TopicFactory, PostFactory, TagFactory from zds.forum.feeds import LastPostsFeedRSS, LastPostsFeedATOM, LastTopicsFeedRSS, LastTopicsFeedATOM from zds.member.factories import ProfileFactory diff --git a/zds/forum/tests/tests_forms.py b/zds/forum/tests/tests_forms.py index f403d80f9a..0b21f32013 100644 --- a/zds/forum/tests/tests_forms.py +++ b/zds/forum/tests/tests_forms.py @@ -2,7 +2,7 @@ from zds.forum.forms import TopicForm, PostForm -from zds.forum.factories import create_category_and_forum, create_topic_in_forum +from zds.forum.tests.factories import create_category_and_forum, create_topic_in_forum from zds.member.factories import ProfileFactory from django.conf import settings diff --git a/zds/forum/tests/tests_views.py b/zds/forum/tests/tests_views.py index 078994acee..74d5d9381f 100644 --- a/zds/forum/tests/tests_views.py +++ b/zds/forum/tests/tests_views.py @@ -1,18 +1,16 @@ from datetime import datetime from unittest.mock import patch -from django.conf import settings -from django.contrib.auth.models import User, Group +from django.contrib.auth.models import Group from django.urls import reverse -from django.shortcuts import get_object_or_404 from django.test import TestCase -from zds.forum.factories import create_category_and_forum, create_topic_in_forum -from zds.forum.factories import PostFactory, TagFactory +from zds.forum.tests.factories import create_category_and_forum, create_topic_in_forum +from zds.forum.tests.factories import PostFactory, TagFactory from zds.forum.models import Topic, Post from zds.notification.models import TopicAnswerSubscription from zds.member.factories import ProfileFactory, StaffProfileFactory -from zds.utils.models import CommentEdit, Hat, Alert +from zds.utils.models import CommentEdit, Hat class LastTopicsViewTests(TestCase): diff --git a/zds/member/tests/tests_models.py b/zds/member/tests/tests_models.py index 821d8977ff..a7711474c9 100644 --- a/zds/member/tests/tests_models.py +++ b/zds/member/tests/tests_models.py @@ -5,7 +5,7 @@ from django.contrib.auth.models import Group from hashlib import md5 -from zds.forum.factories import ForumCategoryFactory, ForumFactory, TopicFactory, PostFactory +from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory, TopicFactory, PostFactory from zds.notification.models import TopicAnswerSubscription from zds.member.factories import ProfileFactory, StaffProfileFactory, DevProfileFactory from zds.member.models import TokenForgotPassword, TokenRegister, Profile diff --git a/zds/member/tests/tests_views.py b/zds/member/tests/tests_views.py index d2d61071f3..7d9f27d9e5 100644 --- a/zds/member/tests/tests_views.py +++ b/zds/member/tests/tests_views.py @@ -30,7 +30,7 @@ from zds.tutorialv2.factories import PublishableContentFactory, PublishedContentFactory, BetaContentFactory from zds.tutorialv2.models.database import PublishableContent, PublishedContent from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents -from zds.forum.factories import ForumCategoryFactory, ForumFactory, TopicFactory, PostFactory +from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory, TopicFactory, PostFactory from zds.forum.models import Topic, Post from zds.gallery.factories import GalleryFactory, UserGalleryFactory from zds.gallery.models import Gallery, UserGallery diff --git a/zds/notification/tests/tests_basics.py b/zds/notification/tests/tests_basics.py index 2205e8180b..574299dc11 100644 --- a/zds/notification/tests/tests_basics.py +++ b/zds/notification/tests/tests_basics.py @@ -8,7 +8,7 @@ from django.db import IntegrityError from django.conf import settings -from zds.forum.factories import ForumCategoryFactory, ForumFactory, TopicFactory, PostFactory, TagFactory +from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory, TopicFactory, PostFactory, TagFactory from zds.forum.models import Topic from zds.gallery.factories import UserGalleryFactory from zds.member.factories import ProfileFactory, StaffProfileFactory, UserFactory diff --git a/zds/notification/tests/tests_tricky.py b/zds/notification/tests/tests_tricky.py index 15642c246d..238fa9df66 100644 --- a/zds/notification/tests/tests_tricky.py +++ b/zds/notification/tests/tests_tricky.py @@ -5,9 +5,8 @@ from django.core import mail from django.test import TestCase from django.test.utils import override_settings -from django.contrib.contenttypes.models import ContentType -from zds.forum.factories import ForumCategoryFactory, ForumFactory, TopicFactory, PostFactory +from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory from zds.forum.models import Topic from zds.gallery.factories import UserGalleryFactory from zds.member.factories import StaffProfileFactory, ProfileFactory diff --git a/zds/pages/tests.py b/zds/pages/tests.py index c14e2401fb..d86b7a5248 100644 --- a/zds/pages/tests.py +++ b/zds/pages/tests.py @@ -4,7 +4,7 @@ from django.utils.translation import gettext_lazy as _ from zds.forum.models import Post -from zds.forum.factories import create_category_and_forum, create_topic_in_forum +from zds.forum.tests.factories import create_category_and_forum, create_topic_in_forum from zds.member.factories import ProfileFactory, StaffProfileFactory from zds.utils.models import CommentEdit from zds.utils.templatetags.emarkdown import render_markdown diff --git a/zds/searchv2/tests/tests_models.py b/zds/searchv2/tests/tests_models.py index 045b145004..523259d210 100644 --- a/zds/searchv2/tests/tests_models.py +++ b/zds/searchv2/tests/tests_models.py @@ -4,8 +4,8 @@ from django.conf import settings from django.test import TestCase -from zds.forum.factories import TopicFactory, PostFactory, Topic, Post -from zds.forum.factories import create_category_and_forum +from zds.forum.tests.factories import TopicFactory, PostFactory, Topic, Post +from zds.forum.tests.factories import create_category_and_forum from zds.member.factories import ProfileFactory, StaffProfileFactory from zds.searchv2.models import ESIndexManager from zds.tutorialv2.factories import PublishableContentFactory, ContainerFactory, ExtractFactory, publish_content diff --git a/zds/searchv2/tests/tests_utils.py b/zds/searchv2/tests/tests_utils.py index 1ce3edfc54..f6c45c48e8 100644 --- a/zds/searchv2/tests/tests_utils.py +++ b/zds/searchv2/tests/tests_utils.py @@ -9,8 +9,8 @@ from zds.tutorialv2.factories import PublishableContentFactory, ContainerFactory, ExtractFactory from zds.tutorialv2.models.database import PublishedContent from zds.tutorialv2.publication_utils import publish_content -from zds.forum.factories import TopicFactory, PostFactory, Topic, Post -from zds.forum.factories import create_category_and_forum +from zds.forum.tests.factories import TopicFactory, PostFactory, Topic, Post +from zds.forum.tests.factories import create_category_and_forum from zds.searchv2.models import ESIndexManager from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents diff --git a/zds/searchv2/tests/tests_views.py b/zds/searchv2/tests/tests_views.py index 9ede142454..4397e36f05 100644 --- a/zds/searchv2/tests/tests_views.py +++ b/zds/searchv2/tests/tests_views.py @@ -9,8 +9,8 @@ from django.urls import reverse from django.contrib.auth.models import Group -from zds.forum.factories import TopicFactory, PostFactory, Topic, Post, TagFactory -from zds.forum.factories import create_category_and_forum +from zds.forum.tests.factories import TopicFactory, PostFactory, Topic, Post, TagFactory +from zds.forum.tests.factories import create_category_and_forum from zds.member.factories import ProfileFactory, StaffProfileFactory from zds.searchv2.models import ESIndexManager diff --git a/zds/tutorialv2/factories.py b/zds/tutorialv2/factories.py index c5f210727a..1aeec5ab22 100644 --- a/zds/tutorialv2/factories.py +++ b/zds/tutorialv2/factories.py @@ -2,7 +2,7 @@ import factory -from zds.forum.factories import PostFactory, TopicFactory +from zds.forum.tests.factories import PostFactory, TopicFactory from zds.gallery.factories import GalleryFactory, UserGalleryFactory from zds.utils.factories import LicenceFactory, SubCategoryFactory from zds.utils.models import Licence diff --git a/zds/tutorialv2/tests/tests_feeds.py b/zds/tutorialv2/tests/tests_feeds.py index f29b72d2d9..e890e0a053 100644 --- a/zds/tutorialv2/tests/tests_feeds.py +++ b/zds/tutorialv2/tests/tests_feeds.py @@ -6,7 +6,7 @@ from zds.gallery.factories import UserGalleryFactory from zds.member.factories import ProfileFactory, StaffProfileFactory, UserFactory -from zds.forum.factories import ForumFactory, ForumCategoryFactory, TagFactory +from zds.forum.tests.factories import ForumFactory, ForumCategoryFactory, TagFactory from zds.tutorialv2.models.database import PublishedContent from zds.tutorialv2.feeds import LastTutorialsFeedRSS, LastTutorialsFeedATOM, LastArticlesFeedRSS, LastArticlesFeedATOM from zds.tutorialv2.factories import ( diff --git a/zds/tutorialv2/tests/tests_lists.py b/zds/tutorialv2/tests/tests_lists.py index 91e5b7c0c8..40ac661c84 100644 --- a/zds/tutorialv2/tests/tests_lists.py +++ b/zds/tutorialv2/tests/tests_lists.py @@ -16,7 +16,7 @@ from zds.tutorialv2.publication_utils import publish_content from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents from zds.gallery.factories import UserGalleryFactory -from zds.forum.factories import ForumFactory, ForumCategoryFactory +from zds.forum.tests.factories import ForumFactory, ForumCategoryFactory from zds.utils.factories import CategoryFactory, SubCategoryFactory, LicenceFactory diff --git a/zds/tutorialv2/tests/tests_move.py b/zds/tutorialv2/tests/tests_move.py index a591e0c587..c54e9b7b62 100644 --- a/zds/tutorialv2/tests/tests_move.py +++ b/zds/tutorialv2/tests/tests_move.py @@ -13,7 +13,7 @@ ) from zds.tutorialv2.models.database import PublishableContent from zds.gallery.factories import UserGalleryFactory -from zds.forum.factories import ForumFactory, ForumCategoryFactory +from zds.forum.tests.factories import ForumFactory, ForumCategoryFactory from zds.tutorialv2.publication_utils import publish_content from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents from zds.utils.factories import SubCategoryFactory, LicenceFactory diff --git a/zds/tutorialv2/tests/tests_views/tests_content.py b/zds/tutorialv2/tests/tests_views/tests_content.py index 0b5decb0a8..d199a3e976 100644 --- a/zds/tutorialv2/tests/tests_views/tests_content.py +++ b/zds/tutorialv2/tests/tests_views/tests_content.py @@ -15,7 +15,7 @@ from django.test import TestCase from django.utils.translation import gettext_lazy as _ -from zds.forum.factories import ForumFactory, ForumCategoryFactory +from zds.forum.tests.factories import ForumFactory, ForumCategoryFactory from zds.forum.models import Topic, Post, TopicRead from zds.gallery.factories import UserGalleryFactory from zds.gallery.models import GALLERY_WRITE, UserGallery, Gallery diff --git a/zds/tutorialv2/tests/tests_views/tests_editcontenttags.py b/zds/tutorialv2/tests/tests_views/tests_editcontenttags.py index da56e36f66..7abff91f30 100644 --- a/zds/tutorialv2/tests/tests_views/tests_editcontenttags.py +++ b/zds/tutorialv2/tests/tests_views/tests_editcontenttags.py @@ -8,7 +8,7 @@ from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents from zds.tutorialv2.factories import PublishableContentFactory from zds.member.factories import ProfileFactory, StaffProfileFactory -from zds.forum.factories import TagFactory +from zds.forum.tests.factories import TagFactory from zds.utils.forms import TagValidator from zds.utils.models import Tag diff --git a/zds/tutorialv2/tests/tests_views/tests_published.py b/zds/tutorialv2/tests/tests_views/tests_published.py index 63967e5e22..ed2555a628 100644 --- a/zds/tutorialv2/tests/tests_views/tests_published.py +++ b/zds/tutorialv2/tests/tests_views/tests_published.py @@ -8,7 +8,7 @@ from django.test.utils import override_settings from django.utils.translation import gettext_lazy as _ -from zds.forum.factories import ForumFactory, ForumCategoryFactory +from zds.forum.tests.factories import ForumFactory, ForumCategoryFactory from zds.gallery.factories import UserGalleryFactory from zds.member.factories import ProfileFactory, StaffProfileFactory, UserFactory from zds.mp.models import PrivateTopic, is_privatetopic_unread diff --git a/zds/utils/management/commands/load_fixtures.py b/zds/utils/management/commands/load_fixtures.py index 872c5dbd0c..14a2db6d4d 100644 --- a/zds/utils/management/commands/load_fixtures.py +++ b/zds/utils/management/commands/load_fixtures.py @@ -12,7 +12,7 @@ from faker import Factory from zds.utils.templatetags.emarkdown import emarkdown -from zds.forum.factories import ForumCategoryFactory, ForumFactory, TopicFactory, PostFactory +from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory, TopicFactory, PostFactory from zds.gallery.factories import GalleryFactory, UserGalleryFactory, ImageFactory from zds.member.factories import StaffProfileFactory, ProfileFactory from django.contrib.auth.models import User, Permission diff --git a/zds/utils/tests/tests_comments.py b/zds/utils/tests/tests_comments.py index 63139be519..042f417089 100644 --- a/zds/utils/tests/tests_comments.py +++ b/zds/utils/tests/tests_comments.py @@ -6,7 +6,7 @@ from rest_framework import status from rest_framework.test import APIClient -from zds.forum.factories import PostFactory, create_category_and_forum, create_topic_in_forum +from zds.forum.tests.factories import PostFactory, create_category_and_forum, create_topic_in_forum from zds.member.factories import ProfileFactory, StaffProfileFactory from zds.tutorialv2.factories import PublishedContentFactory from zds.tutorialv2.models import CONTENT_TYPES diff --git a/zds/utils/tests/tests_context_processors.py b/zds/utils/tests/tests_context_processors.py index 5e0deff8c8..d971ee1753 100644 --- a/zds/utils/tests/tests_context_processors.py +++ b/zds/utils/tests/tests_context_processors.py @@ -3,7 +3,7 @@ from django.contrib.auth.models import AnonymousUser from django.test import TestCase -from zds.forum.factories import ForumCategoryFactory, ForumFactory, PostFactory, TopicFactory +from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory, PostFactory, TopicFactory from zds.member.factories import ProfileFactory, StaffProfileFactory from zds.utils.context_processor import header_notifications as notifications_processor from zds.utils.models import Alert diff --git a/zds/utils/tests/tests_topbar_tags.py b/zds/utils/tests/tests_topbar_tags.py index 4939aec548..7b4564898e 100644 --- a/zds/utils/tests/tests_topbar_tags.py +++ b/zds/utils/tests/tests_topbar_tags.py @@ -2,7 +2,7 @@ from django.test import TestCase -from zds.forum.factories import ForumCategoryFactory, ForumFactory, TopicFactory +from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory, TopicFactory from zds.member.factories import ProfileFactory, StaffProfileFactory from zds.tutorialv2.factories import PublishedContentFactory, PublishableContentFactory, SubCategoryFactory from zds.tutorialv2.publication_utils import publish_content From 86654a708a17977eb8554d047d9aace96f8d40d1 Mon Sep 17 00:00:00 2001 From: Arnaud-D <35631001+Arnaud-D@users.noreply.github.com> Date: Sun, 20 Feb 2022 08:05:26 +0100 Subject: [PATCH 43/53] =?UTF-8?q?D=C3=A9place=20les=20factories=20de=20gal?= =?UTF-8?q?lery=20dans=20le=20dossier=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/featured/tests/tests.py | 2 +- zds/gallery/api/tests.py | 2 +- zds/gallery/{ => tests}/factories.py | 0 zds/gallery/tests/tests_forms.py | 2 +- zds/gallery/tests/tests_models.py | 2 +- zds/gallery/tests/tests_views.py | 2 +- zds/member/tests/tests_models.py | 2 +- zds/member/tests/tests_views.py | 2 +- zds/notification/tests/tests_basics.py | 2 +- zds/notification/tests/tests_tricky.py | 2 +- zds/tutorialv2/factories.py | 2 +- zds/tutorialv2/tests/tests_feeds.py | 2 +- zds/tutorialv2/tests/tests_lists.py | 2 +- zds/tutorialv2/tests/tests_models.py | 2 +- zds/tutorialv2/tests/tests_move.py | 2 +- zds/tutorialv2/tests/tests_opinion_views.py | 2 +- zds/tutorialv2/tests/tests_utils.py | 2 +- zds/tutorialv2/tests/tests_views/tests_content.py | 2 +- zds/tutorialv2/tests/tests_views/tests_published.py | 2 +- zds/tutorialv2/tests/tests_views/tests_stats.py | 2 +- zds/utils/management/commands/load_fixtures.py | 2 +- 21 files changed, 20 insertions(+), 20 deletions(-) rename zds/gallery/{ => tests}/factories.py (100%) diff --git a/zds/featured/tests/tests.py b/zds/featured/tests/tests.py index 4d46101b82..9b00746e9b 100644 --- a/zds/featured/tests/tests.py +++ b/zds/featured/tests/tests.py @@ -8,7 +8,7 @@ from zds.featured.tests.factories import FeaturedResourceFactory from zds.featured.models import FeaturedResource, FeaturedMessage, FeaturedRequested from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory, TopicFactory -from zds.gallery.factories import GalleryFactory, ImageFactory +from zds.gallery.tests.factories import GalleryFactory, ImageFactory from zds.tutorialv2.factories import PublishedContentFactory from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents diff --git a/zds/gallery/api/tests.py b/zds/gallery/api/tests.py index f2e2012d9a..c00f679e36 100644 --- a/zds/gallery/api/tests.py +++ b/zds/gallery/api/tests.py @@ -8,7 +8,7 @@ from rest_framework.test import APITestCase, APIClient from rest_framework_extensions.settings import extensions_api_settings -from zds.gallery.factories import UserGalleryFactory, GalleryFactory, ImageFactory +from zds.gallery.tests.factories import UserGalleryFactory, GalleryFactory, ImageFactory from zds.gallery.models import Gallery, UserGallery, GALLERY_WRITE, Image, GALLERY_READ from zds.member.factories import ProfileFactory from zds.member.api.tests import create_oauth2_client, authenticate_client diff --git a/zds/gallery/factories.py b/zds/gallery/tests/factories.py similarity index 100% rename from zds/gallery/factories.py rename to zds/gallery/tests/factories.py diff --git a/zds/gallery/tests/tests_forms.py b/zds/gallery/tests/tests_forms.py index 5c821449f8..99182f3d0a 100644 --- a/zds/gallery/tests/tests_forms.py +++ b/zds/gallery/tests/tests_forms.py @@ -1,7 +1,7 @@ from django.test import TestCase from django.core.files.uploadedfile import SimpleUploadedFile -from zds.gallery.factories import GalleryFactory +from zds.gallery.tests.factories import GalleryFactory from zds.gallery.forms import GalleryForm, UserGalleryForm, ImageForm, ImageAsAvatarForm, ArchiveImageForm from zds.member.factories import ProfileFactory from django.conf import settings diff --git a/zds/gallery/tests/tests_models.py b/zds/gallery/tests/tests_models.py index f897990985..edddfd24b3 100644 --- a/zds/gallery/tests/tests_models.py +++ b/zds/gallery/tests/tests_models.py @@ -3,7 +3,7 @@ from django.test import TestCase from django.urls import reverse -from zds.gallery.factories import GalleryFactory, UserGalleryFactory, ImageFactory +from zds.gallery.tests.factories import GalleryFactory, UserGalleryFactory, ImageFactory from zds.member.factories import ProfileFactory from django.conf import settings diff --git a/zds/gallery/tests/tests_views.py b/zds/gallery/tests/tests_views.py index 402ffaa271..15004f6535 100644 --- a/zds/gallery/tests/tests_views.py +++ b/zds/gallery/tests/tests_views.py @@ -4,7 +4,7 @@ from django.urls import reverse from zds.member.factories import ProfileFactory -from zds.gallery.factories import GalleryFactory, UserGalleryFactory, ImageFactory +from zds.gallery.tests.factories import GalleryFactory, UserGalleryFactory, ImageFactory from zds.gallery.models import Gallery, UserGallery, Image from django.conf import settings diff --git a/zds/member/tests/tests_models.py b/zds/member/tests/tests_models.py index a7711474c9..38261e242e 100644 --- a/zds/member/tests/tests_models.py +++ b/zds/member/tests/tests_models.py @@ -11,7 +11,7 @@ from zds.member.models import TokenForgotPassword, TokenRegister, Profile from zds.tutorialv2.factories import PublishableContentFactory, PublishedContentFactory from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents -from zds.gallery.factories import GalleryFactory, ImageFactory +from zds.gallery.tests.factories import GalleryFactory, ImageFactory from zds.utils.models import Alert, Hat diff --git a/zds/member/tests/tests_views.py b/zds/member/tests/tests_views.py index 7d9f27d9e5..514bd39986 100644 --- a/zds/member/tests/tests_views.py +++ b/zds/member/tests/tests_views.py @@ -32,7 +32,7 @@ from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory, TopicFactory, PostFactory from zds.forum.models import Topic, Post -from zds.gallery.factories import GalleryFactory, UserGalleryFactory +from zds.gallery.tests.factories import GalleryFactory, UserGalleryFactory from zds.gallery.models import Gallery, UserGallery from zds.pages.models import GroupContact from zds.utils.models import CommentVote, Hat, HatRequest, Alert diff --git a/zds/notification/tests/tests_basics.py b/zds/notification/tests/tests_basics.py index 574299dc11..9cd107502b 100644 --- a/zds/notification/tests/tests_basics.py +++ b/zds/notification/tests/tests_basics.py @@ -10,7 +10,7 @@ from django.conf import settings from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory, TopicFactory, PostFactory, TagFactory from zds.forum.models import Topic -from zds.gallery.factories import UserGalleryFactory +from zds.gallery.tests.factories import UserGalleryFactory from zds.member.factories import ProfileFactory, StaffProfileFactory, UserFactory from zds.mp.models import mark_read from zds.tutorialv2 import signals diff --git a/zds/notification/tests/tests_tricky.py b/zds/notification/tests/tests_tricky.py index 238fa9df66..2a0bd43a04 100644 --- a/zds/notification/tests/tests_tricky.py +++ b/zds/notification/tests/tests_tricky.py @@ -8,7 +8,7 @@ from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory from zds.forum.models import Topic -from zds.gallery.factories import UserGalleryFactory +from zds.gallery.tests.factories import UserGalleryFactory from zds.member.factories import StaffProfileFactory, ProfileFactory from zds.notification.models import ( NewTopicSubscription, diff --git a/zds/tutorialv2/factories.py b/zds/tutorialv2/factories.py index 1aeec5ab22..e4bbbb54b4 100644 --- a/zds/tutorialv2/factories.py +++ b/zds/tutorialv2/factories.py @@ -3,7 +3,7 @@ import factory from zds.forum.tests.factories import PostFactory, TopicFactory -from zds.gallery.factories import GalleryFactory, UserGalleryFactory +from zds.gallery.tests.factories import GalleryFactory, UserGalleryFactory from zds.utils.factories import LicenceFactory, SubCategoryFactory from zds.utils.models import Licence from zds.tutorialv2.models.database import PublishableContent, Validation, ContentReaction diff --git a/zds/tutorialv2/tests/tests_feeds.py b/zds/tutorialv2/tests/tests_feeds.py index e890e0a053..6c306760b7 100644 --- a/zds/tutorialv2/tests/tests_feeds.py +++ b/zds/tutorialv2/tests/tests_feeds.py @@ -4,7 +4,7 @@ from django.test.utils import override_settings from django.contrib.auth.models import Group -from zds.gallery.factories import UserGalleryFactory +from zds.gallery.tests.factories import UserGalleryFactory from zds.member.factories import ProfileFactory, StaffProfileFactory, UserFactory from zds.forum.tests.factories import ForumFactory, ForumCategoryFactory, TagFactory from zds.tutorialv2.models.database import PublishedContent diff --git a/zds/tutorialv2/tests/tests_lists.py b/zds/tutorialv2/tests/tests_lists.py index 40ac661c84..cfd55e3f61 100644 --- a/zds/tutorialv2/tests/tests_lists.py +++ b/zds/tutorialv2/tests/tests_lists.py @@ -15,7 +15,7 @@ ) from zds.tutorialv2.publication_utils import publish_content from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents -from zds.gallery.factories import UserGalleryFactory +from zds.gallery.tests.factories import UserGalleryFactory from zds.forum.tests.factories import ForumFactory, ForumCategoryFactory from zds.utils.factories import CategoryFactory, SubCategoryFactory, LicenceFactory diff --git a/zds/tutorialv2/tests/tests_models.py b/zds/tutorialv2/tests/tests_models.py index a4de9cdd71..4cfb1501a3 100644 --- a/zds/tutorialv2/tests/tests_models.py +++ b/zds/tutorialv2/tests/tests_models.py @@ -16,7 +16,7 @@ ExtractFactory, PublishedContentFactory, ) -from zds.gallery.factories import UserGalleryFactory +from zds.gallery.tests.factories import UserGalleryFactory from zds.tutorialv2.models.database import PublishableContent, PublishedContent from zds.tutorialv2.publication_utils import publish_content from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents diff --git a/zds/tutorialv2/tests/tests_move.py b/zds/tutorialv2/tests/tests_move.py index c54e9b7b62..ef1b7dd612 100644 --- a/zds/tutorialv2/tests/tests_move.py +++ b/zds/tutorialv2/tests/tests_move.py @@ -12,7 +12,7 @@ ExtractFactory, ) from zds.tutorialv2.models.database import PublishableContent -from zds.gallery.factories import UserGalleryFactory +from zds.gallery.tests.factories import UserGalleryFactory from zds.forum.tests.factories import ForumFactory, ForumCategoryFactory from zds.tutorialv2.publication_utils import publish_content from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents diff --git a/zds/tutorialv2/tests/tests_opinion_views.py b/zds/tutorialv2/tests/tests_opinion_views.py index 43b1f6a7fd..f68c59fc5a 100644 --- a/zds/tutorialv2/tests/tests_opinion_views.py +++ b/zds/tutorialv2/tests/tests_opinion_views.py @@ -3,7 +3,7 @@ from django.test import TestCase from django.utils.translation import gettext_lazy as _ -from zds.gallery.factories import UserGalleryFactory +from zds.gallery.tests.factories import UserGalleryFactory from zds.member.factories import ProfileFactory, StaffProfileFactory from zds.tutorialv2.factories import ( PublishableContentFactory, diff --git a/zds/tutorialv2/tests/tests_utils.py b/zds/tutorialv2/tests/tests_utils.py index c4b7e07c61..3d05ddf06b 100644 --- a/zds/tutorialv2/tests/tests_utils.py +++ b/zds/tutorialv2/tests/tests_utils.py @@ -15,7 +15,7 @@ PublishedContentFactory, ContentReactionFactory, ) -from zds.gallery.factories import UserGalleryFactory +from zds.gallery.tests.factories import UserGalleryFactory from zds.tutorialv2.models.versioned import Container from zds.tutorialv2.utils import ( get_target_tagged_tree_for_container, diff --git a/zds/tutorialv2/tests/tests_views/tests_content.py b/zds/tutorialv2/tests/tests_views/tests_content.py index d199a3e976..ed999aa68a 100644 --- a/zds/tutorialv2/tests/tests_views/tests_content.py +++ b/zds/tutorialv2/tests/tests_views/tests_content.py @@ -17,7 +17,7 @@ from zds.forum.tests.factories import ForumFactory, ForumCategoryFactory from zds.forum.models import Topic, Post, TopicRead -from zds.gallery.factories import UserGalleryFactory +from zds.gallery.tests.factories import UserGalleryFactory from zds.gallery.models import GALLERY_WRITE, UserGallery, Gallery from zds.gallery.models import Image from zds.member.factories import ProfileFactory, StaffProfileFactory, UserFactory diff --git a/zds/tutorialv2/tests/tests_views/tests_published.py b/zds/tutorialv2/tests/tests_views/tests_published.py index ed2555a628..4bc98f884b 100644 --- a/zds/tutorialv2/tests/tests_views/tests_published.py +++ b/zds/tutorialv2/tests/tests_views/tests_published.py @@ -9,7 +9,7 @@ from django.utils.translation import gettext_lazy as _ from zds.forum.tests.factories import ForumFactory, ForumCategoryFactory -from zds.gallery.factories import UserGalleryFactory +from zds.gallery.tests.factories import UserGalleryFactory from zds.member.factories import ProfileFactory, StaffProfileFactory, UserFactory from zds.mp.models import PrivateTopic, is_privatetopic_unread from zds.notification.models import ContentReactionAnswerSubscription, Notification diff --git a/zds/tutorialv2/tests/tests_views/tests_stats.py b/zds/tutorialv2/tests/tests_views/tests_stats.py index 86e1d99e56..d886a34d16 100644 --- a/zds/tutorialv2/tests/tests_views/tests_stats.py +++ b/zds/tutorialv2/tests/tests_views/tests_stats.py @@ -8,7 +8,7 @@ from django.test.utils import override_settings from django.urls import reverse -from zds.gallery.factories import UserGalleryFactory +from zds.gallery.tests.factories import UserGalleryFactory from zds.member.factories import ProfileFactory, StaffProfileFactory from zds.tutorialv2.factories import PublishableContentFactory, ContainerFactory, ExtractFactory from zds.tutorialv2.models.database import Validation, PublishedContent diff --git a/zds/utils/management/commands/load_fixtures.py b/zds/utils/management/commands/load_fixtures.py index 14a2db6d4d..26a7ab8fe4 100644 --- a/zds/utils/management/commands/load_fixtures.py +++ b/zds/utils/management/commands/load_fixtures.py @@ -13,7 +13,7 @@ from zds.utils.templatetags.emarkdown import emarkdown from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory, TopicFactory, PostFactory -from zds.gallery.factories import GalleryFactory, UserGalleryFactory, ImageFactory +from zds.gallery.tests.factories import GalleryFactory, UserGalleryFactory, ImageFactory from zds.member.factories import StaffProfileFactory, ProfileFactory from django.contrib.auth.models import User, Permission from zds.member.models import Profile From d37179786dfd6cc97b93387e10e7f963e77b8ea5 Mon Sep 17 00:00:00 2001 From: Arnaud-D <35631001+Arnaud-D@users.noreply.github.com> Date: Sun, 20 Feb 2022 08:06:32 +0100 Subject: [PATCH 44/53] =?UTF-8?q?D=C3=A9place=20les=20factories=20de=20mem?= =?UTF-8?q?ber=20dans=20le=20dossier=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/featured/tests/tests.py | 2 +- zds/forum/api/tests.py | 2 +- zds/forum/tests/tests.py | 2 +- zds/forum/tests/tests_feeds.py | 2 +- zds/forum/tests/tests_forms.py | 2 +- zds/forum/tests/tests_views.py | 2 +- zds/gallery/api/tests.py | 2 +- zds/gallery/tests/tests_forms.py | 2 +- zds/gallery/tests/tests_models.py | 2 +- zds/gallery/tests/tests_views.py | 2 +- zds/member/api/tests.py | 2 +- zds/member/{ => tests}/factories.py | 0 zds/member/tests/tests_forms.py | 4 ++-- zds/member/tests/tests_models.py | 2 +- zds/member/tests/tests_views.py | 2 +- zds/middlewares/tests/tests_setlastvisitmiddleware.py | 2 +- zds/mp/api/tests.py | 2 +- zds/mp/tests/tests_forms.py | 2 +- zds/mp/tests/tests_models.py | 2 +- zds/mp/tests/tests_utils.py | 2 +- zds/mp/tests/tests_views.py | 2 +- zds/notification/api/tests.py | 2 +- zds/notification/tests/tests_basics.py | 2 +- zds/notification/tests/tests_tricky.py | 2 +- zds/pages/tests.py | 2 +- zds/searchv2/tests/tests_models.py | 2 +- zds/searchv2/tests/tests_utils.py | 2 +- zds/searchv2/tests/tests_views.py | 2 +- zds/tutorialv2/api/tests.py | 2 +- zds/tutorialv2/tests/tests_feeds.py | 2 +- zds/tutorialv2/tests/tests_front.py | 2 +- zds/tutorialv2/tests/tests_lists.py | 2 +- zds/tutorialv2/tests/tests_models.py | 2 +- zds/tutorialv2/tests/tests_move.py | 2 +- zds/tutorialv2/tests/tests_opinion_views.py | 2 +- zds/tutorialv2/tests/tests_utils.py | 2 +- zds/tutorialv2/tests/tests_views/tests_addcontributor.py | 4 +--- zds/tutorialv2/tests/tests_views/tests_addsuggestion.py | 2 +- zds/tutorialv2/tests/tests_views/tests_content.py | 2 +- zds/tutorialv2/tests/tests_views/tests_edgecases.py | 2 +- zds/tutorialv2/tests/tests_views/tests_editcontentlicense.py | 2 +- zds/tutorialv2/tests/tests_views/tests_editcontenttags.py | 2 +- zds/tutorialv2/tests/tests_views/tests_published.py | 2 +- zds/tutorialv2/tests/tests_views/tests_removecontributor.py | 2 +- zds/tutorialv2/tests/tests_views/tests_stats.py | 2 +- zds/utils/management/commands/load_fixtures.py | 2 +- zds/utils/management/tests.py | 2 +- zds/utils/tests/test_misc.py | 2 +- zds/utils/tests/tests_comments.py | 2 +- zds/utils/tests/tests_context_processors.py | 2 +- zds/utils/tests/tests_interventions.py | 2 +- zds/utils/tests/tests_models.py | 2 +- zds/utils/tests/tests_topbar_tags.py | 2 +- 53 files changed, 53 insertions(+), 55 deletions(-) rename zds/member/{ => tests}/factories.py (100%) diff --git a/zds/featured/tests/tests.py b/zds/featured/tests/tests.py index 9b00746e9b..4bea8e1642 100644 --- a/zds/featured/tests/tests.py +++ b/zds/featured/tests/tests.py @@ -4,7 +4,7 @@ from django.test import TestCase from django.utils.translation import gettext as _ -from zds.member.factories import StaffProfileFactory, ProfileFactory +from zds.member.tests.factories import StaffProfileFactory, ProfileFactory from zds.featured.tests.factories import FeaturedResourceFactory from zds.featured.models import FeaturedResource, FeaturedMessage, FeaturedRequested from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory, TopicFactory diff --git a/zds/forum/api/tests.py b/zds/forum/api/tests.py index 52a8c87f5a..cf65fc264f 100644 --- a/zds/forum/api/tests.py +++ b/zds/forum/api/tests.py @@ -8,7 +8,7 @@ from rest_framework_extensions.settings import extensions_api_settings from zds.forum.tests.factories import PostFactory, create_category_and_forum, create_topic_in_forum -from zds.member.factories import ProfileFactory +from zds.member.tests.factories import ProfileFactory from zds.utils.models import CommentVote diff --git a/zds/forum/tests/tests.py b/zds/forum/tests/tests.py index 245d41752e..50206bbfaf 100644 --- a/zds/forum/tests/tests.py +++ b/zds/forum/tests/tests.py @@ -9,7 +9,7 @@ from zds.forum.commons import PostEditMixin from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory, TopicFactory, PostFactory, TagFactory from zds.forum.models import Forum, TopicRead, Post, Topic -from zds.member.factories import ProfileFactory, StaffProfileFactory +from zds.member.tests.factories import ProfileFactory, StaffProfileFactory from zds.notification.models import TopicAnswerSubscription from zds.utils import old_slugify from zds.utils.forums import get_tag_by_title diff --git a/zds/forum/tests/tests_feeds.py b/zds/forum/tests/tests_feeds.py index 94899df660..1c6f9f1455 100644 --- a/zds/forum/tests/tests_feeds.py +++ b/zds/forum/tests/tests_feeds.py @@ -5,7 +5,7 @@ from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory, TopicFactory, PostFactory, TagFactory from zds.forum.feeds import LastPostsFeedRSS, LastPostsFeedATOM, LastTopicsFeedRSS, LastTopicsFeedATOM -from zds.member.factories import ProfileFactory +from zds.member.tests.factories import ProfileFactory class LastTopicsFeedRSSTest(TestCase): diff --git a/zds/forum/tests/tests_forms.py b/zds/forum/tests/tests_forms.py index 0b21f32013..f3d96df16c 100644 --- a/zds/forum/tests/tests_forms.py +++ b/zds/forum/tests/tests_forms.py @@ -3,7 +3,7 @@ from zds.forum.forms import TopicForm, PostForm from zds.forum.tests.factories import create_category_and_forum, create_topic_in_forum -from zds.member.factories import ProfileFactory +from zds.member.tests.factories import ProfileFactory from django.conf import settings text_too_long = (settings.ZDS_APP["forum"]["max_post_length"] + 1) * "a" diff --git a/zds/forum/tests/tests_views.py b/zds/forum/tests/tests_views.py index 74d5d9381f..7bd4b43800 100644 --- a/zds/forum/tests/tests_views.py +++ b/zds/forum/tests/tests_views.py @@ -9,7 +9,7 @@ from zds.forum.tests.factories import PostFactory, TagFactory from zds.forum.models import Topic, Post from zds.notification.models import TopicAnswerSubscription -from zds.member.factories import ProfileFactory, StaffProfileFactory +from zds.member.tests.factories import ProfileFactory, StaffProfileFactory from zds.utils.models import CommentEdit, Hat diff --git a/zds/gallery/api/tests.py b/zds/gallery/api/tests.py index c00f679e36..8c6ff962ba 100644 --- a/zds/gallery/api/tests.py +++ b/zds/gallery/api/tests.py @@ -10,7 +10,7 @@ from zds.gallery.tests.factories import UserGalleryFactory, GalleryFactory, ImageFactory from zds.gallery.models import Gallery, UserGallery, GALLERY_WRITE, Image, GALLERY_READ -from zds.member.factories import ProfileFactory +from zds.member.tests.factories import ProfileFactory from zds.member.api.tests import create_oauth2_client, authenticate_client from zds.tutorialv2.factories import PublishableContentFactory from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents diff --git a/zds/gallery/tests/tests_forms.py b/zds/gallery/tests/tests_forms.py index 99182f3d0a..4b5c1e2970 100644 --- a/zds/gallery/tests/tests_forms.py +++ b/zds/gallery/tests/tests_forms.py @@ -3,7 +3,7 @@ from zds.gallery.tests.factories import GalleryFactory from zds.gallery.forms import GalleryForm, UserGalleryForm, ImageForm, ImageAsAvatarForm, ArchiveImageForm -from zds.member.factories import ProfileFactory +from zds.member.tests.factories import ProfileFactory from django.conf import settings diff --git a/zds/gallery/tests/tests_models.py b/zds/gallery/tests/tests_models.py index edddfd24b3..d620501034 100644 --- a/zds/gallery/tests/tests_models.py +++ b/zds/gallery/tests/tests_models.py @@ -4,7 +4,7 @@ from django.urls import reverse from zds.gallery.tests.factories import GalleryFactory, UserGalleryFactory, ImageFactory -from zds.member.factories import ProfileFactory +from zds.member.tests.factories import ProfileFactory from django.conf import settings diff --git a/zds/gallery/tests/tests_views.py b/zds/gallery/tests/tests_views.py index 15004f6535..cf83c1473e 100644 --- a/zds/gallery/tests/tests_views.py +++ b/zds/gallery/tests/tests_views.py @@ -3,7 +3,7 @@ from django.test import TestCase from django.urls import reverse -from zds.member.factories import ProfileFactory +from zds.member.tests.factories import ProfileFactory from zds.gallery.tests.factories import GalleryFactory, UserGalleryFactory, ImageFactory from zds.gallery.models import Gallery, UserGallery, Image from django.conf import settings diff --git a/zds/member/api/tests.py b/zds/member/api/tests.py index af69ec569a..84b8037baf 100644 --- a/zds/member/api/tests.py +++ b/zds/member/api/tests.py @@ -8,7 +8,7 @@ from rest_framework.test import APIClient from zds.api.pagination import REST_PAGE_SIZE, REST_MAX_PAGE_SIZE, REST_PAGE_SIZE_QUERY_PARAM -from zds.member.factories import ProfileFactory, StaffProfileFactory, ProfileNotSyncFactory +from zds.member.tests.factories import ProfileFactory, StaffProfileFactory, ProfileNotSyncFactory from zds.member.models import TokenRegister, BannedEmailProvider from rest_framework_extensions.settings import extensions_api_settings from django.core.cache import caches diff --git a/zds/member/factories.py b/zds/member/tests/factories.py similarity index 100% rename from zds/member/factories.py rename to zds/member/tests/factories.py diff --git a/zds/member/tests/tests_forms.py b/zds/member/tests/tests_forms.py index fb819f7bb9..97acd0d927 100644 --- a/zds/member/tests/tests_forms.py +++ b/zds/member/tests/tests_forms.py @@ -1,6 +1,6 @@ from django.test import TestCase -from zds.member.factories import ProfileFactory, NonAsciiProfileFactory +from zds.member.tests.factories import ProfileFactory, NonAsciiProfileFactory from zds.member.forms import ( LoginForm, RegisterForm, @@ -13,7 +13,7 @@ UsernameAndEmailForm, ) from zds.member.models import BannedEmailProvider -from zds.member.factories import StaffProfileFactory +from zds.member.tests.factories import StaffProfileFactory stringof77chars = "abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789-----" stringof251chars = ( diff --git a/zds/member/tests/tests_models.py b/zds/member/tests/tests_models.py index 38261e242e..9a582e305c 100644 --- a/zds/member/tests/tests_models.py +++ b/zds/member/tests/tests_models.py @@ -7,7 +7,7 @@ from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory, TopicFactory, PostFactory from zds.notification.models import TopicAnswerSubscription -from zds.member.factories import ProfileFactory, StaffProfileFactory, DevProfileFactory +from zds.member.tests.factories import ProfileFactory, StaffProfileFactory, DevProfileFactory from zds.member.models import TokenForgotPassword, TokenRegister, Profile from zds.tutorialv2.factories import PublishableContentFactory, PublishedContentFactory from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents diff --git a/zds/member/tests/tests_views.py b/zds/member/tests/tests_views.py index 514bd39986..8ab183b3b6 100644 --- a/zds/member/tests/tests_views.py +++ b/zds/member/tests/tests_views.py @@ -16,7 +16,7 @@ from zds.member.views import member_from_ip from zds.notification.models import TopicAnswerSubscription -from zds.member.factories import ( +from zds.member.tests.factories import ( ProfileFactory, StaffProfileFactory, NonAsciiProfileFactory, diff --git a/zds/middlewares/tests/tests_setlastvisitmiddleware.py b/zds/middlewares/tests/tests_setlastvisitmiddleware.py index 1997fa311c..cc0cbc171c 100644 --- a/zds/middlewares/tests/tests_setlastvisitmiddleware.py +++ b/zds/middlewares/tests/tests_setlastvisitmiddleware.py @@ -5,7 +5,7 @@ from django.test.utils import override_settings from django.shortcuts import get_object_or_404 -from zds.member.factories import ProfileFactory +from zds.member.tests.factories import ProfileFactory from zds.member.models import Profile from django.conf import settings from copy import deepcopy diff --git a/zds/mp/api/tests.py b/zds/mp/api/tests.py index 9c284a2a81..cdc50b3f55 100644 --- a/zds/mp/api/tests.py +++ b/zds/mp/api/tests.py @@ -11,7 +11,7 @@ from zds.api.pagination import REST_PAGE_SIZE, REST_MAX_PAGE_SIZE, REST_PAGE_SIZE_QUERY_PARAM from zds.member.api.tests import create_oauth2_client, authenticate_client -from zds.member.factories import ProfileFactory, UserFactory +from zds.member.tests.factories import ProfileFactory, UserFactory from zds.mp.factories import PrivateTopicFactory, PrivatePostFactory from zds.mp.models import PrivateTopic diff --git a/zds/mp/tests/tests_forms.py b/zds/mp/tests/tests_forms.py index c22acb8939..622fe0ec5d 100644 --- a/zds/mp/tests/tests_forms.py +++ b/zds/mp/tests/tests_forms.py @@ -2,7 +2,7 @@ from django.contrib.auth.models import Group from django.test import TestCase -from zds.member.factories import ProfileFactory, StaffProfileFactory +from zds.member.tests.factories import ProfileFactory, StaffProfileFactory from zds.mp.forms import PrivateTopicForm, PrivatePostForm from zds.mp.factories import PrivateTopicFactory diff --git a/zds/mp/tests/tests_models.py b/zds/mp/tests/tests_models.py index 233708b737..dd88447407 100644 --- a/zds/mp/tests/tests_models.py +++ b/zds/mp/tests/tests_models.py @@ -6,7 +6,7 @@ from django.contrib.auth.models import Group from django.conf import settings -from zds.member.factories import ProfileFactory +from zds.member.tests.factories import ProfileFactory from zds.mp.factories import PrivateTopicFactory, PrivatePostFactory from zds.mp.models import mark_read, is_privatetopic_unread, is_reachable, NotParticipatingError, NotReachableError diff --git a/zds/mp/tests/tests_utils.py b/zds/mp/tests/tests_utils.py index 745afd1587..670c693b77 100644 --- a/zds/mp/tests/tests_utils.py +++ b/zds/mp/tests/tests_utils.py @@ -3,7 +3,7 @@ from django.urls import reverse from django.test import TestCase from django.conf import settings -from zds.member.factories import ProfileFactory +from zds.member.tests.factories import ProfileFactory from zds.mp.models import PrivateTopic diff --git a/zds/mp/tests/tests_views.py b/zds/mp/tests/tests_views.py index cf4b24d9ea..70395e36ef 100644 --- a/zds/mp/tests/tests_views.py +++ b/zds/mp/tests/tests_views.py @@ -5,7 +5,7 @@ from django.urls import reverse from django.contrib.auth.models import Group -from zds.member.factories import ProfileFactory, UserFactory +from zds.member.tests.factories import ProfileFactory, UserFactory from zds.mp.factories import PrivateTopicFactory, PrivatePostFactory from zds.mp.models import PrivateTopic, PrivatePost, PrivateTopicRead, mark_read from zds.utils.models import Hat diff --git a/zds/notification/api/tests.py b/zds/notification/api/tests.py index 7b2d865f29..4645e03635 100644 --- a/zds/notification/api/tests.py +++ b/zds/notification/api/tests.py @@ -6,7 +6,7 @@ from rest_framework_extensions.settings import extensions_api_settings from zds.member.api.tests import create_oauth2_client, authenticate_client -from zds.member.factories import ProfileFactory +from zds.member.tests.factories import ProfileFactory from zds.mp.factories import PrivateTopicFactory from zds.notification.models import Notification from zds.utils.mps import send_message_mp diff --git a/zds/notification/tests/tests_basics.py b/zds/notification/tests/tests_basics.py index 9cd107502b..ad146bbb06 100644 --- a/zds/notification/tests/tests_basics.py +++ b/zds/notification/tests/tests_basics.py @@ -11,7 +11,7 @@ from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory, TopicFactory, PostFactory, TagFactory from zds.forum.models import Topic from zds.gallery.tests.factories import UserGalleryFactory -from zds.member.factories import ProfileFactory, StaffProfileFactory, UserFactory +from zds.member.tests.factories import ProfileFactory, StaffProfileFactory, UserFactory from zds.mp.models import mark_read from zds.tutorialv2 import signals from zds.notification.models import ( diff --git a/zds/notification/tests/tests_tricky.py b/zds/notification/tests/tests_tricky.py index 2a0bd43a04..ffbe5ab185 100644 --- a/zds/notification/tests/tests_tricky.py +++ b/zds/notification/tests/tests_tricky.py @@ -9,7 +9,7 @@ from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory from zds.forum.models import Topic from zds.gallery.tests.factories import UserGalleryFactory -from zds.member.factories import StaffProfileFactory, ProfileFactory +from zds.member.tests.factories import StaffProfileFactory, ProfileFactory from zds.notification.models import ( NewTopicSubscription, TopicAnswerSubscription, diff --git a/zds/pages/tests.py b/zds/pages/tests.py index d86b7a5248..6865741568 100644 --- a/zds/pages/tests.py +++ b/zds/pages/tests.py @@ -5,7 +5,7 @@ from zds.forum.models import Post from zds.forum.tests.factories import create_category_and_forum, create_topic_in_forum -from zds.member.factories import ProfileFactory, StaffProfileFactory +from zds.member.tests.factories import ProfileFactory, StaffProfileFactory from zds.utils.models import CommentEdit from zds.utils.templatetags.emarkdown import render_markdown diff --git a/zds/searchv2/tests/tests_models.py b/zds/searchv2/tests/tests_models.py index 523259d210..afdd67e1ee 100644 --- a/zds/searchv2/tests/tests_models.py +++ b/zds/searchv2/tests/tests_models.py @@ -6,7 +6,7 @@ from zds.forum.tests.factories import TopicFactory, PostFactory, Topic, Post from zds.forum.tests.factories import create_category_and_forum -from zds.member.factories import ProfileFactory, StaffProfileFactory +from zds.member.tests.factories import ProfileFactory, StaffProfileFactory from zds.searchv2.models import ESIndexManager from zds.tutorialv2.factories import PublishableContentFactory, ContainerFactory, ExtractFactory, publish_content from zds.tutorialv2.models.database import PublishedContent, FakeChapter, PublishableContent diff --git a/zds/searchv2/tests/tests_utils.py b/zds/searchv2/tests/tests_utils.py index f6c45c48e8..be8ba1e6ba 100644 --- a/zds/searchv2/tests/tests_utils.py +++ b/zds/searchv2/tests/tests_utils.py @@ -5,7 +5,7 @@ from django.test import TestCase from django.core.management import call_command -from zds.member.factories import ProfileFactory, StaffProfileFactory +from zds.member.tests.factories import ProfileFactory, StaffProfileFactory from zds.tutorialv2.factories import PublishableContentFactory, ContainerFactory, ExtractFactory from zds.tutorialv2.models.database import PublishedContent from zds.tutorialv2.publication_utils import publish_content diff --git a/zds/searchv2/tests/tests_views.py b/zds/searchv2/tests/tests_views.py index 4397e36f05..7d3c62c3d6 100644 --- a/zds/searchv2/tests/tests_views.py +++ b/zds/searchv2/tests/tests_views.py @@ -12,7 +12,7 @@ from zds.forum.tests.factories import TopicFactory, PostFactory, Topic, Post, TagFactory from zds.forum.tests.factories import create_category_and_forum -from zds.member.factories import ProfileFactory, StaffProfileFactory +from zds.member.tests.factories import ProfileFactory, StaffProfileFactory from zds.searchv2.models import ESIndexManager from zds.tutorialv2.factories import ( PublishableContentFactory, diff --git a/zds/tutorialv2/api/tests.py b/zds/tutorialv2/api/tests.py index fe5be71f26..e86ea50ef3 100644 --- a/zds/tutorialv2/api/tests.py +++ b/zds/tutorialv2/api/tests.py @@ -11,7 +11,7 @@ from rest_framework.test import APITestCase from rest_framework_extensions.settings import extensions_api_settings -from zds.member.factories import ProfileFactory, StaffProfileFactory +from zds.member.tests.factories import ProfileFactory, StaffProfileFactory from zds.tutorialv2.factories import ContentReactionFactory, PublishedContentFactory from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents from zds.tutorialv2.models.database import PublicationEvent diff --git a/zds/tutorialv2/tests/tests_feeds.py b/zds/tutorialv2/tests/tests_feeds.py index 6c306760b7..a557cfc193 100644 --- a/zds/tutorialv2/tests/tests_feeds.py +++ b/zds/tutorialv2/tests/tests_feeds.py @@ -5,7 +5,7 @@ from django.contrib.auth.models import Group from zds.gallery.tests.factories import UserGalleryFactory -from zds.member.factories import ProfileFactory, StaffProfileFactory, UserFactory +from zds.member.tests.factories import ProfileFactory, StaffProfileFactory, UserFactory from zds.forum.tests.factories import ForumFactory, ForumCategoryFactory, TagFactory from zds.tutorialv2.models.database import PublishedContent from zds.tutorialv2.feeds import LastTutorialsFeedRSS, LastTutorialsFeedATOM, LastArticlesFeedRSS, LastArticlesFeedATOM diff --git a/zds/tutorialv2/tests/tests_front.py b/zds/tutorialv2/tests/tests_front.py index 83bb369fc7..0ad1ed3aaf 100644 --- a/zds/tutorialv2/tests/tests_front.py +++ b/zds/tutorialv2/tests/tests_front.py @@ -13,7 +13,7 @@ from selenium.webdriver.support import expected_conditions as ec from selenium.webdriver.support.ui import WebDriverWait -from zds.member.factories import StaffProfileFactory, ProfileFactory +from zds.member.tests.factories import StaffProfileFactory, ProfileFactory from zds.tutorialv2.factories import ( PublishableContentFactory, ContainerFactory, diff --git a/zds/tutorialv2/tests/tests_lists.py b/zds/tutorialv2/tests/tests_lists.py index cfd55e3f61..f610d85372 100644 --- a/zds/tutorialv2/tests/tests_lists.py +++ b/zds/tutorialv2/tests/tests_lists.py @@ -5,7 +5,7 @@ from django.test import TestCase from django.urls import reverse -from zds.member.factories import ProfileFactory, StaffProfileFactory, UserFactory +from zds.member.tests.factories import ProfileFactory, StaffProfileFactory, UserFactory from zds.tutorialv2.factories import ( PublishableContentFactory, ContainerFactory, diff --git a/zds/tutorialv2/tests/tests_models.py b/zds/tutorialv2/tests/tests_models.py index 4cfb1501a3..bccfb4c5e1 100644 --- a/zds/tutorialv2/tests/tests_models.py +++ b/zds/tutorialv2/tests/tests_models.py @@ -9,7 +9,7 @@ from zds.gallery.models import UserGallery -from zds.member.factories import ProfileFactory, StaffProfileFactory +from zds.member.tests.factories import ProfileFactory, StaffProfileFactory from zds.tutorialv2.factories import ( PublishableContentFactory, ContainerFactory, diff --git a/zds/tutorialv2/tests/tests_move.py b/zds/tutorialv2/tests/tests_move.py index ef1b7dd612..1ea2fd0545 100644 --- a/zds/tutorialv2/tests/tests_move.py +++ b/zds/tutorialv2/tests/tests_move.py @@ -5,7 +5,7 @@ from django.test import TestCase from django.urls import reverse -from zds.member.factories import ProfileFactory, StaffProfileFactory +from zds.member.tests.factories import ProfileFactory, StaffProfileFactory from zds.tutorialv2.factories import ( PublishableContentFactory, ContainerFactory, diff --git a/zds/tutorialv2/tests/tests_opinion_views.py b/zds/tutorialv2/tests/tests_opinion_views.py index f68c59fc5a..cdacb0ddb5 100644 --- a/zds/tutorialv2/tests/tests_opinion_views.py +++ b/zds/tutorialv2/tests/tests_opinion_views.py @@ -4,7 +4,7 @@ from django.utils.translation import gettext_lazy as _ from zds.gallery.tests.factories import UserGalleryFactory -from zds.member.factories import ProfileFactory, StaffProfileFactory +from zds.member.tests.factories import ProfileFactory, StaffProfileFactory from zds.tutorialv2.factories import ( PublishableContentFactory, ExtractFactory, diff --git a/zds/tutorialv2/tests/tests_utils.py b/zds/tutorialv2/tests/tests_utils.py index 3d05ddf06b..4700173d5f 100644 --- a/zds/tutorialv2/tests/tests_utils.py +++ b/zds/tutorialv2/tests/tests_utils.py @@ -7,7 +7,7 @@ from django.test import TestCase from django.urls import reverse -from zds.member.factories import ProfileFactory, StaffProfileFactory +from zds.member.tests.factories import ProfileFactory, StaffProfileFactory from zds.tutorialv2.factories import ( PublishableContentFactory, ContainerFactory, diff --git a/zds/tutorialv2/tests/tests_views/tests_addcontributor.py b/zds/tutorialv2/tests/tests_views/tests_addcontributor.py index f2790d1ffc..b54a64f69d 100644 --- a/zds/tutorialv2/tests/tests_views/tests_addcontributor.py +++ b/zds/tutorialv2/tests/tests_views/tests_addcontributor.py @@ -1,5 +1,3 @@ -from unittest.mock import patch - from django.conf import settings from django.test import TestCase from django.urls import reverse @@ -7,7 +5,7 @@ from django.utils.html import escape -from zds.member.factories import ProfileFactory, StaffProfileFactory +from zds.member.tests.factories import ProfileFactory, StaffProfileFactory from zds.tutorialv2.factories import PublishableContentFactory from zds.tutorialv2.forms import ContributionForm from zds.tutorialv2.models.database import ContentContribution, ContentContributionRole diff --git a/zds/tutorialv2/tests/tests_views/tests_addsuggestion.py b/zds/tutorialv2/tests/tests_views/tests_addsuggestion.py index 87d1df5358..acd2079af5 100644 --- a/zds/tutorialv2/tests/tests_views/tests_addsuggestion.py +++ b/zds/tutorialv2/tests/tests_views/tests_addsuggestion.py @@ -3,7 +3,7 @@ from django.utils.translation import gettext_lazy as _ from django.utils.html import escape -from zds.member.factories import ProfileFactory, StaffProfileFactory +from zds.member.tests.factories import ProfileFactory, StaffProfileFactory from zds.tutorialv2.factories import PublishableContentFactory, PublishedContentFactory from zds.tutorialv2.models.database import ContentSuggestion from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents diff --git a/zds/tutorialv2/tests/tests_views/tests_content.py b/zds/tutorialv2/tests/tests_views/tests_content.py index ed999aa68a..beceb86cc9 100644 --- a/zds/tutorialv2/tests/tests_views/tests_content.py +++ b/zds/tutorialv2/tests/tests_views/tests_content.py @@ -20,7 +20,7 @@ from zds.gallery.tests.factories import UserGalleryFactory from zds.gallery.models import GALLERY_WRITE, UserGallery, Gallery from zds.gallery.models import Image -from zds.member.factories import ProfileFactory, StaffProfileFactory, UserFactory +from zds.member.tests.factories import ProfileFactory, StaffProfileFactory, UserFactory from zds.mp.models import PrivateTopic, is_privatetopic_unread, PrivatePost from zds.notification.models import ( TopicAnswerSubscription, diff --git a/zds/tutorialv2/tests/tests_views/tests_edgecases.py b/zds/tutorialv2/tests/tests_views/tests_edgecases.py index 73e73f974b..b3f89c885c 100644 --- a/zds/tutorialv2/tests/tests_views/tests_edgecases.py +++ b/zds/tutorialv2/tests/tests_views/tests_edgecases.py @@ -2,7 +2,7 @@ from django.test import TestCase from django.urls import reverse -from zds.member.factories import ProfileFactory, StaffProfileFactory +from zds.member.tests.factories import ProfileFactory, StaffProfileFactory from zds.tutorialv2.factories import PublishedContentFactory from zds.tutorialv2.models.database import PublishableContent from zds.tutorialv2.tests import override_for_contents, TutorialTestMixin diff --git a/zds/tutorialv2/tests/tests_views/tests_editcontentlicense.py b/zds/tutorialv2/tests/tests_views/tests_editcontentlicense.py index 5ec35aa5e6..f04caf283f 100644 --- a/zds/tutorialv2/tests/tests_views/tests_editcontentlicense.py +++ b/zds/tutorialv2/tests/tests_views/tests_editcontentlicense.py @@ -6,7 +6,7 @@ from zds.tutorialv2.views.contents import EditContentLicense from zds.tutorialv2.forms import EditContentLicenseForm from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents -from zds.member.factories import ProfileFactory, StaffProfileFactory +from zds.member.tests.factories import ProfileFactory, StaffProfileFactory from zds.tutorialv2.factories import PublishableContentFactory from zds.utils.factories import LicenceFactory diff --git a/zds/tutorialv2/tests/tests_views/tests_editcontenttags.py b/zds/tutorialv2/tests/tests_views/tests_editcontenttags.py index 7abff91f30..2f3d4d4f3b 100644 --- a/zds/tutorialv2/tests/tests_views/tests_editcontenttags.py +++ b/zds/tutorialv2/tests/tests_views/tests_editcontenttags.py @@ -7,7 +7,7 @@ from zds.tutorialv2.forms import EditContentTagsForm from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents from zds.tutorialv2.factories import PublishableContentFactory -from zds.member.factories import ProfileFactory, StaffProfileFactory +from zds.member.tests.factories import ProfileFactory, StaffProfileFactory from zds.forum.tests.factories import TagFactory from zds.utils.forms import TagValidator from zds.utils.models import Tag diff --git a/zds/tutorialv2/tests/tests_views/tests_published.py b/zds/tutorialv2/tests/tests_views/tests_published.py index 4bc98f884b..bba0a6b3e6 100644 --- a/zds/tutorialv2/tests/tests_views/tests_published.py +++ b/zds/tutorialv2/tests/tests_views/tests_published.py @@ -10,7 +10,7 @@ from zds.forum.tests.factories import ForumFactory, ForumCategoryFactory from zds.gallery.tests.factories import UserGalleryFactory -from zds.member.factories import ProfileFactory, StaffProfileFactory, UserFactory +from zds.member.tests.factories import ProfileFactory, StaffProfileFactory, UserFactory from zds.mp.models import PrivateTopic, is_privatetopic_unread from zds.notification.models import ContentReactionAnswerSubscription, Notification from zds.tutorialv2.factories import ( diff --git a/zds/tutorialv2/tests/tests_views/tests_removecontributor.py b/zds/tutorialv2/tests/tests_views/tests_removecontributor.py index 27daec1dad..a4eb2206f7 100644 --- a/zds/tutorialv2/tests/tests_views/tests_removecontributor.py +++ b/zds/tutorialv2/tests/tests_views/tests_removecontributor.py @@ -3,7 +3,7 @@ from django.utils.translation import gettext_lazy as _ from django.utils.html import escape -from zds.member.factories import ProfileFactory, StaffProfileFactory +from zds.member.tests.factories import ProfileFactory, StaffProfileFactory from zds.tutorialv2.factories import PublishableContentFactory from zds.tutorialv2.models.database import ContentContribution, ContentContributionRole from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents diff --git a/zds/tutorialv2/tests/tests_views/tests_stats.py b/zds/tutorialv2/tests/tests_views/tests_stats.py index d886a34d16..89a341a493 100644 --- a/zds/tutorialv2/tests/tests_views/tests_stats.py +++ b/zds/tutorialv2/tests/tests_views/tests_stats.py @@ -9,7 +9,7 @@ from django.urls import reverse from zds.gallery.tests.factories import UserGalleryFactory -from zds.member.factories import ProfileFactory, StaffProfileFactory +from zds.member.tests.factories import ProfileFactory, StaffProfileFactory from zds.tutorialv2.factories import PublishableContentFactory, ContainerFactory, ExtractFactory from zds.tutorialv2.models.database import Validation, PublishedContent from zds.tutorialv2.tests import TutorialTestMixin diff --git a/zds/utils/management/commands/load_fixtures.py b/zds/utils/management/commands/load_fixtures.py index 26a7ab8fe4..7741d9bd0a 100644 --- a/zds/utils/management/commands/load_fixtures.py +++ b/zds/utils/management/commands/load_fixtures.py @@ -14,7 +14,7 @@ from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory, TopicFactory, PostFactory from zds.gallery.tests.factories import GalleryFactory, UserGalleryFactory, ImageFactory -from zds.member.factories import StaffProfileFactory, ProfileFactory +from zds.member.tests.factories import StaffProfileFactory, ProfileFactory from django.contrib.auth.models import User, Permission from zds.member.models import Profile from zds.forum.models import Forum, Topic, ForumCategory diff --git a/zds/utils/management/tests.py b/zds/utils/management/tests.py index 80843e02dc..b16fb65d95 100644 --- a/zds/utils/management/tests.py +++ b/zds/utils/management/tests.py @@ -5,7 +5,7 @@ from zds.member.models import Profile from zds.forum.models import Forum, Topic, ForumCategory from zds.utils.models import Tag, Category as TCategory, CategorySubCategory, SubCategory, HelpWriting, Licence -from zds.member.factories import ProfileFactory +from zds.member.tests.factories import ProfileFactory from zds.tutorialv2.models.database import ( PublishableContent, PublishedContent, diff --git a/zds/utils/tests/test_misc.py b/zds/utils/tests/test_misc.py index 4492b833b5..e8b0f35712 100644 --- a/zds/utils/tests/test_misc.py +++ b/zds/utils/tests/test_misc.py @@ -1,6 +1,6 @@ import datetime from django.test import TestCase -from zds.member.factories import ProfileFactory, StaffProfileFactory +from zds.member.tests.factories import ProfileFactory, StaffProfileFactory from zds.tutorialv2.factories import PublishedContentFactory from zds.utils.misc import contains_utf8mb4 from zds.utils.models import Alert diff --git a/zds/utils/tests/tests_comments.py b/zds/utils/tests/tests_comments.py index 042f417089..b0ec42d27b 100644 --- a/zds/utils/tests/tests_comments.py +++ b/zds/utils/tests/tests_comments.py @@ -7,7 +7,7 @@ from rest_framework.test import APIClient from zds.forum.tests.factories import PostFactory, create_category_and_forum, create_topic_in_forum -from zds.member.factories import ProfileFactory, StaffProfileFactory +from zds.member.tests.factories import ProfileFactory, StaffProfileFactory from zds.tutorialv2.factories import PublishedContentFactory from zds.tutorialv2.models import CONTENT_TYPES from zds.tutorialv2.models.database import PublishableContent diff --git a/zds/utils/tests/tests_context_processors.py b/zds/utils/tests/tests_context_processors.py index d971ee1753..27f83e97d8 100644 --- a/zds/utils/tests/tests_context_processors.py +++ b/zds/utils/tests/tests_context_processors.py @@ -4,7 +4,7 @@ from django.test import TestCase from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory, PostFactory, TopicFactory -from zds.member.factories import ProfileFactory, StaffProfileFactory +from zds.member.tests.factories import ProfileFactory, StaffProfileFactory from zds.utils.context_processor import header_notifications as notifications_processor from zds.utils.models import Alert diff --git a/zds/utils/tests/tests_interventions.py b/zds/utils/tests/tests_interventions.py index 0aad773a58..932e0ddf8d 100644 --- a/zds/utils/tests/tests_interventions.py +++ b/zds/utils/tests/tests_interventions.py @@ -7,7 +7,7 @@ from zds.tutorialv2.models.database import Validation from zds.tutorialv2.factories import PublishableContentFactory -from zds.member.factories import ProfileFactory, StaffProfileFactory +from zds.member.tests.factories import ProfileFactory, StaffProfileFactory from zds.utils.factories import SubCategoryFactory, LicenceFactory from zds.utils.mps import send_message_mp, send_mp diff --git a/zds/utils/tests/tests_models.py b/zds/utils/tests/tests_models.py index 31f8933d65..458a913d52 100644 --- a/zds/utils/tests/tests_models.py +++ b/zds/utils/tests/tests_models.py @@ -3,7 +3,7 @@ from django.contrib.auth.models import Group from zds.member.models import Profile -from zds.member.factories import ProfileFactory +from zds.member.tests.factories import ProfileFactory from zds.utils.forms import TagValidator from zds.utils.models import Tag, Hat diff --git a/zds/utils/tests/tests_topbar_tags.py b/zds/utils/tests/tests_topbar_tags.py index 7b4564898e..bc7c387c11 100644 --- a/zds/utils/tests/tests_topbar_tags.py +++ b/zds/utils/tests/tests_topbar_tags.py @@ -3,7 +3,7 @@ from django.test import TestCase from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory, TopicFactory -from zds.member.factories import ProfileFactory, StaffProfileFactory +from zds.member.tests.factories import ProfileFactory, StaffProfileFactory from zds.tutorialv2.factories import PublishedContentFactory, PublishableContentFactory, SubCategoryFactory from zds.tutorialv2.publication_utils import publish_content from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents From 267cc928b41d907223f66d7d21e9d460e58e7a69 Mon Sep 17 00:00:00 2001 From: Arnaud-D <35631001+Arnaud-D@users.noreply.github.com> Date: Sun, 20 Feb 2022 08:07:40 +0100 Subject: [PATCH 45/53] =?UTF-8?q?D=C3=A9place=20les=20factories=20de=20mp?= =?UTF-8?q?=20dans=20le=20dossier=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/member/tests/tests_views.py | 2 +- zds/mp/api/tests.py | 2 +- zds/mp/{ => tests}/factories.py | 0 zds/mp/tests/tests_forms.py | 2 +- zds/mp/tests/tests_models.py | 2 +- zds/mp/tests/tests_views.py | 2 +- zds/notification/api/tests.py | 2 +- 7 files changed, 6 insertions(+), 6 deletions(-) rename zds/mp/{ => tests}/factories.py (100%) diff --git a/zds/member/tests/tests_views.py b/zds/member/tests/tests_views.py index 8ab183b3b6..8d9c9e7c72 100644 --- a/zds/member/tests/tests_views.py +++ b/zds/member/tests/tests_views.py @@ -23,7 +23,7 @@ UserFactory, DevProfileFactory, ) -from zds.mp.factories import PrivateTopicFactory, PrivatePostFactory +from zds.mp.tests.factories import PrivateTopicFactory, PrivatePostFactory from zds.member.models import Profile, KarmaNote, TokenForgotPassword from zds.mp.models import PrivatePost, PrivateTopic from zds.member.models import TokenRegister, Ban, NewEmailProvider, BannedEmailProvider diff --git a/zds/mp/api/tests.py b/zds/mp/api/tests.py index cdc50b3f55..3dd6be9d48 100644 --- a/zds/mp/api/tests.py +++ b/zds/mp/api/tests.py @@ -12,7 +12,7 @@ from zds.api.pagination import REST_PAGE_SIZE, REST_MAX_PAGE_SIZE, REST_PAGE_SIZE_QUERY_PARAM from zds.member.api.tests import create_oauth2_client, authenticate_client from zds.member.tests.factories import ProfileFactory, UserFactory -from zds.mp.factories import PrivateTopicFactory, PrivatePostFactory +from zds.mp.tests.factories import PrivateTopicFactory, PrivatePostFactory from zds.mp.models import PrivateTopic diff --git a/zds/mp/factories.py b/zds/mp/tests/factories.py similarity index 100% rename from zds/mp/factories.py rename to zds/mp/tests/factories.py diff --git a/zds/mp/tests/tests_forms.py b/zds/mp/tests/tests_forms.py index 622fe0ec5d..c3f45fb9d3 100644 --- a/zds/mp/tests/tests_forms.py +++ b/zds/mp/tests/tests_forms.py @@ -4,7 +4,7 @@ from zds.member.tests.factories import ProfileFactory, StaffProfileFactory from zds.mp.forms import PrivateTopicForm, PrivatePostForm -from zds.mp.factories import PrivateTopicFactory +from zds.mp.tests.factories import PrivateTopicFactory class PrivateTopicFormTest(TestCase): diff --git a/zds/mp/tests/tests_models.py b/zds/mp/tests/tests_models.py index dd88447407..9bb3f9aeeb 100644 --- a/zds/mp/tests/tests_models.py +++ b/zds/mp/tests/tests_models.py @@ -7,7 +7,7 @@ from django.conf import settings from zds.member.tests.factories import ProfileFactory -from zds.mp.factories import PrivateTopicFactory, PrivatePostFactory +from zds.mp.tests.factories import PrivateTopicFactory, PrivatePostFactory from zds.mp.models import mark_read, is_privatetopic_unread, is_reachable, NotParticipatingError, NotReachableError # by moment, i wrote the scenario to be simpler diff --git a/zds/mp/tests/tests_views.py b/zds/mp/tests/tests_views.py index 70395e36ef..de6d385038 100644 --- a/zds/mp/tests/tests_views.py +++ b/zds/mp/tests/tests_views.py @@ -6,7 +6,7 @@ from django.contrib.auth.models import Group from zds.member.tests.factories import ProfileFactory, UserFactory -from zds.mp.factories import PrivateTopicFactory, PrivatePostFactory +from zds.mp.tests.factories import PrivateTopicFactory, PrivatePostFactory from zds.mp.models import PrivateTopic, PrivatePost, PrivateTopicRead, mark_read from zds.utils.models import Hat diff --git a/zds/notification/api/tests.py b/zds/notification/api/tests.py index 4645e03635..5c002b1384 100644 --- a/zds/notification/api/tests.py +++ b/zds/notification/api/tests.py @@ -7,7 +7,7 @@ from zds.member.api.tests import create_oauth2_client, authenticate_client from zds.member.tests.factories import ProfileFactory -from zds.mp.factories import PrivateTopicFactory +from zds.mp.tests.factories import PrivateTopicFactory from zds.notification.models import Notification from zds.utils.mps import send_message_mp From c1513d67381df249898824e92ea4f293c7949925 Mon Sep 17 00:00:00 2001 From: Arnaud-D <35631001+Arnaud-D@users.noreply.github.com> Date: Sun, 20 Feb 2022 08:09:59 +0100 Subject: [PATCH 46/53] =?UTF-8?q?D=C3=A9place=20les=20factories=20de=20tut?= =?UTF-8?q?orialv2=20dans=20le=20dossier=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/featured/tests/tests.py | 2 +- zds/gallery/api/tests.py | 2 +- zds/member/tests/tests_models.py | 2 +- zds/member/tests/tests_views.py | 2 +- zds/notification/tests/tests_basics.py | 2 +- zds/notification/tests/tests_tricky.py | 2 +- zds/searchv2/tests/tests_models.py | 2 +- zds/searchv2/tests/tests_utils.py | 2 +- zds/searchv2/tests/tests_views.py | 2 +- zds/tutorialv2/api/tests.py | 2 +- zds/tutorialv2/{ => tests}/factories.py | 0 zds/tutorialv2/tests/tests_feeds.py | 2 +- zds/tutorialv2/tests/tests_front.py | 2 +- zds/tutorialv2/tests/tests_lists.py | 2 +- zds/tutorialv2/tests/tests_models.py | 2 +- zds/tutorialv2/tests/tests_move.py | 2 +- zds/tutorialv2/tests/tests_opinion_views.py | 2 +- zds/tutorialv2/tests/tests_utils.py | 2 +- zds/tutorialv2/tests/tests_views/tests_addcontributor.py | 2 +- zds/tutorialv2/tests/tests_views/tests_addsuggestion.py | 2 +- zds/tutorialv2/tests/tests_views/tests_content.py | 2 +- zds/tutorialv2/tests/tests_views/tests_edgecases.py | 2 +- zds/tutorialv2/tests/tests_views/tests_editcontentlicense.py | 2 +- zds/tutorialv2/tests/tests_views/tests_editcontenttags.py | 2 +- zds/tutorialv2/tests/tests_views/tests_published.py | 2 +- zds/tutorialv2/tests/tests_views/tests_removecontributor.py | 2 +- zds/tutorialv2/tests/tests_views/tests_stats.py | 2 +- zds/utils/api/tests.py | 2 +- zds/utils/management/commands/load_fixtures.py | 2 +- zds/utils/tests/test_misc.py | 2 +- zds/utils/tests/tests_comments.py | 2 +- zds/utils/tests/tests_interventions.py | 2 +- zds/utils/tests/tests_topbar_tags.py | 2 +- 33 files changed, 32 insertions(+), 32 deletions(-) rename zds/tutorialv2/{ => tests}/factories.py (100%) diff --git a/zds/featured/tests/tests.py b/zds/featured/tests/tests.py index 4bea8e1642..faf53b199a 100644 --- a/zds/featured/tests/tests.py +++ b/zds/featured/tests/tests.py @@ -9,7 +9,7 @@ from zds.featured.models import FeaturedResource, FeaturedMessage, FeaturedRequested from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory, TopicFactory from zds.gallery.tests.factories import GalleryFactory, ImageFactory -from zds.tutorialv2.factories import PublishedContentFactory +from zds.tutorialv2.tests.factories import PublishedContentFactory from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents diff --git a/zds/gallery/api/tests.py b/zds/gallery/api/tests.py index 8c6ff962ba..f3080bac77 100644 --- a/zds/gallery/api/tests.py +++ b/zds/gallery/api/tests.py @@ -12,7 +12,7 @@ from zds.gallery.models import Gallery, UserGallery, GALLERY_WRITE, Image, GALLERY_READ from zds.member.tests.factories import ProfileFactory from zds.member.api.tests import create_oauth2_client, authenticate_client -from zds.tutorialv2.factories import PublishableContentFactory +from zds.tutorialv2.tests.factories import PublishableContentFactory from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents diff --git a/zds/member/tests/tests_models.py b/zds/member/tests/tests_models.py index 9a582e305c..262c3d4d48 100644 --- a/zds/member/tests/tests_models.py +++ b/zds/member/tests/tests_models.py @@ -9,7 +9,7 @@ from zds.notification.models import TopicAnswerSubscription from zds.member.tests.factories import ProfileFactory, StaffProfileFactory, DevProfileFactory from zds.member.models import TokenForgotPassword, TokenRegister, Profile -from zds.tutorialv2.factories import PublishableContentFactory, PublishedContentFactory +from zds.tutorialv2.tests.factories import PublishableContentFactory, PublishedContentFactory from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents from zds.gallery.tests.factories import GalleryFactory, ImageFactory from zds.utils.models import Alert, Hat diff --git a/zds/member/tests/tests_views.py b/zds/member/tests/tests_views.py index 8d9c9e7c72..07f8d2a227 100644 --- a/zds/member/tests/tests_views.py +++ b/zds/member/tests/tests_views.py @@ -27,7 +27,7 @@ from zds.member.models import Profile, KarmaNote, TokenForgotPassword from zds.mp.models import PrivatePost, PrivateTopic from zds.member.models import TokenRegister, Ban, NewEmailProvider, BannedEmailProvider -from zds.tutorialv2.factories import PublishableContentFactory, PublishedContentFactory, BetaContentFactory +from zds.tutorialv2.tests.factories import PublishableContentFactory, PublishedContentFactory, BetaContentFactory from zds.tutorialv2.models.database import PublishableContent, PublishedContent from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory, TopicFactory, PostFactory diff --git a/zds/notification/tests/tests_basics.py b/zds/notification/tests/tests_basics.py index ad146bbb06..4886744093 100644 --- a/zds/notification/tests/tests_basics.py +++ b/zds/notification/tests/tests_basics.py @@ -22,7 +22,7 @@ NewTopicSubscription, NewPublicationSubscription, ) -from zds.tutorialv2.factories import ( +from zds.tutorialv2.tests.factories import ( PublishableContentFactory, ContentReactionFactory, PublishedContentFactory, diff --git a/zds/notification/tests/tests_tricky.py b/zds/notification/tests/tests_tricky.py index ffbe5ab185..dbd41a3418 100644 --- a/zds/notification/tests/tests_tricky.py +++ b/zds/notification/tests/tests_tricky.py @@ -19,7 +19,7 @@ PingSubscription, ) from zds.tutorialv2 import signals -from zds.tutorialv2.factories import ( +from zds.tutorialv2.tests.factories import ( PublishableContentFactory, PublishedContentFactory, ContentReactionFactory, diff --git a/zds/searchv2/tests/tests_models.py b/zds/searchv2/tests/tests_models.py index afdd67e1ee..53bf41e91c 100644 --- a/zds/searchv2/tests/tests_models.py +++ b/zds/searchv2/tests/tests_models.py @@ -8,7 +8,7 @@ from zds.forum.tests.factories import create_category_and_forum from zds.member.tests.factories import ProfileFactory, StaffProfileFactory from zds.searchv2.models import ESIndexManager -from zds.tutorialv2.factories import PublishableContentFactory, ContainerFactory, ExtractFactory, publish_content +from zds.tutorialv2.tests.factories import PublishableContentFactory, ContainerFactory, ExtractFactory, publish_content from zds.tutorialv2.models.database import PublishedContent, FakeChapter, PublishableContent from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents diff --git a/zds/searchv2/tests/tests_utils.py b/zds/searchv2/tests/tests_utils.py index be8ba1e6ba..4b3ce0d93a 100644 --- a/zds/searchv2/tests/tests_utils.py +++ b/zds/searchv2/tests/tests_utils.py @@ -6,7 +6,7 @@ from django.core.management import call_command from zds.member.tests.factories import ProfileFactory, StaffProfileFactory -from zds.tutorialv2.factories import PublishableContentFactory, ContainerFactory, ExtractFactory +from zds.tutorialv2.tests.factories import PublishableContentFactory, ContainerFactory, ExtractFactory from zds.tutorialv2.models.database import PublishedContent from zds.tutorialv2.publication_utils import publish_content from zds.forum.tests.factories import TopicFactory, PostFactory, Topic, Post diff --git a/zds/searchv2/tests/tests_views.py b/zds/searchv2/tests/tests_views.py index 7d3c62c3d6..c0a3d2fd89 100644 --- a/zds/searchv2/tests/tests_views.py +++ b/zds/searchv2/tests/tests_views.py @@ -14,7 +14,7 @@ from zds.member.tests.factories import ProfileFactory, StaffProfileFactory from zds.searchv2.models import ESIndexManager -from zds.tutorialv2.factories import ( +from zds.tutorialv2.tests.factories import ( PublishableContentFactory, ContainerFactory, ExtractFactory, diff --git a/zds/tutorialv2/api/tests.py b/zds/tutorialv2/api/tests.py index e86ea50ef3..3d3b897043 100644 --- a/zds/tutorialv2/api/tests.py +++ b/zds/tutorialv2/api/tests.py @@ -12,7 +12,7 @@ from rest_framework_extensions.settings import extensions_api_settings from zds.member.tests.factories import ProfileFactory, StaffProfileFactory -from zds.tutorialv2.factories import ContentReactionFactory, PublishedContentFactory +from zds.tutorialv2.tests.factories import ContentReactionFactory, PublishedContentFactory from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents from zds.tutorialv2.models.database import PublicationEvent from zds.utils.models import CommentVote diff --git a/zds/tutorialv2/factories.py b/zds/tutorialv2/tests/factories.py similarity index 100% rename from zds/tutorialv2/factories.py rename to zds/tutorialv2/tests/factories.py diff --git a/zds/tutorialv2/tests/tests_feeds.py b/zds/tutorialv2/tests/tests_feeds.py index a557cfc193..395da6a68f 100644 --- a/zds/tutorialv2/tests/tests_feeds.py +++ b/zds/tutorialv2/tests/tests_feeds.py @@ -9,7 +9,7 @@ from zds.forum.tests.factories import ForumFactory, ForumCategoryFactory, TagFactory from zds.tutorialv2.models.database import PublishedContent from zds.tutorialv2.feeds import LastTutorialsFeedRSS, LastTutorialsFeedATOM, LastArticlesFeedRSS, LastArticlesFeedATOM -from zds.tutorialv2.factories import ( +from zds.tutorialv2.tests.factories import ( PublishableContentFactory, ContainerFactory, ExtractFactory, diff --git a/zds/tutorialv2/tests/tests_front.py b/zds/tutorialv2/tests/tests_front.py index 0ad1ed3aaf..6b5160ea1f 100644 --- a/zds/tutorialv2/tests/tests_front.py +++ b/zds/tutorialv2/tests/tests_front.py @@ -14,7 +14,7 @@ from selenium.webdriver.support.ui import WebDriverWait from zds.member.tests.factories import StaffProfileFactory, ProfileFactory -from zds.tutorialv2.factories import ( +from zds.tutorialv2.tests.factories import ( PublishableContentFactory, ContainerFactory, ExtractFactory, diff --git a/zds/tutorialv2/tests/tests_lists.py b/zds/tutorialv2/tests/tests_lists.py index f610d85372..ddc1de105d 100644 --- a/zds/tutorialv2/tests/tests_lists.py +++ b/zds/tutorialv2/tests/tests_lists.py @@ -6,7 +6,7 @@ from django.urls import reverse from zds.member.tests.factories import ProfileFactory, StaffProfileFactory, UserFactory -from zds.tutorialv2.factories import ( +from zds.tutorialv2.tests.factories import ( PublishableContentFactory, ContainerFactory, ExtractFactory, diff --git a/zds/tutorialv2/tests/tests_models.py b/zds/tutorialv2/tests/tests_models.py index bccfb4c5e1..f959997ef7 100644 --- a/zds/tutorialv2/tests/tests_models.py +++ b/zds/tutorialv2/tests/tests_models.py @@ -10,7 +10,7 @@ from zds.gallery.models import UserGallery from zds.member.tests.factories import ProfileFactory, StaffProfileFactory -from zds.tutorialv2.factories import ( +from zds.tutorialv2.tests.factories import ( PublishableContentFactory, ContainerFactory, ExtractFactory, diff --git a/zds/tutorialv2/tests/tests_move.py b/zds/tutorialv2/tests/tests_move.py index 1ea2fd0545..257fc24959 100644 --- a/zds/tutorialv2/tests/tests_move.py +++ b/zds/tutorialv2/tests/tests_move.py @@ -6,7 +6,7 @@ from django.urls import reverse from zds.member.tests.factories import ProfileFactory, StaffProfileFactory -from zds.tutorialv2.factories import ( +from zds.tutorialv2.tests.factories import ( PublishableContentFactory, ContainerFactory, ExtractFactory, diff --git a/zds/tutorialv2/tests/tests_opinion_views.py b/zds/tutorialv2/tests/tests_opinion_views.py index cdacb0ddb5..86e7750685 100644 --- a/zds/tutorialv2/tests/tests_opinion_views.py +++ b/zds/tutorialv2/tests/tests_opinion_views.py @@ -5,7 +5,7 @@ from zds.gallery.tests.factories import UserGalleryFactory from zds.member.tests.factories import ProfileFactory, StaffProfileFactory -from zds.tutorialv2.factories import ( +from zds.tutorialv2.tests.factories import ( PublishableContentFactory, ExtractFactory, PublishedContentFactory, diff --git a/zds/tutorialv2/tests/tests_utils.py b/zds/tutorialv2/tests/tests_utils.py index 4700173d5f..6aa603cf5e 100644 --- a/zds/tutorialv2/tests/tests_utils.py +++ b/zds/tutorialv2/tests/tests_utils.py @@ -8,7 +8,7 @@ from django.urls import reverse from zds.member.tests.factories import ProfileFactory, StaffProfileFactory -from zds.tutorialv2.factories import ( +from zds.tutorialv2.tests.factories import ( PublishableContentFactory, ContainerFactory, ExtractFactory, diff --git a/zds/tutorialv2/tests/tests_views/tests_addcontributor.py b/zds/tutorialv2/tests/tests_views/tests_addcontributor.py index b54a64f69d..5191d9d791 100644 --- a/zds/tutorialv2/tests/tests_views/tests_addcontributor.py +++ b/zds/tutorialv2/tests/tests_views/tests_addcontributor.py @@ -6,7 +6,7 @@ from zds.member.tests.factories import ProfileFactory, StaffProfileFactory -from zds.tutorialv2.factories import PublishableContentFactory +from zds.tutorialv2.tests.factories import PublishableContentFactory from zds.tutorialv2.forms import ContributionForm from zds.tutorialv2.models.database import ContentContribution, ContentContributionRole from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents diff --git a/zds/tutorialv2/tests/tests_views/tests_addsuggestion.py b/zds/tutorialv2/tests/tests_views/tests_addsuggestion.py index acd2079af5..47be012bba 100644 --- a/zds/tutorialv2/tests/tests_views/tests_addsuggestion.py +++ b/zds/tutorialv2/tests/tests_views/tests_addsuggestion.py @@ -4,7 +4,7 @@ from django.utils.html import escape from zds.member.tests.factories import ProfileFactory, StaffProfileFactory -from zds.tutorialv2.factories import PublishableContentFactory, PublishedContentFactory +from zds.tutorialv2.tests.factories import PublishableContentFactory, PublishedContentFactory from zds.tutorialv2.models.database import ContentSuggestion from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents diff --git a/zds/tutorialv2/tests/tests_views/tests_content.py b/zds/tutorialv2/tests/tests_views/tests_content.py index beceb86cc9..6ce72d3742 100644 --- a/zds/tutorialv2/tests/tests_views/tests_content.py +++ b/zds/tutorialv2/tests/tests_views/tests_content.py @@ -28,7 +28,7 @@ NewPublicationSubscription, Notification, ) -from zds.tutorialv2.factories import ( +from zds.tutorialv2.tests.factories import ( PublishableContentFactory, ContainerFactory, ExtractFactory, diff --git a/zds/tutorialv2/tests/tests_views/tests_edgecases.py b/zds/tutorialv2/tests/tests_views/tests_edgecases.py index b3f89c885c..971988e1ff 100644 --- a/zds/tutorialv2/tests/tests_views/tests_edgecases.py +++ b/zds/tutorialv2/tests/tests_views/tests_edgecases.py @@ -3,7 +3,7 @@ from django.urls import reverse from zds.member.tests.factories import ProfileFactory, StaffProfileFactory -from zds.tutorialv2.factories import PublishedContentFactory +from zds.tutorialv2.tests.factories import PublishedContentFactory from zds.tutorialv2.models.database import PublishableContent from zds.tutorialv2.tests import override_for_contents, TutorialTestMixin from zds.utils.factories import LicenceFactory, SubCategoryFactory diff --git a/zds/tutorialv2/tests/tests_views/tests_editcontentlicense.py b/zds/tutorialv2/tests/tests_views/tests_editcontentlicense.py index f04caf283f..90579e6be6 100644 --- a/zds/tutorialv2/tests/tests_views/tests_editcontentlicense.py +++ b/zds/tutorialv2/tests/tests_views/tests_editcontentlicense.py @@ -7,7 +7,7 @@ from zds.tutorialv2.forms import EditContentLicenseForm from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents from zds.member.tests.factories import ProfileFactory, StaffProfileFactory -from zds.tutorialv2.factories import PublishableContentFactory +from zds.tutorialv2.tests.factories import PublishableContentFactory from zds.utils.factories import LicenceFactory diff --git a/zds/tutorialv2/tests/tests_views/tests_editcontenttags.py b/zds/tutorialv2/tests/tests_views/tests_editcontenttags.py index 2f3d4d4f3b..c58bc11598 100644 --- a/zds/tutorialv2/tests/tests_views/tests_editcontenttags.py +++ b/zds/tutorialv2/tests/tests_views/tests_editcontenttags.py @@ -6,7 +6,7 @@ from zds.tutorialv2.views.editorialization import EditContentTags from zds.tutorialv2.forms import EditContentTagsForm from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents -from zds.tutorialv2.factories import PublishableContentFactory +from zds.tutorialv2.tests.factories import PublishableContentFactory from zds.member.tests.factories import ProfileFactory, StaffProfileFactory from zds.forum.tests.factories import TagFactory from zds.utils.forms import TagValidator diff --git a/zds/tutorialv2/tests/tests_views/tests_published.py b/zds/tutorialv2/tests/tests_views/tests_published.py index bba0a6b3e6..5e904d1ea5 100644 --- a/zds/tutorialv2/tests/tests_views/tests_published.py +++ b/zds/tutorialv2/tests/tests_views/tests_published.py @@ -13,7 +13,7 @@ from zds.member.tests.factories import ProfileFactory, StaffProfileFactory, UserFactory from zds.mp.models import PrivateTopic, is_privatetopic_unread from zds.notification.models import ContentReactionAnswerSubscription, Notification -from zds.tutorialv2.factories import ( +from zds.tutorialv2.tests.factories import ( PublishableContentFactory, ContainerFactory, ExtractFactory, diff --git a/zds/tutorialv2/tests/tests_views/tests_removecontributor.py b/zds/tutorialv2/tests/tests_views/tests_removecontributor.py index a4eb2206f7..66f9065517 100644 --- a/zds/tutorialv2/tests/tests_views/tests_removecontributor.py +++ b/zds/tutorialv2/tests/tests_views/tests_removecontributor.py @@ -4,7 +4,7 @@ from django.utils.html import escape from zds.member.tests.factories import ProfileFactory, StaffProfileFactory -from zds.tutorialv2.factories import PublishableContentFactory +from zds.tutorialv2.tests.factories import PublishableContentFactory from zds.tutorialv2.models.database import ContentContribution, ContentContributionRole from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents diff --git a/zds/tutorialv2/tests/tests_views/tests_stats.py b/zds/tutorialv2/tests/tests_views/tests_stats.py index 89a341a493..ef97308278 100644 --- a/zds/tutorialv2/tests/tests_views/tests_stats.py +++ b/zds/tutorialv2/tests/tests_views/tests_stats.py @@ -10,7 +10,7 @@ from zds.gallery.tests.factories import UserGalleryFactory from zds.member.tests.factories import ProfileFactory, StaffProfileFactory -from zds.tutorialv2.factories import PublishableContentFactory, ContainerFactory, ExtractFactory +from zds.tutorialv2.tests.factories import PublishableContentFactory, ContainerFactory, ExtractFactory from zds.tutorialv2.models.database import Validation, PublishedContent from zds.tutorialv2.tests import TutorialTestMixin from zds.utils.factories import LicenceFactory diff --git a/zds/utils/api/tests.py b/zds/utils/api/tests.py index 94cca92dac..1669a96ae8 100644 --- a/zds/utils/api/tests.py +++ b/zds/utils/api/tests.py @@ -9,7 +9,7 @@ from zds.api.pagination import REST_PAGE_SIZE, REST_MAX_PAGE_SIZE, REST_PAGE_SIZE_QUERY_PARAM from rest_framework_extensions.settings import extensions_api_settings from django.core.cache import caches -from zds.tutorialv2.factories import PublishableContentFactory +from zds.tutorialv2.tests.factories import PublishableContentFactory from zds.tutorialv2.publication_utils import publish_content diff --git a/zds/utils/management/commands/load_fixtures.py b/zds/utils/management/commands/load_fixtures.py index 7741d9bd0a..a5b45ab859 100644 --- a/zds/utils/management/commands/load_fixtures.py +++ b/zds/utils/management/commands/load_fixtures.py @@ -22,7 +22,7 @@ from zds.utils import old_slugify from django.conf import settings from django.db import transaction, IntegrityError -from zds.tutorialv2.factories import ( +from zds.tutorialv2.tests.factories import ( PublishableContentFactory, ContainerFactory, ExtractFactory, diff --git a/zds/utils/tests/test_misc.py b/zds/utils/tests/test_misc.py index e8b0f35712..cb2b22c318 100644 --- a/zds/utils/tests/test_misc.py +++ b/zds/utils/tests/test_misc.py @@ -1,7 +1,7 @@ import datetime from django.test import TestCase from zds.member.tests.factories import ProfileFactory, StaffProfileFactory -from zds.tutorialv2.factories import PublishedContentFactory +from zds.tutorialv2.tests.factories import PublishedContentFactory from zds.utils.misc import contains_utf8mb4 from zds.utils.models import Alert from zds.utils.context_processor import get_header_notifications diff --git a/zds/utils/tests/tests_comments.py b/zds/utils/tests/tests_comments.py index b0ec42d27b..1a2861ca98 100644 --- a/zds/utils/tests/tests_comments.py +++ b/zds/utils/tests/tests_comments.py @@ -8,7 +8,7 @@ from zds.forum.tests.factories import PostFactory, create_category_and_forum, create_topic_in_forum from zds.member.tests.factories import ProfileFactory, StaffProfileFactory -from zds.tutorialv2.factories import PublishedContentFactory +from zds.tutorialv2.tests.factories import PublishedContentFactory from zds.tutorialv2.models import CONTENT_TYPES from zds.tutorialv2.models.database import PublishableContent from zds.tutorialv2.tests import TutorialTestMixin diff --git a/zds/utils/tests/tests_interventions.py b/zds/utils/tests/tests_interventions.py index 932e0ddf8d..0d5a7bc5b0 100644 --- a/zds/utils/tests/tests_interventions.py +++ b/zds/utils/tests/tests_interventions.py @@ -6,7 +6,7 @@ from django.test import TestCase from zds.tutorialv2.models.database import Validation -from zds.tutorialv2.factories import PublishableContentFactory +from zds.tutorialv2.tests.factories import PublishableContentFactory from zds.member.tests.factories import ProfileFactory, StaffProfileFactory from zds.utils.factories import SubCategoryFactory, LicenceFactory from zds.utils.mps import send_message_mp, send_mp diff --git a/zds/utils/tests/tests_topbar_tags.py b/zds/utils/tests/tests_topbar_tags.py index bc7c387c11..cc59a78b7e 100644 --- a/zds/utils/tests/tests_topbar_tags.py +++ b/zds/utils/tests/tests_topbar_tags.py @@ -4,7 +4,7 @@ from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory, TopicFactory from zds.member.tests.factories import ProfileFactory, StaffProfileFactory -from zds.tutorialv2.factories import PublishedContentFactory, PublishableContentFactory, SubCategoryFactory +from zds.tutorialv2.tests.factories import PublishedContentFactory, PublishableContentFactory, SubCategoryFactory from zds.tutorialv2.publication_utils import publish_content from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents from zds.utils.factories import CategoryFactory as ContentCategoryFactory From b373adc8863373e34af3b72c4098ca243eba4501 Mon Sep 17 00:00:00 2001 From: Arnaud-D <35631001+Arnaud-D@users.noreply.github.com> Date: Sun, 20 Feb 2022 08:11:17 +0100 Subject: [PATCH 47/53] =?UTF-8?q?D=C3=A9place=20les=20factories=20de=20uti?= =?UTF-8?q?ls=20dans=20le=20dossier=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/notification/tests/tests_basics.py | 2 +- zds/notification/tests/tests_tricky.py | 2 +- zds/tutorialv2/tests/factories.py | 2 +- zds/tutorialv2/tests/tests_feeds.py | 2 +- zds/tutorialv2/tests/tests_front.py | 2 +- zds/tutorialv2/tests/tests_lists.py | 2 +- zds/tutorialv2/tests/tests_models.py | 2 +- zds/tutorialv2/tests/tests_move.py | 2 +- zds/tutorialv2/tests/tests_opinion_views.py | 2 +- zds/tutorialv2/tests/tests_utils.py | 2 +- zds/tutorialv2/tests/tests_views/tests_content.py | 2 +- zds/tutorialv2/tests/tests_views/tests_edgecases.py | 2 +- zds/tutorialv2/tests/tests_views/tests_editcontentlicense.py | 2 +- zds/tutorialv2/tests/tests_views/tests_published.py | 2 +- zds/tutorialv2/tests/tests_views/tests_stats.py | 2 +- zds/utils/{ => tests}/factories.py | 0 zds/utils/tests/tests_interventions.py | 2 +- zds/utils/tests/tests_topbar_tags.py | 2 +- 18 files changed, 17 insertions(+), 17 deletions(-) rename zds/utils/{ => tests}/factories.py (100%) diff --git a/zds/notification/tests/tests_basics.py b/zds/notification/tests/tests_basics.py index 4886744093..0b7259ef5d 100644 --- a/zds/notification/tests/tests_basics.py +++ b/zds/notification/tests/tests_basics.py @@ -30,7 +30,7 @@ from zds.tutorialv2.models.database import ContentReaction, PublishableContent from zds.tutorialv2.publication_utils import publish_content from zds.utils import old_slugify -from zds.utils.factories import SubCategoryFactory, LicenceFactory +from zds.utils.tests.factories import SubCategoryFactory, LicenceFactory from zds.utils.mps import send_mp, send_message_mp diff --git a/zds/notification/tests/tests_tricky.py b/zds/notification/tests/tests_tricky.py index dbd41a3418..22b9cf37b3 100644 --- a/zds/notification/tests/tests_tricky.py +++ b/zds/notification/tests/tests_tricky.py @@ -26,7 +26,7 @@ ) from zds.tutorialv2.publication_utils import publish_content, notify_update from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents -from zds.utils.factories import SubCategoryFactory, LicenceFactory +from zds.utils.tests.factories import SubCategoryFactory, LicenceFactory from zds.utils.mps import send_mp, send_message_mp from zds.utils.header_notifications import get_header_notifications diff --git a/zds/tutorialv2/tests/factories.py b/zds/tutorialv2/tests/factories.py index e4bbbb54b4..bbb9270f89 100644 --- a/zds/tutorialv2/tests/factories.py +++ b/zds/tutorialv2/tests/factories.py @@ -4,7 +4,7 @@ from zds.forum.tests.factories import PostFactory, TopicFactory from zds.gallery.tests.factories import GalleryFactory, UserGalleryFactory -from zds.utils.factories import LicenceFactory, SubCategoryFactory +from zds.utils.tests.factories import LicenceFactory, SubCategoryFactory from zds.utils.models import Licence from zds.tutorialv2.models.database import PublishableContent, Validation, ContentReaction from zds.tutorialv2.models.versioned import Container, Extract diff --git a/zds/tutorialv2/tests/tests_feeds.py b/zds/tutorialv2/tests/tests_feeds.py index 395da6a68f..c27d3ed69b 100644 --- a/zds/tutorialv2/tests/tests_feeds.py +++ b/zds/tutorialv2/tests/tests_feeds.py @@ -16,7 +16,7 @@ ) from zds.tutorialv2.publication_utils import publish_content from zds.tutorialv2.tests import TutorialTestMixin -from zds.utils.factories import SubCategoryFactory, LicenceFactory +from zds.utils.tests.factories import SubCategoryFactory, LicenceFactory from copy import deepcopy overridden_zds_app = deepcopy(settings.ZDS_APP) diff --git a/zds/tutorialv2/tests/tests_front.py b/zds/tutorialv2/tests/tests_front.py index 6b5160ea1f..0063c20a59 100644 --- a/zds/tutorialv2/tests/tests_front.py +++ b/zds/tutorialv2/tests/tests_front.py @@ -21,7 +21,7 @@ ) from zds.tutorialv2.models.database import PublishedContent, PublishableContent from zds.tutorialv2.tests import TutorialTestMixin, TutorialFrontMixin -from zds.utils.factories import CategoryFactory, SubCategoryFactory, LicenceFactory +from zds.utils.tests.factories import CategoryFactory, SubCategoryFactory, LicenceFactory overridden_zds_app = deepcopy(settings.ZDS_APP) overridden_zds_app["content"]["repo_private_path"] = settings.BASE_DIR / "contents-private-test" diff --git a/zds/tutorialv2/tests/tests_lists.py b/zds/tutorialv2/tests/tests_lists.py index ddc1de105d..fbaa04603e 100644 --- a/zds/tutorialv2/tests/tests_lists.py +++ b/zds/tutorialv2/tests/tests_lists.py @@ -17,7 +17,7 @@ from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents from zds.gallery.tests.factories import UserGalleryFactory from zds.forum.tests.factories import ForumFactory, ForumCategoryFactory -from zds.utils.factories import CategoryFactory, SubCategoryFactory, LicenceFactory +from zds.utils.tests.factories import CategoryFactory, SubCategoryFactory, LicenceFactory @override_for_contents() diff --git a/zds/tutorialv2/tests/tests_models.py b/zds/tutorialv2/tests/tests_models.py index f959997ef7..465a670516 100644 --- a/zds/tutorialv2/tests/tests_models.py +++ b/zds/tutorialv2/tests/tests_models.py @@ -20,7 +20,7 @@ from zds.tutorialv2.models.database import PublishableContent, PublishedContent from zds.tutorialv2.publication_utils import publish_content from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents -from zds.utils.factories import SubCategoryFactory, LicenceFactory +from zds.utils.tests.factories import SubCategoryFactory, LicenceFactory from zds.utils.models import Tag from django.template.defaultfilters import date diff --git a/zds/tutorialv2/tests/tests_move.py b/zds/tutorialv2/tests/tests_move.py index 257fc24959..753a1f3fb5 100644 --- a/zds/tutorialv2/tests/tests_move.py +++ b/zds/tutorialv2/tests/tests_move.py @@ -16,7 +16,7 @@ from zds.forum.tests.factories import ForumFactory, ForumCategoryFactory from zds.tutorialv2.publication_utils import publish_content from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents -from zds.utils.factories import SubCategoryFactory, LicenceFactory +from zds.utils.tests.factories import SubCategoryFactory, LicenceFactory @override_for_contents() diff --git a/zds/tutorialv2/tests/tests_opinion_views.py b/zds/tutorialv2/tests/tests_opinion_views.py index 86e7750685..e01b0dbb39 100644 --- a/zds/tutorialv2/tests/tests_opinion_views.py +++ b/zds/tutorialv2/tests/tests_opinion_views.py @@ -12,7 +12,7 @@ ) from zds.tutorialv2.models.database import PublishableContent, PublishedContent, PickListOperation from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents -from zds.utils.factories import SubCategoryFactory, LicenceFactory +from zds.utils.tests.factories import SubCategoryFactory, LicenceFactory from zds.utils.models import Alert diff --git a/zds/tutorialv2/tests/tests_utils.py b/zds/tutorialv2/tests/tests_utils.py index 6aa603cf5e..e6f8961d39 100644 --- a/zds/tutorialv2/tests/tests_utils.py +++ b/zds/tutorialv2/tests/tests_utils.py @@ -34,7 +34,7 @@ from zds.tutorialv2.publication_utils import Publicator, PublicatorRegistry from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents from zds import json_handler -from zds.utils.factories import LicenceFactory +from zds.utils.tests.factories import LicenceFactory from zds.utils.models import Alert from zds.utils.header_notifications import get_header_notifications diff --git a/zds/tutorialv2/tests/tests_views/tests_content.py b/zds/tutorialv2/tests/tests_views/tests_content.py index 6ce72d3742..ebc436bbc3 100644 --- a/zds/tutorialv2/tests/tests_views/tests_content.py +++ b/zds/tutorialv2/tests/tests_views/tests_content.py @@ -52,7 +52,7 @@ ) from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents from zds.utils.models import HelpWriting, Alert, Tag, Hat -from zds.utils.factories import HelpWritingFactory, CategoryFactory, SubCategoryFactory, LicenceFactory +from zds.utils.tests.factories import HelpWritingFactory, CategoryFactory, SubCategoryFactory, LicenceFactory from zds.utils.header_notifications import get_header_notifications from zds import json_handler diff --git a/zds/tutorialv2/tests/tests_views/tests_edgecases.py b/zds/tutorialv2/tests/tests_views/tests_edgecases.py index 971988e1ff..2db1ab7ee4 100644 --- a/zds/tutorialv2/tests/tests_views/tests_edgecases.py +++ b/zds/tutorialv2/tests/tests_views/tests_edgecases.py @@ -6,7 +6,7 @@ from zds.tutorialv2.tests.factories import PublishedContentFactory from zds.tutorialv2.models.database import PublishableContent from zds.tutorialv2.tests import override_for_contents, TutorialTestMixin -from zds.utils.factories import LicenceFactory, SubCategoryFactory +from zds.utils.tests.factories import LicenceFactory, SubCategoryFactory @override_for_contents() diff --git a/zds/tutorialv2/tests/tests_views/tests_editcontentlicense.py b/zds/tutorialv2/tests/tests_views/tests_editcontentlicense.py index 90579e6be6..015a4336a5 100644 --- a/zds/tutorialv2/tests/tests_views/tests_editcontentlicense.py +++ b/zds/tutorialv2/tests/tests_views/tests_editcontentlicense.py @@ -8,7 +8,7 @@ from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents from zds.member.tests.factories import ProfileFactory, StaffProfileFactory from zds.tutorialv2.tests.factories import PublishableContentFactory -from zds.utils.factories import LicenceFactory +from zds.utils.tests.factories import LicenceFactory @override_for_contents() diff --git a/zds/tutorialv2/tests/tests_views/tests_published.py b/zds/tutorialv2/tests/tests_views/tests_published.py index 5e904d1ea5..13fbe4821b 100644 --- a/zds/tutorialv2/tests/tests_views/tests_published.py +++ b/zds/tutorialv2/tests/tests_views/tests_published.py @@ -29,7 +29,7 @@ from zds.tutorialv2.publication_utils import publish_content from zds.tutorialv2.tests import TutorialTestMixin from zds.utils.models import Alert, Tag, Hat -from zds.utils.factories import CategoryFactory, SubCategoryFactory, LicenceFactory +from zds.utils.tests.factories import CategoryFactory, SubCategoryFactory, LicenceFactory from zds.utils.header_notifications import get_header_notifications from copy import deepcopy from zds import json_handler diff --git a/zds/tutorialv2/tests/tests_views/tests_stats.py b/zds/tutorialv2/tests/tests_views/tests_stats.py index ef97308278..82513cb984 100644 --- a/zds/tutorialv2/tests/tests_views/tests_stats.py +++ b/zds/tutorialv2/tests/tests_views/tests_stats.py @@ -13,7 +13,7 @@ from zds.tutorialv2.tests.factories import PublishableContentFactory, ContainerFactory, ExtractFactory from zds.tutorialv2.models.database import Validation, PublishedContent from zds.tutorialv2.tests import TutorialTestMixin -from zds.utils.factories import LicenceFactory +from zds.utils.tests.factories import LicenceFactory overridden_zds_app = deepcopy(settings.ZDS_APP) overridden_zds_app["content"]["repo_private_path"] = settings.BASE_DIR / "contents-private-test" diff --git a/zds/utils/factories.py b/zds/utils/tests/factories.py similarity index 100% rename from zds/utils/factories.py rename to zds/utils/tests/factories.py diff --git a/zds/utils/tests/tests_interventions.py b/zds/utils/tests/tests_interventions.py index 0d5a7bc5b0..efffe3b329 100644 --- a/zds/utils/tests/tests_interventions.py +++ b/zds/utils/tests/tests_interventions.py @@ -8,7 +8,7 @@ from zds.tutorialv2.models.database import Validation from zds.tutorialv2.tests.factories import PublishableContentFactory from zds.member.tests.factories import ProfileFactory, StaffProfileFactory -from zds.utils.factories import SubCategoryFactory, LicenceFactory +from zds.utils.tests.factories import SubCategoryFactory, LicenceFactory from zds.utils.mps import send_message_mp, send_mp diff --git a/zds/utils/tests/tests_topbar_tags.py b/zds/utils/tests/tests_topbar_tags.py index cc59a78b7e..413a157484 100644 --- a/zds/utils/tests/tests_topbar_tags.py +++ b/zds/utils/tests/tests_topbar_tags.py @@ -7,7 +7,7 @@ from zds.tutorialv2.tests.factories import PublishedContentFactory, PublishableContentFactory, SubCategoryFactory from zds.tutorialv2.publication_utils import publish_content from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents -from zds.utils.factories import CategoryFactory as ContentCategoryFactory +from zds.utils.tests.factories import CategoryFactory as ContentCategoryFactory from zds.utils.templatetags.topbar import topbar_forum_categories, topbar_publication_categories From 46d3ff19b0bedf5015d01a7974458a7c18eb8509 Mon Sep 17 00:00:00 2001 From: Arnaud-D <35631001+Arnaud-D@users.noreply.github.com> Date: Sun, 20 Feb 2022 13:13:54 +0100 Subject: [PATCH 48/53] Fix fixtures --- fixtures/advanced/aide_tuto_media.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/fixtures/advanced/aide_tuto_media.yaml b/fixtures/advanced/aide_tuto_media.yaml index cc24431f54..3ef53cb61a 100755 --- a/fixtures/advanced/aide_tuto_media.yaml +++ b/fixtures/advanced/aide_tuto_media.yaml @@ -1,27 +1,27 @@ -- factory: zds.utils.factories.HelpWritingFactory +- factory: zds.utils.tests.factories.HelpWritingFactory fields: title: "Rédacteur" tablelabel: "Besoin d'aide pour la rédaction" slug: "redacteur" fixture_image_path: "aide_redacteur.png" -- factory: zds.utils.factories.HelpWritingFactory +- factory: zds.utils.tests.factories.HelpWritingFactory fields: title: "Correcteur" tablelabel: "Besoin d'aide pour la correction" slug: "correcteur" fixture_image_path: "aide_correcteur.png" -- factory: zds.utils.factories.HelpWritingFactory +- factory: zds.utils.tests.factories.HelpWritingFactory fields: title: "Illustrateur" tablelabel: "Besoin d'aide pour l'illustration" slug: "illustrateur" fixture_image_path: "aide_illustrateur.png" -- factory: zds.utils.factories.HelpWritingFactory +- factory: zds.utils.tests.factories.HelpWritingFactory fields: title: "Repreneur" tablelabel: "Cherche un repreneur" slug: "repreneur" - fixture_image_path: "aide_repreneur.png" \ No newline at end of file + fixture_image_path: "aide_repreneur.png" From b3574829578cdfe2bd90f8068f57934f5d7d6aef Mon Sep 17 00:00:00 2001 From: Arnaud-D <35631001+Arnaud-D@users.noreply.github.com> Date: Thu, 3 Mar 2022 22:09:52 +0100 Subject: [PATCH 49/53] =?UTF-8?q?Remplace=20notre=20impl=C3=A9mentation=20?= =?UTF-8?q?de=20LoginRequiredMixin=20par=20celle=20de=20Django=20(#6252)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zds/member/decorator.py | 15 +-------------- zds/member/views.py | 3 ++- zds/tutorialv2/views/alerts.py | 2 +- zds/tutorialv2/views/archives.py | 3 ++- zds/tutorialv2/views/comments.py | 3 ++- zds/tutorialv2/views/containers_extracts.py | 3 ++- zds/tutorialv2/views/contents.py | 5 +++-- zds/tutorialv2/views/validations_contents.py | 3 ++- zds/tutorialv2/views/validations_opinions.py | 3 ++- 9 files changed, 17 insertions(+), 23 deletions(-) diff --git a/zds/member/decorator.py b/zds/member/decorator.py index 8062e9d4ef..3d6d7fc40f 100644 --- a/zds/member/decorator.py +++ b/zds/member/decorator.py @@ -1,4 +1,4 @@ -from django.contrib.auth.decorators import login_required +from django.contrib.auth.mixins import LoginRequiredMixin from django.core.exceptions import PermissionDenied from django.utils.decorators import method_decorator @@ -46,19 +46,6 @@ def dispatch(self, *args, **kwargs): return super().dispatch(*args, **kwargs) -class LoginRequiredMixin: - """ - Represent the basic code that a Generic Class Based View has to use when - the required action needs the user to be logged in. - If the user is not logged in, the user is redirected to the connection form and the former action - is not executed. - """ - - @method_decorator(login_required) - def dispatch(self, *args, **kwargs): - return super().dispatch(*args, **kwargs) - - class LoggedWithReadWriteHability(LoginRequiredMixin): """ Represent the basic code that a Generic Class View has to use when a logged in user with diff --git a/zds/member/views.py b/zds/member/views.py index 8f142e4dd0..ac0cd24f26 100644 --- a/zds/member/views.py +++ b/zds/member/views.py @@ -9,6 +9,7 @@ from django.contrib import messages from django.contrib.auth import authenticate, login, logout from django.contrib.auth.decorators import login_required, permission_required +from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.models import User, Group from django.template.context_processors import csrf from django.core.exceptions import PermissionDenied @@ -40,7 +41,7 @@ DeleteBanSanction, TokenGenerator, ) -from zds.member.decorator import can_write_and_read_now, LoginRequiredMixin, PermissionRequiredMixin +from zds.member.decorator import can_write_and_read_now, PermissionRequiredMixin from zds.member.forms import ( LoginForm, MiniProfileForm, diff --git a/zds/tutorialv2/views/alerts.py b/zds/tutorialv2/views/alerts.py index 0c1d4f0921..0b7ce8a859 100644 --- a/zds/tutorialv2/views/alerts.py +++ b/zds/tutorialv2/views/alerts.py @@ -2,6 +2,7 @@ from django.contrib import messages from django.core.exceptions import PermissionDenied +from django.contrib.auth.mixins import LoginRequiredMixin from django.db import transaction from django.http import Http404 from django.shortcuts import get_object_or_404, redirect @@ -10,7 +11,6 @@ from django.utils.translation import gettext_lazy as _ from django.views.generic import FormView -from zds.member.decorator import LoginRequiredMixin from zds.tutorialv2.models import TYPE_CHOICES_DICT from zds.tutorialv2.models.database import PublishableContent from zds.utils.models import Alert diff --git a/zds/tutorialv2/views/archives.py b/zds/tutorialv2/views/archives.py index 91ded70442..23a2a4cf35 100644 --- a/zds/tutorialv2/views/archives.py +++ b/zds/tutorialv2/views/archives.py @@ -9,6 +9,7 @@ from PIL import Image as ImagePIL from django.conf import settings from django.contrib import messages +from django.contrib.auth.mixins import LoginRequiredMixin from django.urls import reverse from django.utils.translation import gettext_lazy as _ from django.views.generic import FormView @@ -16,7 +17,7 @@ from zds import json_handler from zds.gallery.models import Image, Gallery -from zds.member.decorator import LoginRequiredMixin, LoggedWithReadWriteHability +from zds.member.decorator import LoggedWithReadWriteHability from zds.tutorialv2.forms import ImportContentForm, ImportNewContentForm from zds.tutorialv2.mixins import SingleContentDownloadViewMixin, SingleContentFormViewMixin from zds.tutorialv2.models.database import PublishableContent diff --git a/zds/tutorialv2/views/comments.py b/zds/tutorialv2/views/comments.py index 9849d8c7db..3fd53fa59c 100644 --- a/zds/tutorialv2/views/comments.py +++ b/zds/tutorialv2/views/comments.py @@ -2,6 +2,7 @@ from django.conf import settings from django.contrib import messages +from django.contrib.auth.mixins import LoginRequiredMixin from django.core.exceptions import PermissionDenied from django.db import transaction from django.http import Http404, StreamingHttpResponse, HttpResponse @@ -13,7 +14,7 @@ from django.views.generic import FormView from zds import json_handler -from zds.member.decorator import LoggedWithReadWriteHability, LoginRequiredMixin, PermissionRequiredMixin +from zds.member.decorator import LoggedWithReadWriteHability, PermissionRequiredMixin from zds.member.views import get_client_ip from zds.notification.models import ContentReactionAnswerSubscription from zds.tutorialv2.forms import NoteForm, NoteEditForm diff --git a/zds/tutorialv2/views/containers_extracts.py b/zds/tutorialv2/views/containers_extracts.py index 580a5426e3..ef56962a2d 100644 --- a/zds/tutorialv2/views/containers_extracts.py +++ b/zds/tutorialv2/views/containers_extracts.py @@ -2,6 +2,7 @@ from datetime import datetime from django.contrib import messages +from django.contrib.auth.mixins import LoginRequiredMixin from django.core.exceptions import PermissionDenied from django.http import JsonResponse, Http404 from django.shortcuts import redirect @@ -9,7 +10,7 @@ from django.utils.translation import gettext_lazy as _ from django.views.generic import DeleteView, FormView -from zds.member.decorator import LoggedWithReadWriteHability, LoginRequiredMixin +from zds.member.decorator import LoggedWithReadWriteHability from zds.tutorialv2.forms import ContainerForm, WarnTypoForm, ExtractForm, MoveElementForm from zds.tutorialv2.mixins import ( SingleContentFormViewMixin, diff --git a/zds/tutorialv2/views/contents.py b/zds/tutorialv2/views/contents.py index 01d6c7963a..1483f00bdb 100644 --- a/zds/tutorialv2/views/contents.py +++ b/zds/tutorialv2/views/contents.py @@ -3,6 +3,7 @@ from django.conf import settings from django.contrib import messages +from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.models import User from django.db import transaction from django.shortcuts import redirect, get_object_or_404 @@ -13,8 +14,8 @@ from django.views.generic import DeleteView from zds.gallery.mixins import ImageCreateMixin, NotAnImage -from zds.gallery.models import Gallery, Image -from zds.member.decorator import LoggedWithReadWriteHability, LoginRequiredMixin +from zds.gallery.models import Gallery +from zds.member.decorator import LoggedWithReadWriteHability from zds.member.models import Profile from zds.tutorialv2.forms import ( ContentForm, diff --git a/zds/tutorialv2/views/validations_contents.py b/zds/tutorialv2/views/validations_contents.py index 73a69c8d07..8c44d95e31 100644 --- a/zds/tutorialv2/views/validations_contents.py +++ b/zds/tutorialv2/views/validations_contents.py @@ -4,6 +4,7 @@ from django.conf import settings from django.contrib import messages from django.contrib.auth.models import User +from django.contrib.auth.mixins import LoginRequiredMixin from django.core.exceptions import PermissionDenied from django.urls import reverse from django.db.models import Q @@ -13,7 +14,7 @@ from django.utils.translation import gettext_lazy as _ from django.views.generic import ListView, FormView -from zds.member.decorator import LoginRequiredMixin, PermissionRequiredMixin, LoggedWithReadWriteHability +from zds.member.decorator import PermissionRequiredMixin, LoggedWithReadWriteHability from zds.mp.models import mark_read from zds.tutorialv2.forms import ( AskValidationForm, diff --git a/zds/tutorialv2/views/validations_opinions.py b/zds/tutorialv2/views/validations_opinions.py index 879d27ff41..e46f628822 100644 --- a/zds/tutorialv2/views/validations_opinions.py +++ b/zds/tutorialv2/views/validations_opinions.py @@ -3,6 +3,7 @@ from django.conf import settings from django.contrib import messages +from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.models import User from django.core.exceptions import PermissionDenied from django.db.models import F @@ -14,7 +15,7 @@ from django.views.generic import FormView, ListView from zds.gallery.models import Gallery -from zds.member.decorator import LoggedWithReadWriteHability, LoginRequiredMixin, PermissionRequiredMixin +from zds.member.decorator import LoggedWithReadWriteHability, PermissionRequiredMixin from zds.tutorialv2.forms import ( PublicationForm, RevokeValidationForm, From 44624b90ce9fe10dc8013f274b94d60589662573 Mon Sep 17 00:00:00 2001 From: "Ph. SW" Date: Sat, 5 Mar 2022 11:55:50 +0100 Subject: [PATCH 50/53] =?UTF-8?q?Place=20toujours=20LoginRequiredMixin=20l?= =?UTF-8?q?e=20plus=20=C3=A0=20gauche=20de=20la=20cha=C3=AEne=20d'h=C3=A9r?= =?UTF-8?q?itage=20(#6253)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Comme recommandé par la documentation : https://docs.djangoproject.com/fr/3.2/topics/auth/default/#django.contrib.auth.mixins.LoginRequiredMixin --- zds/tutorialv2/views/alerts.py | 4 ++-- zds/tutorialv2/views/comments.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/zds/tutorialv2/views/alerts.py b/zds/tutorialv2/views/alerts.py index 0b7ce8a859..d120707b1e 100644 --- a/zds/tutorialv2/views/alerts.py +++ b/zds/tutorialv2/views/alerts.py @@ -16,7 +16,7 @@ from zds.utils.models import Alert -class SendContentAlert(FormView, LoginRequiredMixin): +class SendContentAlert(LoginRequiredMixin, FormView): http_method_names = ["post"] @method_decorator(transaction.atomic) @@ -48,7 +48,7 @@ def post(self, request, *args, **kwargs): return redirect(content.get_absolute_url_online()) -class SolveContentAlert(FormView, LoginRequiredMixin): +class SolveContentAlert(LoginRequiredMixin, FormView): @method_decorator(transaction.atomic) def dispatch(self, *args, **kwargs): return super().dispatch(*args, **kwargs) diff --git a/zds/tutorialv2/views/comments.py b/zds/tutorialv2/views/comments.py index 3fd53fa59c..9cfc1d446f 100644 --- a/zds/tutorialv2/views/comments.py +++ b/zds/tutorialv2/views/comments.py @@ -234,7 +234,7 @@ def form_valid(self, form): return super().form_valid(form) -class HideReaction(FormView, LoginRequiredMixin): +class HideReaction(LoginRequiredMixin, FormView): http_method_names = ["post"] @method_decorator(transaction.atomic) @@ -281,7 +281,7 @@ def post(self, request, *args, **kwargs): raise Http404("Aucune réaction trouvée.") -class SendNoteAlert(FormView, LoginRequiredMixin): +class SendNoteAlert(LoginRequiredMixin, FormView): http_method_names = ["post"] @method_decorator(transaction.atomic) @@ -312,7 +312,7 @@ def post(self, request, *args, **kwargs): return redirect(reaction.get_absolute_url()) -class SolveNoteAlert(FormView, LoginRequiredMixin): +class SolveNoteAlert(LoginRequiredMixin, FormView): @method_decorator(transaction.atomic) def dispatch(self, *args, **kwargs): return super().dispatch(*args, **kwargs) From dcfb6145335d5b2429ae45d18d9419a38989ba69 Mon Sep 17 00:00:00 2001 From: Arnaud-D <35631001+Arnaud-D@users.noreply.github.com> Date: Sun, 6 Mar 2022 22:44:05 +0100 Subject: [PATCH 51/53] =?UTF-8?q?Refactorisation=20de=20quelques=20tr?= =?UTF-8?q?=C3=A8s=20gros=20fichiers=20(#6251)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Scinde des gros fichiers du module member * Fix imports * Remets en place quelques tests injustement éliminés --- zds/member/tests/tests_views.py | 1820 ---------------- zds/member/tests/views/__init__.py | 0 zds/member/tests/views/tests_admin.py | 117 + .../tests/views/tests_emailproviders.py | 153 ++ zds/member/tests/views/tests_hats.py | 312 +++ zds/member/tests/views/tests_login.py | 80 + zds/member/tests/views/tests_moderation.py | 518 +++++ .../tests/views/tests_password_recovery.py | 52 + zds/member/tests/views/tests_profile.py | 322 +++ zds/member/tests/views/tests_register.py | 392 ++++ zds/member/tests/views/tests_reports.py | 53 + zds/member/urls.py | 53 +- zds/member/views.py | 1542 ------------- zds/member/views/__init__.py | 29 + zds/member/views/admin.py | 89 + zds/member/views/emailproviders.py | 110 + zds/member/views/hats.py | 212 ++ zds/member/views/login.py | 117 + zds/member/views/moderation.py | 181 ++ zds/member/views/password_recovery.py | 91 + zds/member/views/profile.py | 441 ++++ zds/member/views/register.py | 331 +++ zds/member/views/reports.py | 50 + .../tests/tests_views/tests_content.py | 1935 +---------------- .../tests/tests_views/tests_published.py | 87 +- zds/urls.py | 2 +- 26 files changed, 3748 insertions(+), 5341 deletions(-) delete mode 100644 zds/member/tests/tests_views.py create mode 100644 zds/member/tests/views/__init__.py create mode 100644 zds/member/tests/views/tests_admin.py create mode 100644 zds/member/tests/views/tests_emailproviders.py create mode 100644 zds/member/tests/views/tests_hats.py create mode 100644 zds/member/tests/views/tests_login.py create mode 100644 zds/member/tests/views/tests_moderation.py create mode 100644 zds/member/tests/views/tests_password_recovery.py create mode 100644 zds/member/tests/views/tests_profile.py create mode 100644 zds/member/tests/views/tests_register.py create mode 100644 zds/member/tests/views/tests_reports.py delete mode 100644 zds/member/views.py create mode 100644 zds/member/views/__init__.py create mode 100644 zds/member/views/admin.py create mode 100644 zds/member/views/emailproviders.py create mode 100644 zds/member/views/hats.py create mode 100644 zds/member/views/login.py create mode 100644 zds/member/views/moderation.py create mode 100644 zds/member/views/password_recovery.py create mode 100644 zds/member/views/profile.py create mode 100644 zds/member/views/register.py create mode 100644 zds/member/views/reports.py diff --git a/zds/member/tests/tests_views.py b/zds/member/tests/tests_views.py deleted file mode 100644 index 07f8d2a227..0000000000 --- a/zds/member/tests/tests_views.py +++ /dev/null @@ -1,1820 +0,0 @@ -import os -from datetime import datetime -from smtplib import SMTPException - -from django.core.mail.backends.base import BaseEmailBackend -from unittest.mock import Mock -from oauth2_provider.models import AccessToken, Application - -from django.conf import settings -from django.contrib.auth.models import User, Group -from django.core import mail -from django.urls import reverse -from django.test import TestCase, override_settings -from django.utils.html import escape -from django.utils.translation import gettext_lazy as _ - -from zds.member.views import member_from_ip -from zds.notification.models import TopicAnswerSubscription -from zds.member.tests.factories import ( - ProfileFactory, - StaffProfileFactory, - NonAsciiProfileFactory, - UserFactory, - DevProfileFactory, -) -from zds.mp.tests.factories import PrivateTopicFactory, PrivatePostFactory -from zds.member.models import Profile, KarmaNote, TokenForgotPassword -from zds.mp.models import PrivatePost, PrivateTopic -from zds.member.models import TokenRegister, Ban, NewEmailProvider, BannedEmailProvider -from zds.tutorialv2.tests.factories import PublishableContentFactory, PublishedContentFactory, BetaContentFactory -from zds.tutorialv2.models.database import PublishableContent, PublishedContent -from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents -from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory, TopicFactory, PostFactory -from zds.forum.models import Topic, Post -from zds.gallery.tests.factories import GalleryFactory, UserGalleryFactory -from zds.gallery.models import Gallery, UserGallery -from zds.pages.models import GroupContact -from zds.utils.models import CommentVote, Hat, HatRequest, Alert - - -@override_for_contents() -class MemberTests(TutorialTestMixin, TestCase): - def setUp(self): - settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" - self.mas = ProfileFactory() - settings.ZDS_APP["member"]["bot_account"] = self.mas.user.username - self.anonymous = UserFactory(username=settings.ZDS_APP["member"]["anonymous_account"], password="anything") - self.external = UserFactory(username=settings.ZDS_APP["member"]["external_account"], password="anything") - self.category1 = ForumCategoryFactory(position=1) - self.forum11 = ForumFactory(category=self.category1, position_in_category=1) - self.staff = StaffProfileFactory().user - - self.bot = Group(name=settings.ZDS_APP["member"]["bot_group"]) - self.bot.save() - - def test_karma(self): - user = ProfileFactory() - other_user = ProfileFactory() - self.client.force_login(other_user.user) - r = self.client.post(reverse("member-modify-karma"), {"profile_pk": user.pk, "karma": 42, "note": "warn"}) - self.assertEqual(403, r.status_code) - self.client.logout() - self.client.force_login(self.staff) - # bad id - r = self.client.post( - reverse("member-modify-karma"), {"profile_pk": "blah", "karma": 42, "note": "warn"}, follow=True - ) - self.assertEqual(404, r.status_code) - # good karma - r = self.client.post( - reverse("member-modify-karma"), {"profile_pk": user.pk, "karma": 42, "note": "warn"}, follow=True - ) - self.assertEqual(200, r.status_code) - self.assertIn("{} : 42".format(_("Modification du karma")), r.content.decode("utf-8")) - # more than 100 karma must unvalidate the karma - r = self.client.post( - reverse("member-modify-karma"), {"profile_pk": user.pk, "karma": 420, "note": "warn"}, follow=True - ) - self.assertEqual(200, r.status_code) - self.assertNotIn("{} : 420".format(_("Modification du karma")), r.content.decode("utf-8")) - # empty warning must unvalidate the karma - r = self.client.post( - reverse("member-modify-karma"), {"profile_pk": user.pk, "karma": 41, "note": ""}, follow=True - ) - self.assertEqual(200, r.status_code) - self.assertNotIn("{} : 41".format(_("Modification du karma")), r.content.decode("utf-8")) - - def test_list_members(self): - """ - To test the listing of the members with and without page parameter. - """ - - # create strange member - weird = ProfileFactory() - weird.user.username = "ïtrema718" - weird.user.email = "foo@\xfbgmail.com" - weird.user.save() - - # list of members. - result = self.client.get(reverse("member-list"), follow=False) - self.assertEqual(result.status_code, 200) - - nb_users = len(result.context["members"]) - - # Test that inactive user don't show up - unactive_user = ProfileFactory() - unactive_user.user.is_active = False - unactive_user.user.save() - result = self.client.get(reverse("member-list"), follow=False) - self.assertEqual(result.status_code, 200) - self.assertEqual(nb_users, len(result.context["members"])) - - # Add a Bot and check that list didn't change - bot_profile = ProfileFactory() - bot_profile.user.groups.add(self.bot) - bot_profile.user.save() - result = self.client.get(reverse("member-list"), follow=False) - self.assertEqual(result.status_code, 200) - self.assertEqual(nb_users, len(result.context["members"])) - - # list of members with page parameter. - result = self.client.get(reverse("member-list") + "?page=1", follow=False) - self.assertEqual(result.status_code, 200) - - # page which doesn't exist. - result = self.client.get(reverse("member-list") + "?page=1534", follow=False) - self.assertEqual(result.status_code, 404) - - # page parameter isn't an integer. - result = self.client.get(reverse("member-list") + "?page=abcd", follow=False) - self.assertEqual(result.status_code, 404) - - def test_details_member(self): - """ - To test details of a member given. - """ - - # details of a staff user. - result = self.client.get(reverse("member-detail", args=[self.staff.username]), follow=False) - self.assertEqual(result.status_code, 200) - - # details of an unknown user. - result = self.client.get(reverse("member-detail", args=["unknown_user"]), follow=False) - self.assertEqual(result.status_code, 404) - - def test_redirection_when_using_old_detail_member_url(self): - """ - To test the redirection when accessing the member profile through the old url - """ - user = ProfileFactory().user - result = self.client.get(reverse("member-detail-redirect", args=[user.username]), follow=False) - - self.assertEqual(result.status_code, 301) - - def test_old_detail_member_url_with_unexistant_member(self): - """ - To test wether a 404 error is raised when the user in the old url does not exist - """ - response = self.client.get(reverse("member-detail-redirect", args=["tartempion"]), follow=False) - - self.assertEqual(response.status_code, 404) - - def test_moderation_history(self): - user = ProfileFactory().user - - ban = Ban( - user=user, - moderator=self.staff, - type="Lecture Seule Temporaire", - note="Test de LS", - pubdate=datetime.now(), - ) - ban.save() - - note = KarmaNote( - user=user, - moderator=self.staff, - karma=5, - note="Test de karma", - pubdate=datetime.now(), - ) - note.save() - - # staff rights are required to view the history, check that - self.client.logout() - self.client.force_login(user) - result = self.client.get(user.profile.get_absolute_url(), follow=False) - self.assertNotContains(result, "Historique de modération") - - self.client.logout() - self.client.force_login(self.staff) - result = self.client.get(user.profile.get_absolute_url(), follow=False) - self.assertContains(result, "Historique de modération") - - # check that the note and the sanction are in the context - self.assertIn(ban, result.context["actions"]) - self.assertIn(note, result.context["actions"]) - - # and are displayed - self.assertContains(result, "Test de LS") - self.assertContains(result, "Test de karma") - - def test_profile_page_of_weird_member_username(self): - - # create some user with weird username - user_1 = ProfileFactory() - user_2 = ProfileFactory() - user_3 = ProfileFactory() - user_1.user.username = "ïtrema" - user_1.user.save() - user_2.user.username = ""a" - user_2.user.save() - user_3.user.username = "_`_`_`_" - user_3.user.save() - - # profile pages of weird users. - result = self.client.get(reverse("member-detail", args=[user_1.user.username]), follow=True) - self.assertEqual(result.status_code, 200) - result = self.client.get(reverse("member-detail", args=[user_2.user.username]), follow=True) - self.assertEqual(result.status_code, 200) - result = self.client.get(reverse("member-detail", args=[user_3.user.username]), follow=True) - self.assertEqual(result.status_code, 200) - - def test_modify_member(self): - user = ProfileFactory().user - - # we need staff right for update other profile, so a member who is not staff can't access to the page - self.client.logout() - self.client.force_login(user) - - result = self.client.get(reverse("member-settings-mini-profile", args=["xkcd"]), follow=False) - self.assertEqual(result.status_code, 403) - - self.client.logout() - self.client.force_login(self.staff) - - # an inexistant member return 404 - result = self.client.get(reverse("member-settings-mini-profile", args=["xkcd"]), follow=False) - self.assertEqual(result.status_code, 404) - - # an existant member return 200 - result = self.client.get(reverse("member-settings-mini-profile", args=[self.mas.user.username]), follow=False) - self.assertEqual(result.status_code, 200) - - def test_success_preview_biography(self): - - member = ProfileFactory() - self.client.force_login(member.user) - - response = self.client.post( - reverse("update-member"), - { - "text": "It is **my** life", - "preview": "", - }, - HTTP_X_REQUESTED_WITH="XMLHttpRequest", - ) - - result_string = "".join(a.decode() for a in response.streaming_content) - self.assertIn("my", result_string, "We need the biography to be properly formatted") - - def test_login(self): - """ - To test user login. - """ - user = ProfileFactory() - - # login a user. Good password then redirection to the homepage. - result = self.client.post( - reverse("member-login"), - {"username": user.user.username, "password": "hostel77", "remember": "remember"}, - follow=False, - ) - self.assertRedirects(result, reverse("homepage")) - - # login failed with bad password then no redirection - # (status_code equals 200 and not 302). - result = self.client.post( - reverse("member-login"), - {"username": user.user.username, "password": "hostel", "remember": "remember"}, - follow=False, - ) - self.assertEqual(result.status_code, 200) - self.assertContains( - result, - _( - "Le mot de passe saisi est incorrect. " - "Cliquez sur le lien « Mot de passe oublié ? » " - "si vous ne vous en souvenez plus." - ), - ) - - # login failed with bad username then no redirection - # (status_code equals 200 and not 302). - result = self.client.post( - reverse("member-login"), {"username": "clem", "password": "hostel77", "remember": "remember"}, follow=False - ) - self.assertEqual(result.status_code, 200) - self.assertContains( - result, - _("Ce nom d’utilisateur est inconnu. " "Si vous ne possédez pas de compte, " "vous pouvez vous inscrire."), - ) - - # login a user. Good password and next parameter then - # redirection to the "next" page. - result = self.client.post( - reverse("member-login") + "?next=" + reverse("gallery-list"), - {"username": user.user.username, "password": "hostel77", "remember": "remember"}, - follow=False, - ) - self.assertRedirects(result, reverse("gallery-list")) - - # check the user is redirected to the home page if - # the "next" parameter points to a non-existing page. - result = self.client.post( - reverse("member-login") + "?next=/foobar", - {"username": user.user.username, "password": "hostel77", "remember": "remember"}, - follow=False, - ) - self.assertRedirects(result, reverse("homepage")) - - # check if the login form will redirect if there is - # a next parameter. - self.client.logout() - result = self.client.get(reverse("member-login") + "?next=" + reverse("gallery-list")) - self.assertContains(result, reverse("member-login") + "?next=" + reverse("gallery-list"), count=1) - - def test_register_with_not_allowed_chars(self): - """ - Test register account with not allowed chars - :return: - """ - users = [ - # empty username - {"username": "", "password": "flavour", "password_confirm": "flavour", "email": "firm1@zestedesavoir.com"}, - # space after username - { - "username": "firm1 ", - "password": "flavour", - "password_confirm": "flavour", - "email": "firm1@zestedesavoir.com", - }, - # space before username - { - "username": " firm1", - "password": "flavour", - "password_confirm": "flavour", - "email": "firm1@zestedesavoir.com", - }, - # username with utf8mb4 chars - { - "username": " firm1", - "password": "flavour", - "password_confirm": "flavour", - "email": "firm1@zestedesavoir.com", - }, - ] - - for user in users: - result = self.client.post(reverse("register-member"), user, follow=False) - self.assertEqual(result.status_code, 200) - # check any email has been sent. - self.assertEqual(len(mail.outbox), 0) - # user doesn't exist - self.assertEqual(User.objects.filter(username=user["username"]).count(), 0) - - def test_register(self): - """ - To test user registration. - """ - - # register a new user. - result = self.client.post( - reverse("register-member"), - { - "username": "firm1", - "password": "flavour", - "password_confirm": "flavour", - "email": "firm1@zestedesavoir.com", - }, - follow=False, - ) - self.assertEqual(result.status_code, 200) - - # check email has been sent. - self.assertEqual(len(mail.outbox), 1) - - # check if the new user is well inactive. - user = User.objects.get(username="firm1") - self.assertFalse(user.is_active) - - # make a request on the link which has been sent in mail to - # confirm the registration. - token = TokenRegister.objects.get(user=user) - result = self.client.get(settings.ZDS_APP["site"]["url"] + token.get_absolute_url(), follow=False) - self.assertEqual(result.status_code, 200) - - # check a new email hasn't been sent at the new user. - self.assertEqual(len(mail.outbox), 1) - - # check if the new user is active. - self.assertTrue(User.objects.get(username="firm1").is_active) - - def test_unregister(self): - """ - To test that unregistering user is working. - """ - - # test not logged user can't unregister. - self.client.logout() - result = self.client.post(reverse("member-unregister"), follow=False) - self.assertEqual(result.status_code, 302) - - # test logged user can register. - user = ProfileFactory() - self.client.force_login(user.user) - result = self.client.post(reverse("member-unregister"), follow=False) - self.assertEqual(result.status_code, 302) - self.assertEqual(User.objects.filter(username=user.user.username).count(), 0) - - # Attach a user at tutorials, articles, topics and private topics. After that, - # unregister this user and check that he is well removed in all contents. - user = ProfileFactory() - user2 = ProfileFactory() - alone_gallery = GalleryFactory() - UserGalleryFactory(gallery=alone_gallery, user=user.user) - shared_gallery = GalleryFactory() - UserGalleryFactory(gallery=shared_gallery, user=user.user) - UserGalleryFactory(gallery=shared_gallery, user=user2.user) - # first case : a published tutorial with only one author - published_tutorial_alone = PublishedContentFactory(type="TUTORIAL") - published_tutorial_alone.authors.add(user.user) - published_tutorial_alone.save() - # second case : a published tutorial with two authors - published_tutorial_2 = PublishedContentFactory(type="TUTORIAL") - published_tutorial_2.authors.add(user.user) - published_tutorial_2.authors.add(user2.user) - published_tutorial_2.save() - # third case : a private tutorial with only one author - writing_tutorial_alone = PublishableContentFactory(type="TUTORIAL") - writing_tutorial_alone.authors.add(user.user) - writing_tutorial_alone.save() - writing_tutorial_alone_galler_path = writing_tutorial_alone.gallery.get_gallery_path() - # fourth case : a private tutorial with at least two authors - writing_tutorial_2 = PublishableContentFactory(type="TUTORIAL") - writing_tutorial_2.authors.add(user.user) - writing_tutorial_2.authors.add(user2.user) - writing_tutorial_2.save() - self.client.force_login(self.staff) - # same thing for articles - published_article_alone = PublishedContentFactory(type="ARTICLE") - published_article_alone.authors.add(user.user) - published_article_alone.save() - published_article_2 = PublishedContentFactory(type="ARTICLE") - published_article_2.authors.add(user.user) - published_article_2.authors.add(user2.user) - published_article_2.save() - writing_article_alone = PublishableContentFactory(type="ARTICLE") - writing_article_alone.authors.add(user.user) - writing_article_alone.save() - writing_article_2 = PublishableContentFactory(type="ARTICLE") - writing_article_2.authors.add(user.user) - writing_article_2.authors.add(user2.user) - writing_article_2.save() - # beta content - beta_forum = ForumFactory(category=ForumCategoryFactory()) - beta_content = BetaContentFactory(author_list=[user.user], forum=beta_forum) - beta_content_2 = BetaContentFactory(author_list=[user.user, user2.user], forum=beta_forum) - # about posts and topics - authored_topic = TopicFactory(author=user.user, forum=self.forum11, solved_by=user.user) - answered_topic = TopicFactory(author=user2.user, forum=self.forum11) - PostFactory(topic=answered_topic, author=user.user, position=2) - edited_answer = PostFactory(topic=answered_topic, author=user.user, position=3) - edited_answer.editor = user.user - edited_answer.save() - - upvoted_answer = PostFactory(topic=answered_topic, author=user2.user, position=4) - upvoted_answer.like += 1 - upvoted_answer.save() - CommentVote.objects.create(user=user.user, comment=upvoted_answer, positive=True) - - private_topic = PrivateTopicFactory(author=user.user) - private_topic.participants.add(user2.user) - private_topic.save() - PrivatePostFactory(author=user.user, privatetopic=private_topic, position_in_topic=1) - - # add API key - self.assertEqual(Application.objects.count(), 0) - self.assertEqual(AccessToken.objects.count(), 0) - api_application = Application() - api_application.client_id = "foobar" - api_application.user = user.user - api_application.client_type = "confidential" - api_application.authorization_grant_type = "password" - api_application.client_secret = "42" - api_application.save() - token = AccessToken() - token.user = user.user - token.token = "r@d0m" - token.application = api_application - token.expires = datetime.now() - token.save() - self.assertEqual(Application.objects.count(), 1) - self.assertEqual(AccessToken.objects.count(), 1) - - # add a karma note and a sanction with this user - note = KarmaNote(moderator=user.user, user=user2.user, note="Good!", karma=5) - note.save() - ban = Ban(moderator=user.user, user=user2.user, type="Ban définitif", note="Test") - ban.save() - - # login and unregister: - self.client.force_login(user.user) - result = self.client.post(reverse("member-unregister"), follow=False) - self.assertEqual(result.status_code, 302) - - # check that the bot have taken authorship of tutorial: - self.assertEqual(published_tutorial_alone.authors.count(), 1) - self.assertEqual( - published_tutorial_alone.authors.first().username, settings.ZDS_APP["member"]["external_account"] - ) - self.assertFalse(os.path.exists(writing_tutorial_alone_galler_path)) - self.assertEqual(published_tutorial_2.authors.count(), 1) - self.assertEqual( - published_tutorial_2.authors.filter(username=settings.ZDS_APP["member"]["external_account"]).count(), 0 - ) - - # check that published tutorials remain published and accessible - self.assertIsNotNone(published_tutorial_2.public_version.get_prod_path()) - self.assertTrue(os.path.exists(published_tutorial_2.public_version.get_prod_path())) - self.assertIsNotNone(published_tutorial_alone.public_version.get_prod_path()) - self.assertTrue(os.path.exists(published_tutorial_alone.public_version.get_prod_path())) - self.assertEqual( - self.client.get( - reverse("tutorial:view", args=[published_tutorial_alone.pk, published_tutorial_alone.slug]), - follow=False, - ).status_code, - 200, - ) - self.assertEqual( - self.client.get( - reverse("tutorial:view", args=[published_tutorial_2.pk, published_tutorial_2.slug]), follow=False - ).status_code, - 200, - ) - - # test that published articles remain accessible - self.assertTrue(os.path.exists(published_article_alone.public_version.get_prod_path())) - self.assertEqual( - self.client.get( - reverse("article:view", args=[published_article_alone.pk, published_article_alone.slug]), follow=True - ).status_code, - 200, - ) - self.assertEqual( - self.client.get( - reverse("article:view", args=[published_article_2.pk, published_article_2.slug]), follow=True - ).status_code, - 200, - ) - - # check that the tutorial for which the author was alone does not exists anymore - self.assertEqual(PublishableContent.objects.filter(pk=writing_tutorial_alone.pk).count(), 0) - self.assertFalse(os.path.exists(writing_tutorial_alone.get_repo_path())) - - # check that bot haven't take the authorship of the tuto with more than one author - self.assertEqual(writing_tutorial_2.authors.count(), 1) - self.assertEqual( - writing_tutorial_2.authors.filter(username=settings.ZDS_APP["member"]["external_account"]).count(), 0 - ) - - # authorship for the article for which user was the only author - self.assertEqual(published_article_alone.authors.count(), 1) - self.assertEqual( - published_article_alone.authors.first().username, settings.ZDS_APP["member"]["external_account"] - ) - self.assertEqual(published_article_2.authors.count(), 1) - - self.assertEqual(PublishableContent.objects.filter(pk=writing_article_alone.pk).count(), 0) - self.assertFalse(os.path.exists(writing_article_alone.get_repo_path())) - - # not bot if another author: - self.assertEqual( - published_article_2.authors.filter(username=settings.ZDS_APP["member"]["external_account"]).count(), 0 - ) - self.assertEqual(writing_article_2.authors.count(), 1) - self.assertEqual( - writing_article_2.authors.filter(username=settings.ZDS_APP["member"]["external_account"]).count(), 0 - ) - - # topics, gallery and PMs: - self.assertEqual(Topic.objects.filter(author__username=user.user.username).count(), 0) - self.assertEqual(Topic.objects.filter(solved_by=user.user).count(), 0) - self.assertEqual(Topic.objects.filter(solved_by=self.anonymous).count(), 1) - self.assertEqual(Post.objects.filter(author__username=user.user.username).count(), 0) - self.assertEqual(Post.objects.filter(editor__username=user.user.username).count(), 0) - self.assertEqual(PrivatePost.objects.filter(author__username=user.user.username).count(), 0) - self.assertEqual(PrivateTopic.objects.filter(author__username=user.user.username).count(), 0) - - self.assertIsNotNone(Topic.objects.get(pk=authored_topic.pk)) - self.assertIsNotNone(PrivateTopic.objects.get(pk=private_topic.pk)) - self.assertIsNotNone(Gallery.objects.get(pk=alone_gallery.pk)) - self.assertEqual(alone_gallery.get_linked_users().count(), 1) - self.assertEqual(shared_gallery.get_linked_users().count(), 1) - self.assertEqual(UserGallery.objects.filter(user=user.user).count(), 0) - self.assertEqual(CommentVote.objects.filter(user=user.user, positive=True).count(), 0) - self.assertEqual(Post.objects.filter(pk=upvoted_answer.id).first().like, 0) - - # zep 12, published contents and beta - self.assertIsNotNone(PublishedContent.objects.filter(content__pk=published_tutorial_alone.pk).first()) - self.assertIsNotNone(PublishedContent.objects.filter(content__pk=published_tutorial_2.pk).first()) - self.assertTrue(Topic.objects.get(pk=beta_content.beta_topic.pk).is_locked) - self.assertFalse(Topic.objects.get(pk=beta_content_2.beta_topic.pk).is_locked) - - # check API - self.assertEqual(Application.objects.count(), 0) - self.assertEqual(AccessToken.objects.count(), 0) - - # check that the karma note and the sanction were kept - self.assertTrue(KarmaNote.objects.filter(pk=note.pk).exists()) - self.assertTrue(Ban.objects.filter(pk=ban.pk).exists()) - - def test_forgot_password(self): - """To test nominal scenario of a lost password.""" - - # Empty the test outbox - mail.outbox = [] - - result = self.client.post( - reverse("member-forgot-password"), - { - "username": self.mas.user.username, - "email": "", - }, - follow=False, - ) - - self.assertEqual(result.status_code, 200) - - # check email has been sent - self.assertEqual(len(mail.outbox), 1) - - # clic on the link which has been sent in mail - user = User.objects.get(username=self.mas.user.username) - - token = TokenForgotPassword.objects.get(user=user) - result = self.client.get(settings.ZDS_APP["site"]["url"] + token.get_absolute_url(), follow=False) - - self.assertEqual(result.status_code, 200) - - def test_sanctions(self): - """ - Test various sanctions. - """ - - staff = StaffProfileFactory() - self.client.force_login(staff.user) - - # list of members. - result = self.client.get(reverse("member-list"), follow=False) - self.assertEqual(result.status_code, 200) - nb_users = len(result.context["members"]) - - # Test: LS - user_ls = ProfileFactory() - result = self.client.post( - reverse("member-modify-profile", kwargs={"user_pk": user_ls.user.id}), - {"ls": "", "ls-text": "Texte de test pour LS"}, - follow=False, - ) - user = Profile.objects.get(id=user_ls.id) # Refresh profile from DB - self.assertEqual(result.status_code, 302) - self.assertFalse(user.can_write) - self.assertTrue(user.can_read) - self.assertIsNone(user.end_ban_write) - self.assertIsNone(user.end_ban_read) - ban = Ban.objects.filter(user__id=user.user.id).order_by("-pubdate")[0] - self.assertEqual(ban.type, "Lecture seule illimitée") - self.assertEqual(ban.note, "Texte de test pour LS") - self.assertEqual(len(mail.outbox), 1) - - result = self.client.get(reverse("member-list"), follow=False) - self.assertEqual(result.status_code, 200) - self.assertEqual(nb_users + 1, len(result.context["members"])) # LS guy still shows up, good - - # Test: Un-LS - result = self.client.post( - reverse("member-modify-profile", kwargs={"user_pk": user_ls.user.id}), - {"un-ls": "", "unls-text": "Texte de test pour un-LS"}, - follow=False, - ) - user = Profile.objects.get(id=user_ls.id) # Refresh profile from DB - self.assertEqual(result.status_code, 302) - self.assertTrue(user.can_write) - self.assertTrue(user.can_read) - self.assertIsNone(user.end_ban_write) - self.assertIsNone(user.end_ban_read) - ban = Ban.objects.filter(user__id=user.user.id).order_by("-id")[0] - self.assertEqual(ban.type, "Levée de la lecture seule") - self.assertEqual(ban.note, "Texte de test pour un-LS") - self.assertEqual(len(mail.outbox), 2) - - result = self.client.get(reverse("member-list"), follow=False) - self.assertEqual(result.status_code, 200) - self.assertEqual(nb_users + 1, len(result.context["members"])) # LS guy still shows up, good - - # Test: LS temp - user_ls_temp = ProfileFactory() - result = self.client.post( - reverse("member-modify-profile", kwargs={"user_pk": user_ls_temp.user.id}), - {"ls-temp": "", "ls-jrs": 10, "ls-text": "Texte de test pour LS TEMP"}, - follow=False, - ) - user = Profile.objects.get(id=user_ls_temp.id) # Refresh profile from DB - self.assertEqual(result.status_code, 302) - self.assertFalse(user.can_write) - self.assertTrue(user.can_read) - self.assertIsNotNone(user.end_ban_write) - self.assertIsNone(user.end_ban_read) - ban = Ban.objects.filter(user__id=user.user.id).order_by("-id")[0] - self.assertIn("Lecture seule temporaire", ban.type) - self.assertEqual(ban.note, "Texte de test pour LS TEMP") - self.assertEqual(len(mail.outbox), 3) - - # reset nb_users - result = self.client.get(reverse("member-list"), follow=False) - self.assertEqual(result.status_code, 200) - nb_users = len(result.context["members"]) - - # Test: BAN - user_ban = ProfileFactory() - result = self.client.post( - reverse("member-modify-profile", kwargs={"user_pk": user_ban.user.id}), - {"ban": "", "ban-text": "Texte de test pour BAN"}, - follow=False, - ) - user = Profile.objects.get(id=user_ban.id) # Refresh profile from DB - self.assertEqual(result.status_code, 302) - self.assertTrue(user.can_write) - self.assertFalse(user.can_read) - self.assertIsNone(user.end_ban_write) - self.assertIsNone(user.end_ban_read) - ban = Ban.objects.filter(user__id=user.user.id).order_by("-id")[0] - self.assertEqual(ban.type, "Bannissement illimité") - self.assertEqual(ban.note, "Texte de test pour BAN") - self.assertEqual(len(mail.outbox), 4) - - result = self.client.get(reverse("member-list"), follow=False) - self.assertEqual(result.status_code, 200) - self.assertEqual(nb_users, len(result.context["members"])) # Banned guy doesn't show up, good - - # Test: un-BAN - result = self.client.post( - reverse("member-modify-profile", kwargs={"user_pk": user_ban.user.id}), - {"un-ban": "", "unban-text": "Texte de test pour BAN"}, - follow=False, - ) - user = Profile.objects.get(id=user_ban.id) # Refresh profile from DB - self.assertEqual(result.status_code, 302) - self.assertTrue(user.can_write) - self.assertTrue(user.can_read) - self.assertIsNone(user.end_ban_write) - self.assertIsNone(user.end_ban_read) - ban = Ban.objects.filter(user__id=user.user.id).order_by("-id")[0] - self.assertEqual(ban.type, "Levée du bannissement") - self.assertEqual(ban.note, "Texte de test pour BAN") - self.assertEqual(len(mail.outbox), 5) - - result = self.client.get(reverse("member-list"), follow=False) - self.assertEqual(result.status_code, 200) - self.assertEqual(nb_users + 1, len(result.context["members"])) # UnBanned guy shows up, good - - # Test: BAN temp - user_ban_temp = ProfileFactory() - result = self.client.post( - reverse("member-modify-profile", kwargs={"user_pk": user_ban_temp.user.id}), - {"ban-temp": "", "ban-jrs": 10, "ban-text": "Texte de test pour BAN TEMP"}, - follow=False, - ) - user = Profile.objects.get(id=user_ban_temp.id) # Refresh profile from DB - self.assertEqual(result.status_code, 302) - self.assertTrue(user.can_write) - self.assertFalse(user.can_read) - self.assertIsNone(user.end_ban_write) - self.assertIsNotNone(user.end_ban_read) - ban = Ban.objects.filter(user__id=user.user.id).order_by("-id")[0] - self.assertIn("Bannissement temporaire", ban.type) - self.assertEqual(ban.note, "Texte de test pour BAN TEMP") - self.assertEqual(len(mail.outbox), 6) - - def test_sanctions_with_not_staff_user(self): - user = ProfileFactory().user - - # we need staff right for update the sanction of a user, so a member who is not staff can't access to the page - self.client.logout() - self.client.force_login(user) - - # Test: LS - result = self.client.post( - reverse("member-modify-profile", kwargs={"user_pk": self.staff.id}), - {"ls": "", "ls-text": "Texte de test pour LS"}, - follow=False, - ) - - self.assertEqual(result.status_code, 403) - - # if the user is staff, he can update the sanction of a user - self.client.logout() - self.client.force_login(self.staff) - - # Test: LS - result = self.client.post( - reverse("member-modify-profile", kwargs={"user_pk": user.id}), - {"ls": "", "ls-text": "Texte de test pour LS"}, - follow=False, - ) - - self.assertEqual(result.status_code, 302) - - def test_failed_bot_sanctions(self): - - staff = StaffProfileFactory() - self.client.force_login(staff.user) - - bot_profile = ProfileFactory() - bot_profile.user.groups.add(self.bot) - bot_profile.user.save() - - # Test: LS - result = self.client.post( - reverse("member-modify-profile", kwargs={"user_pk": bot_profile.user.id}), - {"ls": "", "ls-text": "Texte de test pour LS"}, - follow=False, - ) - user = Profile.objects.get(id=bot_profile.id) # Refresh profile from DB - self.assertEqual(result.status_code, 403) - self.assertTrue(user.can_write) - self.assertTrue(user.can_read) - self.assertIsNone(user.end_ban_write) - self.assertIsNone(user.end_ban_read) - - def test_nonascii(self): - user = NonAsciiProfileFactory() - result = self.client.get( - reverse("member-login") + "?next=" + reverse("member-detail", args=[user.user.username]), follow=False - ) - self.assertEqual(result.status_code, 200) - - def test_promote_interface(self): - """ - Test promotion interface. - """ - - # create users (one regular, one staff and one superuser) - tester = ProfileFactory() - staff = StaffProfileFactory() - tester.user.is_active = False - tester.user.save() - staff.user.is_superuser = True - staff.user.save() - - # create groups - group = Group.objects.create(name="DummyGroup_1") - groupbis = Group.objects.create(name="DummyGroup_2") - - # create Forums, Posts and subscribe member to them. - category1 = ForumCategoryFactory(position=1) - forum1 = ForumFactory(category=category1, position_in_category=1) - forum1.groups.add(group) - forum1.save() - forum2 = ForumFactory(category=category1, position_in_category=2) - forum2.groups.add(groupbis) - forum2.save() - forum3 = ForumFactory(category=category1, position_in_category=3) - topic1 = TopicFactory(forum=forum1, author=staff.user) - topic2 = TopicFactory(forum=forum2, author=staff.user) - topic3 = TopicFactory(forum=forum3, author=staff.user) - - # LET THE TEST BEGIN ! - - # tester shouldn't be able to connect - login_check = self.client.login(username=tester.user.username, password="hostel77") - self.assertEqual(login_check, False) - - # connect as staff (superuser) - self.client.force_login(staff.user) - - # check that we can go through the page - result = self.client.get(reverse("member-settings-promote", kwargs={"user_pk": tester.user.id}), follow=False) - self.assertEqual(result.status_code, 200) - - # give groups thanks to staff (but account still not activated) - result = self.client.post( - reverse("member-settings-promote", kwargs={"user_pk": tester.user.id}), - { - "groups": [group.id, groupbis.id], - }, - follow=False, - ) - self.assertEqual(result.status_code, 302) - tester = Profile.objects.get(id=tester.id) # refresh - - self.assertEqual(len(tester.user.groups.all()), 2) - self.assertFalse(tester.user.is_active) - - # Now our tester is going to follow one post in every forum (3) - TopicAnswerSubscription.objects.toggle_follow(topic1, tester.user) - TopicAnswerSubscription.objects.toggle_follow(topic2, tester.user) - TopicAnswerSubscription.objects.toggle_follow(topic3, tester.user) - - self.assertEqual(len(TopicAnswerSubscription.objects.get_objects_followed_by(tester.user)), 3) - - # retract all right, keep one group only and activate account - result = self.client.post( - reverse("member-settings-promote", kwargs={"user_pk": tester.user.id}), - {"groups": [group.id], "activation": "on"}, - follow=False, - ) - self.assertEqual(result.status_code, 302) - tester = Profile.objects.get(id=tester.id) # refresh - - self.assertEqual(len(tester.user.groups.all()), 1) - self.assertTrue(tester.user.is_active) - self.assertEqual(len(TopicAnswerSubscription.objects.get_objects_followed_by(tester.user)), 2) - - # no groups specified - result = self.client.post( - reverse("member-settings-promote", kwargs={"user_pk": tester.user.id}), {"activation": "on"}, follow=False - ) - self.assertEqual(result.status_code, 302) - tester = Profile.objects.get(id=tester.id) # refresh - self.assertEqual(len(TopicAnswerSubscription.objects.get_objects_followed_by(tester.user)), 1) - - # Finally, check that user can connect and can not access the interface - login_check = self.client.login(username=tester.user.username, password="hostel77") - self.assertEqual(login_check, True) - result = self.client.post( - reverse("member-settings-promote", kwargs={"user_pk": staff.user.id}), {"activation": "on"}, follow=False - ) - self.assertEqual(result.status_code, 403) # forbidden ! - - def test_filter_member_ip(self): - """ - Test filter member by ip. - """ - - # create users (one regular and one staff and superuser) - tester = ProfileFactory() - staff = StaffProfileFactory() - - # test login normal user - result = self.client.post( - reverse("member-login"), - {"username": tester.user.username, "password": "hostel77", "remember": "remember"}, - follow=False, - ) - # good password then redirection - self.assertEqual(result.status_code, 302) - - # Check that the filter can't be access from normal user - result = self.client.post( - reverse("member-from-ip", kwargs={"ip_address": tester.last_ip_address}), {}, follow=False - ) - self.assertEqual(result.status_code, 403) - - # log the staff user - result = self.client.post( - reverse("member-login"), - {"username": staff.user.username, "password": "hostel77", "remember": "remember"}, - follow=False, - ) - # good password then redirection - self.assertEqual(result.status_code, 302) - - # test that we retrieve correctly the 2 members (staff + user) from this ip - result = self.client.post( - reverse("member-from-ip", kwargs={"ip_address": staff.last_ip_address}), {}, follow=False - ) - self.assertEqual(result.status_code, 200) - self.assertEqual(len(result.context["members"]), 2) - - def test_modify_user_karma(self): - """ - To test karma of a user modified by a staff user. - """ - tester = ProfileFactory() - staff = StaffProfileFactory() - - # login as user - result = self.client.post( - reverse("member-login"), {"username": tester.user.username, "password": "hostel77"}, follow=False - ) - self.assertEqual(result.status_code, 302) - - # check that user can't use this feature - result = self.client.post(reverse("member-modify-karma"), follow=False) - self.assertEqual(result.status_code, 403) - - # login as staff - result = self.client.post( - reverse("member-login"), {"username": staff.user.username, "password": "hostel77"}, follow=False - ) - self.assertEqual(result.status_code, 302) - - # try to give a few bad points to the tester - result = self.client.post( - reverse("member-modify-karma"), - {"profile_pk": tester.pk, "note": "Bad tester is bad !", "karma": "-50"}, - follow=False, - ) - self.assertEqual(result.status_code, 302) - tester = Profile.objects.get(pk=tester.pk) - self.assertEqual(tester.karma, -50) - self.assertEqual(KarmaNote.objects.filter(user=tester.user).count(), 1) - - # Now give a few good points - result = self.client.post( - reverse("member-modify-karma"), - {"profile_pk": tester.pk, "note": "Good tester is good !", "karma": "10"}, - follow=False, - ) - self.assertEqual(result.status_code, 302) - tester = Profile.objects.get(pk=tester.pk) - self.assertEqual(tester.karma, -40) - self.assertEqual(KarmaNote.objects.filter(user=tester.user).count(), 2) - - # Now access some unknow user - result = self.client.post( - reverse("member-modify-karma"), - {"profile_pk": 9999, "note": "Good tester is good !", "karma": "10"}, - follow=False, - ) - self.assertEqual(result.status_code, 404) - - # Now give unknow point - result = self.client.post( - reverse("member-modify-karma"), - {"profile_pk": tester.pk, "note": "Good tester is good !", "karma": ""}, - follow=False, - ) - self.assertEqual(result.status_code, 302) - tester = Profile.objects.get(pk=tester.pk) - self.assertEqual(tester.karma, -40) - self.assertEqual(KarmaNote.objects.filter(user=tester.user).count(), 3) - - # Now give no point at all - result = self.client.post( - reverse("member-modify-karma"), {"profile_pk": tester.pk, "note": "Good tester is good !"}, follow=False - ) - self.assertEqual(result.status_code, 302) - tester = Profile.objects.get(pk=tester.pk) - self.assertEqual(tester.karma, -40) - self.assertEqual(KarmaNote.objects.filter(user=tester.user).count(), 4) - - # Now access without post - result = self.client.get(reverse("member-modify-karma"), follow=False) - self.assertEqual(result.status_code, 405) - - def test_karma_and_pseudo_change(self): - """ - To test that a karma note is added when a member change its pseudo - """ - tester = ProfileFactory() - old_pseudo = tester.user.username - self.client.force_login(tester.user) - data = {"username": "dummy", "email": tester.user.email} - result = self.client.post(reverse("update-username-email-member"), data, follow=False) - - self.assertEqual(result.status_code, 302) - notes = KarmaNote.objects.filter(user=tester.user).all() - self.assertEqual(len(notes), 1) - self.assertTrue(old_pseudo in notes[0].note and "dummy" in notes[0].note) - - def test_members_are_contactable(self): - """ - The PM button is displayed to logged in users, except if it's the profile - of a banned user. - """ - user_ban = ProfileFactory() - user_ban.can_read = False - user_ban.can_write = False - user_ban.save() - user_1 = ProfileFactory() - user_2 = ProfileFactory() - - phrase = "Envoyer un message" - - # The PM button is hidden for anonymous users - result = self.client.get(reverse("member-detail", args=[user_1.user.username]), follow=False) - self.assertNotContains(result, phrase) - - # Also for anonymous users viewing banned members profiles - result = self.client.get(reverse("member-detail", args=[user_ban.user.username]), follow=False) - self.assertNotContains(result, phrase) - - self.client.force_login(user_2.user) - - # If an user is logged in, the PM button is shown for other normal users - result = self.client.get(reverse("member-detail", args=[user_1.user.username]), follow=False) - self.assertContains(result, phrase) - - # But not for banned users - result = self.client.get(reverse("member-detail", args=[user_ban.user.username]), follow=False) - self.assertNotContains(result, phrase) - - self.client.logout() - self.client.force_login(user_1.user) - - # Neither for his own profile - result = self.client.get(reverse("member-detail", args=[user_1.user.username]), follow=False) - self.assertNotContains(result, phrase) - - self.client.logout() - - def test_github_token(self): - user = ProfileFactory() - dev = DevProfileFactory() - - # test that github settings are only availables for dev - self.client.force_login(user.user) - result = self.client.get(reverse("update-github"), follow=False) - self.assertEqual(result.status_code, 403) - result = self.client.post(reverse("remove-github"), follow=False) - self.assertEqual(result.status_code, 403) - self.client.logout() - - # now, test the form - self.client.force_login(dev.user) - result = self.client.get(reverse("update-github"), follow=False) - self.assertEqual(result.status_code, 200) - result = self.client.post( - reverse("update-github"), - { - "github_token": "test", - }, - follow=False, - ) - self.assertEqual(result.status_code, 302) - - # refresh - dev = Profile.objects.get(pk=dev.pk) - self.assertEqual(dev.github_token, "test") - - # test the option to remove the token - result = self.client.post(reverse("remove-github"), follow=False) - self.assertEqual(result.status_code, 302) - - # refresh - dev = Profile.objects.get(pk=dev.pk) - self.assertEqual(dev.github_token, "") - - def test_markdown_help_settings(self): - user = ProfileFactory().user - - # login and check that the Markdown help is displayed - self.client.force_login(user) - result = self.client.get(reverse("pages-index"), follow=False) - self.assertContains(result, 'data-show-markdown-help="true"') - - # disable Markdown help - user.profile.show_markdown_help = False - user.profile.save() - result = self.client.get(reverse("pages-index"), follow=False) - self.assertContains(result, 'data-show-markdown-help="false"') - - def test_new_provider_with_new_account(self): - new_providers_count = NewEmailProvider.objects.count() - - # register a new user - self.client.post( - reverse("register-member"), - { - "username": "new", - "password": "hostel77", - "password_confirm": "hostel77", - "email": "test@unknown-provider-register.com", - }, - follow=False, - ) - - user = User.objects.get(username="new") - token = TokenRegister.objects.get(user=user) - self.client.get(token.get_absolute_url(), follow=False) - - # A new provider object should have been created - self.assertEqual(new_providers_count + 1, NewEmailProvider.objects.count()) - - def test_new_provider_with_email_edit(self): - new_providers_count = NewEmailProvider.objects.count() - user = ProfileFactory().user - self.client.force_login(user) - # Edit the email with an unknown provider - self.client.post( - reverse("update-username-email-member"), - {"username": user.username, "email": "test@unknown-provider-edit.com"}, - follow=False, - ) - # A new provider object should have been created - self.assertEqual(new_providers_count + 1, NewEmailProvider.objects.count()) - - def test_new_providers_list(self): - # create a new provider - user = ProfileFactory().user - provider = NewEmailProvider.objects.create(use="NEW_ACCOUNT", user=user, provider="test.com") - # check that the list is not available for a non-staff member - self.client.logout() - result = self.client.get(reverse("new-email-providers"), follow=False) - self.assertEqual(result.status_code, 302) - self.client.force_login(user) - result = self.client.get(reverse("new-email-providers"), follow=False) - self.assertEqual(result.status_code, 403) - # and that it contains the provider we created - self.client.force_login(self.staff) - result = self.client.get(reverse("new-email-providers"), follow=False) - self.assertEqual(result.status_code, 200) - self.assertIn(provider, result.context["providers"]) - - def test_check_new_provider(self): - # create two new providers - user = ProfileFactory().user - provider1 = NewEmailProvider.objects.create(use="NEW_ACCOUNT", user=user, provider="test1.com") - provider2 = NewEmailProvider.objects.create(use="EMAIl_EDIT", user=user, provider="test2.com") - # check that this option is only available for a staff member - self.client.force_login(user) - result = self.client.post(reverse("check-new-email-provider", args=[provider1.pk]), follow=False) - self.assertEqual(result.status_code, 403) - # test approval - self.client.force_login(self.staff) - result = self.client.post(reverse("check-new-email-provider", args=[provider1.pk]), follow=False) - self.assertEqual(result.status_code, 302) - self.assertFalse(NewEmailProvider.objects.filter(pk=provider1.pk).exists()) - self.assertFalse(BannedEmailProvider.objects.filter(provider=provider1.provider).exists()) - # test ban - self.client.force_login(self.staff) - result = self.client.post(reverse("check-new-email-provider", args=[provider2.pk]), {"ban": "on"}, follow=False) - self.assertEqual(result.status_code, 302) - self.assertFalse(NewEmailProvider.objects.filter(pk=provider2.pk).exists()) - self.assertTrue(BannedEmailProvider.objects.filter(provider=provider2.provider).exists()) - - def test_banned_providers_list(self): - user = ProfileFactory().user - # create a banned provider - provider = BannedEmailProvider.objects.create(moderator=self.staff, provider="test.com") - # check that the list is not available for a non-staff member - self.client.logout() - result = self.client.get(reverse("banned-email-providers"), follow=False) - self.assertEqual(result.status_code, 302) - self.client.force_login(user) - result = self.client.get(reverse("banned-email-providers"), follow=False) - self.assertEqual(result.status_code, 403) - # and that it contains the provider we created - self.client.force_login(self.staff) - result = self.client.get(reverse("banned-email-providers"), follow=False) - self.assertEqual(result.status_code, 200) - self.assertIn(provider, result.context["providers"]) - - def test_add_banned_provider(self): - # test that this page is only available for staff - user = ProfileFactory().user - self.client.logout() - result = self.client.get(reverse("add-banned-email-provider"), follow=False) - self.assertEqual(result.status_code, 302) - self.client.force_login(user) - result = self.client.get(reverse("add-banned-email-provider"), follow=False) - self.assertEqual(result.status_code, 403) - self.client.force_login(self.staff) - result = self.client.get(reverse("add-banned-email-provider"), follow=False) - self.assertEqual(result.status_code, 200) - - # add a provider - result = self.client.post(reverse("add-banned-email-provider"), {"provider": "new-provider.com"}, follow=False) - self.assertEqual(result.status_code, 302) - self.assertTrue(BannedEmailProvider.objects.filter(provider="new-provider.com").exists()) - - # check that it cannot be added again - result = self.client.post(reverse("add-banned-email-provider"), {"provider": "new-provider.com"}, follow=False) - self.assertEqual(result.status_code, 200) - self.assertEqual(1, BannedEmailProvider.objects.filter(provider="new-provider.com").count()) - - def test_members_with_provider(self): - # create two members with the same provider - member1 = ProfileFactory().user - member2 = ProfileFactory().user - member1.email = "test1@test-members.com" - member1.save() - member2.email = "test2@test-members.com" - member2.save() - # ban this provider - provider = BannedEmailProvider.objects.create(moderator=self.staff, provider="test-members.com") - # check that this page is only available for staff - self.client.logout() - result = self.client.get(reverse("members-with-provider", args=[provider.pk]), follow=False) - self.assertEqual(result.status_code, 302) - self.client.force_login(member1) - result = self.client.get(reverse("members-with-provider", args=[provider.pk]), follow=False) - self.assertEqual(result.status_code, 403) - self.client.force_login(self.staff) - result = self.client.get(reverse("members-with-provider", args=[provider.pk]), follow=False) - self.assertEqual(result.status_code, 200) - # check that it contains the two members - self.assertIn(member1.profile, result.context["members"]) - self.assertIn(member2.profile, result.context["members"]) - - def test_remove_banned_provider(self): - user = ProfileFactory().user - # add a banned provider - provider = BannedEmailProvider.objects.create(moderator=self.staff, provider="test-remove.com") - # check that this option is only available for a staff member - self.client.force_login(user) - result = self.client.post(reverse("check-new-email-provider", args=[provider.pk]), follow=False) - self.assertEqual(result.status_code, 403) - # test that it removes the provider - self.client.force_login(self.staff) - result = self.client.post(reverse("remove-banned-email-provider", args=[provider.pk]), follow=False) - self.assertEqual(result.status_code, 302) - self.assertFalse(BannedEmailProvider.objects.filter(pk=provider.pk).exists()) - - def test_hats_on_profile(self): - hat_name = "A hat" - - profile = ProfileFactory() - user = profile.user - # Test that hats don't appear if there are no hats - self.client.force_login(user) - result = self.client.get(profile.get_absolute_url()) - self.assertNotContains(result, _("Casquettes")) - # Test that they don't appear with a staff member but that the link to add one does appear - self.client.force_login(self.staff) - result = self.client.get(profile.get_absolute_url()) - self.assertNotContains(result, _("Casquettes")) - self.assertContains(result, _("Ajouter une casquette")) - # Add a hat and check that it appears - self.client.post(reverse("add-hat", args=[user.pk]), {"hat": hat_name}, follow=False) - self.assertIn(hat_name, profile.hats.values_list("name", flat=True)) - result = self.client.get(profile.get_absolute_url()) - self.assertContains(result, _("Casquettes")) - self.assertContains(result, hat_name) - # And also for a member that is not staff - self.client.force_login(user) - result = self.client.get(profile.get_absolute_url()) - self.assertContains(result, _("Casquettes")) - self.assertContains(result, hat_name) - # Test that a hat linked to a group appears - result = self.client.get(self.staff.profile.get_absolute_url()) - self.assertContains(result, _("Casquettes")) - self.assertContains(result, "Staff") - - def test_add_hat(self): - short_hat = "A new hat" - long_hat = "A very long hat" * 3 - utf8mb4_hat = "🍊" - - profile = ProfileFactory() - user = profile.user - # check that this option is only available for a staff member - self.client.force_login(user) - result = self.client.post(reverse("add-hat", args=[user.pk]), {"hat": short_hat}, follow=False) - self.assertEqual(result.status_code, 403) - # login as staff - self.client.force_login(self.staff) - # test that it doesn't work with a too long hat (> 40 characters) - result = self.client.post(reverse("add-hat", args=[user.pk]), {"hat": long_hat}, follow=False) - self.assertEqual(result.status_code, 302) - self.assertNotIn(long_hat, profile.hats.values_list("name", flat=True)) - # test that it doesn't work with a hat using utf8mb4 characters - result = self.client.post(reverse("add-hat", args=[user.pk]), {"hat": utf8mb4_hat}, follow=False) - self.assertEqual(result.status_code, 302) - self.assertNotIn(utf8mb4_hat, profile.hats.values_list("name", flat=True)) - # test that it doesn't work with a hat linked to a group - result = self.client.post(reverse("add-hat", args=[user.pk]), {"hat": "Staff"}, follow=False) - self.assertEqual(result.status_code, 302) - self.assertNotIn(long_hat, profile.hats.values_list("name", flat=True)) - # test that it works with a short hat (<= 40 characters) - result = self.client.post(reverse("add-hat", args=[user.pk]), {"hat": short_hat}, follow=False) - self.assertEqual(result.status_code, 302) - self.assertIn(short_hat, profile.hats.values_list("name", flat=True)) - # test that if the hat already exists, it is used - result = self.client.post(reverse("add-hat", args=[self.staff.pk]), {"hat": short_hat}, follow=False) - self.assertEqual(result.status_code, 302) - self.assertIn(short_hat, self.staff.profile.hats.values_list("name", flat=True)) - self.assertEqual(Hat.objects.filter(name=short_hat).count(), 1) - - def test_remove_hat(self): - hat_name = "A hat" - - profile = ProfileFactory() - user = profile.user - # add a hat with a staff member - self.client.force_login(self.staff) - self.client.post(reverse("add-hat", args=[user.pk]), {"hat": hat_name}, follow=False) - self.assertIn(hat_name, profile.hats.values_list("name", flat=True)) - hat = Hat.objects.get(name=hat_name) - # test that this option is not available for an other user - self.client.force_login(ProfileFactory().user) - result = self.client.post(reverse("remove-hat", args=[user.pk, hat.pk]), follow=False) - self.assertEqual(result.status_code, 403) - self.assertIn(hat, profile.hats.all()) - # but check that it works for the user having the hat - self.client.force_login(user) - result = self.client.post(reverse("remove-hat", args=[user.pk, hat.pk]), follow=False) - self.assertEqual(result.status_code, 302) - self.assertNotIn(hat, profile.hats.all()) - # test that it works for a staff member - profile.hats.add(hat) # we have to add the hat again for this test - self.client.force_login(self.staff) - result = self.client.post(reverse("remove-hat", args=[user.pk, hat.pk]), follow=False) - self.assertEqual(result.status_code, 302) - self.assertNotIn(hat, profile.hats.all()) - # but check that the hat still exists in database - self.assertTrue(Hat.objects.filter(name=hat_name).exists()) - - def test_old_smileys(self): - """Test the cookie""" - - # NOTE: we have to use the "real" login and logout pages here - cookie_key = settings.ZDS_APP["member"]["old_smileys_cookie_key"] - - profile_without_clem = ProfileFactory() - profile_without_clem = Profile.objects.get(pk=profile_without_clem.pk) - self.assertFalse(profile_without_clem.use_old_smileys) - - user_without_clem = profile_without_clem.user - profile_with_clem = ProfileFactory() - profile_with_clem.use_old_smileys = True - profile_with_clem.save() - user_with_clem = profile_with_clem.user - - settings.ZDS_APP["member"]["old_smileys_allowed"] = True - - # test that the cookie is set when connection - result = self.client.post( - reverse("member-login"), - {"username": user_with_clem.username, "password": "hostel77", "remember": "remember"}, - follow=False, - ) - self.assertEqual(result.status_code, 302) - self.client.get(reverse("homepage")) - - self.assertIn(cookie_key, self.client.cookies) - self.assertNotEqual(self.client.cookies[cookie_key]["expires"], 0) - - # test that logout set the cookies expiration to 0 (= no more cookie) - self.client.post(reverse("member-logout"), follow=True) - self.client.get(reverse("homepage")) - self.assertEqual(self.client.cookies[cookie_key]["expires"], 0) - - # test that user without the setting have the cookie with expiration 0 (= no cookie) - result = self.client.post( - reverse("member-login"), - {"username": user_without_clem.username, "password": "hostel77", "remember": "remember"}, - follow=False, - ) - - self.assertEqual(result.status_code, 302) - self.assertEqual(self.client.cookies[cookie_key]["expires"], 0) - - # setting use_smileys sets the cookie - self.client.post( - reverse("update-member"), - {"biography": "", "site": "", "avatar_url": "", "sign": "", "options": ["use_old_smileys"]}, - ) - self.client.get(reverse("homepage")) - - profile_without_clem = Profile.objects.get(pk=profile_without_clem.pk) - self.assertTrue(profile_without_clem.use_old_smileys) - self.assertNotEqual(self.client.cookies[cookie_key]["expires"], 0) - - # ... and that not setting it removes the cookie - self.client.post( - reverse("update-member"), {"biography": "", "site": "", "avatar_url": "", "sign": "", "options": []} - ) - self.client.get(reverse("homepage")) - - profile_without_clem = Profile.objects.get(pk=profile_without_clem.pk) - self.assertFalse(profile_without_clem.use_old_smileys) - self.assertEqual(self.client.cookies[cookie_key]["expires"], 0) - - def test_hats_settings(self): - hat_name = "A hat" - other_hat_name = "Another hat" - hat, _ = Hat.objects.get_or_create(name__iexact=hat_name, defaults={"name": hat_name}) - requests_count = HatRequest.objects.count() - profile = ProfileFactory() - profile.hats.add(hat) - # login and check that the hat appears - self.client.force_login(profile.user) - result = self.client.get(reverse("hats-settings")) - self.assertEqual(result.status_code, 200) - self.assertContains(result, hat_name) - # check that it's impossible to ask for a hat the user already has - result = self.client.post( - reverse("hats-settings"), - { - "hat": hat_name, - "reason": "test", - }, - follow=False, - ) - self.assertEqual(result.status_code, 200) - self.assertEqual(HatRequest.objects.count(), requests_count) # request wasn't sent - # ask for another hat - result = self.client.post( - reverse("hats-settings"), - { - "hat": other_hat_name, - "reason": "test", - }, - follow=False, - ) - self.assertEqual(result.status_code, 302) - self.assertEqual(HatRequest.objects.count(), requests_count + 1) # request was sent! - # check the request appears - result = self.client.get(reverse("hats-settings")) - self.assertEqual(result.status_code, 200) - self.assertContains(result, other_hat_name) - # and check it's impossible to ask for it again - result = self.client.post( - reverse("hats-settings"), - { - "hat": other_hat_name, - "reason": "test", - }, - follow=False, - ) - self.assertEqual(result.status_code, 200) - self.assertEqual(HatRequest.objects.count(), requests_count + 1) # request wasn't sent - # check that it's impossible to ask for a hat linked to a group - result = self.client.post( - reverse("hats-settings"), - { - "hat": "Staff", - "reason": "test", - }, - follow=False, - ) - self.assertEqual(result.status_code, 200) - self.assertEqual(HatRequest.objects.count(), requests_count + 1) # request wasn't sent - - def test_requested_hats(self): - hat_name = "A hat" - # ask for a hat - profile = ProfileFactory() - self.client.force_login(profile.user) - result = self.client.post( - reverse("hats-settings"), - { - "hat": hat_name, - "reason": "test", - }, - follow=False, - ) - self.assertEqual(result.status_code, 302) - # test this page is only available for staff - result = self.client.get(reverse("requested-hats")) - self.assertEqual(result.status_code, 403) - # login as staff - self.client.force_login(self.staff) - # test the count displayed on the user menu is right - requests_count = HatRequest.objects.count() - result = self.client.get(reverse("pages-index")) - self.assertEqual(result.status_code, 200) - self.assertContains(result, f"({requests_count})") - # test that the hat asked appears on the requested hats page - result = self.client.get(reverse("requested-hats")) - self.assertEqual(result.status_code, 200) - self.assertContains(result, hat_name) - - def test_hat_request_detail(self): - hat_name = "A hat" - # ask for a hat - profile = ProfileFactory() - self.client.force_login(profile.user) - result = self.client.post( - reverse("hats-settings"), - { - "hat": hat_name, - "reason": "test", - }, - follow=False, - ) - self.assertEqual(result.status_code, 302) - request = HatRequest.objects.latest("date") - # test this page is available for the request author - result = self.client.get(request.get_absolute_url()) - self.assertEqual(result.status_code, 200) - # test it's not available for another user - other_user = ProfileFactory().user - self.client.force_login(other_user) - result = self.client.get(request.get_absolute_url()) - self.assertEqual(result.status_code, 403) - # login as staff - self.client.force_login(self.staff) - # test the page works - result = self.client.get(request.get_absolute_url()) - self.assertEqual(result.status_code, 200) - self.assertContains(result, hat_name) - self.assertContains(result, profile.user.username) - self.assertContains(result, request.reason) - - def test_solve_hat_request(self): - hat_name = "A hat" - # ask for a hat - profile = ProfileFactory() - self.client.force_login(profile.user) - result = self.client.post( - reverse("hats-settings"), - { - "hat": hat_name, - "reason": "test", - }, - follow=False, - ) - self.assertEqual(result.status_code, 302) - request = HatRequest.objects.latest("date") - # test this page is only available for staff - result = self.client.post(reverse("solve-hat-request", args=[request.pk]), follow=False) - self.assertEqual(result.status_code, 403) - # test denying as staff - self.client.force_login(self.staff) - result = self.client.post(reverse("solve-hat-request", args=[request.pk]), follow=False) - self.assertEqual(result.status_code, 302) - self.assertNotIn(hat_name, [h.name for h in profile.hats.all()]) - request = HatRequest.objects.get(pk=request.pk) # reload - self.assertEqual(request.is_granted, False) - # add a new request and test granting - HatRequest.objects.create(user=profile.user, hat=hat_name, reason="test") - request = HatRequest.objects.latest("date") - result = self.client.post(reverse("solve-hat-request", args=[request.pk]), {"grant": "on"}, follow=False) - self.assertEqual(result.status_code, 302) - self.assertIn(hat_name, [h.name for h in profile.hats.all()]) - request = HatRequest.objects.get(pk=request.pk) # reload - self.assertEqual(request.is_granted, True) - - def test_hats_list(self): - # test the page is accessible without being authenticated - self.client.logout() - result = self.client.get(reverse("hats-list")) - self.assertEqual(result.status_code, 200) - # and while being authenticated - self.client.force_login(self.staff) - result = self.client.get(reverse("hats-list")) - self.assertEqual(result.status_code, 200) - # test that it does contain the name of a hat - self.assertContains(result, "Staff") # this hat hat was created with the staff user - - def test_hat_detail(self): - # we will use the staff hat, created with the staff user - hat = Hat.objects.get(name="Staff") - # test the page is accessible without being authenticated - self.client.logout() - result = self.client.get(hat.get_absolute_url()) - self.assertEqual(result.status_code, 200) - # and while being authenticated - self.client.force_login(self.staff) - result = self.client.get(hat.get_absolute_url()) - self.assertEqual(result.status_code, 200) - # test that it does contain the name of a hat - self.assertContains(result, hat.name) - # and the name of a user having it - self.client.logout() # to prevent the username from being shown in topbar - result = self.client.get(hat.get_absolute_url()) - self.assertEqual(result.status_code, 200) - self.assertContains(result, self.staff.username) - # if we display this group on the contact page... - GroupContact.objects.create(group=Group.objects.get(name="staff"), description="group description", position=1) - # the description should be shown on this page too - result = self.client.get(hat.get_absolute_url()) - self.assertEqual(result.status_code, 200) - self.assertContains(result, "group description") - - def test_profile_report(self): - profile = ProfileFactory() - self.client.logout() - alerts_count = Alert.objects.count() - # test that authentication is required to report a profile - result = self.client.post(reverse("report-profile", args=[profile.pk]), {"reason": "test"}, follow=False) - self.assertEqual(result.status_code, 302) - self.assertEqual(alerts_count, Alert.objects.count()) - # login and check it doesn't work without reason - self.client.force_login(self.staff) - result = self.client.post(reverse("report-profile", args=[profile.pk]), {"reason": ""}, follow=False) - self.assertEqual(result.status_code, 302) - self.assertEqual(alerts_count, Alert.objects.count()) - # add a reason and check it works - result = self.client.post(reverse("report-profile", args=[profile.pk]), {"reason": "test"}, follow=False) - self.assertEqual(result.status_code, 302) - self.assertEqual(alerts_count + 1, Alert.objects.count()) - # test alert solving - alert = Alert.objects.latest("pubdate") - pm_count = PrivateTopic.objects.count() - result = self.client.post(reverse("solve-profile-alert", args=[alert.pk]), {"text": "ok"}, follow=False) - self.assertEqual(result.status_code, 302) - alert = Alert.objects.get(pk=alert.pk) # refresh - self.assertTrue(alert.solved) - self.assertEqual(pm_count + 1, PrivateTopic.objects.count()) - - -mail_backend = Mock() - - -class FakeBackend(BaseEmailBackend): - def send_messages(self, email_messages): - return mail_backend.send_messages(email_messages) - - -@override_settings(EMAIL_BACKEND="zds.member.tests.tests_views.FakeBackend") -class RegisterTest(TestCase): - def test_exception_on_mail(self): - def send_messages(messages): - print("message sent") - raise SMTPException(messages) - - mail_backend.send_messages = send_messages - - result = self.client.post( - reverse("register-member"), - { - "username": "firm1", - "password": "flavour", - "password_confirm": "flavour", - "email": "firm1@zestedesavoir.com", - }, - follow=False, - ) - self.assertEqual(result.status_code, 200) - self.assertIn(escape("Impossible d'envoyer l'email."), result.content.decode("utf-8")) - - -class IpListingsTests(TestCase): - """Test the member_from_ip function : listing users from a same IPV4/IPV6 address or same IPV6 network.""" - - def setUp(self) -> None: - self.staff = StaffProfileFactory().user - self.regular_user = ProfileFactory() - - self.user_ipv4_same_ip_1 = ProfileFactory(last_ip_address="155.128.92.54") - self.user_ipv4_same_ip_1.user.username = "user_ipv4_same_ip_1" - self.user_ipv4_same_ip_1.user.save() - - self.user_ipv4_same_ip_2 = ProfileFactory(last_ip_address="155.128.92.54") - self.user_ipv4_same_ip_2.user.username = "user_ipv4_same_ip_2" - self.user_ipv4_same_ip_2.user.save() - - self.user_ipv4_different_ip = ProfileFactory(last_ip_address="155.128.92.55") - self.user_ipv4_different_ip.user.username = "user_ipv4_different_ip" - self.user_ipv4_different_ip.user.save() - - self.user_ipv6_same_ip_1 = ProfileFactory(last_ip_address="2001:8f8:1425:60a0:7981:9852:1493:3721") - self.user_ipv6_same_ip_1.user.username = "user_ipv6_same_ip_1" - self.user_ipv6_same_ip_1.user.save() - - self.user_ipv6_same_ip_2 = ProfileFactory(last_ip_address="2001:8f8:1425:60a0:7981:9852:1493:3721") - self.user_ipv6_same_ip_2.user.username = "user_ipv6_same_ip_2" - self.user_ipv6_same_ip_2.user.save() - - self.user_ipv6_same_network = ProfileFactory(last_ip_address="2001:8f8:1425:60a0:9852:7981:3721:1493") - self.user_ipv6_same_network.user.username = "user_ipv6_same_network" - self.user_ipv6_same_network.user.save() - - self.user_ipv6_different_network = ProfileFactory(last_ip_address="8f8:60a0:3721:1425:7981:1493:2001:9852") - self.user_ipv6_different_network.user.username = "user_ipv6_different_network" - self.user_ipv6_different_network.user.save() - - def test_same_ipv4(self) -> None: - self.client.force_login(self.staff) - response = self.client.get(reverse(member_from_ip, args=[self.user_ipv4_same_ip_1.last_ip_address])) - self.assertContains(response, self.user_ipv4_same_ip_1.user.username) - self.assertContains(response, self.user_ipv4_same_ip_2.user.username) - self.assertContains(response, self.user_ipv4_same_ip_1.last_ip_address) - self.assertNotContains(response, self.user_ipv4_different_ip.user.username) - - def test_different_ipv4(self) -> None: - self.client.force_login(self.staff) - response = self.client.get(reverse(member_from_ip, args=[self.user_ipv4_different_ip.last_ip_address])) - self.assertContains(response, self.user_ipv4_different_ip.user.username) - self.assertContains(response, self.user_ipv4_different_ip.last_ip_address) - self.assertNotContains(response, self.user_ipv6_same_ip_1.user.username) - - def test_same_ipv6_and_same_ipv6_network(self) -> None: - self.client.force_login(self.staff) - response = self.client.get(reverse(member_from_ip, args=[self.user_ipv6_same_ip_1.last_ip_address])) - self.assertContains(response, self.user_ipv6_same_ip_1.user.username) - self.assertContains(response, self.user_ipv6_same_ip_2.user.username) - self.assertContains(response, self.user_ipv6_same_network.user.username) - self.assertNotContains(response, self.user_ipv6_different_network.user.username) - - def test_same_ipv6_network_but_different_ip(self) -> None: - self.client.force_login(self.staff) - response = self.client.get(reverse(member_from_ip, args=[self.user_ipv6_same_network.last_ip_address])) - self.assertContains(response, self.user_ipv6_same_network.user.username) - self.assertContains(response, self.user_ipv6_same_ip_1.user.username) - self.assertContains(response, self.user_ipv6_same_ip_2.user.username) - self.assertNotContains(response, self.user_ipv6_different_network.user.username) - - def test_different_ipv6_network(self) -> None: - self.client.force_login(self.staff) - response = self.client.get(reverse(member_from_ip, args=[self.user_ipv6_different_network.last_ip_address])) - self.assertContains(response, self.user_ipv6_different_network.user.username) - self.assertNotContains(response, self.user_ipv6_same_ip_1.user.username) - self.assertNotContains(response, self.user_ipv6_same_ip_2.user.username) - self.assertNotContains(response, self.user_ipv6_same_network.user.username) - - def test_access_rights_to_ip_page_as_regular_user(self) -> None: - self.client.force_login(self.regular_user.user) - response = self.client.get(reverse(member_from_ip, args=["0.0.0.0"])) - self.assertEqual(response.status_code, 403) - - def test_access_rights_to_ip_page_as_anonymous(self) -> None: - response = self.client.get(reverse(member_from_ip, args=["0.0.0.0"])) - self.assertEqual(response.status_code, 302) - - def test_access_rights_to_ip_page_as_staff(self) -> None: - self.client.force_login(self.staff) - response = self.client.get(reverse(member_from_ip, args=["0.0.0.0"])) - self.assertEqual(response.status_code, 200) - - def test_template_used_by_ip_page(self) -> None: - self.client.force_login(self.staff) - response = self.client.get(reverse(member_from_ip, args=["0.0.0.0"])) - self.assertTemplateUsed(response, "member/admin/memberip.html") diff --git a/zds/member/tests/views/__init__.py b/zds/member/tests/views/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/zds/member/tests/views/tests_admin.py b/zds/member/tests/views/tests_admin.py new file mode 100644 index 0000000000..0d60d4fb45 --- /dev/null +++ b/zds/member/tests/views/tests_admin.py @@ -0,0 +1,117 @@ +from django.conf import settings +from django.contrib.auth.models import Group +from django.urls import reverse +from django.test import TestCase + +from zds.notification.models import TopicAnswerSubscription +from zds.member.tests.factories import ProfileFactory, StaffProfileFactory, UserFactory +from zds.member.models import Profile +from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory, TopicFactory + + +class MemberTests(TestCase): + def setUp(self): + settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" + self.mas = ProfileFactory() + settings.ZDS_APP["member"]["bot_account"] = self.mas.user.username + self.anonymous = UserFactory(username=settings.ZDS_APP["member"]["anonymous_account"], password="anything") + self.external = UserFactory(username=settings.ZDS_APP["member"]["external_account"], password="anything") + self.category1 = ForumCategoryFactory(position=1) + self.forum11 = ForumFactory(category=self.category1, position_in_category=1) + self.staff = StaffProfileFactory().user + + self.bot = Group(name=settings.ZDS_APP["member"]["bot_group"]) + self.bot.save() + + def test_promote_interface(self): + """ + Test promotion interface. + """ + + # create users (one regular, one staff and one superuser) + tester = ProfileFactory() + staff = StaffProfileFactory() + tester.user.is_active = False + tester.user.save() + staff.user.is_superuser = True + staff.user.save() + + # create groups + group = Group.objects.create(name="DummyGroup_1") + groupbis = Group.objects.create(name="DummyGroup_2") + + # create Forums, Posts and subscribe member to them. + category1 = ForumCategoryFactory(position=1) + forum1 = ForumFactory(category=category1, position_in_category=1) + forum1.groups.add(group) + forum1.save() + forum2 = ForumFactory(category=category1, position_in_category=2) + forum2.groups.add(groupbis) + forum2.save() + forum3 = ForumFactory(category=category1, position_in_category=3) + topic1 = TopicFactory(forum=forum1, author=staff.user) + topic2 = TopicFactory(forum=forum2, author=staff.user) + topic3 = TopicFactory(forum=forum3, author=staff.user) + + # LET THE TEST BEGIN ! + + # tester shouldn't be able to connect + login_check = self.client.login(username=tester.user.username, password="hostel77") + self.assertEqual(login_check, False) + + # connect as staff (superuser) + self.client.force_login(staff.user) + + # check that we can go through the page + result = self.client.get(reverse("member-settings-promote", kwargs={"user_pk": tester.user.id}), follow=False) + self.assertEqual(result.status_code, 200) + + # give groups thanks to staff (but account still not activated) + result = self.client.post( + reverse("member-settings-promote", kwargs={"user_pk": tester.user.id}), + { + "groups": [group.id, groupbis.id], + }, + follow=False, + ) + self.assertEqual(result.status_code, 302) + tester = Profile.objects.get(id=tester.id) # refresh + + self.assertEqual(len(tester.user.groups.all()), 2) + self.assertFalse(tester.user.is_active) + + # Now our tester is going to follow one post in every forum (3) + TopicAnswerSubscription.objects.toggle_follow(topic1, tester.user) + TopicAnswerSubscription.objects.toggle_follow(topic2, tester.user) + TopicAnswerSubscription.objects.toggle_follow(topic3, tester.user) + + self.assertEqual(len(TopicAnswerSubscription.objects.get_objects_followed_by(tester.user)), 3) + + # retract all right, keep one group only and activate account + result = self.client.post( + reverse("member-settings-promote", kwargs={"user_pk": tester.user.id}), + {"groups": [group.id], "activation": "on"}, + follow=False, + ) + self.assertEqual(result.status_code, 302) + tester = Profile.objects.get(id=tester.id) # refresh + + self.assertEqual(len(tester.user.groups.all()), 1) + self.assertTrue(tester.user.is_active) + self.assertEqual(len(TopicAnswerSubscription.objects.get_objects_followed_by(tester.user)), 2) + + # no groups specified + result = self.client.post( + reverse("member-settings-promote", kwargs={"user_pk": tester.user.id}), {"activation": "on"}, follow=False + ) + self.assertEqual(result.status_code, 302) + tester = Profile.objects.get(id=tester.id) # refresh + self.assertEqual(len(TopicAnswerSubscription.objects.get_objects_followed_by(tester.user)), 1) + + # Finally, check that user can connect and can not access the interface + login_check = self.client.login(username=tester.user.username, password="hostel77") + self.assertEqual(login_check, True) + result = self.client.post( + reverse("member-settings-promote", kwargs={"user_pk": staff.user.id}), {"activation": "on"}, follow=False + ) + self.assertEqual(result.status_code, 403) # forbidden ! diff --git a/zds/member/tests/views/tests_emailproviders.py b/zds/member/tests/views/tests_emailproviders.py new file mode 100644 index 0000000000..a3039ab422 --- /dev/null +++ b/zds/member/tests/views/tests_emailproviders.py @@ -0,0 +1,153 @@ +from django.conf import settings +from django.contrib.auth.models import Group, User +from django.urls import reverse +from django.test import TestCase + +from zds.member.tests.factories import ProfileFactory, StaffProfileFactory, UserFactory +from zds.member.models import NewEmailProvider, BannedEmailProvider, TokenRegister +from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory + + +class EmailProvidersTests(TestCase): + def setUp(self): + settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" + self.mas = ProfileFactory() + settings.ZDS_APP["member"]["bot_account"] = self.mas.user.username + self.anonymous = UserFactory(username=settings.ZDS_APP["member"]["anonymous_account"], password="anything") + self.external = UserFactory(username=settings.ZDS_APP["member"]["external_account"], password="anything") + self.category1 = ForumCategoryFactory(position=1) + self.forum11 = ForumFactory(category=self.category1, position_in_category=1) + self.staff = StaffProfileFactory().user + + self.bot = Group(name=settings.ZDS_APP["member"]["bot_group"]) + self.bot.save() + + def test_new_provider_with_email_edit(self): + new_providers_count = NewEmailProvider.objects.count() + user = ProfileFactory().user + self.client.force_login(user) + # Edit the email with an unknown provider + self.client.post( + reverse("update-username-email-member"), + {"username": user.username, "email": "test@unknown-provider-edit.com"}, + follow=False, + ) + # A new provider object should have been created + self.assertEqual(new_providers_count + 1, NewEmailProvider.objects.count()) + + def test_new_providers_list(self): + # create a new provider + user = ProfileFactory().user + provider = NewEmailProvider.objects.create(use="NEW_ACCOUNT", user=user, provider="test.com") + # check that the list is not available for a non-staff member + self.client.logout() + result = self.client.get(reverse("new-email-providers"), follow=False) + self.assertEqual(result.status_code, 302) + self.client.force_login(user) + result = self.client.get(reverse("new-email-providers"), follow=False) + self.assertEqual(result.status_code, 403) + # and that it contains the provider we created + self.client.force_login(self.staff) + result = self.client.get(reverse("new-email-providers"), follow=False) + self.assertEqual(result.status_code, 200) + self.assertIn(provider, result.context["providers"]) + + def test_check_new_provider(self): + # create two new providers + user = ProfileFactory().user + provider1 = NewEmailProvider.objects.create(use="NEW_ACCOUNT", user=user, provider="test1.com") + provider2 = NewEmailProvider.objects.create(use="EMAIl_EDIT", user=user, provider="test2.com") + # check that this option is only available for a staff member + self.client.force_login(user) + result = self.client.post(reverse("check-new-email-provider", args=[provider1.pk]), follow=False) + self.assertEqual(result.status_code, 403) + # test approval + self.client.force_login(self.staff) + result = self.client.post(reverse("check-new-email-provider", args=[provider1.pk]), follow=False) + self.assertEqual(result.status_code, 302) + self.assertFalse(NewEmailProvider.objects.filter(pk=provider1.pk).exists()) + self.assertFalse(BannedEmailProvider.objects.filter(provider=provider1.provider).exists()) + # test ban + self.client.force_login(self.staff) + result = self.client.post(reverse("check-new-email-provider", args=[provider2.pk]), {"ban": "on"}, follow=False) + self.assertEqual(result.status_code, 302) + self.assertFalse(NewEmailProvider.objects.filter(pk=provider2.pk).exists()) + self.assertTrue(BannedEmailProvider.objects.filter(provider=provider2.provider).exists()) + + def test_banned_providers_list(self): + user = ProfileFactory().user + # create a banned provider + provider = BannedEmailProvider.objects.create(moderator=self.staff, provider="test.com") + # check that the list is not available for a non-staff member + self.client.logout() + result = self.client.get(reverse("banned-email-providers"), follow=False) + self.assertEqual(result.status_code, 302) + self.client.force_login(user) + result = self.client.get(reverse("banned-email-providers"), follow=False) + self.assertEqual(result.status_code, 403) + # and that it contains the provider we created + self.client.force_login(self.staff) + result = self.client.get(reverse("banned-email-providers"), follow=False) + self.assertEqual(result.status_code, 200) + self.assertIn(provider, result.context["providers"]) + + def test_add_banned_provider(self): + # test that this page is only available for staff + user = ProfileFactory().user + self.client.logout() + result = self.client.get(reverse("add-banned-email-provider"), follow=False) + self.assertEqual(result.status_code, 302) + self.client.force_login(user) + result = self.client.get(reverse("add-banned-email-provider"), follow=False) + self.assertEqual(result.status_code, 403) + self.client.force_login(self.staff) + result = self.client.get(reverse("add-banned-email-provider"), follow=False) + self.assertEqual(result.status_code, 200) + + # add a provider + result = self.client.post(reverse("add-banned-email-provider"), {"provider": "new-provider.com"}, follow=False) + self.assertEqual(result.status_code, 302) + self.assertTrue(BannedEmailProvider.objects.filter(provider="new-provider.com").exists()) + + # check that it cannot be added again + result = self.client.post(reverse("add-banned-email-provider"), {"provider": "new-provider.com"}, follow=False) + self.assertEqual(result.status_code, 200) + self.assertEqual(1, BannedEmailProvider.objects.filter(provider="new-provider.com").count()) + + def test_members_with_provider(self): + # create two members with the same provider + member1 = ProfileFactory().user + member2 = ProfileFactory().user + member1.email = "test1@test-members.com" + member1.save() + member2.email = "test2@test-members.com" + member2.save() + # ban this provider + provider = BannedEmailProvider.objects.create(moderator=self.staff, provider="test-members.com") + # check that this page is only available for staff + self.client.logout() + result = self.client.get(reverse("members-with-provider", args=[provider.pk]), follow=False) + self.assertEqual(result.status_code, 302) + self.client.force_login(member1) + result = self.client.get(reverse("members-with-provider", args=[provider.pk]), follow=False) + self.assertEqual(result.status_code, 403) + self.client.force_login(self.staff) + result = self.client.get(reverse("members-with-provider", args=[provider.pk]), follow=False) + self.assertEqual(result.status_code, 200) + # check that it contains the two members + self.assertIn(member1.profile, result.context["members"]) + self.assertIn(member2.profile, result.context["members"]) + + def test_remove_banned_provider(self): + user = ProfileFactory().user + # add a banned provider + provider = BannedEmailProvider.objects.create(moderator=self.staff, provider="test-remove.com") + # check that this option is only available for a staff member + self.client.force_login(user) + result = self.client.post(reverse("check-new-email-provider", args=[provider.pk]), follow=False) + self.assertEqual(result.status_code, 403) + # test that it removes the provider + self.client.force_login(self.staff) + result = self.client.post(reverse("remove-banned-email-provider", args=[provider.pk]), follow=False) + self.assertEqual(result.status_code, 302) + self.assertFalse(BannedEmailProvider.objects.filter(pk=provider.pk).exists()) diff --git a/zds/member/tests/views/tests_hats.py b/zds/member/tests/views/tests_hats.py new file mode 100644 index 0000000000..05b87b0fab --- /dev/null +++ b/zds/member/tests/views/tests_hats.py @@ -0,0 +1,312 @@ +from django.conf import settings +from django.contrib.auth.models import Group +from django.urls import reverse +from django.test import TestCase +from django.utils.translation import gettext_lazy as _ + +from zds.member.tests.factories import ProfileFactory, StaffProfileFactory, UserFactory +from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory +from zds.pages.models import GroupContact +from zds.utils.models import Hat, HatRequest + + +class HatTests(TestCase): + def setUp(self): + settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" + self.mas = ProfileFactory() + settings.ZDS_APP["member"]["bot_account"] = self.mas.user.username + self.anonymous = UserFactory(username=settings.ZDS_APP["member"]["anonymous_account"], password="anything") + self.external = UserFactory(username=settings.ZDS_APP["member"]["external_account"], password="anything") + self.category1 = ForumCategoryFactory(position=1) + self.forum11 = ForumFactory(category=self.category1, position_in_category=1) + self.staff = StaffProfileFactory().user + + self.bot = Group(name=settings.ZDS_APP["member"]["bot_group"]) + self.bot.save() + + def test_hats_on_profile(self): + hat_name = "A hat" + + profile = ProfileFactory() + user = profile.user + # Test that hats don't appear if there are no hats + self.client.force_login(user) + result = self.client.get(profile.get_absolute_url()) + self.assertNotContains(result, _("Casquettes")) + # Test that they don't appear with a staff member but that the link to add one does appear + self.client.force_login(self.staff) + result = self.client.get(profile.get_absolute_url()) + self.assertNotContains(result, _("Casquettes")) + self.assertContains(result, _("Ajouter une casquette")) + # Add a hat and check that it appears + self.client.post(reverse("add-hat", args=[user.pk]), {"hat": hat_name}, follow=False) + self.assertIn(hat_name, profile.hats.values_list("name", flat=True)) + result = self.client.get(profile.get_absolute_url()) + self.assertContains(result, _("Casquettes")) + self.assertContains(result, hat_name) + # And also for a member that is not staff + self.client.force_login(user) + result = self.client.get(profile.get_absolute_url()) + self.assertContains(result, _("Casquettes")) + self.assertContains(result, hat_name) + # Test that a hat linked to a group appears + result = self.client.get(self.staff.profile.get_absolute_url()) + self.assertContains(result, _("Casquettes")) + self.assertContains(result, "Staff") + + def test_add_hat(self): + short_hat = "A new hat" + long_hat = "A very long hat" * 3 + utf8mb4_hat = "🍊" + + profile = ProfileFactory() + user = profile.user + # check that this option is only available for a staff member + self.client.force_login(user) + result = self.client.post(reverse("add-hat", args=[user.pk]), {"hat": short_hat}, follow=False) + self.assertEqual(result.status_code, 403) + # login as staff + self.client.force_login(self.staff) + # test that it doesn't work with a too long hat (> 40 characters) + result = self.client.post(reverse("add-hat", args=[user.pk]), {"hat": long_hat}, follow=False) + self.assertEqual(result.status_code, 302) + self.assertNotIn(long_hat, profile.hats.values_list("name", flat=True)) + # test that it doesn't work with a hat using utf8mb4 characters + result = self.client.post(reverse("add-hat", args=[user.pk]), {"hat": utf8mb4_hat}, follow=False) + self.assertEqual(result.status_code, 302) + self.assertNotIn(utf8mb4_hat, profile.hats.values_list("name", flat=True)) + # test that it doesn't work with a hat linked to a group + result = self.client.post(reverse("add-hat", args=[user.pk]), {"hat": "Staff"}, follow=False) + self.assertEqual(result.status_code, 302) + self.assertNotIn(long_hat, profile.hats.values_list("name", flat=True)) + # test that it works with a short hat (<= 40 characters) + result = self.client.post(reverse("add-hat", args=[user.pk]), {"hat": short_hat}, follow=False) + self.assertEqual(result.status_code, 302) + self.assertIn(short_hat, profile.hats.values_list("name", flat=True)) + # test that if the hat already exists, it is used + result = self.client.post(reverse("add-hat", args=[self.staff.pk]), {"hat": short_hat}, follow=False) + self.assertEqual(result.status_code, 302) + self.assertIn(short_hat, self.staff.profile.hats.values_list("name", flat=True)) + self.assertEqual(Hat.objects.filter(name=short_hat).count(), 1) + + def test_remove_hat(self): + hat_name = "A hat" + + profile = ProfileFactory() + user = profile.user + # add a hat with a staff member + self.client.force_login(self.staff) + self.client.post(reverse("add-hat", args=[user.pk]), {"hat": hat_name}, follow=False) + self.assertIn(hat_name, profile.hats.values_list("name", flat=True)) + hat = Hat.objects.get(name=hat_name) + # test that this option is not available for an other user + self.client.force_login(ProfileFactory().user) + result = self.client.post(reverse("remove-hat", args=[user.pk, hat.pk]), follow=False) + self.assertEqual(result.status_code, 403) + self.assertIn(hat, profile.hats.all()) + # but check that it works for the user having the hat + self.client.force_login(user) + result = self.client.post(reverse("remove-hat", args=[user.pk, hat.pk]), follow=False) + self.assertEqual(result.status_code, 302) + self.assertNotIn(hat, profile.hats.all()) + # test that it works for a staff member + profile.hats.add(hat) # we have to add the hat again for this test + self.client.force_login(self.staff) + result = self.client.post(reverse("remove-hat", args=[user.pk, hat.pk]), follow=False) + self.assertEqual(result.status_code, 302) + self.assertNotIn(hat, profile.hats.all()) + # but check that the hat still exists in database + self.assertTrue(Hat.objects.filter(name=hat_name).exists()) + + def test_hats_settings(self): + hat_name = "A hat" + other_hat_name = "Another hat" + hat, _ = Hat.objects.get_or_create(name__iexact=hat_name, defaults={"name": hat_name}) + requests_count = HatRequest.objects.count() + profile = ProfileFactory() + profile.hats.add(hat) + # login and check that the hat appears + self.client.force_login(profile.user) + result = self.client.get(reverse("hats-settings")) + self.assertEqual(result.status_code, 200) + self.assertContains(result, hat_name) + # check that it's impossible to ask for a hat the user already has + result = self.client.post( + reverse("hats-settings"), + { + "hat": hat_name, + "reason": "test", + }, + follow=False, + ) + self.assertEqual(result.status_code, 200) + self.assertEqual(HatRequest.objects.count(), requests_count) # request wasn't sent + # ask for another hat + result = self.client.post( + reverse("hats-settings"), + { + "hat": other_hat_name, + "reason": "test", + }, + follow=False, + ) + self.assertEqual(result.status_code, 302) + self.assertEqual(HatRequest.objects.count(), requests_count + 1) # request was sent! + # check the request appears + result = self.client.get(reverse("hats-settings")) + self.assertEqual(result.status_code, 200) + self.assertContains(result, other_hat_name) + # and check it's impossible to ask for it again + result = self.client.post( + reverse("hats-settings"), + { + "hat": other_hat_name, + "reason": "test", + }, + follow=False, + ) + self.assertEqual(result.status_code, 200) + self.assertEqual(HatRequest.objects.count(), requests_count + 1) # request wasn't sent + # check that it's impossible to ask for a hat linked to a group + result = self.client.post( + reverse("hats-settings"), + { + "hat": "Staff", + "reason": "test", + }, + follow=False, + ) + self.assertEqual(result.status_code, 200) + self.assertEqual(HatRequest.objects.count(), requests_count + 1) # request wasn't sent + + def test_requested_hats(self): + hat_name = "A hat" + # ask for a hat + profile = ProfileFactory() + self.client.force_login(profile.user) + result = self.client.post( + reverse("hats-settings"), + { + "hat": hat_name, + "reason": "test", + }, + follow=False, + ) + self.assertEqual(result.status_code, 302) + # test this page is only available for staff + result = self.client.get(reverse("requested-hats")) + self.assertEqual(result.status_code, 403) + # login as staff + self.client.force_login(self.staff) + # test the count displayed on the user menu is right + requests_count = HatRequest.objects.count() + result = self.client.get(reverse("pages-index")) + self.assertEqual(result.status_code, 200) + self.assertContains(result, f"({requests_count})") + # test that the hat asked appears on the requested hats page + result = self.client.get(reverse("requested-hats")) + self.assertEqual(result.status_code, 200) + self.assertContains(result, hat_name) + + def test_hat_request_detail(self): + hat_name = "A hat" + # ask for a hat + profile = ProfileFactory() + self.client.force_login(profile.user) + result = self.client.post( + reverse("hats-settings"), + { + "hat": hat_name, + "reason": "test", + }, + follow=False, + ) + self.assertEqual(result.status_code, 302) + request = HatRequest.objects.latest("date") + # test this page is available for the request author + result = self.client.get(request.get_absolute_url()) + self.assertEqual(result.status_code, 200) + # test it's not available for another user + other_user = ProfileFactory().user + self.client.force_login(other_user) + result = self.client.get(request.get_absolute_url()) + self.assertEqual(result.status_code, 403) + # login as staff + self.client.force_login(self.staff) + # test the page works + result = self.client.get(request.get_absolute_url()) + self.assertEqual(result.status_code, 200) + self.assertContains(result, hat_name) + self.assertContains(result, profile.user.username) + self.assertContains(result, request.reason) + + def test_solve_hat_request(self): + hat_name = "A hat" + # ask for a hat + profile = ProfileFactory() + self.client.force_login(profile.user) + result = self.client.post( + reverse("hats-settings"), + { + "hat": hat_name, + "reason": "test", + }, + follow=False, + ) + self.assertEqual(result.status_code, 302) + request = HatRequest.objects.latest("date") + # test this page is only available for staff + result = self.client.post(reverse("solve-hat-request", args=[request.pk]), follow=False) + self.assertEqual(result.status_code, 403) + # test denying as staff + self.client.force_login(self.staff) + result = self.client.post(reverse("solve-hat-request", args=[request.pk]), follow=False) + self.assertEqual(result.status_code, 302) + self.assertNotIn(hat_name, [h.name for h in profile.hats.all()]) + request = HatRequest.objects.get(pk=request.pk) # reload + self.assertEqual(request.is_granted, False) + # add a new request and test granting + HatRequest.objects.create(user=profile.user, hat=hat_name, reason="test") + request = HatRequest.objects.latest("date") + result = self.client.post(reverse("solve-hat-request", args=[request.pk]), {"grant": "on"}, follow=False) + self.assertEqual(result.status_code, 302) + self.assertIn(hat_name, [h.name for h in profile.hats.all()]) + request = HatRequest.objects.get(pk=request.pk) # reload + self.assertEqual(request.is_granted, True) + + def test_hats_list(self): + # test the page is accessible without being authenticated + self.client.logout() + result = self.client.get(reverse("hats-list")) + self.assertEqual(result.status_code, 200) + # and while being authenticated + self.client.force_login(self.staff) + result = self.client.get(reverse("hats-list")) + self.assertEqual(result.status_code, 200) + # test that it does contain the name of a hat + self.assertContains(result, "Staff") # this hat hat was created with the staff user + + def test_hat_detail(self): + # we will use the staff hat, created with the staff user + hat = Hat.objects.get(name="Staff") + # test the page is accessible without being authenticated + self.client.logout() + result = self.client.get(hat.get_absolute_url()) + self.assertEqual(result.status_code, 200) + # and while being authenticated + self.client.force_login(self.staff) + result = self.client.get(hat.get_absolute_url()) + self.assertEqual(result.status_code, 200) + # test that it does contain the name of a hat + self.assertContains(result, hat.name) + # and the name of a user having it + self.client.logout() # to prevent the username from being shown in topbar + result = self.client.get(hat.get_absolute_url()) + self.assertEqual(result.status_code, 200) + self.assertContains(result, self.staff.username) + # if we display this group on the contact page... + GroupContact.objects.create(group=Group.objects.get(name="staff"), description="group description", position=1) + # the description should be shown on this page too + result = self.client.get(hat.get_absolute_url()) + self.assertEqual(result.status_code, 200) + self.assertContains(result, "group description") diff --git a/zds/member/tests/views/tests_login.py b/zds/member/tests/views/tests_login.py new file mode 100644 index 0000000000..f56c00e321 --- /dev/null +++ b/zds/member/tests/views/tests_login.py @@ -0,0 +1,80 @@ +from django.urls import reverse +from django.test import TestCase +from django.utils.translation import gettext_lazy as _ + +from zds.member.tests.factories import ProfileFactory, NonAsciiProfileFactory + + +class MemberTests(TestCase): + def test_login(self): + """ + To test user login. + """ + user = ProfileFactory() + + # login a user. Good password then redirection to the homepage. + result = self.client.post( + reverse("member-login"), + {"username": user.user.username, "password": "hostel77", "remember": "remember"}, + follow=False, + ) + self.assertRedirects(result, reverse("homepage")) + + # login failed with bad password then no redirection + # (status_code equals 200 and not 302). + result = self.client.post( + reverse("member-login"), + {"username": user.user.username, "password": "hostel", "remember": "remember"}, + follow=False, + ) + self.assertEqual(result.status_code, 200) + self.assertContains( + result, + _( + "Le mot de passe saisi est incorrect. " + "Cliquez sur le lien « Mot de passe oublié ? » " + "si vous ne vous en souvenez plus." + ), + ) + + # login failed with bad username then no redirection + # (status_code equals 200 and not 302). + result = self.client.post( + reverse("member-login"), {"username": "clem", "password": "hostel77", "remember": "remember"}, follow=False + ) + self.assertEqual(result.status_code, 200) + self.assertContains( + result, + _("Ce nom d’utilisateur est inconnu. " "Si vous ne possédez pas de compte, " "vous pouvez vous inscrire."), + ) + + # login a user. Good password and next parameter then + # redirection to the "next" page. + result = self.client.post( + reverse("member-login") + "?next=" + reverse("gallery-list"), + {"username": user.user.username, "password": "hostel77", "remember": "remember"}, + follow=False, + ) + self.assertRedirects(result, reverse("gallery-list")) + + # check the user is redirected to the home page if + # the "next" parameter points to a non-existing page. + result = self.client.post( + reverse("member-login") + "?next=/foobar", + {"username": user.user.username, "password": "hostel77", "remember": "remember"}, + follow=False, + ) + self.assertRedirects(result, reverse("homepage")) + + # check if the login form will redirect if there is + # a next parameter. + self.client.logout() + result = self.client.get(reverse("member-login") + "?next=" + reverse("gallery-list")) + self.assertContains(result, reverse("member-login") + "?next=" + reverse("gallery-list"), count=1) + + def test_nonascii(self): + user = NonAsciiProfileFactory() + result = self.client.get( + reverse("member-login") + "?next=" + reverse("member-detail", args=[user.user.username]), follow=False + ) + self.assertEqual(result.status_code, 200) diff --git a/zds/member/tests/views/tests_moderation.py b/zds/member/tests/views/tests_moderation.py new file mode 100644 index 0000000000..ae1f8a71d9 --- /dev/null +++ b/zds/member/tests/views/tests_moderation.py @@ -0,0 +1,518 @@ +from datetime import datetime + +from django.conf import settings +from django.core import mail +from django.contrib.auth.models import Group +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ +from django.test import TestCase + + +from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory +from zds.member.views.moderation import member_from_ip +from zds.member.tests.factories import ProfileFactory, StaffProfileFactory, UserFactory +from zds.member.models import Profile, Ban, KarmaNote + + +class TestsModeration(TestCase): + def setUp(self): + settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" + self.mas = ProfileFactory() + settings.ZDS_APP["member"]["bot_account"] = self.mas.user.username + self.anonymous = UserFactory(username=settings.ZDS_APP["member"]["anonymous_account"], password="anything") + self.external = UserFactory(username=settings.ZDS_APP["member"]["external_account"], password="anything") + self.category1 = ForumCategoryFactory(position=1) + self.forum11 = ForumFactory(category=self.category1, position_in_category=1) + self.staff = StaffProfileFactory().user + + self.bot = Group(name=settings.ZDS_APP["member"]["bot_group"]) + self.bot.save() + + def test_sanctions(self): + """ + Test various sanctions. + """ + + staff = StaffProfileFactory() + self.client.force_login(staff.user) + + # list of members. + result = self.client.get(reverse("member-list"), follow=False) + self.assertEqual(result.status_code, 200) + nb_users = len(result.context["members"]) + + # Test: LS + user_ls = ProfileFactory() + result = self.client.post( + reverse("member-modify-profile", kwargs={"user_pk": user_ls.user.id}), + {"ls": "", "ls-text": "Texte de test pour LS"}, + follow=False, + ) + user = Profile.objects.get(id=user_ls.id) # Refresh profile from DB + self.assertEqual(result.status_code, 302) + self.assertFalse(user.can_write) + self.assertTrue(user.can_read) + self.assertIsNone(user.end_ban_write) + self.assertIsNone(user.end_ban_read) + ban = Ban.objects.filter(user__id=user.user.id).order_by("-pubdate")[0] + self.assertEqual(ban.type, "Lecture seule illimitée") + self.assertEqual(ban.note, "Texte de test pour LS") + self.assertEqual(len(mail.outbox), 1) + + result = self.client.get(reverse("member-list"), follow=False) + self.assertEqual(result.status_code, 200) + self.assertEqual(nb_users + 1, len(result.context["members"])) # LS guy still shows up, good + + # Test: Un-LS + result = self.client.post( + reverse("member-modify-profile", kwargs={"user_pk": user_ls.user.id}), + {"un-ls": "", "unls-text": "Texte de test pour un-LS"}, + follow=False, + ) + user = Profile.objects.get(id=user_ls.id) # Refresh profile from DB + self.assertEqual(result.status_code, 302) + self.assertTrue(user.can_write) + self.assertTrue(user.can_read) + self.assertIsNone(user.end_ban_write) + self.assertIsNone(user.end_ban_read) + ban = Ban.objects.filter(user__id=user.user.id).order_by("-id")[0] + self.assertEqual(ban.type, "Levée de la lecture seule") + self.assertEqual(ban.note, "Texte de test pour un-LS") + self.assertEqual(len(mail.outbox), 2) + + result = self.client.get(reverse("member-list"), follow=False) + self.assertEqual(result.status_code, 200) + self.assertEqual(nb_users + 1, len(result.context["members"])) # LS guy still shows up, good + + # Test: LS temp + user_ls_temp = ProfileFactory() + result = self.client.post( + reverse("member-modify-profile", kwargs={"user_pk": user_ls_temp.user.id}), + {"ls-temp": "", "ls-jrs": 10, "ls-text": "Texte de test pour LS TEMP"}, + follow=False, + ) + user = Profile.objects.get(id=user_ls_temp.id) # Refresh profile from DB + self.assertEqual(result.status_code, 302) + self.assertFalse(user.can_write) + self.assertTrue(user.can_read) + self.assertIsNotNone(user.end_ban_write) + self.assertIsNone(user.end_ban_read) + ban = Ban.objects.filter(user__id=user.user.id).order_by("-id")[0] + self.assertIn("Lecture seule temporaire", ban.type) + self.assertEqual(ban.note, "Texte de test pour LS TEMP") + self.assertEqual(len(mail.outbox), 3) + + # reset nb_users + result = self.client.get(reverse("member-list"), follow=False) + self.assertEqual(result.status_code, 200) + nb_users = len(result.context["members"]) + + # Test: BAN + user_ban = ProfileFactory() + result = self.client.post( + reverse("member-modify-profile", kwargs={"user_pk": user_ban.user.id}), + {"ban": "", "ban-text": "Texte de test pour BAN"}, + follow=False, + ) + user = Profile.objects.get(id=user_ban.id) # Refresh profile from DB + self.assertEqual(result.status_code, 302) + self.assertTrue(user.can_write) + self.assertFalse(user.can_read) + self.assertIsNone(user.end_ban_write) + self.assertIsNone(user.end_ban_read) + ban = Ban.objects.filter(user__id=user.user.id).order_by("-id")[0] + self.assertEqual(ban.type, "Bannissement illimité") + self.assertEqual(ban.note, "Texte de test pour BAN") + self.assertEqual(len(mail.outbox), 4) + + result = self.client.get(reverse("member-list"), follow=False) + self.assertEqual(result.status_code, 200) + self.assertEqual(nb_users, len(result.context["members"])) # Banned guy doesn't show up, good + + # Test: un-BAN + result = self.client.post( + reverse("member-modify-profile", kwargs={"user_pk": user_ban.user.id}), + {"un-ban": "", "unban-text": "Texte de test pour BAN"}, + follow=False, + ) + user = Profile.objects.get(id=user_ban.id) # Refresh profile from DB + self.assertEqual(result.status_code, 302) + self.assertTrue(user.can_write) + self.assertTrue(user.can_read) + self.assertIsNone(user.end_ban_write) + self.assertIsNone(user.end_ban_read) + ban = Ban.objects.filter(user__id=user.user.id).order_by("-id")[0] + self.assertEqual(ban.type, "Levée du bannissement") + self.assertEqual(ban.note, "Texte de test pour BAN") + self.assertEqual(len(mail.outbox), 5) + + result = self.client.get(reverse("member-list"), follow=False) + self.assertEqual(result.status_code, 200) + self.assertEqual(nb_users + 1, len(result.context["members"])) # UnBanned guy shows up, good + + # Test: BAN temp + user_ban_temp = ProfileFactory() + result = self.client.post( + reverse("member-modify-profile", kwargs={"user_pk": user_ban_temp.user.id}), + {"ban-temp": "", "ban-jrs": 10, "ban-text": "Texte de test pour BAN TEMP"}, + follow=False, + ) + user = Profile.objects.get(id=user_ban_temp.id) # Refresh profile from DB + self.assertEqual(result.status_code, 302) + self.assertTrue(user.can_write) + self.assertFalse(user.can_read) + self.assertIsNone(user.end_ban_write) + self.assertIsNotNone(user.end_ban_read) + ban = Ban.objects.filter(user__id=user.user.id).order_by("-id")[0] + self.assertIn("Bannissement temporaire", ban.type) + self.assertEqual(ban.note, "Texte de test pour BAN TEMP") + self.assertEqual(len(mail.outbox), 6) + + def test_sanctions_with_not_staff_user(self): + user = ProfileFactory().user + + # we need staff right for update the sanction of a user, so a member who is not staff can't access to the page + self.client.logout() + self.client.force_login(user) + + # Test: LS + result = self.client.post( + reverse("member-modify-profile", kwargs={"user_pk": self.staff.id}), + {"ls": "", "ls-text": "Texte de test pour LS"}, + follow=False, + ) + + self.assertEqual(result.status_code, 403) + + # if the user is staff, he can update the sanction of a user + self.client.logout() + self.client.force_login(self.staff) + + # Test: LS + result = self.client.post( + reverse("member-modify-profile", kwargs={"user_pk": user.id}), + {"ls": "", "ls-text": "Texte de test pour LS"}, + follow=False, + ) + + self.assertEqual(result.status_code, 302) + + def test_failed_bot_sanctions(self): + + staff = StaffProfileFactory() + self.client.force_login(staff.user) + + bot_profile = ProfileFactory() + bot_profile.user.groups.add(self.bot) + bot_profile.user.save() + + # Test: LS + result = self.client.post( + reverse("member-modify-profile", kwargs={"user_pk": bot_profile.user.id}), + {"ls": "", "ls-text": "Texte de test pour LS"}, + follow=False, + ) + user = Profile.objects.get(id=bot_profile.id) # Refresh profile from DB + self.assertEqual(result.status_code, 403) + self.assertTrue(user.can_write) + self.assertTrue(user.can_read) + self.assertIsNone(user.end_ban_write) + self.assertIsNone(user.end_ban_read) + + def test_karma(self): + user = ProfileFactory() + other_user = ProfileFactory() + self.client.force_login(other_user.user) + r = self.client.post(reverse("member-modify-karma"), {"profile_pk": user.pk, "karma": 42, "note": "warn"}) + self.assertEqual(403, r.status_code) + self.client.logout() + self.client.force_login(self.staff) + # bad id + r = self.client.post( + reverse("member-modify-karma"), {"profile_pk": "blah", "karma": 42, "note": "warn"}, follow=True + ) + self.assertEqual(404, r.status_code) + # good karma + r = self.client.post( + reverse("member-modify-karma"), {"profile_pk": user.pk, "karma": 42, "note": "warn"}, follow=True + ) + self.assertEqual(200, r.status_code) + self.assertIn("{} : 42".format(_("Modification du karma")), r.content.decode("utf-8")) + # more than 100 karma must unvalidate the karma + r = self.client.post( + reverse("member-modify-karma"), {"profile_pk": user.pk, "karma": 420, "note": "warn"}, follow=True + ) + self.assertEqual(200, r.status_code) + self.assertNotIn("{} : 420".format(_("Modification du karma")), r.content.decode("utf-8")) + # empty warning must unvalidate the karma + r = self.client.post( + reverse("member-modify-karma"), {"profile_pk": user.pk, "karma": 41, "note": ""}, follow=True + ) + self.assertEqual(200, r.status_code) + self.assertNotIn("{} : 41".format(_("Modification du karma")), r.content.decode("utf-8")) + + def test_modify_user_karma(self): + """ + To test karma of a user modified by a staff user. + """ + tester = ProfileFactory() + staff = StaffProfileFactory() + + # login as user + result = self.client.post( + reverse("member-login"), {"username": tester.user.username, "password": "hostel77"}, follow=False + ) + self.assertEqual(result.status_code, 302) + + # check that user can't use this feature + result = self.client.post(reverse("member-modify-karma"), follow=False) + self.assertEqual(result.status_code, 403) + + # login as staff + result = self.client.post( + reverse("member-login"), {"username": staff.user.username, "password": "hostel77"}, follow=False + ) + self.assertEqual(result.status_code, 302) + + # try to give a few bad points to the tester + result = self.client.post( + reverse("member-modify-karma"), + {"profile_pk": tester.pk, "note": "Bad tester is bad !", "karma": "-50"}, + follow=False, + ) + self.assertEqual(result.status_code, 302) + tester = Profile.objects.get(pk=tester.pk) + self.assertEqual(tester.karma, -50) + self.assertEqual(KarmaNote.objects.filter(user=tester.user).count(), 1) + + # Now give a few good points + result = self.client.post( + reverse("member-modify-karma"), + {"profile_pk": tester.pk, "note": "Good tester is good !", "karma": "10"}, + follow=False, + ) + self.assertEqual(result.status_code, 302) + tester = Profile.objects.get(pk=tester.pk) + self.assertEqual(tester.karma, -40) + self.assertEqual(KarmaNote.objects.filter(user=tester.user).count(), 2) + + # Now access some unknow user + result = self.client.post( + reverse("member-modify-karma"), + {"profile_pk": 9999, "note": "Good tester is good !", "karma": "10"}, + follow=False, + ) + self.assertEqual(result.status_code, 404) + + # Now give unknow point + result = self.client.post( + reverse("member-modify-karma"), + {"profile_pk": tester.pk, "note": "Good tester is good !", "karma": ""}, + follow=False, + ) + self.assertEqual(result.status_code, 302) + tester = Profile.objects.get(pk=tester.pk) + self.assertEqual(tester.karma, -40) + self.assertEqual(KarmaNote.objects.filter(user=tester.user).count(), 3) + + # Now give no point at all + result = self.client.post( + reverse("member-modify-karma"), {"profile_pk": tester.pk, "note": "Good tester is good !"}, follow=False + ) + self.assertEqual(result.status_code, 302) + tester = Profile.objects.get(pk=tester.pk) + self.assertEqual(tester.karma, -40) + self.assertEqual(KarmaNote.objects.filter(user=tester.user).count(), 4) + + # Now access without post + result = self.client.get(reverse("member-modify-karma"), follow=False) + self.assertEqual(result.status_code, 405) + + def test_karma_and_pseudo_change(self): + """ + To test that a karma note is added when a member change its pseudo + """ + tester = ProfileFactory() + old_pseudo = tester.user.username + self.client.force_login(tester.user) + data = {"username": "dummy", "email": tester.user.email} + result = self.client.post(reverse("update-username-email-member"), data, follow=False) + + self.assertEqual(result.status_code, 302) + notes = KarmaNote.objects.filter(user=tester.user).all() + self.assertEqual(len(notes), 1) + self.assertTrue(old_pseudo in notes[0].note and "dummy" in notes[0].note) + + def test_moderation_history(self): + user = ProfileFactory().user + + ban = Ban( + user=user, + moderator=self.staff, + type="Lecture Seule Temporaire", + note="Test de LS", + pubdate=datetime.now(), + ) + ban.save() + + note = KarmaNote( + user=user, + moderator=self.staff, + karma=5, + note="Test de karma", + pubdate=datetime.now(), + ) + note.save() + + # staff rights are required to view the history, check that + self.client.logout() + self.client.force_login(user) + result = self.client.get(user.profile.get_absolute_url(), follow=False) + self.assertNotContains(result, "Historique de modération") + + self.client.logout() + self.client.force_login(self.staff) + result = self.client.get(user.profile.get_absolute_url(), follow=False) + self.assertContains(result, "Historique de modération") + + # check that the note and the sanction are in the context + self.assertIn(ban, result.context["actions"]) + self.assertIn(note, result.context["actions"]) + + # and are displayed + self.assertContains(result, "Test de LS") + self.assertContains(result, "Test de karma") + + def test_filter_member_ip(self): + """ + Test filter member by ip. + """ + + # create users (one regular and one staff and superuser) + tester = ProfileFactory() + staff = StaffProfileFactory() + + # test login normal user + result = self.client.post( + reverse("member-login"), + {"username": tester.user.username, "password": "hostel77", "remember": "remember"}, + follow=False, + ) + # good password then redirection + self.assertEqual(result.status_code, 302) + + # Check that the filter can't be access from normal user + result = self.client.post( + reverse("member-from-ip", kwargs={"ip_address": tester.last_ip_address}), {}, follow=False + ) + self.assertEqual(result.status_code, 403) + + # log the staff user + result = self.client.post( + reverse("member-login"), + {"username": staff.user.username, "password": "hostel77", "remember": "remember"}, + follow=False, + ) + # good password then redirection + self.assertEqual(result.status_code, 302) + + # test that we retrieve correctly the 2 members (staff + user) from this ip + result = self.client.post( + reverse("member-from-ip", kwargs={"ip_address": staff.last_ip_address}), {}, follow=False + ) + self.assertEqual(result.status_code, 200) + self.assertEqual(len(result.context["members"]), 2) + + +class IpListingsTests(TestCase): + """Test the member_from_ip function : listing users from a same IPV4/IPV6 address or same IPV6 network.""" + + def setUp(self) -> None: + self.staff = StaffProfileFactory().user + self.regular_user = ProfileFactory() + + self.user_ipv4_same_ip_1 = ProfileFactory(last_ip_address="155.128.92.54") + self.user_ipv4_same_ip_1.user.username = "user_ipv4_same_ip_1" + self.user_ipv4_same_ip_1.user.save() + + self.user_ipv4_same_ip_2 = ProfileFactory(last_ip_address="155.128.92.54") + self.user_ipv4_same_ip_2.user.username = "user_ipv4_same_ip_2" + self.user_ipv4_same_ip_2.user.save() + + self.user_ipv4_different_ip = ProfileFactory(last_ip_address="155.128.92.55") + self.user_ipv4_different_ip.user.username = "user_ipv4_different_ip" + self.user_ipv4_different_ip.user.save() + + self.user_ipv6_same_ip_1 = ProfileFactory(last_ip_address="2001:8f8:1425:60a0:7981:9852:1493:3721") + self.user_ipv6_same_ip_1.user.username = "user_ipv6_same_ip_1" + self.user_ipv6_same_ip_1.user.save() + + self.user_ipv6_same_ip_2 = ProfileFactory(last_ip_address="2001:8f8:1425:60a0:7981:9852:1493:3721") + self.user_ipv6_same_ip_2.user.username = "user_ipv6_same_ip_2" + self.user_ipv6_same_ip_2.user.save() + + self.user_ipv6_same_network = ProfileFactory(last_ip_address="2001:8f8:1425:60a0:9852:7981:3721:1493") + self.user_ipv6_same_network.user.username = "user_ipv6_same_network" + self.user_ipv6_same_network.user.save() + + self.user_ipv6_different_network = ProfileFactory(last_ip_address="8f8:60a0:3721:1425:7981:1493:2001:9852") + self.user_ipv6_different_network.user.username = "user_ipv6_different_network" + self.user_ipv6_different_network.user.save() + + def test_same_ipv4(self) -> None: + self.client.force_login(self.staff) + response = self.client.get(reverse(member_from_ip, args=[self.user_ipv4_same_ip_1.last_ip_address])) + self.assertContains(response, self.user_ipv4_same_ip_1.user.username) + self.assertContains(response, self.user_ipv4_same_ip_2.user.username) + self.assertContains(response, self.user_ipv4_same_ip_1.last_ip_address) + self.assertNotContains(response, self.user_ipv4_different_ip.user.username) + + def test_different_ipv4(self) -> None: + self.client.force_login(self.staff) + response = self.client.get(reverse(member_from_ip, args=[self.user_ipv4_different_ip.last_ip_address])) + self.assertContains(response, self.user_ipv4_different_ip.user.username) + self.assertContains(response, self.user_ipv4_different_ip.last_ip_address) + self.assertNotContains(response, self.user_ipv6_same_ip_1.user.username) + + def test_same_ipv6_and_same_ipv6_network(self) -> None: + self.client.force_login(self.staff) + response = self.client.get(reverse(member_from_ip, args=[self.user_ipv6_same_ip_1.last_ip_address])) + self.assertContains(response, self.user_ipv6_same_ip_1.user.username) + self.assertContains(response, self.user_ipv6_same_ip_2.user.username) + self.assertContains(response, self.user_ipv6_same_network.user.username) + self.assertNotContains(response, self.user_ipv6_different_network.user.username) + + def test_same_ipv6_network_but_different_ip(self) -> None: + self.client.force_login(self.staff) + response = self.client.get(reverse(member_from_ip, args=[self.user_ipv6_same_network.last_ip_address])) + self.assertContains(response, self.user_ipv6_same_network.user.username) + self.assertContains(response, self.user_ipv6_same_ip_1.user.username) + self.assertContains(response, self.user_ipv6_same_ip_2.user.username) + self.assertNotContains(response, self.user_ipv6_different_network.user.username) + + def test_different_ipv6_network(self) -> None: + self.client.force_login(self.staff) + response = self.client.get(reverse(member_from_ip, args=[self.user_ipv6_different_network.last_ip_address])) + self.assertContains(response, self.user_ipv6_different_network.user.username) + self.assertNotContains(response, self.user_ipv6_same_ip_1.user.username) + self.assertNotContains(response, self.user_ipv6_same_ip_2.user.username) + self.assertNotContains(response, self.user_ipv6_same_network.user.username) + + def test_access_rights_to_ip_page_as_regular_user(self) -> None: + self.client.force_login(self.regular_user.user) + response = self.client.get(reverse(member_from_ip, args=["0.0.0.0"])) + self.assertEqual(response.status_code, 403) + + def test_access_rights_to_ip_page_as_anonymous(self) -> None: + response = self.client.get(reverse(member_from_ip, args=["0.0.0.0"])) + self.assertEqual(response.status_code, 302) + + def test_access_rights_to_ip_page_as_staff(self) -> None: + self.client.force_login(self.staff) + response = self.client.get(reverse(member_from_ip, args=["0.0.0.0"])) + self.assertEqual(response.status_code, 200) + + def test_template_used_by_ip_page(self) -> None: + self.client.force_login(self.staff) + response = self.client.get(reverse(member_from_ip, args=["0.0.0.0"])) + self.assertTemplateUsed(response, "member/admin/memberip.html") diff --git a/zds/member/tests/views/tests_password_recovery.py b/zds/member/tests/views/tests_password_recovery.py new file mode 100644 index 0000000000..0dd342247f --- /dev/null +++ b/zds/member/tests/views/tests_password_recovery.py @@ -0,0 +1,52 @@ +from django.conf import settings +from django.contrib.auth.models import User, Group +from django.core import mail +from django.urls import reverse +from django.test import TestCase + +from zds.member.tests.factories import ProfileFactory, StaffProfileFactory, UserFactory +from zds.member.models import TokenForgotPassword +from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory + + +class MemberTests(TestCase): + def setUp(self): + settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" + self.mas = ProfileFactory() + settings.ZDS_APP["member"]["bot_account"] = self.mas.user.username + self.anonymous = UserFactory(username=settings.ZDS_APP["member"]["anonymous_account"], password="anything") + self.external = UserFactory(username=settings.ZDS_APP["member"]["external_account"], password="anything") + self.category1 = ForumCategoryFactory(position=1) + self.forum11 = ForumFactory(category=self.category1, position_in_category=1) + self.staff = StaffProfileFactory().user + + self.bot = Group(name=settings.ZDS_APP["member"]["bot_group"]) + self.bot.save() + + def test_forgot_password(self): + """To test nominal scenario of a lost password.""" + + # Empty the test outbox + mail.outbox = [] + + result = self.client.post( + reverse("member-forgot-password"), + { + "username": self.mas.user.username, + "email": "", + }, + follow=False, + ) + + self.assertEqual(result.status_code, 200) + + # check email has been sent + self.assertEqual(len(mail.outbox), 1) + + # clic on the link which has been sent in mail + user = User.objects.get(username=self.mas.user.username) + + token = TokenForgotPassword.objects.get(user=user) + result = self.client.get(settings.ZDS_APP["site"]["url"] + token.get_absolute_url(), follow=False) + + self.assertEqual(result.status_code, 200) diff --git a/zds/member/tests/views/tests_profile.py b/zds/member/tests/views/tests_profile.py new file mode 100644 index 0000000000..a8f115bdac --- /dev/null +++ b/zds/member/tests/views/tests_profile.py @@ -0,0 +1,322 @@ +from django.conf import settings +from django.contrib.auth.models import Group +from django.urls import reverse +from django.test import TestCase + +from zds.member.tests.factories import ( + ProfileFactory, + StaffProfileFactory, + UserFactory, + DevProfileFactory, +) +from zds.member.models import Profile +from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents +from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory + + +@override_for_contents() +class MemberTests(TutorialTestMixin, TestCase): + def setUp(self): + settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" + self.mas = ProfileFactory() + settings.ZDS_APP["member"]["bot_account"] = self.mas.user.username + self.anonymous = UserFactory(username=settings.ZDS_APP["member"]["anonymous_account"], password="anything") + self.external = UserFactory(username=settings.ZDS_APP["member"]["external_account"], password="anything") + self.category1 = ForumCategoryFactory(position=1) + self.forum11 = ForumFactory(category=self.category1, position_in_category=1) + self.staff = StaffProfileFactory().user + + self.bot = Group(name=settings.ZDS_APP["member"]["bot_group"]) + self.bot.save() + + def test_list_members(self): + """ + To test the listing of the members with and without page parameter. + """ + + # create strange member + weird = ProfileFactory() + weird.user.username = "ïtrema718" + weird.user.email = "foo@\xfbgmail.com" + weird.user.save() + + # list of members. + result = self.client.get(reverse("member-list"), follow=False) + self.assertEqual(result.status_code, 200) + + nb_users = len(result.context["members"]) + + # Test that inactive user don't show up + unactive_user = ProfileFactory() + unactive_user.user.is_active = False + unactive_user.user.save() + result = self.client.get(reverse("member-list"), follow=False) + self.assertEqual(result.status_code, 200) + self.assertEqual(nb_users, len(result.context["members"])) + + # Add a Bot and check that list didn't change + bot_profile = ProfileFactory() + bot_profile.user.groups.add(self.bot) + bot_profile.user.save() + result = self.client.get(reverse("member-list"), follow=False) + self.assertEqual(result.status_code, 200) + self.assertEqual(nb_users, len(result.context["members"])) + + # list of members with page parameter. + result = self.client.get(reverse("member-list") + "?page=1", follow=False) + self.assertEqual(result.status_code, 200) + + # page which doesn't exist. + result = self.client.get(reverse("member-list") + "?page=1534", follow=False) + self.assertEqual(result.status_code, 404) + + # page parameter isn't an integer. + result = self.client.get(reverse("member-list") + "?page=abcd", follow=False) + self.assertEqual(result.status_code, 404) + + def test_details_member(self): + """ + To test details of a member given. + """ + + # details of a staff user. + result = self.client.get(reverse("member-detail", args=[self.staff.username]), follow=False) + self.assertEqual(result.status_code, 200) + + # details of an unknown user. + result = self.client.get(reverse("member-detail", args=["unknown_user"]), follow=False) + self.assertEqual(result.status_code, 404) + + def test_redirection_when_using_old_detail_member_url(self): + """ + To test the redirection when accessing the member profile through the old url + """ + user = ProfileFactory().user + result = self.client.get(reverse("member-detail-redirect", args=[user.username]), follow=False) + + self.assertEqual(result.status_code, 301) + + def test_old_detail_member_url_with_unexistant_member(self): + """ + To test wether a 404 error is raised when the user in the old url does not exist + """ + response = self.client.get(reverse("member-detail-redirect", args=["tartempion"]), follow=False) + + self.assertEqual(response.status_code, 404) + + def test_profile_page_of_weird_member_username(self): + + # create some user with weird username + user_1 = ProfileFactory() + user_2 = ProfileFactory() + user_3 = ProfileFactory() + user_1.user.username = "ïtrema" + user_1.user.save() + user_2.user.username = ""a" + user_2.user.save() + user_3.user.username = "_`_`_`_" + user_3.user.save() + + # profile pages of weird users. + result = self.client.get(reverse("member-detail", args=[user_1.user.username]), follow=True) + self.assertEqual(result.status_code, 200) + result = self.client.get(reverse("member-detail", args=[user_2.user.username]), follow=True) + self.assertEqual(result.status_code, 200) + result = self.client.get(reverse("member-detail", args=[user_3.user.username]), follow=True) + self.assertEqual(result.status_code, 200) + + def test_modify_member(self): + user = ProfileFactory().user + + # we need staff right for update other profile, so a member who is not staff can't access to the page + self.client.logout() + self.client.force_login(user) + + result = self.client.get(reverse("member-settings-mini-profile", args=["xkcd"]), follow=False) + self.assertEqual(result.status_code, 403) + + self.client.logout() + self.client.force_login(self.staff) + + # an inexistant member return 404 + result = self.client.get(reverse("member-settings-mini-profile", args=["xkcd"]), follow=False) + self.assertEqual(result.status_code, 404) + + # an existant member return 200 + result = self.client.get(reverse("member-settings-mini-profile", args=[self.mas.user.username]), follow=False) + self.assertEqual(result.status_code, 200) + + def test_success_preview_biography(self): + + member = ProfileFactory() + self.client.force_login(member.user) + + response = self.client.post( + reverse("update-member"), + { + "text": "It is **my** life", + "preview": "", + }, + HTTP_X_REQUESTED_WITH="XMLHttpRequest", + ) + + result_string = "".join(a.decode() for a in response.streaming_content) + self.assertIn("my", result_string, "We need the biography to be properly formatted") + + def test_members_are_contactable(self): + """ + The PM button is displayed to logged in users, except if it's the profile + of a banned user. + """ + user_ban = ProfileFactory() + user_ban.can_read = False + user_ban.can_write = False + user_ban.save() + user_1 = ProfileFactory() + user_2 = ProfileFactory() + + phrase = "Envoyer un message" + + # The PM button is hidden for anonymous users + result = self.client.get(reverse("member-detail", args=[user_1.user.username]), follow=False) + self.assertNotContains(result, phrase) + + # Also for anonymous users viewing banned members profiles + result = self.client.get(reverse("member-detail", args=[user_ban.user.username]), follow=False) + self.assertNotContains(result, phrase) + + self.client.force_login(user_2.user) + + # If an user is logged in, the PM button is shown for other normal users + result = self.client.get(reverse("member-detail", args=[user_1.user.username]), follow=False) + self.assertContains(result, phrase) + + # But not for banned users + result = self.client.get(reverse("member-detail", args=[user_ban.user.username]), follow=False) + self.assertNotContains(result, phrase) + + self.client.logout() + self.client.force_login(user_1.user) + + # Neither for his own profile + result = self.client.get(reverse("member-detail", args=[user_1.user.username]), follow=False) + self.assertNotContains(result, phrase) + + self.client.logout() + + def test_github_token(self): + user = ProfileFactory() + dev = DevProfileFactory() + + # test that github settings are only availables for dev + self.client.force_login(user.user) + result = self.client.get(reverse("update-github"), follow=False) + self.assertEqual(result.status_code, 403) + result = self.client.post(reverse("remove-github"), follow=False) + self.assertEqual(result.status_code, 403) + self.client.logout() + + # now, test the form + self.client.force_login(dev.user) + result = self.client.get(reverse("update-github"), follow=False) + self.assertEqual(result.status_code, 200) + result = self.client.post( + reverse("update-github"), + { + "github_token": "test", + }, + follow=False, + ) + self.assertEqual(result.status_code, 302) + + # refresh + dev = Profile.objects.get(pk=dev.pk) + self.assertEqual(dev.github_token, "test") + + # test the option to remove the token + result = self.client.post(reverse("remove-github"), follow=False) + self.assertEqual(result.status_code, 302) + + # refresh + dev = Profile.objects.get(pk=dev.pk) + self.assertEqual(dev.github_token, "") + + def test_markdown_help_settings(self): + user = ProfileFactory().user + + # login and check that the Markdown help is displayed + self.client.force_login(user) + result = self.client.get(reverse("pages-index"), follow=False) + self.assertContains(result, 'data-show-markdown-help="true"') + + # disable Markdown help + user.profile.show_markdown_help = False + user.profile.save() + result = self.client.get(reverse("pages-index"), follow=False) + self.assertContains(result, 'data-show-markdown-help="false"') + + def test_old_smileys(self): + """Test the cookie""" + + # NOTE: we have to use the "real" login and logout pages here + cookie_key = settings.ZDS_APP["member"]["old_smileys_cookie_key"] + + profile_without_clem = ProfileFactory() + profile_without_clem = Profile.objects.get(pk=profile_without_clem.pk) + self.assertFalse(profile_without_clem.use_old_smileys) + + user_without_clem = profile_without_clem.user + profile_with_clem = ProfileFactory() + profile_with_clem.use_old_smileys = True + profile_with_clem.save() + user_with_clem = profile_with_clem.user + + settings.ZDS_APP["member"]["old_smileys_allowed"] = True + + # test that the cookie is set when connection + result = self.client.post( + reverse("member-login"), + {"username": user_with_clem.username, "password": "hostel77", "remember": "remember"}, + follow=False, + ) + self.assertEqual(result.status_code, 302) + self.client.get(reverse("homepage")) + + self.assertIn(cookie_key, self.client.cookies) + self.assertNotEqual(self.client.cookies[cookie_key]["expires"], 0) + + # test that logout set the cookies expiration to 0 (= no more cookie) + self.client.post(reverse("member-logout"), follow=True) + self.client.get(reverse("homepage")) + self.assertEqual(self.client.cookies[cookie_key]["expires"], 0) + + # test that user without the setting have the cookie with expiration 0 (= no cookie) + result = self.client.post( + reverse("member-login"), + {"username": user_without_clem.username, "password": "hostel77", "remember": "remember"}, + follow=False, + ) + + self.assertEqual(result.status_code, 302) + self.assertEqual(self.client.cookies[cookie_key]["expires"], 0) + + # setting use_smileys sets the cookie + self.client.post( + reverse("update-member"), + {"biography": "", "site": "", "avatar_url": "", "sign": "", "options": ["use_old_smileys"]}, + ) + self.client.get(reverse("homepage")) + + profile_without_clem = Profile.objects.get(pk=profile_without_clem.pk) + self.assertTrue(profile_without_clem.use_old_smileys) + self.assertNotEqual(self.client.cookies[cookie_key]["expires"], 0) + + # ... and that not setting it removes the cookie + self.client.post( + reverse("update-member"), {"biography": "", "site": "", "avatar_url": "", "sign": "", "options": []} + ) + self.client.get(reverse("homepage")) + + profile_without_clem = Profile.objects.get(pk=profile_without_clem.pk) + self.assertFalse(profile_without_clem.use_old_smileys) + self.assertEqual(self.client.cookies[cookie_key]["expires"], 0) diff --git a/zds/member/tests/views/tests_register.py b/zds/member/tests/views/tests_register.py new file mode 100644 index 0000000000..2d3d9040a0 --- /dev/null +++ b/zds/member/tests/views/tests_register.py @@ -0,0 +1,392 @@ +import os +from datetime import datetime +from smtplib import SMTPException +from unittest.mock import Mock + +from oauth2_provider.models import AccessToken, Application + +from django.conf import settings +from django.contrib.auth.models import User, Group +from django.core import mail +from django.core.mail.backends.base import BaseEmailBackend +from django.urls import reverse +from django.utils.html import escape +from django.test import TestCase, override_settings + +from zds.member.tests.factories import ProfileFactory, UserFactory, StaffProfileFactory +from zds.mp.tests.factories import PrivateTopicFactory, PrivatePostFactory +from zds.member.models import KarmaNote, NewEmailProvider +from zds.mp.models import PrivatePost, PrivateTopic +from zds.member.models import TokenRegister, Ban +from zds.tutorialv2.tests.factories import PublishableContentFactory, PublishedContentFactory, BetaContentFactory +from zds.tutorialv2.models.database import PublishableContent, PublishedContent +from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents +from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory, TopicFactory, PostFactory +from zds.forum.models import Topic, Post +from zds.gallery.tests.factories import GalleryFactory, UserGalleryFactory +from zds.gallery.models import Gallery, UserGallery +from zds.utils.models import CommentVote + + +@override_for_contents() +class TestRegister(TutorialTestMixin, TestCase): + def setUp(self): + settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" + self.mas = ProfileFactory() + settings.ZDS_APP["member"]["bot_account"] = self.mas.user.username + self.anonymous = UserFactory(username=settings.ZDS_APP["member"]["anonymous_account"], password="anything") + self.external = UserFactory(username=settings.ZDS_APP["member"]["external_account"], password="anything") + self.category1 = ForumCategoryFactory(position=1) + self.forum11 = ForumFactory(category=self.category1, position_in_category=1) + self.staff = StaffProfileFactory().user + + self.bot = Group(name=settings.ZDS_APP["member"]["bot_group"]) + self.bot.save() + + def test_register(self): + """ + To test user registration. + """ + + # register a new user. + result = self.client.post( + reverse("register-member"), + { + "username": "firm1", + "password": "flavour", + "password_confirm": "flavour", + "email": "firm1@zestedesavoir.com", + }, + follow=False, + ) + self.assertEqual(result.status_code, 200) + + # check email has been sent. + self.assertEqual(len(mail.outbox), 1) + + # check if the new user is well inactive. + user = User.objects.get(username="firm1") + self.assertFalse(user.is_active) + + # make a request on the link which has been sent in mail to + # confirm the registration. + token = TokenRegister.objects.get(user=user) + result = self.client.get(settings.ZDS_APP["site"]["url"] + token.get_absolute_url(), follow=False) + self.assertEqual(result.status_code, 200) + + # check a new email hasn't been sent at the new user. + self.assertEqual(len(mail.outbox), 1) + + # check if the new user is active. + self.assertTrue(User.objects.get(username="firm1").is_active) + + def test_unregister(self): + """ + To test that unregistering user is working. + """ + + # test not logged user can't unregister. + self.client.logout() + result = self.client.post(reverse("member-unregister"), follow=False) + self.assertEqual(result.status_code, 302) + + # test logged user can unregister. + user = ProfileFactory() + self.client.force_login(user.user) + result = self.client.post(reverse("member-unregister"), follow=False) + self.assertEqual(result.status_code, 302) + self.assertEqual(User.objects.filter(username=user.user.username).count(), 0) + + # Attach a user at tutorials, articles, topics and private topics. After that, + # unregister this user and check that he is well removed in all contents. + user = ProfileFactory() + user2 = ProfileFactory() + alone_gallery = GalleryFactory() + UserGalleryFactory(gallery=alone_gallery, user=user.user) + shared_gallery = GalleryFactory() + UserGalleryFactory(gallery=shared_gallery, user=user.user) + UserGalleryFactory(gallery=shared_gallery, user=user2.user) + # first case : a published tutorial with only one author + published_tutorial_alone = PublishedContentFactory(type="TUTORIAL") + published_tutorial_alone.authors.add(user.user) + published_tutorial_alone.save() + # second case : a published tutorial with two authors + published_tutorial_2 = PublishedContentFactory(type="TUTORIAL") + published_tutorial_2.authors.add(user.user) + published_tutorial_2.authors.add(user2.user) + published_tutorial_2.save() + # third case : a private tutorial with only one author + writing_tutorial_alone = PublishableContentFactory(type="TUTORIAL") + writing_tutorial_alone.authors.add(user.user) + writing_tutorial_alone.save() + writing_tutorial_alone_galler_path = writing_tutorial_alone.gallery.get_gallery_path() + # fourth case : a private tutorial with at least two authors + writing_tutorial_2 = PublishableContentFactory(type="TUTORIAL") + writing_tutorial_2.authors.add(user.user) + writing_tutorial_2.authors.add(user2.user) + writing_tutorial_2.save() + self.client.force_login(self.staff) + # same thing for articles + published_article_alone = PublishedContentFactory(type="ARTICLE") + published_article_alone.authors.add(user.user) + published_article_alone.save() + published_article_2 = PublishedContentFactory(type="ARTICLE") + published_article_2.authors.add(user.user) + published_article_2.authors.add(user2.user) + published_article_2.save() + writing_article_alone = PublishableContentFactory(type="ARTICLE") + writing_article_alone.authors.add(user.user) + writing_article_alone.save() + writing_article_2 = PublishableContentFactory(type="ARTICLE") + writing_article_2.authors.add(user.user) + writing_article_2.authors.add(user2.user) + writing_article_2.save() + # beta content + beta_forum = ForumFactory(category=ForumCategoryFactory()) + beta_content = BetaContentFactory(author_list=[user.user], forum=beta_forum) + beta_content_2 = BetaContentFactory(author_list=[user.user, user2.user], forum=beta_forum) + # about posts and topics + authored_topic = TopicFactory(author=user.user, forum=self.forum11, solved_by=user.user) + answered_topic = TopicFactory(author=user2.user, forum=self.forum11) + PostFactory(topic=answered_topic, author=user.user, position=2) + edited_answer = PostFactory(topic=answered_topic, author=user.user, position=3) + edited_answer.editor = user.user + edited_answer.save() + + upvoted_answer = PostFactory(topic=answered_topic, author=user2.user, position=4) + upvoted_answer.like += 1 + upvoted_answer.save() + CommentVote.objects.create(user=user.user, comment=upvoted_answer, positive=True) + + private_topic = PrivateTopicFactory(author=user.user) + private_topic.participants.add(user2.user) + private_topic.save() + PrivatePostFactory(author=user.user, privatetopic=private_topic, position_in_topic=1) + + # add API key + self.assertEqual(Application.objects.count(), 0) + self.assertEqual(AccessToken.objects.count(), 0) + api_application = Application() + api_application.client_id = "foobar" + api_application.user = user.user + api_application.client_type = "confidential" + api_application.authorization_grant_type = "password" + api_application.client_secret = "42" + api_application.save() + token = AccessToken() + token.user = user.user + token.token = "r@d0m" + token.application = api_application + token.expires = datetime.now() + token.save() + self.assertEqual(Application.objects.count(), 1) + self.assertEqual(AccessToken.objects.count(), 1) + + # add a karma note and a sanction with this user + note = KarmaNote(moderator=user.user, user=user2.user, note="Good!", karma=5) + note.save() + ban = Ban(moderator=user.user, user=user2.user, type="Ban définitif", note="Test") + ban.save() + + # login and unregister: + self.client.force_login(user.user) + result = self.client.post(reverse("member-unregister"), follow=False) + self.assertEqual(result.status_code, 302) + + # check that the bot have taken authorship of tutorial: + self.assertEqual(published_tutorial_alone.authors.count(), 1) + self.assertEqual( + published_tutorial_alone.authors.first().username, settings.ZDS_APP["member"]["external_account"] + ) + self.assertFalse(os.path.exists(writing_tutorial_alone_galler_path)) + self.assertEqual(published_tutorial_2.authors.count(), 1) + self.assertEqual( + published_tutorial_2.authors.filter(username=settings.ZDS_APP["member"]["external_account"]).count(), 0 + ) + + # check that published tutorials remain published and accessible + self.assertIsNotNone(published_tutorial_2.public_version.get_prod_path()) + self.assertTrue(os.path.exists(published_tutorial_2.public_version.get_prod_path())) + self.assertIsNotNone(published_tutorial_alone.public_version.get_prod_path()) + self.assertTrue(os.path.exists(published_tutorial_alone.public_version.get_prod_path())) + self.assertEqual( + self.client.get( + reverse("tutorial:view", args=[published_tutorial_alone.pk, published_tutorial_alone.slug]), + follow=False, + ).status_code, + 200, + ) + self.assertEqual( + self.client.get( + reverse("tutorial:view", args=[published_tutorial_2.pk, published_tutorial_2.slug]), follow=False + ).status_code, + 200, + ) + + # test that published articles remain accessible + self.assertTrue(os.path.exists(published_article_alone.public_version.get_prod_path())) + self.assertEqual( + self.client.get( + reverse("article:view", args=[published_article_alone.pk, published_article_alone.slug]), follow=True + ).status_code, + 200, + ) + self.assertEqual( + self.client.get( + reverse("article:view", args=[published_article_2.pk, published_article_2.slug]), follow=True + ).status_code, + 200, + ) + + # check that the tutorial for which the author was alone does not exists anymore + self.assertEqual(PublishableContent.objects.filter(pk=writing_tutorial_alone.pk).count(), 0) + self.assertFalse(os.path.exists(writing_tutorial_alone.get_repo_path())) + + # check that bot haven't take the authorship of the tuto with more than one author + self.assertEqual(writing_tutorial_2.authors.count(), 1) + self.assertEqual( + writing_tutorial_2.authors.filter(username=settings.ZDS_APP["member"]["external_account"]).count(), 0 + ) + + # authorship for the article for which user was the only author + self.assertEqual(published_article_alone.authors.count(), 1) + self.assertEqual( + published_article_alone.authors.first().username, settings.ZDS_APP["member"]["external_account"] + ) + self.assertEqual(published_article_2.authors.count(), 1) + + self.assertEqual(PublishableContent.objects.filter(pk=writing_article_alone.pk).count(), 0) + self.assertFalse(os.path.exists(writing_article_alone.get_repo_path())) + + # not bot if another author: + self.assertEqual( + published_article_2.authors.filter(username=settings.ZDS_APP["member"]["external_account"]).count(), 0 + ) + self.assertEqual(writing_article_2.authors.count(), 1) + self.assertEqual( + writing_article_2.authors.filter(username=settings.ZDS_APP["member"]["external_account"]).count(), 0 + ) + + # topics, gallery and PMs: + self.assertEqual(Topic.objects.filter(author__username=user.user.username).count(), 0) + self.assertEqual(Topic.objects.filter(solved_by=user.user).count(), 0) + self.assertEqual(Topic.objects.filter(solved_by=self.anonymous).count(), 1) + self.assertEqual(Post.objects.filter(author__username=user.user.username).count(), 0) + self.assertEqual(Post.objects.filter(editor__username=user.user.username).count(), 0) + self.assertEqual(PrivatePost.objects.filter(author__username=user.user.username).count(), 0) + self.assertEqual(PrivateTopic.objects.filter(author__username=user.user.username).count(), 0) + + self.assertIsNotNone(Topic.objects.get(pk=authored_topic.pk)) + self.assertIsNotNone(PrivateTopic.objects.get(pk=private_topic.pk)) + self.assertIsNotNone(Gallery.objects.get(pk=alone_gallery.pk)) + self.assertEqual(alone_gallery.get_linked_users().count(), 1) + self.assertEqual(shared_gallery.get_linked_users().count(), 1) + self.assertEqual(UserGallery.objects.filter(user=user.user).count(), 0) + self.assertEqual(CommentVote.objects.filter(user=user.user, positive=True).count(), 0) + self.assertEqual(Post.objects.filter(pk=upvoted_answer.id).first().like, 0) + + # zep 12, published contents and beta + self.assertIsNotNone(PublishedContent.objects.filter(content__pk=published_tutorial_alone.pk).first()) + self.assertIsNotNone(PublishedContent.objects.filter(content__pk=published_tutorial_2.pk).first()) + self.assertTrue(Topic.objects.get(pk=beta_content.beta_topic.pk).is_locked) + self.assertFalse(Topic.objects.get(pk=beta_content_2.beta_topic.pk).is_locked) + + # check API + self.assertEqual(Application.objects.count(), 0) + self.assertEqual(AccessToken.objects.count(), 0) + + # check that the karma note and the sanction were kept + self.assertTrue(KarmaNote.objects.filter(pk=note.pk).exists()) + self.assertTrue(Ban.objects.filter(pk=ban.pk).exists()) + + def test_register_with_not_allowed_chars(self): + """ + Test register account with not allowed chars + :return: + """ + users = [ + # empty username + {"username": "", "password": "flavour", "password_confirm": "flavour", "email": "firm1@zestedesavoir.com"}, + # space after username + { + "username": "firm1 ", + "password": "flavour", + "password_confirm": "flavour", + "email": "firm1@zestedesavoir.com", + }, + # space before username + { + "username": " firm1", + "password": "flavour", + "password_confirm": "flavour", + "email": "firm1@zestedesavoir.com", + }, + # username with utf8mb4 chars + { + "username": " firm1", + "password": "flavour", + "password_confirm": "flavour", + "email": "firm1@zestedesavoir.com", + }, + ] + + for user in users: + result = self.client.post(reverse("register-member"), user, follow=False) + self.assertEqual(result.status_code, 200) + # check any email has been sent. + self.assertEqual(len(mail.outbox), 0) + # user doesn't exist + self.assertEqual(User.objects.filter(username=user["username"]).count(), 0) + + def test_new_provider_with_new_account(self): + new_providers_count = NewEmailProvider.objects.count() + + # register a new user + self.client.post( + reverse("register-member"), + { + "username": "new", + "password": "hostel77", + "password_confirm": "hostel77", + "email": "test@unknown-provider-register.com", + }, + follow=False, + ) + + user = User.objects.get(username="new") + token = TokenRegister.objects.get(user=user) + self.client.get(token.get_absolute_url(), follow=False) + + # A new provider object should have been created + self.assertEqual(new_providers_count + 1, NewEmailProvider.objects.count()) + + +mail_backend = Mock() + + +class FakeBackend(BaseEmailBackend): + def send_messages(self, email_messages): + return mail_backend.send_messages(email_messages) + + +@override_settings(EMAIL_BACKEND="zds.member.tests.views.tests_register.FakeBackend") +class RegisterTest(TestCase): + def test_exception_on_mail(self): + def send_messages(messages): + print("message sent") + raise SMTPException(messages) + + mail_backend.send_messages = send_messages + + result = self.client.post( + reverse("register-member"), + { + "username": "firm1", + "password": "flavour", + "password_confirm": "flavour", + "email": "firm1@zestedesavoir.com", + }, + follow=False, + ) + self.assertEqual(result.status_code, 200) + self.assertIn(escape("Impossible d'envoyer l'email."), result.content.decode("utf-8")) diff --git a/zds/member/tests/views/tests_reports.py b/zds/member/tests/views/tests_reports.py new file mode 100644 index 0000000000..c5d8aaac1e --- /dev/null +++ b/zds/member/tests/views/tests_reports.py @@ -0,0 +1,53 @@ +from django.conf import settings +from django.contrib.auth.models import Group +from django.urls import reverse +from django.test import TestCase + +from zds.member.tests.factories import ProfileFactory, StaffProfileFactory, UserFactory + +from zds.mp.models import PrivateTopic +from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents +from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory +from zds.utils.models import Alert + + +@override_for_contents() +class MemberTests(TutorialTestMixin, TestCase): + def setUp(self): + settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" + self.mas = ProfileFactory() + settings.ZDS_APP["member"]["bot_account"] = self.mas.user.username + self.anonymous = UserFactory(username=settings.ZDS_APP["member"]["anonymous_account"], password="anything") + self.external = UserFactory(username=settings.ZDS_APP["member"]["external_account"], password="anything") + self.category1 = ForumCategoryFactory(position=1) + self.forum11 = ForumFactory(category=self.category1, position_in_category=1) + self.staff = StaffProfileFactory().user + + self.bot = Group(name=settings.ZDS_APP["member"]["bot_group"]) + self.bot.save() + + def test_profile_report(self): + profile = ProfileFactory() + self.client.logout() + alerts_count = Alert.objects.count() + # test that authentication is required to report a profile + result = self.client.post(reverse("report-profile", args=[profile.pk]), {"reason": "test"}, follow=False) + self.assertEqual(result.status_code, 302) + self.assertEqual(alerts_count, Alert.objects.count()) + # login and check it doesn't work without reason + self.client.force_login(self.staff) + result = self.client.post(reverse("report-profile", args=[profile.pk]), {"reason": ""}, follow=False) + self.assertEqual(result.status_code, 302) + self.assertEqual(alerts_count, Alert.objects.count()) + # add a reason and check it works + result = self.client.post(reverse("report-profile", args=[profile.pk]), {"reason": "test"}, follow=False) + self.assertEqual(result.status_code, 302) + self.assertEqual(alerts_count + 1, Alert.objects.count()) + # test alert solving + alert = Alert.objects.latest("pubdate") + pm_count = PrivateTopic.objects.count() + result = self.client.post(reverse("solve-profile-alert", args=[alert.pk]), {"text": "ok"}, follow=False) + self.assertEqual(result.status_code, 302) + alert = Alert.objects.get(pk=alert.pk) # refresh + self.assertTrue(alert.solved) + self.assertEqual(pm_count + 1, PrivateTopic.objects.count()) diff --git a/zds/member/urls.py b/zds/member/urls.py index fb17bfbd93..5a54a8752c 100644 --- a/zds/member/urls.py +++ b/zds/member/urls.py @@ -1,35 +1,23 @@ from django.urls import re_path, path -from zds.member.views import ( - MemberList, - MemberDetail, +from zds.member.views import MemberList +from zds.member.views.profile import ( UpdateMember, UpdateGitHubToken, remove_github_token, UpdateAvatarMember, UpdatePasswordMember, UpdateUsernameEmailMember, - RegisterView, - SendValidationEmailView, + redirect_old_profile_to_new, +) +from zds.member.views.moderation import ( modify_karma, - modify_profile, settings_mini_profile, member_from_ip, - settings_promote, - login_view, - logout_view, - forgot_password, - new_password, - activate_account, - generate_token_account, - unregister, - warning_unregister, - BannedEmailProvidersList, - NewEmailProvidersList, - AddBannedEmailProvider, - remove_banned_email_provider, - check_new_email_provider, - MembersWithProviderList, + modify_profile, +) +from zds.member.views.login import login_view, logout_view +from zds.member.views.hats import ( HatsSettings, RequestedHatsList, HatRequestDetail, @@ -39,10 +27,27 @@ HatsList, HatDetail, SolvedHatRequestsList, - CreateProfileReportView, - SolveProfileReportView, - redirect_old_profile_to_new, ) +from zds.member.views.emailproviders import ( + BannedEmailProvidersList, + NewEmailProvidersList, + AddBannedEmailProvider, + remove_banned_email_provider, + check_new_email_provider, + MembersWithProviderList, +) +from zds.member.views.register import ( + RegisterView, + SendValidationEmailView, + unregister, + warning_unregister, + activate_account, + generate_token_account, +) +from zds.member.views.password_recovery import forgot_password, new_password +from zds.member.views.admin import settings_promote +from zds.member.views.reports import CreateProfileReportView, SolveProfileReportView + urlpatterns = [ # list diff --git a/zds/member/views.py b/zds/member/views.py deleted file mode 100644 index ac0cd24f26..0000000000 --- a/zds/member/views.py +++ /dev/null @@ -1,1542 +0,0 @@ -import ipaddress -import uuid -from datetime import datetime, timedelta -from urllib.parse import unquote - -from oauth2_provider.models import AccessToken - -from django.conf import settings -from django.contrib import messages -from django.contrib.auth import authenticate, login, logout -from django.contrib.auth.decorators import login_required, permission_required -from django.contrib.auth.mixins import LoginRequiredMixin -from django.contrib.auth.models import User, Group -from django.template.context_processors import csrf -from django.core.exceptions import PermissionDenied -from django.core.mail import EmailMultiAlternatives -from django.urls import reverse, reverse_lazy, resolve, Resolver404, NoReverseMatch -from django.db import transaction -from django.db.models import Q -from django.http import Http404, HttpResponseBadRequest, StreamingHttpResponse -from django.shortcuts import redirect, render, get_object_or_404 -from django.template.loader import render_to_string -from django.utils.decorators import method_decorator -from django.utils.text import format_lazy -from django.utils.translation import gettext_lazy as _ -from django.utils.translation import gettext as __ -from django.views.decorators.http import require_POST -from django.views.generic import DetailView, UpdateView, CreateView, FormView, View - -from zds.forum.models import Topic, TopicRead -from zds.gallery.forms import ImageAsAvatarForm -from zds.gallery.models import UserGallery -from zds.member import NEW_ACCOUNT, EMAIL_EDIT -from zds.member.commons import ( - ProfileCreate, - TemporaryReadingOnlySanction, - ReadingOnlySanction, - DeleteReadingOnlySanction, - TemporaryBanSanction, - BanSanction, - DeleteBanSanction, - TokenGenerator, -) -from zds.member.decorator import can_write_and_read_now, PermissionRequiredMixin -from zds.member.forms import ( - LoginForm, - MiniProfileForm, - ProfileForm, - RegisterForm, - ChangePasswordForm, - ChangeUserForm, - NewPasswordForm, - PromoteMemberForm, - KarmaForm, - UsernameAndEmailForm, - GitHubTokenForm, - BannedEmailProviderForm, - HatRequestForm, -) -from zds.member.models import ( - Profile, - TokenForgotPassword, - TokenRegister, - KarmaNote, - Ban, - BannedEmailProvider, - NewEmailProvider, - set_old_smileys_cookie, - remove_old_smileys_cookie, -) -from zds.mp.models import PrivatePost, PrivateTopic -from zds.notification.models import TopicAnswerSubscription, NewPublicationSubscription -from zds.pages.models import GroupContact -from zds.tutorialv2.models import CONTENT_TYPES -from zds.tutorialv2.models.database import PublishedContent, PickListOperation, ContentContribution, ContentReaction -from zds.utils.models import ( - Comment, - CommentVote, - Alert, - CommentEdit, - Hat, - HatRequest, - get_hat_from_settings, - get_hat_to_add, -) -from zds.utils.mps import send_mp -from zds.utils.paginator import ZdSPagingListView -from zds.utils.templatetags.pluralize_fr import pluralize_fr -from zds.utils.tokens import generate_token -import logging - - -class MemberList(ZdSPagingListView): - """Display the list of registered users.""" - - context_object_name = "members" - paginate_by = settings.ZDS_APP["member"]["members_per_page"] - template_name = "member/index.html" - - def get_queryset(self): - self.queryset = Profile.objects.contactable_members() - return super().get_queryset() - - -class MemberDetail(DetailView): - """Display details about a profile.""" - - context_object_name = "usr" - model = User - template_name = "member/profile.html" - - def get_object(self, queryset=None): - # Use unquote to accept twicely quoted URLs (for instance in MPs - # sent through emarkdown parser). - return get_object_or_404(User, username=unquote(self.kwargs["user_name"])) - - def get_summaries(self, profile): - """ - Returns a summary of this profile's activity, as a list of list of tuples. - Each first-level list item is an activity category (e.g. contents, forums, etc.) - Each second-level list item is a stat in this activity category. - Each tuple is (link url, count, displayed name of the item), where the link url can be None if it's not a link. - - :param profile: The profile. - :return: The summary data. - """ - summaries = [] - - if self.request.user.has_perm("member.change_post"): - count_post = profile.get_post_count_as_staff() - else: - count_post = profile.get_post_count() - - count_topic = profile.get_topic_count() - count_followed_topic = profile.get_followed_topic_count() - count_tutorials = profile.get_public_tutos().count() - count_articles = profile.get_public_articles().count() - count_opinions = profile.get_public_opinions().count() - - summary = [] - if count_tutorials + count_articles + count_opinions == 0: - summary.append((None, 0, __("Aucun contenu publié"))) - - if count_tutorials > 0: - summary.append( - ( - reverse_lazy("tutorial:find-tutorial", args=(profile.user.username,)), - count_tutorials, - __("tutoriel{}").format(pluralize_fr(count_tutorials)), - ) - ) - if count_articles > 0: - summary.append( - ( - reverse_lazy("article:find-article", args=(profile.user.username,)), - count_articles, - __("article{}").format(pluralize_fr(count_articles)), - ) - ) - if count_opinions > 0: - summary.append( - ( - reverse_lazy("opinion:find-opinion", args=(profile.user.username,)), - count_opinions, - __("billet{}").format(pluralize_fr(count_opinions)), - ) - ) - summaries.append(summary) - - summary = [] - if count_post > 0: - summary.append( - ( - reverse_lazy("post-find", args=(profile.user.pk,)), - count_post, - __("message{}").format(pluralize_fr(count_post)), - ) - ) - else: - summary.append((None, 0, __("Aucun message"))) - if count_topic > 0: - summary.append( - ( - reverse_lazy("topic-find", args=(profile.user.pk,)), - count_topic, - __("sujet{} créé{}").format(pluralize_fr(count_topic), pluralize_fr(count_topic)), - ) - ) - user = self.request.user - is_user_profile = user.is_authenticated and User.objects.get(pk=user.pk).profile == profile - if count_followed_topic > 0 and is_user_profile: - summary.append( - ( - reverse_lazy("followed-topic-find"), - count_followed_topic, - __("sujet{} suivi{}").format( - pluralize_fr(count_followed_topic), pluralize_fr(count_followed_topic) - ), - ) - ) - - summaries.append(summary) - - return summaries - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - usr = context["usr"] - profile = usr.profile - context["profile"] = profile - context["topics"] = list(Topic.objects.last_topics_of_a_member(usr, self.request.user)) - followed_query_set = TopicAnswerSubscription.objects.get_objects_followed_by(self.request.user.id) - followed_topics = list(set(followed_query_set) & set(context["topics"])) - for topic in context["topics"]: - topic.is_followed = topic in followed_topics - context["articles"] = PublishedContent.objects.last_articles_of_a_member_loaded(usr) - context["opinions"] = PublishedContent.objects.last_opinions_of_a_member_loaded(usr) - context["tutorials"] = PublishedContent.objects.last_tutorials_of_a_member_loaded(usr) - context["articles_and_tutorials"] = PublishedContent.objects.last_tutorials_and_articles_of_a_member_loaded(usr) - context["topic_read"] = TopicRead.objects.list_read_topic_pk(self.request.user, context["topics"]) - context["subscriber_count"] = NewPublicationSubscription.objects.get_subscriptions(self.object).count() - context["contribution_articles_count"] = ( - ContentContribution.objects.filter( - user__pk=usr.pk, content__sha_public__isnull=False, content__type=CONTENT_TYPES[1]["name"] - ) - .values_list("content", flat=True) - .distinct() - .count() - ) - context["contribution_tutorials_count"] = ( - ContentContribution.objects.filter( - user__pk=usr.pk, content__sha_public__isnull=False, content__type=CONTENT_TYPES[0]["name"] - ) - .values_list("content", flat=True) - .distinct() - .count() - ) - context["content_reactions_count"] = ContentReaction.objects.filter(author=usr).count() - - if self.request.user.has_perm("member.change_profile"): - sanctions = list(Ban.objects.filter(user=usr).select_related("moderator")) - notes = list(KarmaNote.objects.filter(user=usr).select_related("moderator")) - actions = sanctions + notes - actions.sort(key=lambda action: action.pubdate) - actions.reverse() - context["actions"] = actions - context["karmaform"] = KarmaForm(profile) - context["alerts"] = profile.alerts_on_this_profile.all().order_by("-pubdate") - context["has_unsolved_alerts"] = profile.alerts_on_this_profile.filter(solved=False).exists() - - context["summaries"] = self.get_summaries(profile) - return context - - -def redirect_old_profile_to_new(request, user_name): - user = get_object_or_404(User, username=user_name) - return redirect(user.profile, permanent=True) - - -class UpdateMember(UpdateView): - """Update a profile.""" - - form_class = ProfileForm - template_name = "member/settings/profile.html" - - @method_decorator(login_required) - def dispatch(self, *args, **kwargs): - return super().dispatch(*args, **kwargs) - - def get_object(self, queryset=None): - return get_object_or_404(Profile, user=self.request.user) - - def get_form(self, form_class=ProfileForm): - profile = self.get_object() - form = form_class( - initial={ - "biography": profile.biography, - "site": profile.site, - "avatar_url": profile.avatar_url, - "show_sign": profile.show_sign, - "is_hover_enabled": profile.is_hover_enabled, - "use_old_smileys": profile.use_old_smileys, - "allow_temp_visual_changes": profile.allow_temp_visual_changes, - "show_markdown_help": profile.show_markdown_help, - "email_for_answer": profile.email_for_answer, - "email_for_new_mp": profile.email_for_new_mp, - "sign": profile.sign, - "licence": profile.licence, - } - ) - - return form - - def post(self, request, *args, **kwargs): - form = self.form_class(request.POST) - - if "preview" in request.POST and request.is_ajax(): - content = render(request, "misc/preview.part.html", {"text": request.POST.get("text")}) - return StreamingHttpResponse(content) - - if form.is_valid(): - return self.form_valid(form) - - return render(request, self.template_name, {"form": form}) - - def form_valid(self, form): - profile = self.get_object() - self.update_profile(profile, form) - self.save_profile(profile) - - response = redirect(self.get_success_url()) - set_old_smileys_cookie(response, profile) - return response - - def update_profile(self, profile, form): - cleaned_data_options = form.cleaned_data.get("options") - profile.biography = form.data["biography"] - profile.site = form.data["site"] - profile.show_sign = "show_sign" in cleaned_data_options - profile.is_hover_enabled = "is_hover_enabled" in cleaned_data_options - profile.use_old_smileys = "use_old_smileys" in cleaned_data_options - profile.allow_temp_visual_changes = "allow_temp_visual_changes" in cleaned_data_options - profile.show_markdown_help = "show_markdown_help" in cleaned_data_options - profile.email_for_answer = "email_for_answer" in cleaned_data_options - profile.email_for_new_mp = "email_for_new_mp" in cleaned_data_options - profile.avatar_url = form.data["avatar_url"] - profile.sign = form.data["sign"] - profile.licence = form.cleaned_data["licence"] - - def get_success_url(self): - return reverse("update-member") - - def save_profile(self, profile): - try: - profile.save() - profile.user.save() - except Profile.DoesNotExist: - messages.error(self.request, self.get_error_message()) - return redirect(reverse("update-member")) - messages.success(self.request, self.get_success_message()) - - def get_success_message(self): - return _("Le profil a correctement été mis à jour.") - - def get_error_message(self): - return _("Une erreur est survenue.") - - -class UpdateGitHubToken(UpdateView): - """Update the GitHub token.""" - - form_class = GitHubTokenForm - template_name = "member/settings/github.html" - - @method_decorator(login_required) - def dispatch(self, request, *args, **kwargs): - if not request.user.profile.is_dev(): - raise PermissionDenied - return super().dispatch(request, *args, **kwargs) - - def get_object(self, queryset=None): - return get_object_or_404(Profile, user=self.request.user) - - def get_form(self, form_class=GitHubTokenForm): - return form_class() - - 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 form_valid(self, form): - profile = self.get_object() - profile.github_token = form.data["github_token"] - profile.save() - messages.success(self.request, self.get_success_message()) - - return redirect(self.get_success_url()) - - def get_success_url(self): - return reverse("update-github") - - def get_success_message(self): - return _("Votre token GitHub a été mis à jour.") - - def get_error_message(self): - return _("Une erreur est survenue.") - - -@require_POST -@login_required -def remove_github_token(request): - """Remove the current user token.""" - - profile = get_object_or_404(Profile, user=request.user) - if not profile.is_dev(): - raise PermissionDenied - - profile.github_token = "" - profile.save() - - messages.success(request, _("Votre token GitHub a été supprimé.")) - return redirect("update-github") - - -class UpdateAvatarMember(UpdateMember): - """Update the avatar of a logged in user.""" - - form_class = ImageAsAvatarForm - - def get_success_url(self): - profile = self.get_object() - - return reverse("member-detail", args=[profile.user.username]) - - def get_form(self, form_class=ImageAsAvatarForm): - return form_class(self.request.POST) - - def update_profile(self, profile, form): - profile.avatar_url = form.data["avatar_url"] - - def get_success_message(self): - return _("L'avatar a correctement été mis à jour.") - - -class UpdatePasswordMember(UpdateMember): - """Password-related user settings.""" - - form_class = ChangePasswordForm - template_name = "member/settings/account.html" - - def post(self, request, *args, **kwargs): - form = self.form_class(request.user, request.POST) - - if form.is_valid(): - return self.form_valid(form) - - return render(request, self.template_name, {"form": form}) - - def get_form(self, form_class=ChangePasswordForm): - return form_class(self.request.user) - - def update_profile(self, profile, form): - profile.user.set_password(form.data["password_new"]) - - def get_success_message(self): - return _("Le mot de passe a correctement été mis à jour.") - - def get_success_url(self): - return reverse("update-password-member") - - -class UpdateUsernameEmailMember(UpdateMember): - """Settings related to username and email.""" - - form_class = ChangeUserForm - template_name = "member/settings/user.html" - - def post(self, request, *args, **kwargs): - form = self.form_class(request.user, request.POST) - - if form.is_valid(): - return self.form_valid(form) - - return render(request, self.template_name, {"form": form}) - - def get_form(self, form_class=ChangeUserForm): - return form_class(self.request.user) - - def update_profile(self, profile, form): - profile.show_email = "show_email" in form.cleaned_data.get("options") - new_username = form.cleaned_data.get("username") - previous_username = form.cleaned_data.get("previous_username") - new_email = form.cleaned_data.get("email") - previous_email = form.cleaned_data.get("previous_email") - if new_username and new_username != previous_username: - # Add a karma message for the staff - bot = get_object_or_404(User, username=settings.ZDS_APP["member"]["bot_account"]) - KarmaNote( - user=profile.user, - moderator=bot, - note=_("{} s'est renommé {}").format(profile.user.username, new_username), - karma=0, - ).save() - # Change the username - profile.user.username = new_username - # update skeleton - profile.username_skeleton = Profile.find_username_skeleton(new_username) - if new_email and new_email != previous_email: - profile.user.email = new_email - # Create an alert for the staff if it's a new provider - provider = provider = new_email.split("@")[-1].lower() - if ( - not NewEmailProvider.objects.filter(provider=provider).exists() - and not User.objects.filter(email__iendswith=f"@{provider}").exclude(pk=profile.user.pk).exists() - ): - NewEmailProvider.objects.create(user=profile.user, provider=provider, use=EMAIL_EDIT) - - def get_success_url(self): - profile = self.get_object() - - return profile.get_absolute_url() - - -class RegisterView(CreateView, ProfileCreate, TokenGenerator): - """Create a profile.""" - - form_class = RegisterForm - template_name = "member/register/index.html" - - def dispatch(self, *args, **kwargs): - return super().dispatch(*args, **kwargs) - - def get_object(self, queryset=None): - return get_object_or_404(Profile, user=self.request.user) - - def get_form(self, form_class=RegisterForm): - return form_class() - - 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 form_valid(self, form): - profile = self.create_profile(form.data) - profile.last_ip_address = get_client_ip(self.request) - profile.username_skeleton = Profile.find_username_skeleton(profile.user.username) - self.save_profile(profile) - token = self.generate_token(profile.user) - try: - self.send_email(token, profile.user) - except Exception as e: - logging.getLogger(__name__).warning("Mail not sent", exc_info=e) - messages.warning(self.request, _("Impossible d'envoyer l'email.")) - self.object = None - return self.form_invalid(form) - return render(self.request, self.get_success_template()) - - def get_success_template(self): - return "member/register/success.html" - - -class SendValidationEmailView(FormView, TokenGenerator): - """Send a validation email on demand.""" - - form_class = UsernameAndEmailForm - template_name = "member/register/send_validation_email.html" - - usr = None - - def get_user(self, username, email): - - if username: - self.usr = get_object_or_404(User, username=username) - - elif email: - self.usr = get_object_or_404(User, email=email) - - def get_form(self, form_class=UsernameAndEmailForm): - return form_class() - - def post(self, request, *args, **kwargs): - form = self.form_class(request.POST) - - if form.is_valid(): - # Fetch the user - self.get_user(form.data["username"], form.data["email"]) - - # User should not already be active - if not self.usr.is_active: - return self.form_valid(form) - else: - if form.data["username"]: - form.errors["username"] = form.error_class([self.get_error_message()]) - else: - form.errors["email"] = form.error_class([self.get_error_message()]) - - return render(request, self.template_name, {"form": form}) - - def form_valid(self, form): - # Delete old token - token = TokenRegister.objects.filter(user=self.usr) - if token.count() >= 1: - token.all().delete() - - # Generate new token and send email - token = self.generate_token(self.usr) - try: - self.send_email(token, self.usr) - except Exception as e: - logging.getLogger(__name__).warning("Mail not sent", exc_info=e) - messages.warning(_("Impossible d'envoyer l'email.")) - return self.form_invalid(form) - - return render(self.request, self.get_success_template()) - - def get_success_template(self): - return "member/register/send_validation_email_success.html" - - def get_error_message(self): - return _("Le compte est déjà activé.") - - -@login_required -def warning_unregister(request): - """ - Display a warning page showing what will happen when the user - unregisters. - """ - return render(request, "member/settings/unregister.html", {"user": request.user}) - - -@login_required -@require_POST -@transaction.atomic -def unregister(request): - """Allow members to unregister.""" - - anonymous = get_object_or_404(User, username=settings.ZDS_APP["member"]["anonymous_account"]) - external = get_object_or_404(User, username=settings.ZDS_APP["member"]["external_account"]) - current = request.user - # Nota : as of v21 all about content paternity is held by a proper receiver in zds.tutorialv2.models.database - PickListOperation.objects.filter(staff_user=current).update(staff_user=anonymous) - PickListOperation.objects.filter(canceler_user=current).update(canceler_user=anonymous) - # Comments likes / dislikes - votes = CommentVote.objects.filter(user=current) - for vote in votes: - if vote.positive: - vote.comment.like -= 1 - else: - vote.comment.dislike -= 1 - vote.comment.save() - votes.delete() - # All contents anonymization - Comment.objects.filter(author=current).update(author=anonymous) - PrivatePost.objects.filter(author=current).update(author=anonymous) - CommentEdit.objects.filter(editor=current).update(editor=anonymous) - CommentEdit.objects.filter(deleted_by=current).update(deleted_by=anonymous) - # Karma notes, alerts and sanctions anonymization (to keep them) - KarmaNote.objects.filter(moderator=current).update(moderator=anonymous) - Ban.objects.filter(moderator=current).update(moderator=anonymous) - Alert.objects.filter(author=current).update(author=anonymous) - Alert.objects.filter(moderator=current).update(moderator=anonymous) - BannedEmailProvider.objects.filter(moderator=current).update(moderator=anonymous) - # Solved hat requests anonymization - HatRequest.objects.filter(moderator=current).update(moderator=anonymous) - # In case current user has been moderator in the past - Comment.objects.filter(editor=current).update(editor=anonymous) - for topic in PrivateTopic.objects.filter(Q(author=current) | Q(participants__in=[current])): - if topic.one_participant_remaining(): - topic.delete() - else: - topic.remove_participant(current) - topic.save() - Topic.objects.filter(solved_by=current).update(solved_by=anonymous) - Topic.objects.filter(author=current).update(author=anonymous) - - # Any content exclusively owned by the unregistering member will - # be deleted just before the User object (using a pre_delete - # receiver). - # - # Regarding galleries, there are two cases: - # - # - "personal galleries" with one owner (the unregistering - # user). The user's ownership is removed and replaced by an - # anonymous user in order not to lost the gallery. - # - # - "personal galleries" with many other owners. It is safe to - # remove the user's ownership, the gallery won't be lost. - - galleries = UserGallery.objects.filter(user=current) - for gallery in galleries: - if gallery.gallery.get_linked_users().count() == 1: - anonymous_gallery = UserGallery() - anonymous_gallery.user = external - anonymous_gallery.mode = "w" - anonymous_gallery.gallery = gallery.gallery - anonymous_gallery.save() - galleries.delete() - - # Remove API access (tokens + applications) - for token in AccessToken.objects.filter(user=current): - token.revoke() - - logout(request) - User.objects.filter(pk=current.pk).delete() - return redirect(reverse("homepage")) - - -@require_POST -@can_write_and_read_now -@login_required -@permission_required("member.change_profile", raise_exception=True) -@transaction.atomic -def modify_profile(request, user_pk): - """Modify the sanction of a user if there is a POST request.""" - - profile = get_object_or_404(Profile, user__pk=user_pk) - if profile.is_private(): - raise PermissionDenied - if request.user.profile == profile: - messages.error(request, _("Vous ne pouvez pas vous sanctionner vous-même !")) - raise PermissionDenied - - if "ls" in request.POST: - state = ReadingOnlySanction(request.POST) - elif "ls-temp" in request.POST: - state = TemporaryReadingOnlySanction(request.POST) - elif "ban" in request.POST: - state = BanSanction(request.POST) - elif "ban-temp" in request.POST: - state = TemporaryBanSanction(request.POST) - elif "un-ls" in request.POST: - state = DeleteReadingOnlySanction(request.POST) - else: - # un-ban - state = DeleteBanSanction(request.POST) - - try: - ban = state.get_sanction(request.user, profile.user) - except ValueError: - raise HttpResponseBadRequest - - state.apply_sanction(profile, ban) - - if "un-ls" in request.POST or "un-ban" in request.POST: - msg = state.get_message_unsanction() - else: - msg = state.get_message_sanction() - - msg = msg.format( - ban.user, ban.moderator, ban.type, state.get_detail(), ban.note, settings.ZDS_APP["site"]["literal_name"] - ) - - state.notify_member(ban, msg) - return redirect(profile.get_absolute_url()) - - -# Settings for public profile - - -@can_write_and_read_now -@login_required -@permission_required("member.change_profile", raise_exception=True) -def settings_mini_profile(request, user_name): - """Minimal settings of users for staff.""" - - # Extra information about the current user - profile = get_object_or_404(Profile, user__username=user_name) - if request.method == "POST": - form = MiniProfileForm(request.POST) - data = {"form": form, "profile": profile} - if form.is_valid(): - profile.biography = form.data["biography"] - profile.site = form.data["site"] - profile.avatar_url = form.data["avatar_url"] - profile.sign = form.data["sign"] - - # Save profile and redirect user to the settings page - # with a message indicating the operation state. - - try: - profile.save() - except: - messages.error(request, _("Une erreur est survenue.")) - return redirect(reverse("member-settings-mini-profile")) - - messages.success(request, _("Le profil a correctement été mis à jour.")) - return redirect(reverse("member-detail", args=[profile.user.username])) - else: - return render(request, "member/settings/profile.html", data) - else: - form = MiniProfileForm( - initial={ - "biography": profile.biography, - "site": profile.site, - "avatar_url": profile.avatar_url, - "sign": profile.sign, - } - ) - data = {"form": form, "profile": profile} - messages.warning( - request, - _( - "Le profil que vous éditez n'est pas le vôtre. " - "Soyez encore plus prudent lors de l'édition de celui-ci !" - ), - ) - return render(request, "member/settings/profile.html", data) - - -class NewEmailProvidersList(LoginRequiredMixin, PermissionRequiredMixin, ZdSPagingListView): - permissions = ["member.change_bannedemailprovider"] - paginate_by = settings.ZDS_APP["member"]["providers_per_page"] - - model = NewEmailProvider - context_object_name = "providers" - template_name = "member/admin/new_email_providers.html" - queryset = NewEmailProvider.objects.select_related("user").select_related("user__profile").order_by("-date") - - -@require_POST -@login_required -@permission_required("member.change_bannedemailprovider", raise_exception=True) -def check_new_email_provider(request, provider_pk): - """Remove an alert about a new provider.""" - - provider = get_object_or_404(NewEmailProvider, pk=provider_pk) - if "ban" in request.POST and not BannedEmailProvider.objects.filter(provider=provider.provider).exists(): - BannedEmailProvider.objects.create(provider=provider.provider, moderator=request.user) - provider.delete() - - messages.success(request, _("Action effectuée.")) - return redirect("new-email-providers") - - -class BannedEmailProvidersList(LoginRequiredMixin, PermissionRequiredMixin, ZdSPagingListView): - """List the banned email providers.""" - - permissions = ["member.change_bannedemailprovider"] - paginate_by = settings.ZDS_APP["member"]["providers_per_page"] - - model = BannedEmailProvider - context_object_name = "providers" - template_name = "member/admin/banned_email_providers.html" - queryset = ( - BannedEmailProvider.objects.select_related("moderator").select_related("moderator__profile").order_by("-date") - ) - - -class MembersWithProviderList(LoginRequiredMixin, PermissionRequiredMixin, ZdSPagingListView): - """List users using a banned email provider.""" - - permissions = ["member.change_bannedemailprovider"] - paginate_by = settings.ZDS_APP["member"]["members_per_page"] - - model = User - context_object_name = "members" - template_name = "member/admin/members_with_provider.html" - - def get_object(self): - return get_object_or_404(BannedEmailProvider, pk=self.kwargs["provider_pk"]) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["provider"] = self.get_object() - return context - - def get_queryset(self): - provider = self.get_object() - return ( - Profile.objects.select_related("user") - .order_by("-last_visit") - .filter(user__email__icontains=f"@{provider.provider}") - ) - - -class AddBannedEmailProvider(LoginRequiredMixin, PermissionRequiredMixin, CreateView): - """Add an email provider to the banned list.""" - - permissions = ["member.change_bannedemailprovider"] - - model = BannedEmailProvider - template_name = "member/admin/add_banned_email_provider.html" - form_class = BannedEmailProviderForm - success_url = reverse_lazy("banned-email-providers") - - def form_valid(self, form): - form.instance.moderator = self.request.user - messages.success(self.request, _("Le fournisseur a été banni.")) - return super().form_valid(form) - - -@require_POST -@login_required -@permission_required("member.change_bannedemailprovider", raise_exception=True) -def remove_banned_email_provider(request, provider_pk): - """Unban an email provider.""" - - provider = get_object_or_404(BannedEmailProvider, pk=provider_pk) - provider.delete() - - messages.success(request, _("Le fournisseur « {} » a été débanni.").format(provider.provider)) - return redirect("banned-email-providers") - - -class HatsList(ZdSPagingListView): - """Display the list of hats.""" - - context_object_name = "hats" - paginate_by = settings.ZDS_APP["member"]["hats_per_page"] - template_name = "member/hats.html" - queryset = ( - Hat.objects.order_by("name") - .select_related("group") - .prefetch_related("group__user_set") - .prefetch_related("group__user_set__profile") - .prefetch_related("profile_set") - .prefetch_related("profile_set__user") - ) - - -class HatDetail(DetailView): - model = Hat - context_object_name = "hat" - template_name = "member/hat.html" - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - hat = context["hat"] - if self.request.user.is_authenticated: - context["is_required"] = HatRequest.objects.filter( - user=self.request.user, hat__iexact=hat.name, is_granted__isnull=True - ).exists() - if hat.group: - context["users"] = hat.group.user_set.select_related("profile") - try: - context["groupcontact"] = hat.group.groupcontact - except GroupContact.DoesNotExist: - context["groupcontact"] = None # group not displayed on contact page - else: - context["users"] = [p.user for p in hat.profile_set.select_related("user")] - return context - - -class HatsSettings(LoginRequiredMixin, CreateView): - model = HatRequest - template_name = "member/settings/hats.html" - form_class = HatRequestForm - - def get_initial(self): - initial = super().get_initial() - if "ask" in self.request.GET: - try: - hat = Hat.objects.get(pk=int(self.request.GET["ask"])) - initial["hat"] = hat.name - except (ValueError, Hat.DoesNotExist): - pass - return initial - - def post(self, request, *args, **kwargs): - if "preview" in request.POST and request.is_ajax(): - content = render(request, "misc/preview.part.html", {"text": request.POST.get("text")}) - return StreamingHttpResponse(content) - - return super().post(request, *args, **kwargs) - - def form_valid(self, form): - form.instance.user = self.request.user - messages.success(self.request, _("Votre demande a bien été envoyée.")) - return super().form_valid(form) - - def get_success_url(self): - # To remove #send-request HTML-anchor. - return "{}#".format(reverse("hats-settings")) - - -class RequestedHatsList(LoginRequiredMixin, PermissionRequiredMixin, ZdSPagingListView): - permissions = ["utils.change_hat"] - paginate_by = settings.ZDS_APP["member"]["requested_hats_per_page"] - - model = HatRequest - context_object_name = "requests" - template_name = "member/admin/requested_hats.html" - queryset = ( - HatRequest.objects.filter(is_granted__isnull=True) - .select_related("user") - .select_related("user__profile") - .order_by("-date") - ) - - -class SolvedHatRequestsList(LoginRequiredMixin, PermissionRequiredMixin, ZdSPagingListView): - permissions = ["utils.change_hat"] - paginate_by = settings.ZDS_APP["member"]["requested_hats_per_page"] - - model = HatRequest - context_object_name = "requests" - template_name = "member/admin/solved_hat_requests.html" - queryset = ( - HatRequest.objects.filter(is_granted__isnull=False) - .select_related("user") - .select_related("user__profile") - .select_related("moderator") - .select_related("moderator__profile") - .order_by("-solved_at") - ) - - -class HatRequestDetail(LoginRequiredMixin, DetailView): - model = HatRequest - context_object_name = "hat_request" - template_name = "member/admin/hat_request.html" - - def get_object(self, queryset=None): - request = super().get_object() - if request.user != self.request.user and not self.request.user.has_perm("utils.change_hat"): - raise PermissionDenied - return request - - -@require_POST -@login_required -@permission_required("utils.change_hat", raise_exception=True) -@transaction.atomic -def solve_hat_request(request, request_pk): - """ - Solve a hat request by granting or denying the requested hat - according to moderator's decision. - """ - - hat_request = get_object_or_404(HatRequest, pk=request_pk) - - if hat_request.is_granted is not None: - raise PermissionDenied - - try: - hat_request.solve( - "grant" in request.POST, request.user, request.POST.get("comment", ""), request.POST.get("hat", None) - ) - messages.success(request, _("La demande a été résolue.")) - return redirect("requested-hats") - except ValueError as e: - messages.error(request, str(e)) - return redirect(hat_request.get_absolute_url()) - - -@require_POST -@login_required -@permission_required("utils.change_hat", raise_exception=True) -@transaction.atomic -def add_hat(request, user_pk): - """ - Add a hat to a user. - Creates the hat if it doesn't exist. - """ - - user = get_object_or_404(User, pk=user_pk) - - hat_name = request.POST.get("hat", "") - - try: - hat = get_hat_to_add(hat_name, user) - user.profile.hats.add(hat) - try: # if hat was requested, remove the relevant request - hat_request = HatRequest.objects.get(user=user, hat__iexact=hat.name, is_granted__isnull=True) - hat_request.solve( - is_granted=False, - comment=_( - "La demande a été automatiquement annulée car " "la casquette vous a été accordée manuellement." - ), - ) - except HatRequest.DoesNotExist: - pass - messages.success(request, _("La casquette a bien été ajoutée.")) - except ValueError as e: - messages.error(request, str(e)) - - return redirect(user.profile.get_absolute_url()) - - -@require_POST -@login_required -@transaction.atomic -def remove_hat(request, user_pk, hat_pk): - """Remove a hat from a user.""" - - user = get_object_or_404(User, pk=user_pk) - hat = get_object_or_404(Hat, pk=hat_pk) - if user != request.user and not request.user.has_perm("utils.change_hat"): - raise PermissionDenied - if hat not in user.profile.hats.all(): - raise Http404 - - user.profile.hats.remove(hat) - - messages.success(request, _("La casquette a bien été retirée.")) - return redirect(user.profile.get_absolute_url()) - - -def login_view(request): - """Logs user in.""" - next_page = request.GET.get("next", "/") - if next_page in [reverse("member-login"), reverse("register-member"), reverse("member-logout")]: - next_page = "/" - csrf_tk = {"next_page": next_page} - csrf_tk.update(csrf(request)) - error = False - - if request.method != "POST": - form = LoginForm() - else: - form = LoginForm(request.POST) - if form.is_valid(): - username = form.cleaned_data["username"] - password = form.cleaned_data["password"] - user = authenticate(username=username, password=password) - if user is None: - initial = {"username": username} - if User.objects.filter(username=username).exists(): - messages.error( - request, - _( - "Le mot de passe saisi est incorrect. " - "Cliquez sur le lien « Mot de passe oublié ? » " - "si vous ne vous en souvenez plus." - ), - ) - else: - messages.error( - request, - _( - "Ce nom d’utilisateur est inconnu. " - "Si vous ne possédez pas de compte, " - "vous pouvez vous inscrire." - ), - ) - form = LoginForm(initial=initial) - if next_page is not None: - form.helper.form_action += "?next=" + next_page - csrf_tk["error"] = error - csrf_tk["form"] = form - return render(request, "member/login.html", {"form": form, "csrf_tk": csrf_tk}) - profile = get_object_or_404(Profile, user=user) - if not user.is_active: - messages.error( - request, - _( - "Vous n'avez pas encore activé votre compte, " - "vous devez le faire pour pouvoir vous " - "connecter sur le site. Regardez dans vos " - "mails : {}." - ).format(user.email), - ) - elif not profile.can_read_now(): - messages.error( - request, - _( - "Vous n'êtes pas autorisé à vous connecter " - "sur le site, vous avez été banni par un " - "modérateur." - ), - ) - else: - login(request, user) - request.session["get_token"] = generate_token() - if "remember" not in request.POST: - request.session.set_expiry(0) - profile.last_ip_address = get_client_ip(request) - profile.save() - # Redirect the user if needed. - # Set the cookie for Clem smileys. - # (For people switching account or clearing cookies - # after a browser session.) - try: - response = redirect(resolve(next_page).url_name) - except NoReverseMatch: - response = redirect(next_page) - except Resolver404: - response = redirect(reverse("homepage")) - set_old_smileys_cookie(response, profile) - return response - - if next_page is not None: - form.helper.form_action += "?next=" + next_page - csrf_tk["error"] = error - csrf_tk["form"] = form - return render(request, "member/login.html", {"form": form, "csrf_tk": csrf_tk}) - - -@login_required -@require_POST -def logout_view(request): - """Log user out.""" - - logout(request) - request.session.clear() - response = redirect(reverse("homepage")) - # disable Clem smileys: - remove_old_smileys_cookie(response) - return response - - -def forgot_password(request): - """If the user has forgotten his password, they can get a new one.""" - - if request.method == "POST": - form = UsernameAndEmailForm(request.POST) - if form.is_valid(): - - # Get data from form - data = form.data - username = data["username"] - email = data["email"] - - # Fetch the user, we need his email address - usr = None - if username: - usr = get_object_or_404(User, Q(username=username)) - - if email: - usr = get_object_or_404(User, Q(email=email)) - - # Generate a valid token during one hour - uuid_token = str(uuid.uuid4()) - date_end = datetime.now() + timedelta(days=0, hours=1, minutes=0, seconds=0) - token = TokenForgotPassword(user=usr, token=uuid_token, date_end=date_end) - token.save() - - # Send email - subject = _("{} - Mot de passe oublié").format(settings.ZDS_APP["site"]["literal_name"]) - from_email = "{} <{}>".format( - settings.ZDS_APP["site"]["literal_name"], settings.ZDS_APP["site"]["email_noreply"] - ) - context = { - "username": usr.username, - "site_name": settings.ZDS_APP["site"]["literal_name"], - "site_url": settings.ZDS_APP["site"]["url"], - "url": settings.ZDS_APP["site"]["url"] + token.get_absolute_url(), - } - message_html = render_to_string("email/member/confirm_forgot_password.html", context) - message_txt = render_to_string("email/member/confirm_forgot_password.txt", context) - - msg = EmailMultiAlternatives(subject, message_txt, from_email, [usr.email]) - msg.attach_alternative(message_html, "text/html") - msg.send() - return render(request, "member/forgot_password/success.html") - else: - return render(request, "member/forgot_password/index.html", {"form": form}) - form = UsernameAndEmailForm() - return render(request, "member/forgot_password/index.html", {"form": form}) - - -def new_password(request): - """Create a new password for a user.""" - - try: - token = request.GET["token"] - except KeyError: - return redirect(reverse("homepage")) - token = get_object_or_404(TokenForgotPassword, token=token) - if request.method == "POST": - form = NewPasswordForm(token.user.username, request.POST) - if form.is_valid(): - data = form.data - password = data["password"] - # User can't confirm his request if it is too late - - if datetime.now() > token.date_end: - return render(request, "member/new_password/failed.html") - token.user.set_password(password) - token.user.save() - token.delete() - return render(request, "member/new_password/success.html") - else: - return render(request, "member/new_password/index.html", {"form": form}) - form = NewPasswordForm(identifier=token.user.username) - return render(request, "member/new_password/index.html", {"form": form}) - - -def activate_account(request): - """Activate an account with a token.""" - try: - token = request.GET["token"] - except KeyError: - return redirect(reverse("homepage")) - token = get_object_or_404(TokenRegister, token=token) - usr = token.user - - # User can't confirm their request if their account is already active - if usr.is_active: - return render(request, "member/register/token_already_used.html") - - # User can't confirm their request if it is too late - if datetime.now() > token.date_end: - return render(request, "member/register/token_failed.html", {"token": token}) - usr.is_active = True - usr.save() - - # Send welcome message - bot = get_object_or_404(User, username=settings.ZDS_APP["member"]["bot_account"]) - msg = render_to_string( - "member/messages/account_activated.md", - { - "username": usr.username, - "site_name": settings.ZDS_APP["site"]["literal_name"], - "library_url": settings.ZDS_APP["site"]["url"] + reverse("publication:list"), - "opinions_url": settings.ZDS_APP["site"]["url"] + reverse("opinion:list"), - "forums_url": settings.ZDS_APP["site"]["url"] + reverse("cats-forums-list"), - }, - ) - - send_mp( - bot, - [usr], - _("Bienvenue sur {}").format(settings.ZDS_APP["site"]["literal_name"]), - _("Le manuel du nouveau membre"), - msg, - send_by_mail=False, - leave=True, - direct=False, - hat=get_hat_from_settings("moderation"), - ) - token.delete() - - # Create an alert for the staff if it's a new provider - if usr.email: - provider = usr.email.split("@")[-1].lower() - if ( - not NewEmailProvider.objects.filter(provider=provider).exists() - and not User.objects.filter(email__iendswith=f"@{provider}").exclude(pk=usr.pk).exists() - ): - NewEmailProvider.objects.create(user=usr, provider=provider, use=NEW_ACCOUNT) - - form = LoginForm(initial={"username": usr.username}) - return render(request, "member/register/token_success.html", {"usr": usr, "form": form}) - - -def generate_token_account(request): - """Generate a token for an account.""" - - try: - token = request.GET["token"] - except KeyError: - return redirect(reverse("homepage")) - token = get_object_or_404(TokenRegister, token=token) - - # Push date - - date_end = datetime.now() + timedelta(days=0, hours=1, minutes=0, seconds=0) - token.date_end = date_end - token.save() - - # Send email - subject = _("{} - Confirmation d'inscription").format(settings.ZDS_APP["site"]["literal_name"]) - from_email = "{} <{}>".format(settings.ZDS_APP["site"]["literal_name"], settings.ZDS_APP["site"]["email_noreply"]) - context = { - "username": token.user.username, - "site_url": settings.ZDS_APP["site"]["url"], - "site_name": settings.ZDS_APP["site"]["literal_name"], - "url": settings.ZDS_APP["site"]["url"] + token.get_absolute_url(), - } - message_html = render_to_string("email/member/confirm_registration.html", context) - message_txt = render_to_string("email/member/confirm_registration.txt", context) - - msg = EmailMultiAlternatives(subject, message_txt, from_email, [token.user.email]) - msg.attach_alternative(message_html, "text/html") - try: - msg.send() - except: - msg = None - return render(request, "member/register/success.html", {}) - - -def get_client_ip(request): - """Retrieve the real IP address of the client.""" - - if "HTTP_X_REAL_IP" in request.META: # nginx - return request.META.get("HTTP_X_REAL_IP") - elif "REMOTE_ADDR" in request.META: - # other - return request.META.get("REMOTE_ADDR") - else: - # Should never happen - return "0.0.0.0" - - -@login_required -def settings_promote(request, user_pk): - """ - Manage groups and activation status of a user. - Only superusers are allowed to use this. - """ - - if not request.user.is_superuser: - raise PermissionDenied - - profile = get_object_or_404(Profile, user__pk=user_pk) - user = profile.user - - if request.method == "POST": - form = PromoteMemberForm(request.POST) - data = dict(form.data) - - groups = Group.objects.all() - usergroups = user.groups.all() - - if "groups" in data: - for group in groups: - if str(group.id) in data["groups"]: - if group not in usergroups: - user.groups.add(group) - messages.success( - request, _("{0} appartient maintenant au groupe {1}.").format(user.username, group.name) - ) - else: - if group in usergroups: - user.groups.remove(group) - messages.warning( - request, - _("{0} n'appartient maintenant plus au groupe {1}.").format(user.username, group.name), - ) - else: - user.groups.clear() - messages.warning(request, _("{0} n'appartient (plus ?) à aucun groupe.").format(user.username)) - - if "activation" in data and "on" in data["activation"]: - user.is_active = True - messages.success(request, _("{0} est maintenant activé.").format(user.username)) - else: - user.is_active = False - messages.warning(request, _("{0} est désactivé.").format(user.username)) - - user.save() - - usergroups = user.groups.all() - bot = get_object_or_404(User, username=settings.ZDS_APP["member"]["bot_account"]) - msg = _( - "Bonjour {0},\n\n" "Un administrateur vient de modifier les groupes " "auxquels vous appartenez. \n" - ).format(user.username) - if len(usergroups) > 0: - msg = format_lazy("{}{}", msg, _("Voici la liste des groupes dont vous faites dorénavant partie :\n\n")) - for group in usergroups: - msg += f"* {group.name}\n" - else: - msg = format_lazy("{}{}", msg, _("* Vous ne faites partie d'aucun groupe")) - send_mp( - bot, - [user], - _("Modification des groupes"), - "", - msg, - send_by_mail=True, - leave=True, - hat=get_hat_from_settings("moderation"), - ) - - return redirect(profile.get_absolute_url()) - - form = PromoteMemberForm(initial={"groups": user.groups.all(), "activation": user.is_active}) - return render(request, "member/admin/promote.html", {"usr": user, "profile": profile, "form": form}) - - -@login_required -@permission_required("member.change_profile", raise_exception=True) -def member_from_ip(request, ip_address): - """List users connected from a particular IP, and an IPV6 subnetwork.""" - - members = Profile.objects.filter(last_ip_address=ip_address).order_by("-last_visit") - members_and_ip = {"members": members, "ip": ip_address} - - if ":" in ip_address: # Check if it's an IPV6 - network_ip = ipaddress.ip_network(ip_address + "/64", strict=False).network_address # Get the network / block - # Remove the additional ":" at the end of the network adresse, so we can filter the IP adresses on this network - network_ip = str(network_ip)[:-1] - network_members = Profile.objects.filter(last_ip_address__startswith=network_ip).order_by("-last_visit") - members_and_ip["network_members"] = network_members - members_and_ip["network_ip"] = network_ip - - return render(request, "member/admin/memberip.html", members_and_ip) - - -@login_required -@permission_required("member.change_profile", raise_exception=True) -@require_POST -def modify_karma(request): - """Add a Karma note to a user profile.""" - - try: - profile_pk = int(request.POST["profile_pk"]) - except (KeyError, ValueError): - raise Http404 - - profile = get_object_or_404(Profile, pk=profile_pk) - if profile.is_private(): - raise PermissionDenied - - note = KarmaNote(user=profile.user, moderator=request.user, note=request.POST.get("note", "").strip()) - - try: - note.karma = int(request.POST["karma"]) - except (KeyError, ValueError): - note.karma = 0 - - try: - if not note.note: - raise ValueError("note cannot be empty") - elif note.karma > 100 or note.karma < -100: - raise ValueError(f"Max karma amount has to be between -100 and 100, you entered {note.karma}") - else: - note.save() - profile.karma += note.karma - profile.save() - except ValueError as e: - logging.getLogger(__name__).warn(f"ValueError: modifying karma failed because {e}") - - return redirect(reverse("member-detail", args=[profile.user.username])) - - -class CreateProfileReportView(LoginRequiredMixin, View): - def post(self, request, *args, **kwargs): - profile = get_object_or_404(Profile, pk=kwargs["profile_pk"]) - reason = request.POST.get("reason", "") - if reason == "": - messages.warning(request, _("Veuillez saisir une raison.")) - else: - alert = Alert(author=request.user, profile=profile, scope="PROFILE", text=reason, pubdate=datetime.now()) - alert.save() - messages.success( - request, _("Votre signalement a été transmis à l'équipe de modération. " "Merci de votre aide !") - ) - return redirect(profile.get_absolute_url()) - - -class SolveProfileReportView(LoginRequiredMixin, PermissionRequiredMixin, View): - permissions = ["member.change_profile"] - - def post(self, request, *args, **kwargs): - alert = get_object_or_404(Alert, pk=kwargs["alert_pk"], solved=False, scope="PROFILE") - text = request.POST.get("text", "") - if text: - msg_title = _("Signalement traité : profil de {}").format(alert.profile.user.username) - msg_content = render_to_string( - "member/messages/alert_solved.md", - { - "alert_author": alert.author.username, - "reported_user": alert.profile.user.username, - "moderator": request.user.username, - "staff_message": text, - }, - ) - alert.solve(request.user, text, msg_title, msg_content) - else: - alert.solve(request.user) - messages.success(request, _("Merci, l'alerte a bien été résolue.")) - return redirect(alert.profile.get_absolute_url()) diff --git a/zds/member/views/__init__.py b/zds/member/views/__init__.py new file mode 100644 index 0000000000..5162a97826 --- /dev/null +++ b/zds/member/views/__init__.py @@ -0,0 +1,29 @@ +from django.conf import settings + +from zds.member.models import Profile +from zds.utils.paginator import ZdSPagingListView + + +def get_client_ip(request): + """Retrieve the real IP address of the client.""" + + if "HTTP_X_REAL_IP" in request.META: # nginx + return request.META.get("HTTP_X_REAL_IP") + elif "REMOTE_ADDR" in request.META: + # other + return request.META.get("REMOTE_ADDR") + else: + # Should never happen + return "0.0.0.0" + + +class MemberList(ZdSPagingListView): + """Display the list of registered users.""" + + context_object_name = "members" + paginate_by = settings.ZDS_APP["member"]["members_per_page"] + template_name = "member/index.html" + + def get_queryset(self): + self.queryset = Profile.objects.contactable_members() + return super().get_queryset() diff --git a/zds/member/views/admin.py b/zds/member/views/admin.py new file mode 100644 index 0000000000..e5a8dcd3dd --- /dev/null +++ b/zds/member/views/admin.py @@ -0,0 +1,89 @@ +from django.conf import settings +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.contrib.auth.models import Group, User +from django.core.exceptions import PermissionDenied +from django.shortcuts import get_object_or_404, redirect, render +from django.utils.text import format_lazy +from django.utils.translation import gettext_lazy as _ + +from zds.member.forms import PromoteMemberForm +from zds.member.models import Profile +from zds.utils.models import get_hat_from_settings +from zds.utils.mps import send_mp + + +@login_required +def settings_promote(request, user_pk): + """ + Manage groups and activation status of a user. + Only superusers are allowed to use this. + """ + + if not request.user.is_superuser: + raise PermissionDenied + + profile = get_object_or_404(Profile, user__pk=user_pk) + user = profile.user + + if request.method == "POST": + form = PromoteMemberForm(request.POST) + data = dict(form.data) + + groups = Group.objects.all() + usergroups = user.groups.all() + + if "groups" in data: + for group in groups: + if str(group.id) in data["groups"]: + if group not in usergroups: + user.groups.add(group) + messages.success( + request, _("{0} appartient maintenant au groupe {1}.").format(user.username, group.name) + ) + else: + if group in usergroups: + user.groups.remove(group) + messages.warning( + request, + _("{0} n'appartient maintenant plus au groupe {1}.").format(user.username, group.name), + ) + else: + user.groups.clear() + messages.warning(request, _("{0} n'appartient (plus ?) à aucun groupe.").format(user.username)) + + if "activation" in data and "on" in data["activation"]: + user.is_active = True + messages.success(request, _("{0} est maintenant activé.").format(user.username)) + else: + user.is_active = False + messages.warning(request, _("{0} est désactivé.").format(user.username)) + + user.save() + + usergroups = user.groups.all() + bot = get_object_or_404(User, username=settings.ZDS_APP["member"]["bot_account"]) + msg = _( + "Bonjour {0},\n\n" "Un administrateur vient de modifier les groupes " "auxquels vous appartenez. \n" + ).format(user.username) + if len(usergroups) > 0: + msg = format_lazy("{}{}", msg, _("Voici la liste des groupes dont vous faites dorénavant partie :\n\n")) + for group in usergroups: + msg += f"* {group.name}\n" + else: + msg = format_lazy("{}{}", msg, _("* Vous ne faites partie d'aucun groupe")) + send_mp( + bot, + [user], + _("Modification des groupes"), + "", + msg, + send_by_mail=True, + leave=True, + hat=get_hat_from_settings("moderation"), + ) + + return redirect(profile.get_absolute_url()) + + form = PromoteMemberForm(initial={"groups": user.groups.all(), "activation": user.is_active}) + return render(request, "member/admin/promote.html", {"usr": user, "profile": profile, "form": form}) diff --git a/zds/member/views/emailproviders.py b/zds/member/views/emailproviders.py new file mode 100644 index 0000000000..f9776c5ffa --- /dev/null +++ b/zds/member/views/emailproviders.py @@ -0,0 +1,110 @@ +from django.conf import settings +from django.contrib import messages +from django.contrib.auth.decorators import login_required, permission_required +from django.contrib.auth.models import User +from django.shortcuts import redirect, get_object_or_404 +from django.views.decorators.http import require_POST +from django.views.generic import CreateView +from django.urls import reverse_lazy +from django.utils.translation import gettext_lazy as _ + +from zds.member.decorator import LoginRequiredMixin, PermissionRequiredMixin +from zds.member.forms import BannedEmailProviderForm +from zds.member.models import NewEmailProvider, BannedEmailProvider, Profile + +from zds.utils.paginator import ZdSPagingListView + + +class NewEmailProvidersList(LoginRequiredMixin, PermissionRequiredMixin, ZdSPagingListView): + permissions = ["member.change_bannedemailprovider"] + paginate_by = settings.ZDS_APP["member"]["providers_per_page"] + + model = NewEmailProvider + context_object_name = "providers" + template_name = "member/admin/new_email_providers.html" + queryset = NewEmailProvider.objects.select_related("user").select_related("user__profile").order_by("-date") + + +@require_POST +@login_required +@permission_required("member.change_bannedemailprovider", raise_exception=True) +def check_new_email_provider(request, provider_pk): + """Remove an alert about a new provider.""" + + provider = get_object_or_404(NewEmailProvider, pk=provider_pk) + if "ban" in request.POST and not BannedEmailProvider.objects.filter(provider=provider.provider).exists(): + BannedEmailProvider.objects.create(provider=provider.provider, moderator=request.user) + provider.delete() + + messages.success(request, _("Action effectuée.")) + return redirect("new-email-providers") + + +class BannedEmailProvidersList(LoginRequiredMixin, PermissionRequiredMixin, ZdSPagingListView): + """List the banned email providers.""" + + permissions = ["member.change_bannedemailprovider"] + paginate_by = settings.ZDS_APP["member"]["providers_per_page"] + + model = BannedEmailProvider + context_object_name = "providers" + template_name = "member/admin/banned_email_providers.html" + queryset = ( + BannedEmailProvider.objects.select_related("moderator").select_related("moderator__profile").order_by("-date") + ) + + +class MembersWithProviderList(LoginRequiredMixin, PermissionRequiredMixin, ZdSPagingListView): + """List users using a banned email provider.""" + + permissions = ["member.change_bannedemailprovider"] + paginate_by = settings.ZDS_APP["member"]["members_per_page"] + + model = User + context_object_name = "members" + template_name = "member/admin/members_with_provider.html" + + def get_object(self): + return get_object_or_404(BannedEmailProvider, pk=self.kwargs["provider_pk"]) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["provider"] = self.get_object() + return context + + def get_queryset(self): + provider = self.get_object() + return ( + Profile.objects.select_related("user") + .order_by("-last_visit") + .filter(user__email__icontains=f"@{provider.provider}") + ) + + +class AddBannedEmailProvider(LoginRequiredMixin, PermissionRequiredMixin, CreateView): + """Add an email provider to the banned list.""" + + permissions = ["member.change_bannedemailprovider"] + + model = BannedEmailProvider + template_name = "member/admin/add_banned_email_provider.html" + form_class = BannedEmailProviderForm + success_url = reverse_lazy("banned-email-providers") + + def form_valid(self, form): + form.instance.moderator = self.request.user + messages.success(self.request, _("Le fournisseur a été banni.")) + return super().form_valid(form) + + +@require_POST +@login_required +@permission_required("member.change_bannedemailprovider", raise_exception=True) +def remove_banned_email_provider(request, provider_pk): + """Unban an email provider.""" + + provider = get_object_or_404(BannedEmailProvider, pk=provider_pk) + provider.delete() + + messages.success(request, _("Le fournisseur « {} » a été débanni.").format(provider.provider)) + return redirect("banned-email-providers") diff --git a/zds/member/views/hats.py b/zds/member/views/hats.py new file mode 100644 index 0000000000..2766e1d84a --- /dev/null +++ b/zds/member/views/hats.py @@ -0,0 +1,212 @@ +from django.conf import settings +from django.contrib import messages +from django.contrib.auth.decorators import login_required, permission_required +from django.contrib.auth.models import User +from django.core.exceptions import PermissionDenied +from django.db import transaction +from django.http import Http404, StreamingHttpResponse +from django.shortcuts import redirect, render, get_object_or_404 +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ +from django.views.decorators.http import require_POST +from django.views.generic import DetailView, CreateView + +from zds.member.decorator import LoginRequiredMixin, PermissionRequiredMixin +from zds.member.forms import HatRequestForm +from zds.pages.models import GroupContact +from zds.utils.models import HatRequest, Hat, get_hat_to_add +from zds.utils.paginator import ZdSPagingListView + + +class HatsList(ZdSPagingListView): + """Display the list of hats.""" + + context_object_name = "hats" + paginate_by = settings.ZDS_APP["member"]["hats_per_page"] + template_name = "member/hats.html" + queryset = ( + Hat.objects.order_by("name") + .select_related("group") + .prefetch_related("group__user_set") + .prefetch_related("group__user_set__profile") + .prefetch_related("profile_set") + .prefetch_related("profile_set__user") + ) + + +class HatDetail(DetailView): + model = Hat + context_object_name = "hat" + template_name = "member/hat.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + hat = context["hat"] + if self.request.user.is_authenticated: + context["is_required"] = HatRequest.objects.filter( + user=self.request.user, hat__iexact=hat.name, is_granted__isnull=True + ).exists() + if hat.group: + context["users"] = hat.group.user_set.select_related("profile") + try: + context["groupcontact"] = hat.group.groupcontact + except GroupContact.DoesNotExist: + context["groupcontact"] = None # group not displayed on contact page + else: + context["users"] = [p.user for p in hat.profile_set.select_related("user")] + return context + + +class HatsSettings(LoginRequiredMixin, CreateView): + model = HatRequest + template_name = "member/settings/hats.html" + form_class = HatRequestForm + + def get_initial(self): + initial = super().get_initial() + if "ask" in self.request.GET: + try: + hat = Hat.objects.get(pk=int(self.request.GET["ask"])) + initial["hat"] = hat.name + except (ValueError, Hat.DoesNotExist): + pass + return initial + + def post(self, request, *args, **kwargs): + if "preview" in request.POST and request.is_ajax(): + content = render(request, "misc/preview.part.html", {"text": request.POST.get("text")}) + return StreamingHttpResponse(content) + + return super().post(request, *args, **kwargs) + + def form_valid(self, form): + form.instance.user = self.request.user + messages.success(self.request, _("Votre demande a bien été envoyée.")) + return super().form_valid(form) + + def get_success_url(self): + # To remove #send-request HTML-anchor. + return "{}#".format(reverse("hats-settings")) + + +class RequestedHatsList(LoginRequiredMixin, PermissionRequiredMixin, ZdSPagingListView): + permissions = ["utils.change_hat"] + paginate_by = settings.ZDS_APP["member"]["requested_hats_per_page"] + + model = HatRequest + context_object_name = "requests" + template_name = "member/admin/requested_hats.html" + queryset = ( + HatRequest.objects.filter(is_granted__isnull=True) + .select_related("user") + .select_related("user__profile") + .order_by("-date") + ) + + +class SolvedHatRequestsList(LoginRequiredMixin, PermissionRequiredMixin, ZdSPagingListView): + permissions = ["utils.change_hat"] + paginate_by = settings.ZDS_APP["member"]["requested_hats_per_page"] + + model = HatRequest + context_object_name = "requests" + template_name = "member/admin/solved_hat_requests.html" + queryset = ( + HatRequest.objects.filter(is_granted__isnull=False) + .select_related("user") + .select_related("user__profile") + .select_related("moderator") + .select_related("moderator__profile") + .order_by("-solved_at") + ) + + +class HatRequestDetail(LoginRequiredMixin, DetailView): + model = HatRequest + context_object_name = "hat_request" + template_name = "member/admin/hat_request.html" + + def get_object(self, queryset=None): + request = super().get_object() + if request.user != self.request.user and not self.request.user.has_perm("utils.change_hat"): + raise PermissionDenied + return request + + +@require_POST +@login_required +@permission_required("utils.change_hat", raise_exception=True) +@transaction.atomic +def solve_hat_request(request, request_pk): + """ + Solve a hat request by granting or denying the requested hat + according to moderator's decision. + """ + + hat_request = get_object_or_404(HatRequest, pk=request_pk) + + if hat_request.is_granted is not None: + raise PermissionDenied + + try: + hat_request.solve( + "grant" in request.POST, request.user, request.POST.get("comment", ""), request.POST.get("hat", None) + ) + messages.success(request, _("La demande a été résolue.")) + return redirect("requested-hats") + except ValueError as e: + messages.error(request, str(e)) + return redirect(hat_request.get_absolute_url()) + + +@require_POST +@login_required +@permission_required("utils.change_hat", raise_exception=True) +@transaction.atomic +def add_hat(request, user_pk): + """ + Add a hat to a user. + Creates the hat if it doesn't exist. + """ + + user = get_object_or_404(User, pk=user_pk) + + hat_name = request.POST.get("hat", "") + + try: + hat = get_hat_to_add(hat_name, user) + user.profile.hats.add(hat) + try: # if hat was requested, remove the relevant request + hat_request = HatRequest.objects.get(user=user, hat__iexact=hat.name, is_granted__isnull=True) + hat_request.solve( + is_granted=False, + comment=_( + "La demande a été automatiquement annulée car " "la casquette vous a été accordée manuellement." + ), + ) + except HatRequest.DoesNotExist: + pass + messages.success(request, _("La casquette a bien été ajoutée.")) + except ValueError as e: + messages.error(request, str(e)) + + return redirect(user.profile.get_absolute_url()) + + +@require_POST +@login_required +@transaction.atomic +def remove_hat(request, user_pk, hat_pk): + """Remove a hat from a user.""" + + user = get_object_or_404(User, pk=user_pk) + hat = get_object_or_404(Hat, pk=hat_pk) + if user != request.user and not request.user.has_perm("utils.change_hat"): + raise PermissionDenied + if hat not in user.profile.hats.all(): + raise Http404 + + user.profile.hats.remove(hat) + + messages.success(request, _("La casquette a bien été retirée.")) + return redirect(user.profile.get_absolute_url()) diff --git a/zds/member/views/login.py b/zds/member/views/login.py new file mode 100644 index 0000000000..712b10e68b --- /dev/null +++ b/zds/member/views/login.py @@ -0,0 +1,117 @@ +from django.contrib import messages +from django.contrib.auth import authenticate, login, logout +from django.contrib.auth.decorators import login_required +from django.contrib.auth.models import User +from django.template.context_processors import csrf +from django.urls import reverse, resolve, Resolver404, NoReverseMatch +from django.shortcuts import redirect, render, get_object_or_404 +from django.utils.translation import gettext_lazy as _ +from django.views.decorators.http import require_POST + +from zds.member.forms import LoginForm +from zds.member.models import Profile, set_old_smileys_cookie, remove_old_smileys_cookie +from zds.member.views import get_client_ip +from zds.utils.tokens import generate_token + + +def login_view(request): + """Logs user in.""" + next_page = request.GET.get("next", "/") + if next_page in [reverse("member-login"), reverse("register-member"), reverse("member-logout")]: + next_page = "/" + csrf_tk = {"next_page": next_page} + csrf_tk.update(csrf(request)) + error = False + + if request.method != "POST": + form = LoginForm() + else: + form = LoginForm(request.POST) + if form.is_valid(): + username = form.cleaned_data["username"] + password = form.cleaned_data["password"] + user = authenticate(username=username, password=password) + if user is None: + initial = {"username": username} + if User.objects.filter(username=username).exists(): + messages.error( + request, + _( + "Le mot de passe saisi est incorrect. " + "Cliquez sur le lien « Mot de passe oublié ? » " + "si vous ne vous en souvenez plus." + ), + ) + else: + messages.error( + request, + _( + "Ce nom d’utilisateur est inconnu. " + "Si vous ne possédez pas de compte, " + "vous pouvez vous inscrire." + ), + ) + form = LoginForm(initial=initial) + if next_page is not None: + form.helper.form_action += "?next=" + next_page + csrf_tk["error"] = error + csrf_tk["form"] = form + return render(request, "member/login.html", {"form": form, "csrf_tk": csrf_tk}) + profile = get_object_or_404(Profile, user=user) + if not user.is_active: + messages.error( + request, + _( + "Vous n'avez pas encore activé votre compte, " + "vous devez le faire pour pouvoir vous " + "connecter sur le site. Regardez dans vos " + "mails : {}." + ).format(user.email), + ) + elif not profile.can_read_now(): + messages.error( + request, + _( + "Vous n'êtes pas autorisé à vous connecter " + "sur le site, vous avez été banni par un " + "modérateur." + ), + ) + else: + login(request, user) + request.session["get_token"] = generate_token() + if "remember" not in request.POST: + request.session.set_expiry(0) + profile.last_ip_address = get_client_ip(request) + profile.save() + # Redirect the user if needed. + # Set the cookie for Clem smileys. + # (For people switching account or clearing cookies + # after a browser session.) + try: + response = redirect(resolve(next_page).url_name) + except NoReverseMatch: + response = redirect(next_page) + except Resolver404: + response = redirect(reverse("homepage")) + set_old_smileys_cookie(response, profile) + return response + + if next_page is not None: + form.helper.form_action += "?next=" + next_page + csrf_tk["error"] = error + csrf_tk["form"] = form + return render(request, "member/login.html", {"form": form, "csrf_tk": csrf_tk}) + + +@login_required +@require_POST +def logout_view(request): + """Log user out.""" + + logout(request) + request.session.clear() + response = redirect(reverse("homepage")) + # disable Clem smileys: + remove_old_smileys_cookie(response) + return response diff --git a/zds/member/views/moderation.py b/zds/member/views/moderation.py new file mode 100644 index 0000000000..a742a52a4a --- /dev/null +++ b/zds/member/views/moderation.py @@ -0,0 +1,181 @@ +import ipaddress + +from django.conf import settings +from django.contrib import messages +from django.contrib.auth.decorators import login_required, permission_required +from django.core.exceptions import PermissionDenied +from django.db import transaction +from django.http import HttpResponseBadRequest +from django.urls import reverse +from django.http import Http404 +from django.shortcuts import redirect, render, get_object_or_404 +from django.utils.translation import gettext_lazy as _ +from django.views.decorators.http import require_POST + +from zds.member.commons import ( + TemporaryReadingOnlySanction, + ReadingOnlySanction, + DeleteReadingOnlySanction, + TemporaryBanSanction, + BanSanction, + DeleteBanSanction, +) +from zds.member.decorator import can_write_and_read_now +from zds.member.forms import MiniProfileForm +from zds.member.models import Profile, KarmaNote +import logging + + +@login_required +@permission_required("member.change_profile", raise_exception=True) +def member_from_ip(request, ip_address): + """List users connected from a particular IP, and an IPV6 subnetwork.""" + + members = Profile.objects.filter(last_ip_address=ip_address).order_by("-last_visit") + members_and_ip = {"members": members, "ip": ip_address} + + if ":" in ip_address: # Check if it's an IPV6 + network_ip = ipaddress.ip_network(ip_address + "/64", strict=False).network_address # Get the network / block + # Remove the additional ":" at the end of the network adresse, so we can filter the IP adresses on this network + network_ip = str(network_ip)[:-1] + network_members = Profile.objects.filter(last_ip_address__startswith=network_ip).order_by("-last_visit") + members_and_ip["network_members"] = network_members + members_and_ip["network_ip"] = network_ip + + return render(request, "member/admin/memberip.html", members_and_ip) + + +@login_required +@permission_required("member.change_profile", raise_exception=True) +@require_POST +def modify_karma(request): + """Add a Karma note to a user profile.""" + + try: + profile_pk = int(request.POST["profile_pk"]) + except (KeyError, ValueError): + raise Http404 + + profile = get_object_or_404(Profile, pk=profile_pk) + if profile.is_private(): + raise PermissionDenied + + note = KarmaNote(user=profile.user, moderator=request.user, note=request.POST.get("note", "").strip()) + + try: + note.karma = int(request.POST["karma"]) + except (KeyError, ValueError): + note.karma = 0 + + try: + if not note.note: + raise ValueError("note cannot be empty") + elif note.karma > 100 or note.karma < -100: + raise ValueError(f"Max karma amount has to be between -100 and 100, you entered {note.karma}") + else: + note.save() + profile.karma += note.karma + profile.save() + except ValueError as e: + logging.getLogger(__name__).warning(f"ValueError: modifying karma failed because {e}") + + return redirect(reverse("member-detail", args=[profile.user.username])) + + +@can_write_and_read_now +@login_required +@permission_required("member.change_profile", raise_exception=True) +def settings_mini_profile(request, user_name): + """Minimal settings of users for staff.""" + + # Extra information about the current user + profile = get_object_or_404(Profile, user__username=user_name) + if request.method == "POST": + form = MiniProfileForm(request.POST) + data = {"form": form, "profile": profile} + if form.is_valid(): + profile.biography = form.data["biography"] + profile.site = form.data["site"] + profile.avatar_url = form.data["avatar_url"] + profile.sign = form.data["sign"] + + # Save profile and redirect user to the settings page + # with a message indicating the operation state. + + try: + profile.save() + except: + messages.error(request, _("Une erreur est survenue.")) + return redirect(reverse("member-settings-mini-profile")) + + messages.success(request, _("Le profil a correctement été mis à jour.")) + return redirect(reverse("member-detail", args=[profile.user.username])) + else: + return render(request, "member/settings/profile.html", data) + else: + form = MiniProfileForm( + initial={ + "biography": profile.biography, + "site": profile.site, + "avatar_url": profile.avatar_url, + "sign": profile.sign, + } + ) + data = {"form": form, "profile": profile} + messages.warning( + request, + _( + "Le profil que vous éditez n'est pas le vôtre. " + "Soyez encore plus prudent lors de l'édition de celui-ci !" + ), + ) + return render(request, "member/settings/profile.html", data) + + +@require_POST +@can_write_and_read_now +@login_required +@permission_required("member.change_profile", raise_exception=True) +@transaction.atomic +def modify_profile(request, user_pk): + """Modify the sanction of a user if there is a POST request.""" + + profile = get_object_or_404(Profile, user__pk=user_pk) + if profile.is_private(): + raise PermissionDenied + if request.user.profile == profile: + messages.error(request, _("Vous ne pouvez pas vous sanctionner vous-même !")) + raise PermissionDenied + + if "ls" in request.POST: + state = ReadingOnlySanction(request.POST) + elif "ls-temp" in request.POST: + state = TemporaryReadingOnlySanction(request.POST) + elif "ban" in request.POST: + state = BanSanction(request.POST) + elif "ban-temp" in request.POST: + state = TemporaryBanSanction(request.POST) + elif "un-ls" in request.POST: + state = DeleteReadingOnlySanction(request.POST) + else: + # un-ban + state = DeleteBanSanction(request.POST) + + try: + ban = state.get_sanction(request.user, profile.user) + except ValueError: + raise HttpResponseBadRequest + + state.apply_sanction(profile, ban) + + if "un-ls" in request.POST or "un-ban" in request.POST: + msg = state.get_message_unsanction() + else: + msg = state.get_message_sanction() + + msg = msg.format( + ban.user, ban.moderator, ban.type, state.get_detail(), ban.note, settings.ZDS_APP["site"]["literal_name"] + ) + + state.notify_member(ban, msg) + return redirect(profile.get_absolute_url()) diff --git a/zds/member/views/password_recovery.py b/zds/member/views/password_recovery.py new file mode 100644 index 0000000000..71a65dd483 --- /dev/null +++ b/zds/member/views/password_recovery.py @@ -0,0 +1,91 @@ +import uuid +from datetime import datetime, timedelta + +from django.conf import settings +from django.contrib.auth.models import User +from django.core.mail import EmailMultiAlternatives +from django.urls import reverse +from django.db.models import Q +from django.shortcuts import redirect, render, get_object_or_404 +from django.template.loader import render_to_string +from django.utils.translation import gettext_lazy as _ + +from zds.member.forms import NewPasswordForm, UsernameAndEmailForm +from zds.member.models import TokenForgotPassword + + +def forgot_password(request): + """If the user has forgotten his password, they can get a new one.""" + + if request.method == "POST": + form = UsernameAndEmailForm(request.POST) + if form.is_valid(): + + # Get data from form + data = form.data + username = data["username"] + email = data["email"] + + # Fetch the user, we need his email address + usr = None + if username: + usr = get_object_or_404(User, Q(username=username)) + + if email: + usr = get_object_or_404(User, Q(email=email)) + + # Generate a valid token during one hour + uuid_token = str(uuid.uuid4()) + date_end = datetime.now() + timedelta(days=0, hours=1, minutes=0, seconds=0) + token = TokenForgotPassword(user=usr, token=uuid_token, date_end=date_end) + token.save() + + # Send email + subject = _("{} - Mot de passe oublié").format(settings.ZDS_APP["site"]["literal_name"]) + from_email = "{} <{}>".format( + settings.ZDS_APP["site"]["literal_name"], settings.ZDS_APP["site"]["email_noreply"] + ) + context = { + "username": usr.username, + "site_name": settings.ZDS_APP["site"]["literal_name"], + "site_url": settings.ZDS_APP["site"]["url"], + "url": settings.ZDS_APP["site"]["url"] + token.get_absolute_url(), + } + message_html = render_to_string("email/member/confirm_forgot_password.html", context) + message_txt = render_to_string("email/member/confirm_forgot_password.txt", context) + + msg = EmailMultiAlternatives(subject, message_txt, from_email, [usr.email]) + msg.attach_alternative(message_html, "text/html") + msg.send() + return render(request, "member/forgot_password/success.html") + else: + return render(request, "member/forgot_password/index.html", {"form": form}) + form = UsernameAndEmailForm() + return render(request, "member/forgot_password/index.html", {"form": form}) + + +def new_password(request): + """Create a new password for a user.""" + + try: + token = request.GET["token"] + except KeyError: + return redirect(reverse("homepage")) + token = get_object_or_404(TokenForgotPassword, token=token) + if request.method == "POST": + form = NewPasswordForm(token.user.username, request.POST) + if form.is_valid(): + data = form.data + password = data["password"] + # User can't confirm his request if it is too late + + if datetime.now() > token.date_end: + return render(request, "member/new_password/failed.html") + token.user.set_password(password) + token.user.save() + token.delete() + return render(request, "member/new_password/success.html") + else: + return render(request, "member/new_password/index.html", {"form": form}) + form = NewPasswordForm(identifier=token.user.username) + return render(request, "member/new_password/index.html", {"form": form}) diff --git a/zds/member/views/profile.py b/zds/member/views/profile.py new file mode 100644 index 0000000000..4a69027e32 --- /dev/null +++ b/zds/member/views/profile.py @@ -0,0 +1,441 @@ +from urllib.parse import unquote + +from django.conf import settings +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.contrib.auth.models import User +from django.core.exceptions import PermissionDenied +from django.urls import reverse, reverse_lazy +from django.http import StreamingHttpResponse +from django.shortcuts import redirect, render, get_object_or_404 +from django.utils.decorators import method_decorator +from django.utils.translation import gettext_lazy as _ +from django.utils.translation import gettext as __ +from django.views.decorators.http import require_POST +from django.views.generic import DetailView, UpdateView + +from zds.forum.models import Topic, TopicRead +from zds.gallery.forms import ImageAsAvatarForm +from zds.member import EMAIL_EDIT + +from zds.member.forms import ( + ProfileForm, + ChangePasswordForm, + ChangeUserForm, + KarmaForm, + GitHubTokenForm, +) +from zds.member.models import ( + Profile, + KarmaNote, + Ban, + NewEmailProvider, + set_old_smileys_cookie, +) +from zds.notification.models import TopicAnswerSubscription, NewPublicationSubscription +from zds.tutorialv2.models import CONTENT_TYPES +from zds.tutorialv2.models.database import PublishedContent, ContentContribution, ContentReaction +from zds.utils.templatetags.pluralize_fr import pluralize_fr + + +class MemberDetail(DetailView): + """Display details about a profile.""" + + context_object_name = "usr" + model = User + template_name = "member/profile.html" + + def get_object(self, queryset=None): + # Use unquote to accept twicely quoted URLs (for instance in MPs + # sent through emarkdown parser). + return get_object_or_404(User, username=unquote(self.kwargs["user_name"])) + + def get_summaries(self, profile): + """ + Returns a summary of this profile's activity, as a list of list of tuples. + Each first-level list item is an activity category (e.g. contents, forums, etc.) + Each second-level list item is a stat in this activity category. + Each tuple is (link url, count, displayed name of the item), where the link url can be None if it's not a link. + + :param profile: The profile. + :return: The summary data. + """ + summaries = [] + + if self.request.user.has_perm("member.change_post"): + count_post = profile.get_post_count_as_staff() + else: + count_post = profile.get_post_count() + + count_topic = profile.get_topic_count() + count_followed_topic = profile.get_followed_topic_count() + count_tutorials = profile.get_public_tutos().count() + count_articles = profile.get_public_articles().count() + count_opinions = profile.get_public_opinions().count() + + summary = [] + if count_tutorials + count_articles + count_opinions == 0: + summary.append((None, 0, __("Aucun contenu publié"))) + + if count_tutorials > 0: + summary.append( + ( + reverse_lazy("tutorial:find-tutorial", args=(profile.user.username,)), + count_tutorials, + __("tutoriel{}").format(pluralize_fr(count_tutorials)), + ) + ) + if count_articles > 0: + summary.append( + ( + reverse_lazy("article:find-article", args=(profile.user.username,)), + count_articles, + __("article{}").format(pluralize_fr(count_articles)), + ) + ) + if count_opinions > 0: + summary.append( + ( + reverse_lazy("opinion:find-opinion", args=(profile.user.username,)), + count_opinions, + __("billet{}").format(pluralize_fr(count_opinions)), + ) + ) + summaries.append(summary) + + summary = [] + if count_post > 0: + summary.append( + ( + reverse_lazy("post-find", args=(profile.user.pk,)), + count_post, + __("message{}").format(pluralize_fr(count_post)), + ) + ) + else: + summary.append((None, 0, __("Aucun message"))) + if count_topic > 0: + summary.append( + ( + reverse_lazy("topic-find", args=(profile.user.pk,)), + count_topic, + __("sujet{} créé{}").format(pluralize_fr(count_topic), pluralize_fr(count_topic)), + ) + ) + user = self.request.user + is_user_profile = user.is_authenticated and User.objects.get(pk=user.pk).profile == profile + if count_followed_topic > 0 and is_user_profile: + summary.append( + ( + reverse_lazy("followed-topic-find"), + count_followed_topic, + __("sujet{} suivi{}").format( + pluralize_fr(count_followed_topic), pluralize_fr(count_followed_topic) + ), + ) + ) + + summaries.append(summary) + + return summaries + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + usr = context["usr"] + profile = usr.profile + context["profile"] = profile + context["topics"] = list(Topic.objects.last_topics_of_a_member(usr, self.request.user)) + followed_query_set = TopicAnswerSubscription.objects.get_objects_followed_by(self.request.user.id) + followed_topics = list(set(followed_query_set) & set(context["topics"])) + for topic in context["topics"]: + topic.is_followed = topic in followed_topics + context["articles"] = PublishedContent.objects.last_articles_of_a_member_loaded(usr) + context["opinions"] = PublishedContent.objects.last_opinions_of_a_member_loaded(usr) + context["tutorials"] = PublishedContent.objects.last_tutorials_of_a_member_loaded(usr) + context["articles_and_tutorials"] = PublishedContent.objects.last_tutorials_and_articles_of_a_member_loaded(usr) + context["topic_read"] = TopicRead.objects.list_read_topic_pk(self.request.user, context["topics"]) + context["subscriber_count"] = NewPublicationSubscription.objects.get_subscriptions(self.object).count() + context["contribution_articles_count"] = ( + ContentContribution.objects.filter( + user__pk=usr.pk, content__sha_public__isnull=False, content__type=CONTENT_TYPES[1]["name"] + ) + .values_list("content", flat=True) + .distinct() + .count() + ) + context["contribution_tutorials_count"] = ( + ContentContribution.objects.filter( + user__pk=usr.pk, content__sha_public__isnull=False, content__type=CONTENT_TYPES[0]["name"] + ) + .values_list("content", flat=True) + .distinct() + .count() + ) + context["content_reactions_count"] = ContentReaction.objects.filter(author=usr).count() + + if self.request.user.has_perm("member.change_profile"): + sanctions = list(Ban.objects.filter(user=usr).select_related("moderator")) + notes = list(KarmaNote.objects.filter(user=usr).select_related("moderator")) + actions = sanctions + notes + actions.sort(key=lambda action: action.pubdate) + actions.reverse() + context["actions"] = actions + context["karmaform"] = KarmaForm(profile) + context["alerts"] = profile.alerts_on_this_profile.all().order_by("-pubdate") + context["has_unsolved_alerts"] = profile.alerts_on_this_profile.filter(solved=False).exists() + + context["summaries"] = self.get_summaries(profile) + return context + + +def redirect_old_profile_to_new(request, user_name): + user = get_object_or_404(User, username=user_name) + return redirect(user.profile, permanent=True) + + +class UpdateMember(UpdateView): + """Update a profile.""" + + form_class = ProfileForm + template_name = "member/settings/profile.html" + + @method_decorator(login_required) + def dispatch(self, *args, **kwargs): + return super().dispatch(*args, **kwargs) + + def get_object(self, queryset=None): + return get_object_or_404(Profile, user=self.request.user) + + def get_form(self, form_class=ProfileForm): + profile = self.get_object() + form = form_class( + initial={ + "biography": profile.biography, + "site": profile.site, + "avatar_url": profile.avatar_url, + "show_sign": profile.show_sign, + "is_hover_enabled": profile.is_hover_enabled, + "use_old_smileys": profile.use_old_smileys, + "allow_temp_visual_changes": profile.allow_temp_visual_changes, + "show_markdown_help": profile.show_markdown_help, + "email_for_answer": profile.email_for_answer, + "email_for_new_mp": profile.email_for_new_mp, + "sign": profile.sign, + "licence": profile.licence, + } + ) + + return form + + def post(self, request, *args, **kwargs): + form = self.form_class(request.POST) + + if "preview" in request.POST and request.is_ajax(): + content = render(request, "misc/preview.part.html", {"text": request.POST.get("text")}) + return StreamingHttpResponse(content) + + if form.is_valid(): + return self.form_valid(form) + + return render(request, self.template_name, {"form": form}) + + def form_valid(self, form): + profile = self.get_object() + self.update_profile(profile, form) + self.save_profile(profile) + + response = redirect(self.get_success_url()) + set_old_smileys_cookie(response, profile) + return response + + def update_profile(self, profile, form): + cleaned_data_options = form.cleaned_data.get("options") + profile.biography = form.data["biography"] + profile.site = form.data["site"] + profile.show_sign = "show_sign" in cleaned_data_options + profile.is_hover_enabled = "is_hover_enabled" in cleaned_data_options + profile.use_old_smileys = "use_old_smileys" in cleaned_data_options + profile.allow_temp_visual_changes = "allow_temp_visual_changes" in cleaned_data_options + profile.show_markdown_help = "show_markdown_help" in cleaned_data_options + profile.email_for_answer = "email_for_answer" in cleaned_data_options + profile.email_for_new_mp = "email_for_new_mp" in cleaned_data_options + profile.avatar_url = form.data["avatar_url"] + profile.sign = form.data["sign"] + profile.licence = form.cleaned_data["licence"] + + def get_success_url(self): + return reverse("update-member") + + def save_profile(self, profile): + try: + profile.save() + profile.user.save() + except Profile.DoesNotExist: + messages.error(self.request, self.get_error_message()) + return redirect(reverse("update-member")) + messages.success(self.request, self.get_success_message()) + + def get_success_message(self): + return _("Le profil a correctement été mis à jour.") + + def get_error_message(self): + return _("Une erreur est survenue.") + + +class UpdateGitHubToken(UpdateView): + """Update the GitHub token.""" + + form_class = GitHubTokenForm + template_name = "member/settings/github.html" + + @method_decorator(login_required) + def dispatch(self, request, *args, **kwargs): + if not request.user.profile.is_dev(): + raise PermissionDenied + return super().dispatch(request, *args, **kwargs) + + def get_object(self, queryset=None): + return get_object_or_404(Profile, user=self.request.user) + + def get_form(self, form_class=GitHubTokenForm): + return form_class() + + 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 form_valid(self, form): + profile = self.get_object() + profile.github_token = form.data["github_token"] + profile.save() + messages.success(self.request, self.get_success_message()) + + return redirect(self.get_success_url()) + + def get_success_url(self): + return reverse("update-github") + + def get_success_message(self): + return _("Votre token GitHub a été mis à jour.") + + def get_error_message(self): + return _("Une erreur est survenue.") + + +@require_POST +@login_required +def remove_github_token(request): + """Remove the current user token.""" + + profile = get_object_or_404(Profile, user=request.user) + if not profile.is_dev(): + raise PermissionDenied + + profile.github_token = "" + profile.save() + + messages.success(request, _("Votre token GitHub a été supprimé.")) + return redirect("update-github") + + +class UpdateAvatarMember(UpdateMember): + """Update the avatar of a logged in user.""" + + form_class = ImageAsAvatarForm + + def get_success_url(self): + profile = self.get_object() + + return reverse("member-detail", args=[profile.user.username]) + + def get_form(self, form_class=ImageAsAvatarForm): + return form_class(self.request.POST) + + def update_profile(self, profile, form): + profile.avatar_url = form.data["avatar_url"] + + def get_success_message(self): + return _("L'avatar a correctement été mis à jour.") + + +class UpdatePasswordMember(UpdateMember): + """Password-related user settings.""" + + form_class = ChangePasswordForm + template_name = "member/settings/account.html" + + def post(self, request, *args, **kwargs): + form = self.form_class(request.user, request.POST) + + if form.is_valid(): + return self.form_valid(form) + + return render(request, self.template_name, {"form": form}) + + def get_form(self, form_class=ChangePasswordForm): + return form_class(self.request.user) + + def update_profile(self, profile, form): + profile.user.set_password(form.data["password_new"]) + + def get_success_message(self): + return _("Le mot de passe a correctement été mis à jour.") + + def get_success_url(self): + return reverse("update-password-member") + + +class UpdateUsernameEmailMember(UpdateMember): + """Settings related to username and email.""" + + form_class = ChangeUserForm + template_name = "member/settings/user.html" + + def post(self, request, *args, **kwargs): + form = self.form_class(request.user, request.POST) + + if form.is_valid(): + return self.form_valid(form) + + return render(request, self.template_name, {"form": form}) + + def get_form(self, form_class=ChangeUserForm): + return form_class(self.request.user) + + def update_profile(self, profile, form): + profile.show_email = "show_email" in form.cleaned_data.get("options") + new_username = form.cleaned_data.get("username") + previous_username = form.cleaned_data.get("previous_username") + new_email = form.cleaned_data.get("email") + previous_email = form.cleaned_data.get("previous_email") + if new_username and new_username != previous_username: + # Add a karma message for the staff + bot = get_object_or_404(User, username=settings.ZDS_APP["member"]["bot_account"]) + KarmaNote( + user=profile.user, + moderator=bot, + note=_("{} s'est renommé {}").format(profile.user.username, new_username), + karma=0, + ).save() + # Change the username + profile.user.username = new_username + # update skeleton + profile.username_skeleton = Profile.find_username_skeleton(new_username) + if new_email and new_email != previous_email: + profile.user.email = new_email + # Create an alert for the staff if it's a new provider + provider = provider = new_email.split("@")[-1].lower() + if ( + not NewEmailProvider.objects.filter(provider=provider).exists() + and not User.objects.filter(email__iendswith=f"@{provider}").exclude(pk=profile.user.pk).exists() + ): + NewEmailProvider.objects.create(user=profile.user, provider=provider, use=EMAIL_EDIT) + + def get_success_url(self): + profile = self.get_object() + + return profile.get_absolute_url() diff --git a/zds/member/views/register.py b/zds/member/views/register.py new file mode 100644 index 0000000000..d92e6f6121 --- /dev/null +++ b/zds/member/views/register.py @@ -0,0 +1,331 @@ +from datetime import datetime, timedelta + +from oauth2_provider.models import AccessToken + +from django.conf import settings +from django.contrib import messages +from django.contrib.auth import logout +from django.contrib.auth.decorators import login_required +from django.contrib.auth.models import User +from django.core.mail import EmailMultiAlternatives +from django.urls import reverse +from django.db import transaction +from django.db.models import Q +from django.shortcuts import redirect, render, get_object_or_404 +from django.template.loader import render_to_string +from django.utils.translation import gettext_lazy as _ +from django.views.decorators.http import require_POST +from django.views.generic import CreateView, FormView + +from zds.forum.models import Topic +from zds.gallery.models import UserGallery +from zds.member import NEW_ACCOUNT +from zds.member.commons import ( + ProfileCreate, + TokenGenerator, +) +from zds.member.forms import RegisterForm, UsernameAndEmailForm, LoginForm +from zds.member.models import ( + Profile, + TokenRegister, + KarmaNote, + Ban, + BannedEmailProvider, + NewEmailProvider, +) +from zds.member.views import get_client_ip +from zds.mp.models import PrivatePost, PrivateTopic +from zds.tutorialv2.models.database import PickListOperation +from zds.utils.models import ( + Comment, + CommentVote, + Alert, + CommentEdit, + HatRequest, + get_hat_from_settings, +) +import logging + +from zds.utils.mps import send_mp + + +class RegisterView(CreateView, ProfileCreate, TokenGenerator): + """Create a profile.""" + + form_class = RegisterForm + template_name = "member/register/index.html" + + def dispatch(self, *args, **kwargs): + return super().dispatch(*args, **kwargs) + + def get_object(self, queryset=None): + return get_object_or_404(Profile, user=self.request.user) + + def get_form(self, form_class=RegisterForm): + return form_class() + + 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 form_valid(self, form): + profile = self.create_profile(form.data) + profile.last_ip_address = get_client_ip(self.request) + profile.username_skeleton = Profile.find_username_skeleton(profile.user.username) + self.save_profile(profile) + token = self.generate_token(profile.user) + try: + self.send_email(token, profile.user) + except Exception as e: + logging.getLogger(__name__).warning("Mail not sent", exc_info=e) + messages.warning(self.request, _("Impossible d'envoyer l'email.")) + self.object = None + return self.form_invalid(form) + return render(self.request, self.get_success_template()) + + def get_success_template(self): + return "member/register/success.html" + + +class SendValidationEmailView(FormView, TokenGenerator): + """Send a validation email on demand.""" + + form_class = UsernameAndEmailForm + template_name = "member/register/send_validation_email.html" + + usr = None + + def get_user(self, username, email): + + if username: + self.usr = get_object_or_404(User, username=username) + + elif email: + self.usr = get_object_or_404(User, email=email) + + def get_form(self, form_class=UsernameAndEmailForm): + return form_class() + + def post(self, request, *args, **kwargs): + form = self.form_class(request.POST) + + if form.is_valid(): + # Fetch the user + self.get_user(form.data["username"], form.data["email"]) + + # User should not already be active + if not self.usr.is_active: + return self.form_valid(form) + else: + if form.data["username"]: + form.errors["username"] = form.error_class([self.get_error_message()]) + else: + form.errors["email"] = form.error_class([self.get_error_message()]) + + return render(request, self.template_name, {"form": form}) + + def form_valid(self, form): + # Delete old token + token = TokenRegister.objects.filter(user=self.usr) + if token.count() >= 1: + token.all().delete() + + # Generate new token and send email + token = self.generate_token(self.usr) + try: + self.send_email(token, self.usr) + except Exception as e: + logging.getLogger(__name__).warning("Mail not sent", exc_info=e) + messages.warning(_("Impossible d'envoyer l'email.")) + return self.form_invalid(form) + + return render(self.request, self.get_success_template()) + + def get_success_template(self): + return "member/register/send_validation_email_success.html" + + def get_error_message(self): + return _("Le compte est déjà activé.") + + +@login_required +def warning_unregister(request): + """ + Display a warning page showing what will happen when the user + unregisters. + """ + return render(request, "member/settings/unregister.html", {"user": request.user}) + + +@login_required +@require_POST +@transaction.atomic +def unregister(request): + """Allow members to unregister.""" + + anonymous = get_object_or_404(User, username=settings.ZDS_APP["member"]["anonymous_account"]) + external = get_object_or_404(User, username=settings.ZDS_APP["member"]["external_account"]) + current = request.user + # Nota : as of v21 all about content paternity is held by a proper receiver in zds.tutorialv2.models.database + PickListOperation.objects.filter(staff_user=current).update(staff_user=anonymous) + PickListOperation.objects.filter(canceler_user=current).update(canceler_user=anonymous) + # Comments likes / dislikes + votes = CommentVote.objects.filter(user=current) + for vote in votes: + if vote.positive: + vote.comment.like -= 1 + else: + vote.comment.dislike -= 1 + vote.comment.save() + votes.delete() + # All contents anonymization + Comment.objects.filter(author=current).update(author=anonymous) + PrivatePost.objects.filter(author=current).update(author=anonymous) + CommentEdit.objects.filter(editor=current).update(editor=anonymous) + CommentEdit.objects.filter(deleted_by=current).update(deleted_by=anonymous) + # Karma notes, alerts and sanctions anonymization (to keep them) + KarmaNote.objects.filter(moderator=current).update(moderator=anonymous) + Ban.objects.filter(moderator=current).update(moderator=anonymous) + Alert.objects.filter(author=current).update(author=anonymous) + Alert.objects.filter(moderator=current).update(moderator=anonymous) + BannedEmailProvider.objects.filter(moderator=current).update(moderator=anonymous) + # Solved hat requests anonymization + HatRequest.objects.filter(moderator=current).update(moderator=anonymous) + # In case current user has been moderator in the past + Comment.objects.filter(editor=current).update(editor=anonymous) + for topic in PrivateTopic.objects.filter(Q(author=current) | Q(participants__in=[current])): + if topic.one_participant_remaining(): + topic.delete() + else: + topic.remove_participant(current) + topic.save() + Topic.objects.filter(solved_by=current).update(solved_by=anonymous) + Topic.objects.filter(author=current).update(author=anonymous) + + # Any content exclusively owned by the unregistering member will + # be deleted just before the User object (using a pre_delete + # receiver). + # + # Regarding galleries, there are two cases: + # + # - "personal galleries" with one owner (the unregistering + # user). The user's ownership is removed and replaced by an + # anonymous user in order not to lost the gallery. + # + # - "personal galleries" with many other owners. It is safe to + # remove the user's ownership, the gallery won't be lost. + + galleries = UserGallery.objects.filter(user=current) + for gallery in galleries: + if gallery.gallery.get_linked_users().count() == 1: + anonymous_gallery = UserGallery() + anonymous_gallery.user = external + anonymous_gallery.mode = "w" + anonymous_gallery.gallery = gallery.gallery + anonymous_gallery.save() + galleries.delete() + + # Remove API access (tokens + applications) + for token in AccessToken.objects.filter(user=current): + token.revoke() + + logout(request) + User.objects.filter(pk=current.pk).delete() + return redirect(reverse("homepage")) + + +def activate_account(request): + """Activate an account with a token.""" + try: + token = request.GET["token"] + except KeyError: + return redirect(reverse("homepage")) + token = get_object_or_404(TokenRegister, token=token) + usr = token.user + + # User can't confirm their request if their account is already active + if usr.is_active: + return render(request, "member/register/token_already_used.html") + + # User can't confirm their request if it is too late + if datetime.now() > token.date_end: + return render(request, "member/register/token_failed.html", {"token": token}) + usr.is_active = True + usr.save() + + # Send welcome message + bot = get_object_or_404(User, username=settings.ZDS_APP["member"]["bot_account"]) + msg = render_to_string( + "member/messages/account_activated.md", + { + "username": usr.username, + "site_name": settings.ZDS_APP["site"]["literal_name"], + "library_url": settings.ZDS_APP["site"]["url"] + reverse("publication:list"), + "opinions_url": settings.ZDS_APP["site"]["url"] + reverse("opinion:list"), + "forums_url": settings.ZDS_APP["site"]["url"] + reverse("cats-forums-list"), + }, + ) + + send_mp( + bot, + [usr], + _("Bienvenue sur {}").format(settings.ZDS_APP["site"]["literal_name"]), + _("Le manuel du nouveau membre"), + msg, + send_by_mail=False, + leave=True, + direct=False, + hat=get_hat_from_settings("moderation"), + ) + token.delete() + + # Create an alert for the staff if it's a new provider + if usr.email: + provider = usr.email.split("@")[-1].lower() + if ( + not NewEmailProvider.objects.filter(provider=provider).exists() + and not User.objects.filter(email__iendswith=f"@{provider}").exclude(pk=usr.pk).exists() + ): + NewEmailProvider.objects.create(user=usr, provider=provider, use=NEW_ACCOUNT) + + form = LoginForm(initial={"username": usr.username}) + return render(request, "member/register/token_success.html", {"usr": usr, "form": form}) + + +def generate_token_account(request): + """Generate a token for an account.""" + + try: + token = request.GET["token"] + except KeyError: + return redirect(reverse("homepage")) + token = get_object_or_404(TokenRegister, token=token) + + # Push date + + date_end = datetime.now() + timedelta(days=0, hours=1, minutes=0, seconds=0) + token.date_end = date_end + token.save() + + # Send email + subject = _("{} - Confirmation d'inscription").format(settings.ZDS_APP["site"]["literal_name"]) + from_email = "{} <{}>".format(settings.ZDS_APP["site"]["literal_name"], settings.ZDS_APP["site"]["email_noreply"]) + context = { + "username": token.user.username, + "site_url": settings.ZDS_APP["site"]["url"], + "site_name": settings.ZDS_APP["site"]["literal_name"], + "url": settings.ZDS_APP["site"]["url"] + token.get_absolute_url(), + } + message_html = render_to_string("email/member/confirm_registration.html", context) + message_txt = render_to_string("email/member/confirm_registration.txt", context) + + msg = EmailMultiAlternatives(subject, message_txt, from_email, [token.user.email]) + msg.attach_alternative(message_html, "text/html") + try: + msg.send() + except: + msg = None + return render(request, "member/register/success.html", {}) diff --git a/zds/member/views/reports.py b/zds/member/views/reports.py new file mode 100644 index 0000000000..c9a258734b --- /dev/null +++ b/zds/member/views/reports.py @@ -0,0 +1,50 @@ +from datetime import datetime + +from django.contrib import messages +from django.shortcuts import redirect, get_object_or_404 +from django.template.loader import render_to_string +from django.utils.translation import gettext_lazy as _ +from django.views.generic import View + +from zds.member.decorator import LoginRequiredMixin, PermissionRequiredMixin +from zds.member.models import Profile +from zds.utils.models import Alert + + +class CreateProfileReportView(LoginRequiredMixin, View): + def post(self, request, *args, **kwargs): + profile = get_object_or_404(Profile, pk=kwargs["profile_pk"]) + reason = request.POST.get("reason", "") + if reason == "": + messages.warning(request, _("Veuillez saisir une raison.")) + else: + alert = Alert(author=request.user, profile=profile, scope="PROFILE", text=reason, pubdate=datetime.now()) + alert.save() + messages.success( + request, _("Votre signalement a été transmis à l'équipe de modération. " "Merci de votre aide !") + ) + return redirect(profile.get_absolute_url()) + + +class SolveProfileReportView(LoginRequiredMixin, PermissionRequiredMixin, View): + permissions = ["member.change_profile"] + + def post(self, request, *args, **kwargs): + alert = get_object_or_404(Alert, pk=kwargs["alert_pk"], solved=False, scope="PROFILE") + text = request.POST.get("text", "") + if text: + msg_title = _("Signalement traité : profil de {}").format(alert.profile.user.username) + msg_content = render_to_string( + "member/messages/alert_solved.md", + { + "alert_author": alert.author.username, + "reported_user": alert.profile.user.username, + "moderator": request.user.username, + "staff_message": text, + }, + ) + alert.solve(request.user, text, msg_title, msg_content) + else: + alert.solve(request.user) + messages.success(request, _("Merci, l'alerte a bien été résolue.")) + return redirect(alert.profile.get_absolute_url()) diff --git a/zds/tutorialv2/tests/tests_views/tests_content.py b/zds/tutorialv2/tests/tests_views/tests_content.py index ebc436bbc3..4f4d51b8da 100644 --- a/zds/tutorialv2/tests/tests_views/tests_content.py +++ b/zds/tutorialv2/tests/tests_views/tests_content.py @@ -1,5 +1,4 @@ import datetime -from json import loads import shutil import tempfile import zipfile @@ -10,7 +9,6 @@ from django.conf import settings from django.contrib import messages from django.contrib.auth.models import Group -from django.core import mail from django.urls import reverse from django.test import TestCase from django.utils.translation import gettext_lazy as _ @@ -21,7 +19,7 @@ from zds.gallery.models import GALLERY_WRITE, UserGallery, Gallery from zds.gallery.models import Image from zds.member.tests.factories import ProfileFactory, StaffProfileFactory, UserFactory -from zds.mp.models import PrivateTopic, is_privatetopic_unread, PrivatePost +from zds.mp.models import PrivateTopic, PrivatePost from zds.notification.models import ( TopicAnswerSubscription, ContentReactionAnswerSubscription, @@ -40,20 +38,16 @@ PublishableContent, Validation, PublishedContent, - ContentReaction, - ContentRead, ) from zds.tutorialv2.publication_utils import ( - publish_content, PublicatorRegistry, Publicator, ZMarkdownRebberLatexPublicator, ZMarkdownEpubPublicator, ) from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents -from zds.utils.models import HelpWriting, Alert, Tag, Hat -from zds.utils.tests.factories import HelpWritingFactory, CategoryFactory, SubCategoryFactory, LicenceFactory -from zds.utils.header_notifications import get_header_notifications +from zds.utils.models import HelpWriting, Tag +from zds.utils.tests.factories import HelpWritingFactory, SubCategoryFactory, LicenceFactory from zds import json_handler @@ -3581,1926 +3575,3 @@ def test_no_invalid_titles(self): self.assertEqual(result.status_code, 302) self.assertNotEqual(PublishableContent.objects.all().count(), prev_count) prev_count += 1 - - -@override_for_contents() -class PublishedContentTests(TutorialTestMixin, TestCase): - def setUp(self): - self.overridden_zds_app["content"]["default_licence_pk"] = LicenceFactory().pk - # don't build PDF to speed up the tests - self.overridden_zds_app["content"]["build_pdf_when_published"] = False - - self.staff = StaffProfileFactory().user - - settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" - self.mas = ProfileFactory().user - self.overridden_zds_app["member"]["bot_account"] = self.mas.username - - bot = Group(name=self.overridden_zds_app["member"]["bot_group"]) - bot.save() - self.external = UserFactory(username=self.overridden_zds_app["member"]["external_account"], password="anything") - - self.beta_forum = ForumFactory( - pk=self.overridden_zds_app["forum"]["beta_forum_id"], - category=ForumCategoryFactory(position=1), - position_in_category=1, - ) # ensure that the forum, for the beta versions, is created - - self.licence = LicenceFactory() - self.subcategory = SubCategoryFactory() - - self.user_author = ProfileFactory().user - self.user_staff = StaffProfileFactory().user - self.user_guest = ProfileFactory().user - - self.hat, _ = Hat.objects.get_or_create(name__iexact="A hat", defaults={"name": "A hat"}) - self.user_guest.profile.hats.add(self.hat) - - # create a tutorial - self.tuto = PublishableContentFactory(type="TUTORIAL") - self.tuto.authors.add(self.user_author) - UserGalleryFactory(gallery=self.tuto.gallery, user=self.user_author, mode="W") - self.tuto.licence = self.licence - self.tuto.subcategory.add(self.subcategory) - self.tuto.save() - - # fill it with one part, containing one chapter, containing one extract - self.tuto_draft = self.tuto.load_version() - self.part1 = ContainerFactory(parent=self.tuto_draft, db_object=self.tuto) - self.chapter1 = ContainerFactory(parent=self.part1, db_object=self.tuto) - self.extract1 = ExtractFactory(container=self.chapter1, db_object=self.tuto) - - # then, publish it ! - version = self.tuto_draft.current_version - self.published = publish_content(self.tuto, self.tuto_draft, is_major_update=True) - - self.tuto.sha_public = version - self.tuto.sha_draft = version - self.tuto.public_version = self.published - self.tuto.save() - - def test_published(self): - """Just a small test to ensure that the setUp() function produce a proper published content""" - - result = self.client.get(reverse("tutorial:view", kwargs={"pk": self.tuto.pk, "slug": self.tuto.slug})) - self.assertEqual(result.status_code, 200) - - # test access for guest user - self.client.force_login(self.user_guest) - - result = self.client.get(reverse("tutorial:view", kwargs={"pk": self.tuto.pk, "slug": self.tuto.slug})) - self.assertEqual(result.status_code, 200) - - def test_public_access(self): - """Test that everybody have access to a content after its publication""" - - text_validation = "Valide moi ce truc, please !" - text_publication = "Aussi tôt dit, aussi tôt fait !" - - # 1. Article: - article = PublishableContentFactory(type="ARTICLE") - - article.authors.add(self.user_author) - UserGalleryFactory(gallery=article.gallery, user=self.user_author, mode="W") - article.licence = self.licence - article.save() - - # populate the article - article_draft = article.load_version() - ExtractFactory(container=article_draft, db_object=article) - ExtractFactory(container=article_draft, db_object=article) - - # connect with author: - self.client.force_login(self.user_author) - - # ask validation - self.assertEqual(Validation.objects.count(), 0) - - result = self.client.post( - reverse("validation:ask", kwargs={"pk": article.pk, "slug": article.slug}), - {"text": text_validation, "version": article_draft.current_version}, - follow=False, - ) - self.assertEqual(result.status_code, 302) - - # login with staff and publish - self.client.force_login(self.user_staff) - - validation = Validation.objects.filter(content=article).last() - - result = self.client.post( - reverse("validation:reserve", kwargs={"pk": validation.pk}), {"version": validation.version}, follow=False - ) - self.assertEqual(result.status_code, 302) - - # accept - result = self.client.post( - reverse("validation:accept", kwargs={"pk": validation.pk}), - {"text": text_publication, "is_major": True}, - follow=False, - ) - self.assertEqual(result.status_code, 302) - - published = PublishedContent.objects.filter(content=article).first() - self.assertIsNotNone(published) - - # test access to staff - result = self.client.get(reverse("article:view", kwargs={"pk": article.pk, "slug": article_draft.slug})) - self.assertEqual(result.status_code, 200) - - # test access to public - self.client.logout() - result = self.client.get(reverse("article:view", kwargs={"pk": article.pk, "slug": article_draft.slug})) - self.assertEqual(result.status_code, 200) - - # test access for guest user - self.client.force_login(self.user_guest) - result = self.client.get(reverse("article:view", kwargs={"pk": article.pk, "slug": article_draft.slug})) - self.assertEqual(result.status_code, 200) - - # 2. middle-size tutorial (just to test the access to chapters) - midsize_tuto = PublishableContentFactory(type="TUTORIAL") - - midsize_tuto.authors.add(self.user_author) - UserGalleryFactory(gallery=midsize_tuto.gallery, user=self.user_author, mode="W") - midsize_tuto.licence = self.licence - midsize_tuto.save() - - # populate the midsize_tuto - midsize_tuto_draft = midsize_tuto.load_version() - chapter1 = ContainerFactory(parent=midsize_tuto_draft, db_object=midsize_tuto) - ExtractFactory(container=chapter1, db_object=midsize_tuto) - - # connect with author: - self.client.force_login(self.user_author) - - # ask validation - result = self.client.post( - reverse("validation:ask", kwargs={"pk": midsize_tuto.pk, "slug": midsize_tuto.slug}), - {"text": text_validation, "version": midsize_tuto_draft.current_version}, - follow=False, - ) - self.assertEqual(result.status_code, 302) - - # login with staff and publish - self.client.force_login(self.user_staff) - - validation = Validation.objects.filter(content=midsize_tuto).last() - - result = self.client.post( - reverse("validation:reserve", kwargs={"pk": validation.pk}), {"version": validation.version}, follow=False - ) - self.assertEqual(result.status_code, 302) - - # accept - result = self.client.post( - reverse("validation:accept", kwargs={"pk": validation.pk}), - {"text": text_publication, "is_major": True}, - follow=False, - ) - self.assertEqual(result.status_code, 302) - - published = PublishedContent.objects.filter(content=midsize_tuto).first() - self.assertIsNotNone(published) - - # test access to staff - result = self.client.get( - reverse("tutorial:view", kwargs={"pk": midsize_tuto.pk, "slug": midsize_tuto_draft.slug}) - ) - self.assertEqual(result.status_code, 200) - - result = self.client.get( - reverse( - "tutorial:view-container", - kwargs={"pk": midsize_tuto.pk, "slug": midsize_tuto_draft.slug, "container_slug": chapter1.slug}, - ) - ) - self.assertEqual(result.status_code, 200) - - # test access to public - self.client.logout() - result = self.client.get( - reverse("tutorial:view", kwargs={"pk": midsize_tuto.pk, "slug": midsize_tuto_draft.slug}) - ) - self.assertEqual(result.status_code, 200) - - result = self.client.get( - reverse( - "tutorial:view-container", - kwargs={"pk": midsize_tuto.pk, "slug": midsize_tuto_draft.slug, "container_slug": chapter1.slug}, - ) - ) - self.assertEqual(result.status_code, 200) - - # test access for guest user - self.client.force_login(self.user_guest) - result = self.client.get( - reverse("tutorial:view", kwargs={"pk": midsize_tuto.pk, "slug": midsize_tuto_draft.slug}) - ) - self.assertEqual(result.status_code, 200) - - result = self.client.get( - reverse( - "tutorial:view-container", - kwargs={"pk": midsize_tuto.pk, "slug": midsize_tuto_draft.slug, "container_slug": chapter1.slug}, - ) - ) - self.assertEqual(result.status_code, 200) - - # 3. a big tutorial (just to test the access to parts and chapters) - bigtuto = PublishableContentFactory(type="TUTORIAL") - - bigtuto.authors.add(self.user_author) - UserGalleryFactory(gallery=bigtuto.gallery, user=self.user_author, mode="W") - bigtuto.licence = self.licence - bigtuto.save() - - # populate the bigtuto - bigtuto_draft = bigtuto.load_version() - part1 = ContainerFactory(parent=bigtuto_draft, db_object=bigtuto) - chapter1 = ContainerFactory(parent=part1, db_object=bigtuto) - ExtractFactory(container=chapter1, db_object=bigtuto) - - # connect with author: - self.client.force_login(self.user_author) - - # ask validation - result = self.client.post( - reverse("validation:ask", kwargs={"pk": bigtuto.pk, "slug": bigtuto.slug}), - {"text": text_validation, "version": bigtuto_draft.current_version}, - follow=False, - ) - self.assertEqual(result.status_code, 302) - - # login with staff and publish - self.client.force_login(self.user_staff) - - validation = Validation.objects.filter(content=bigtuto).last() - - result = self.client.post( - reverse("validation:reserve", kwargs={"pk": validation.pk}), {"version": validation.version}, follow=False - ) - self.assertEqual(result.status_code, 302) - - # accept - result = self.client.post( - reverse("validation:accept", kwargs={"pk": validation.pk}), - { - "text": text_publication, - }, - follow=False, - ) - self.assertEqual(result.status_code, 302) - - published = PublishedContent.objects.filter(content=bigtuto).first() - self.assertIsNotNone(published) - - # test access to staff - result = self.client.get(reverse("tutorial:view", kwargs={"pk": bigtuto.pk, "slug": bigtuto_draft.slug})) - self.assertEqual(result.status_code, 200) - - result = self.client.get( - reverse( - "tutorial:view-container", - kwargs={"pk": bigtuto.pk, "slug": bigtuto_draft.slug, "container_slug": part1.slug}, - ) - ) - self.assertEqual(result.status_code, 200) - - result = self.client.get( - reverse( - "tutorial:view-container", - kwargs={ - "pk": bigtuto.pk, - "slug": bigtuto_draft.slug, - "parent_container_slug": part1.slug, - "container_slug": chapter1.slug, - }, - ) - ) - self.assertEqual(result.status_code, 200) - - # test access to public - self.client.logout() - result = self.client.get(reverse("tutorial:view", kwargs={"pk": bigtuto.pk, "slug": bigtuto_draft.slug})) - self.assertEqual(result.status_code, 200) - - result = self.client.get( - reverse( - "tutorial:view-container", - kwargs={"pk": bigtuto.pk, "slug": bigtuto_draft.slug, "container_slug": part1.slug}, - ) - ) - self.assertEqual(result.status_code, 200) - - result = self.client.get( - reverse( - "tutorial:view-container", - kwargs={ - "pk": bigtuto.pk, - "slug": bigtuto_draft.slug, - "parent_container_slug": part1.slug, - "container_slug": chapter1.slug, - }, - ) - ) - self.assertEqual(result.status_code, 200) - - # test access for guest user - self.client.force_login(self.user_guest) - result = self.client.get(reverse("tutorial:view", kwargs={"pk": bigtuto.pk, "slug": bigtuto_draft.slug})) - self.assertEqual(result.status_code, 200) - - result = self.client.get( - reverse( - "tutorial:view-container", - kwargs={"pk": bigtuto.pk, "slug": bigtuto_draft.slug, "container_slug": part1.slug}, - ) - ) - self.assertEqual(result.status_code, 200) - - result = self.client.get( - reverse( - "tutorial:view-container", - kwargs={ - "pk": bigtuto.pk, - "slug": bigtuto_draft.slug, - "parent_container_slug": part1.slug, - "container_slug": chapter1.slug, - }, - ) - ) - self.assertEqual(result.status_code, 200) - - # just for the fun of it, lets then revoke publication - self.client.force_login(self.user_staff) - - result = self.client.post( - reverse("validation:revoke", kwargs={"pk": bigtuto.pk, "slug": bigtuto.slug}), - {"text": "Pour le fun", "version": bigtuto_draft.current_version}, - follow=False, - ) - self.assertEqual(result.status_code, 302) - - # now, let's get a whole bunch of good old fashioned 404 (and not 403 or 302 !!) - result = self.client.get(reverse("tutorial:view", kwargs={"pk": bigtuto.pk, "slug": bigtuto_draft.slug})) - self.assertEqual(result.status_code, 404) - - result = self.client.get( - reverse( - "tutorial:view-container", - kwargs={"pk": bigtuto.pk, "slug": bigtuto_draft.slug, "container_slug": part1.slug}, - ) - ) - self.assertEqual(result.status_code, 404) - - result = self.client.get( - reverse( - "tutorial:view-container", - kwargs={ - "pk": bigtuto.pk, - "slug": bigtuto_draft.slug, - "parent_container_slug": part1.slug, - "container_slug": chapter1.slug, - }, - ) - ) - self.assertEqual(result.status_code, 404) - - # test access to public - self.client.logout() - result = self.client.get(reverse("tutorial:view", kwargs={"pk": bigtuto.pk, "slug": bigtuto_draft.slug})) - self.assertEqual(result.status_code, 404) - - result = self.client.get( - reverse( - "tutorial:view-container", - kwargs={"pk": bigtuto.pk, "slug": bigtuto_draft.slug, "container_slug": part1.slug}, - ) - ) - self.assertEqual(result.status_code, 404) - - result = self.client.get( - reverse( - "tutorial:view-container", - kwargs={ - "pk": bigtuto.pk, - "slug": bigtuto_draft.slug, - "parent_container_slug": part1.slug, - "container_slug": chapter1.slug, - }, - ) - ) - self.assertEqual(result.status_code, 404) - - # test access for guest user - self.client.force_login(self.user_guest) - - result = self.client.get(reverse("tutorial:view", kwargs={"pk": bigtuto.pk, "slug": bigtuto_draft.slug})) - self.assertEqual(result.status_code, 404) - - result = self.client.get( - reverse( - "tutorial:view-container", - kwargs={"pk": bigtuto.pk, "slug": bigtuto_draft.slug, "container_slug": part1.slug}, - ) - ) - self.assertEqual(result.status_code, 404) - - result = self.client.get( - reverse( - "tutorial:view-container", - kwargs={ - "pk": bigtuto.pk, - "slug": bigtuto_draft.slug, - "parent_container_slug": part1.slug, - "container_slug": chapter1.slug, - }, - ) - ) - self.assertEqual(result.status_code, 404) - - def test_add_note(self): - - message_to_post = "la ZEP-12" - - self.client.force_login(self.user_guest) - - result = self.client.post( - reverse("content:add-reaction") + f"?pk={self.published.content.pk}", - {"text": message_to_post, "last_note": 0, "with_hat": self.hat.pk}, - follow=True, - ) - self.assertEqual(result.status_code, 200) - self.assertEqual(ContentReaction.objects.latest("pubdate").hat, self.hat) - - reactions = ContentReaction.objects.all() - self.assertEqual(len(reactions), 1) - self.assertEqual(reactions[0].text, message_to_post) - - reads = ContentRead.objects.filter(user=self.user_guest).all() - self.assertEqual(len(reads), 1) - self.assertEqual(reads[0].content.pk, self.tuto.pk) - self.assertEqual(reads[0].note.pk, reactions[0].pk) - - self.assertEqual( - self.client.get(reverse("tutorial:view", args=[self.tuto.pk, self.tuto.slug])).status_code, 200 - ) - result = self.client.post( - reverse("content:add-reaction") + f"?clementine={self.published.content.pk}", - {"text": message_to_post, "last_note": "0"}, - follow=True, - ) - self.assertEqual(result.status_code, 404) - - # visit the tutorial trigger the creation of a ContentRead - self.client.force_login(self.user_staff) - - self.assertEqual( - self.client.get(reverse("tutorial:view", args=[self.tuto.pk, self.tuto.slug])).status_code, 200 - ) - - reads = ContentRead.objects.filter(user=self.user_staff).all() - # simple visit does not trigger follow but remembers reading - self.assertEqual(len(reads), 1) - interventions = [ - post["url"] for post in get_header_notifications(self.user_staff)["general_notifications"]["list"] - ] - self.assertTrue(reads.first().note.get_absolute_url() not in interventions) - - # login with author - self.client.force_login(self.user_author) - - # test preview (without JS) - result = self.client.post( - reverse("content:add-reaction") + f"?pk={self.published.content.pk}", - {"text": message_to_post, "last_note": reactions[0].pk, "preview": True}, - ) - self.assertEqual(result.status_code, 200) - - self.assertTrue(message_to_post in result.context["text"]) - - # test preview (with JS) - result = self.client.post( - reverse("content:add-reaction") + f"?pk={self.published.content.pk}", - {"text": message_to_post, "last_note": reactions[0].pk, "preview": True}, - HTTP_X_REQUESTED_WITH="XMLHttpRequest", - ) - self.assertEqual(result.status_code, 200) - - result_string = "".join(a.decode() for a in result.streaming_content) - self.assertTrue(message_to_post in result_string) - - # test quoting (without JS) - result = self.client.get( - reverse("content:add-reaction") + "?pk={}&cite={}".format(self.published.content.pk, reactions[0].pk) - ) - self.assertEqual(result.status_code, 200) - - text_field_value = result.context["form"].initial["text"] - - self.assertTrue(message_to_post in text_field_value) - self.assertTrue(self.user_guest.username in text_field_value) - self.assertTrue(reactions[0].get_absolute_url() in text_field_value) - - # test quoting (with JS) - result = self.client.get( - reverse("content:add-reaction") + "?pk={}&cite={}".format(self.published.content.pk, reactions[0].pk), - HTTP_X_REQUESTED_WITH="XMLHttpRequest", - ) - - self.assertEqual(result.status_code, 200) - json = {} - - try: - json = json_handler.loads("".join(a.decode() for a in result.streaming_content)) - except Exception as e: # broad exception on purpose - self.assertEqual(e, "") - - self.assertTrue("text" in json) - text_field_value = json["text"] - - self.assertTrue(message_to_post in text_field_value) - self.assertTrue(self.user_guest.username in text_field_value) - self.assertTrue(reactions[0].get_absolute_url() in text_field_value) - - # test that if the wrong last_note is given, user get a message - self.assertEqual(ContentReaction.objects.count(), 1) - - result = self.client.post( - reverse("content:add-reaction") + f"?pk={self.published.content.pk}", - {"text": message_to_post, "last_note": -1}, # wrong pk - follow=False, - ) - self.assertEqual(result.status_code, 200) - - self.assertEqual(ContentReaction.objects.count(), 1) # no new reaction has been posted - self.assertTrue(result.context["newnote"]) # message appears ! - - def test_hide_reaction(self): - text_hidden = ( - "Ever notice how you come across somebody once in a while you shouldn't have fucked with? That's me." - ) - - self.client.force_login(self.user_guest) - - self.client.post( - reverse("content:add-reaction") + f"?pk={self.tuto.pk}", - {"text": "message", "last_note": "0"}, - follow=True, - ) - - self.client.force_login(self.user_staff) - - reaction = ContentReaction.objects.filter(related_content__pk=self.tuto.pk).first() - - result = self.client.post( - reverse("content:hide-reaction", args=[reaction.pk]), {"text_hidden": text_hidden}, follow=False - ) - self.assertEqual(result.status_code, 302) - - reaction = ContentReaction.objects.get(pk=reaction.pk) - self.assertFalse(reaction.is_visible) - self.assertEqual(reaction.text_hidden, text_hidden[:80]) - self.assertEqual(reaction.editor, self.user_staff) - - # test that someone else is not abble to quote the text - self.client.force_login(self.user_guest) - - result = self.client.get( - reverse("content:add-reaction") + f"?pk={self.tuto.pk}&cite={reaction.pk}", follow=False - ) - self.assertEqual(result.status_code, 403) # unable to quote a reaction if hidden - - # then, unhide it ! - self.client.force_login(self.user_guest) - - result = self.client.post(reverse("content:show-reaction", args=[reaction.pk]), follow=False) - - self.assertEqual(result.status_code, 302) - - reaction = ContentReaction.objects.get(pk=reaction.pk) - self.assertTrue(reaction.is_visible) - - def test_alert_reaction(self): - - self.client.force_login(self.user_guest) - - self.client.post( - reverse("content:add-reaction") + f"?pk={self.tuto.pk}", - {"text": "message", "last_note": "0"}, - follow=True, - ) - reaction = ContentReaction.objects.filter(related_content__pk=self.tuto.pk).first() - self.client.force_login(self.user_author) - result = self.client.post( - reverse("content:alert-reaction", args=[reaction.pk]), - {"signal_text": "No. Try not. Do... or do not. There is no try."}, - follow=False, - ) - self.assertEqual(result.status_code, 302) - self.assertIsNotNone(Alert.objects.filter(author__pk=self.user_author.pk, comment__pk=reaction.pk).first()) - result = self.client.post( - reverse("content:resolve-reaction"), - { - "alert_pk": Alert.objects.filter(author__pk=self.user_author.pk, comment__pk=reaction.pk).first().pk, - "text": "No. Try not. Do... or do not. There is no try.", - }, - follow=False, - ) - self.assertEqual(result.status_code, 403) - self.client.force_login(self.user_staff) - result = self.client.post( - reverse("content:resolve-reaction"), - { - "alert_pk": Alert.objects.filter(author__pk=self.user_author.pk, comment__pk=reaction.pk).first().pk, - "text": "Much to learn, you still have.", - }, - follow=False, - ) - self.assertEqual(result.status_code, 302) - self.assertIsNone( - Alert.objects.filter(author__pk=self.user_author.pk, comment__pk=reaction.pk, solved=False).first() - ) - reaction = ContentReaction.objects.filter(related_content__pk=self.tuto.pk).first() - - # test that edition of a comment with an alert by an admin also solve the alert - self.client.force_login(self.user_author) - result = self.client.post( - reverse("content:alert-reaction", args=[reaction.pk]), - {"signal_text": "No. Try not. Do... or do not. There is no try."}, - follow=False, - ) - self.assertEqual(result.status_code, 302) - self.assertIsNotNone( - Alert.objects.filter(author__pk=self.user_author.pk, comment__pk=reaction.pk, solved=False).first() - ) - - self.client.force_login(self.user_staff) - result = self.client.post( - reverse("content:update-reaction") + f"?message={reaction.pk}&pk={self.tuto.pk}", - {"text": "Much to learn, you still have."}, - follow=False, - ) - self.assertEqual(result.status_code, 302) - self.assertIsNone( - Alert.objects.filter(author__pk=self.user_author.pk, comment__pk=reaction.pk, solved=False).first() - ) - - def test_warn_typo_without_accessible_author(self): - - self.client.force_login(self.user_guest) - result = self.client.post( - reverse("content:warn-typo") + f"?pk={self.tuto.pk}", - { - "pk": self.tuto.pk, - "version": self.published.sha_public, - "text": "This is how they controlled it. " - "It took us 15 years and three supercomputers to MacGyver a system for the gate on Earth. ", - "target": "", - }, - follow=True, - ) - self.assertEqual(result.status_code, 200) - self.assertIsNone(PrivateTopic.objects.filter(participants__in=[self.external]).first()) - - # add a banned user: - user_banned = ProfileFactory( - can_write=False, - end_ban_write=datetime.date(2048, 0o1, 0o1), - can_read=False, - end_ban_read=datetime.date(2048, 0o1, 0o1), - ) - self.tuto.authors.add(user_banned.user) - self.tuto.save() - - result = self.client.post( - reverse("content:warn-typo") + f"?pk={self.tuto.pk}", - { - "pk": self.tuto.pk, - "version": self.published.sha_public, - "text": "This is how they controlled it. " - "It took us 15 years and three supercomputers to MacGyver a system for the gate on Earth. ", - "target": "", - }, - follow=True, - ) - self.assertIsNone(PrivateTopic.objects.filter(participants__in=[self.external]).first()) - self.assertEqual(result.status_code, 200) - - def test_find_tutorial_or_article(self): - """test the behavior of `article:find-article` and `tutorial:find-tutorial` urls""" - - self.client.force_login(self.user_author) - - tuto_in_beta = PublishableContentFactory(type="TUTORIAL") - tuto_in_beta.authors.add(self.user_author) - tuto_in_beta.sha_beta = "whatever" - tuto_in_beta.save() - - tuto_draft = PublishableContentFactory(type="TUTORIAL") - tuto_draft.authors.add(self.user_author) - tuto_draft.save() - - article_in_validation = PublishableContentFactory(type="ARTICLE") - article_in_validation.authors.add(self.user_author) - article_in_validation.sha_validation = "whatever" # the article is in validation - article_in_validation.save() - - # test without filters - response = self.client.get(reverse("tutorial:find-tutorial", args=[self.user_author.username]), follow=False) - self.assertEqual(200, response.status_code) - contents = response.context["tutorials"] - self.assertEqual(len(contents), 3) # 3 tutorials - - response = self.client.get(reverse("article:find-article", args=[self.user_author.username]), follow=False) - self.assertEqual(200, response.status_code) - contents = response.context["articles"] - self.assertEqual(len(contents), 1) # 1 article - - # test a non-existing filter - response = self.client.get( - reverse("tutorial:find-tutorial", args=[self.user_author.username]) + "?filter=whatever", follow=False - ) - self.assertEqual(404, response.status_code) # this filter does not exists ! - - # test 'redaction' filter - response = self.client.get( - reverse("tutorial:find-tutorial", args=[self.user_author.username]) + "?filter=redaction", follow=False - ) - self.assertEqual(200, response.status_code) - contents = response.context["tutorials"] - self.assertEqual(len(contents), 1) # one tutorial in redaction - self.assertEqual(contents[0].pk, tuto_draft.pk) - - response = self.client.get( - reverse("article:find-article", args=[self.user_author.username]) + "?filter=redaction", follow=False - ) - self.assertEqual(200, response.status_code) - contents = response.context["articles"] - self.assertEqual(len(contents), 0) # no article in redaction - - # test beta filter - response = self.client.get( - reverse("tutorial:find-tutorial", args=[self.user_author.username]) + "?filter=beta", follow=False - ) - self.assertEqual(200, response.status_code) - contents = response.context["tutorials"] - self.assertEqual(len(contents), 1) # one tutorial in beta - self.assertEqual(contents[0].pk, tuto_in_beta.pk) - - response = self.client.get( - reverse("article:find-article", args=[self.user_author.username]) + "?filter=beta", follow=False - ) - self.assertEqual(200, response.status_code) - contents = response.context["articles"] - self.assertEqual(len(contents), 0) # no article in beta - - # test validation filter - response = self.client.get( - reverse("tutorial:find-tutorial", args=[self.user_author.username]) + "?filter=validation", follow=False - ) - self.assertEqual(200, response.status_code) - contents = response.context["tutorials"] - self.assertEqual(len(contents), 0) # no tutorial in validation - - response = self.client.get( - reverse("article:find-article", args=[self.user_author.username]) + "?filter=validation", follow=False - ) - self.assertEqual(200, response.status_code) - contents = response.context["articles"] - self.assertEqual(len(contents), 1) # one article in validation - self.assertEqual(contents[0].pk, article_in_validation.pk) - - # test public filter - response = self.client.get( - reverse("tutorial:find-tutorial", args=[self.user_author.username]) + "?filter=public", follow=False - ) - self.assertEqual(200, response.status_code) - contents = response.context["tutorials"] - self.assertEqual(len(contents), 1) # one published tutorial - self.assertEqual(contents[0].pk, self.tuto.pk) - - response = self.client.get( - reverse("article:find-article", args=[self.user_author.username]) + "?filter=public", follow=False - ) - self.assertEqual(200, response.status_code) - contents = response.context["articles"] - self.assertEqual(len(contents), 0) # no published article - - self.client.logout() - - # test validation filter - response = self.client.get( - reverse("tutorial:find-tutorial", args=[self.user_author.username]) + "?filter=validation", follow=False - ) - self.assertEqual(403, response.status_code) # not allowed for public - - # test redaction filter - response = self.client.get( - reverse("tutorial:find-tutorial", args=[self.user_author.username]) + "?filter=redaction", follow=False - ) - self.assertEqual(403, response.status_code) # not allowed for public - - # test beta filter - response = self.client.get( - reverse("tutorial:find-tutorial", args=[self.user_author.username]) + "?filter=beta", follow=False - ) - self.assertEqual(200, response.status_code) - contents = response.context["tutorials"] - self.assertEqual(len(contents), 1) # one tutorial in beta - self.assertEqual(contents[0].pk, tuto_in_beta.pk) - - response = self.client.get( - reverse("article:find-article", args=[self.user_author.username]) + "?filter=beta", follow=False - ) - self.assertEqual(200, response.status_code) - contents = response.context["articles"] - self.assertEqual(len(contents), 0) # no article in beta - - # test public filter - response = self.client.get( - reverse("tutorial:find-tutorial", args=[self.user_author.username]) + "?filter=public", follow=False - ) - self.assertEqual(200, response.status_code) - contents = response.context["tutorials"] - self.assertEqual(len(contents), 1) # one published tutorial - self.assertEqual(contents[0].pk, self.tuto.pk) - - response = self.client.get( - reverse("article:find-article", args=[self.user_author.username]) + "?filter=public", follow=False - ) - self.assertEqual(200, response.status_code) - contents = response.context["articles"] - self.assertEqual(len(contents), 0) # no published article - - # test no filter → same answer as 'public' - response = self.client.get(reverse("tutorial:find-tutorial", args=[self.user_author.username]), follow=False) - self.assertEqual(200, response.status_code) - contents = response.context["tutorials"] - self.assertEqual(len(contents), 1) # one published tutorial - self.assertEqual(contents[0].pk, self.tuto.pk) - - response = self.client.get(reverse("article:find-article", args=[self.user_author.username]), follow=False) - self.assertEqual(200, response.status_code) - contents = response.context["articles"] - self.assertEqual(len(contents), 0) # no published article - - self.client.force_login(self.user_staff) - - response = self.client.get(reverse("tutorial:find-tutorial", args=[self.user_author.username]), follow=False) - self.assertEqual(200, response.status_code) - contents = response.context["tutorials"] - self.assertEqual(len(contents), 1) # 1 published tutorial by user_author ! - - # staff can use all filters without a 403 ! - - # test validation filter: - response = self.client.get( - reverse("tutorial:find-tutorial", args=[self.user_author.username]) + "?filter=validation", follow=False - ) - self.assertEqual(403, response.status_code) - - # test redaction filter: - response = self.client.get( - reverse("tutorial:find-tutorial", args=[self.user_author.username]) + "?filter=redaction", follow=False - ) - self.assertEqual(403, response.status_code) - - # test beta filter: - response = self.client.get( - reverse("tutorial:find-tutorial", args=[self.user_author.username]) + "?filter=beta", follow=False - ) - self.assertEqual(200, response.status_code) - - # test redaction filter: - response = self.client.get( - reverse("tutorial:find-tutorial", args=[self.user_author.username]) + "?filter=redaction", follow=False - ) - self.assertEqual(403, response.status_code) - - def test_last_reactions(self): - """Test and ensure the behavior of last_read_note() and first_unread_note(). - - Note: for a unknown reason, `get_current_user()` does not return the good answer if a page is not - visited before, therefore this test will visit the index after each login (because :p)""" - - # login with guest - self.client.force_login(self.user_guest) - - result = self.client.get(reverse("pages-index")) # go to whatever page - self.assertEqual(result.status_code, 200) - - self.assertEqual(ContentRead.objects.filter(user=self.user_guest).count(), 0) - - tuto = PublishableContent.objects.get(pk=self.tuto.pk) - - # no reaction yet: - self.assertIsNone(tuto.last_read_note()) - self.assertIsNone(tuto.first_unread_note()) - self.assertIsNone(tuto.first_note()) - - # post a reaction - result = self.client.post( - reverse("content:add-reaction") + f"?pk={self.tuto.pk}", - {"text": "message", "last_note": "0"}, - follow=True, - ) - self.assertEqual(result.status_code, 200) - - reactions = ContentReaction.objects.filter(related_content=self.tuto).all() - self.assertEqual(len(reactions), 1) - - self.assertEqual(ContentRead.objects.filter(user=self.user_guest).count(), 1) # last reaction read - - tuto = PublishableContent.objects.get(pk=self.tuto.pk) - - self.assertEqual(tuto.first_note(), reactions[0]) - self.assertEqual(tuto.last_read_note(), reactions[0]) - self.assertEqual(tuto.first_unread_note(), reactions[0]) # if no next reaction, first unread=last read - - self.client.logout() - - # login with author (could be staff, we don't care in this test) - self.client.force_login(self.user_author) - - result = self.client.get(reverse("pages-index")) # go to whatever page - self.assertEqual(result.status_code, 200) - - self.assertIsNone( - ContentReactionAnswerSubscription.objects.get_existing(user=self.user_author, content_object=tuto) - ) - - self.assertEqual(tuto.last_read_note(), reactions[0]) # if never read, last note=first note - self.assertEqual(tuto.first_unread_note(), reactions[0]) - - # post another reaction - result = self.client.post( - reverse("content:add-reaction") + f"?pk={self.tuto.pk}", - {"text": "message", "last_note": reactions[0].pk}, - follow=True, - ) - self.assertEqual(result.status_code, 200) - - tuto = PublishableContent.objects.get(pk=self.tuto.pk) - - reactions = list(ContentReaction.objects.filter(related_content=self.tuto).all()) - self.assertEqual(len(reactions), 2) - - self.assertTrue( - ContentReactionAnswerSubscription.objects.get_existing(user=self.user_author, content_object=tuto).is_active - ) - - self.assertEqual(tuto.first_note(), reactions[0]) # first note is still first note - self.assertEqual(tuto.last_read_note(), reactions[1]) - self.assertEqual(tuto.first_unread_note(), reactions[1]) - - # test if not connected - self.client.logout() - - result = self.client.get(reverse("pages-index")) # go to whatever page - self.assertEqual(result.status_code, 200) - - tuto = PublishableContent.objects.get(pk=self.tuto.pk) - self.assertEqual(tuto.last_read_note(), reactions[0]) # last read note = first note - self.assertEqual(tuto.first_unread_note(), reactions[0]) # first unread note = first note - - # visit tutorial - result = self.client.get(reverse("tutorial:view", kwargs={"pk": tuto.pk, "slug": tuto.slug})) - self.assertEqual(result.status_code, 200) - - # but nothing has changed (because not connected = no notifications and no 'tracking') - tuto = PublishableContent.objects.get(pk=self.tuto.pk) - self.assertEqual(tuto.last_read_note(), reactions[0]) # last read note = first note - self.assertEqual(tuto.first_unread_note(), reactions[0]) # first unread note = first note - - # re-login with guest - self.client.force_login(self.user_guest) - - result = self.client.get(reverse("pages-index")) # go to whatever page - self.assertEqual(result.status_code, 200) - - self.assertEqual(ContentRead.objects.filter(user=self.user_guest).count(), 1) # already read first reaction - reads = ContentRead.objects.filter(user=self.user_guest).all() - - tuto = PublishableContent.objects.get(pk=self.tuto.pk) - self.assertEqual(tuto.last_read_note(), reactions[0]) - self.assertEqual(tuto.first_unread_note(), reactions[1]) # new reaction of author is unread - - # visit tutorial to get rid of the notification - result = self.client.get(reverse("tutorial:view", kwargs={"pk": tuto.pk, "slug": tuto.slug})) - self.assertEqual(result.status_code, 200) - - tuto = PublishableContent.objects.get(pk=self.tuto.pk) - self.assertEqual(tuto.last_read_note(), reactions[1]) # now, new reaction is read ! - self.assertEqual(tuto.first_unread_note(), reactions[1]) - - self.assertEqual(ContentRead.objects.filter(user=self.user_guest).count(), 1) - self.assertNotEqual(reads, ContentRead.objects.filter(user=self.user_guest).all()) # not the same message - - self.client.logout() - - result = self.client.get(reverse("pages-index")) # go to whatever page - self.assertEqual(result.status_code, 200) - - def test_reaction_follow_email(self): - settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" - self.assertEqual(0, len(mail.outbox)) - - profile = ProfileFactory() - self.client.force_login(profile.user) - response = self.client.post(reverse("content:follow-reactions", args=[self.tuto.pk]), {"email": "1"}) - self.assertEqual(302, response.status_code) - - self.assertIsNotNone( - ContentReactionAnswerSubscription.objects.get_existing( - profile.user, self.tuto, is_active=True, by_email=True - ) - ) - - self.client.logout() - - self.client.force_login(self.user_author) - - # post another reaction - self.client.post( - reverse("content:add-reaction") + f"?pk={self.tuto.pk}", - {"text": "message", "last_note": "0"}, - follow=True, - ) - - self.assertEqual(1, len(mail.outbox)) - - def test_note_with_bad_param(self): - self.client.force_login(self.user_staff) - url_template = reverse("content:update-reaction") + "?pk={}&message={}" - result = self.client.get(url_template.format(self.tuto.pk, 454545665895123)) - self.assertEqual(404, result.status_code) - reaction = ContentReaction(related_content=self.tuto, author=self.user_guest, position=1) - reaction.update_content("blah") - reaction.save() - self.tuto.last_note = reaction - self.tuto.save() - result = self.client.get(url_template.format(861489632, reaction.pk)) - self.assertEqual(404, result.status_code) - - def test_cant_edit_not_owned_note(self): - article = PublishedContentFactory(author_list=[self.user_author], type="ARTICLE") - new_user = ProfileFactory().user - new_reaction = ContentReaction(related_content=article, position=1) - new_reaction.author = self.user_guest - new_reaction.update_content("I will find you.") - - new_reaction.save() - self.client.force_login(new_user) - resp = self.client.get(reverse("content:update-reaction") + f"?message={new_reaction.pk}&pk={article.pk}") - self.assertEqual(403, resp.status_code) - resp = self.client.post( - reverse("content:update-reaction") + f"?message={new_reaction.pk}&pk={article.pk}", - {"text": "I edited it"}, - ) - self.assertEqual(403, resp.status_code) - - def test_quote_note(self): - """Ensure the behavior of the `&cite=xxx` parameter on 'content:add-reaction'""" - - tuto = PublishableContent.objects.get(pk=self.tuto.pk) - text = ( - "À force de temps, de patience et de crachats, " - "on met un pépin de callebasse dans le derrière d'un moustique (proverbe créole)" - ) - - # add note : - reaction = ContentReaction(related_content=tuto, position=1) - reaction.author = self.user_guest - reaction.update_content(text) - reaction.save() - - self.client.force_login(self.user_author) - - # cite note - result = self.client.get(reverse("content:add-reaction") + f"?pk={tuto.pk}&cite={reaction.pk}", follow=True) - self.assertEqual(200, result.status_code) - - self.assertTrue(text in result.context["form"].initial["text"]) # ok, text quoted ! - - # cite with a abnormal parameter raises 404 - result = self.client.get( - reverse("content:add-reaction") + "?pk={}&cite={}".format(tuto.pk, "lililol"), follow=True - ) - self.assertEqual(404, result.status_code) - - # cite not existing note just gives the form empty - result = self.client.get( - reverse("content:add-reaction") + "?pk={}&cite={}".format(tuto.pk, 99999999), follow=True - ) - self.assertEqual(200, result.status_code) - - self.assertTrue("text" not in result.context["form"]) # nothing quoted, so no text cited - - # it's not possible to cite an hidden note (get 403) - reaction.is_visible = False - reaction.save() - - result = self.client.get(reverse("content:add-reaction") + f"?pk={tuto.pk}&cite={reaction.pk}", follow=True) - self.assertEqual(403, result.status_code) - - def test_cant_view_private_even_if_draft_is_equal_to_public(self): - content = PublishedContentFactory(author_list=[self.user_author]) - self.client.force_login(self.user_guest) - resp = self.client.get(reverse("content:view", args=[content.pk, content.slug])) - self.assertEqual(403, resp.status_code) - - def test_republish_with_different_slug(self): - """Ensure that a new PublishedContent object is created and well filled""" - - self.assertEqual(PublishedContent.objects.count(), 1) - - old_published = PublishedContent.objects.filter(content__pk=self.tuto.pk).first() - self.assertIsNotNone(old_published) - self.assertFalse(old_published.must_redirect) - - # connect with author: - self.client.force_login(self.user_author) - - # change title - tuto = PublishableContent.objects.get(pk=self.tuto.pk) - old_slug = tuto.slug - random = "Whatever, we don't care about the details" - - result = self.client.post( - reverse("content:edit", args=[tuto.pk, tuto.slug]), - { - "title": "{} ({})".format(self.tuto.title, "modified"), # will change slug - "description": random, - "introduction": random, - "conclusion": random, - "type": "TUTORIAL", - "licence": self.tuto.licence.pk, - "subcategory": self.subcategory.pk, - "last_hash": tuto.load_version().compute_hash(), - "image": (settings.BASE_DIR / "fixtures" / "logo.png").open("rb"), - }, - follow=False, - ) - self.assertEqual(result.status_code, 302) - - tuto = PublishableContent.objects.get(pk=self.tuto.pk) - self.assertNotEqual(tuto.slug, old_slug) - - # ask validation - text_validation = "Valide moi ce truc, please !" - text_publication = "Aussi tôt dit, aussi tôt fait !" - self.assertEqual(Validation.objects.count(), 0) - - result = self.client.post( - reverse("validation:ask", kwargs={"pk": tuto.pk, "slug": tuto.slug}), - {"text": text_validation, "version": tuto.sha_draft}, - follow=False, - ) - self.assertEqual(result.status_code, 302) - - # login with staff and publish - self.client.force_login(self.user_staff) - - validation = Validation.objects.filter(content__pk=tuto.pk).last() - - result = self.client.post( - reverse("validation:reserve", kwargs={"pk": validation.pk}), {"version": validation.version}, follow=False - ) - self.assertEqual(result.status_code, 302) - - # accept - result = self.client.post( - reverse("validation:accept", kwargs={"pk": validation.pk}), - {"text": text_publication, "is_major": False}, # minor modification (just the title) - follow=False, - ) - self.assertEqual(result.status_code, 302) - - self.assertEqual(PublishedContent.objects.count(), 2) - - old_published = PublishedContent.objects.get(pk=old_published.pk) - self.assertIsNotNone(old_published) # still exists - self.assertTrue(old_published.must_redirect) # do redirection if any - self.assertIsNone(old_published.update_date) - - new_published = PublishedContent.objects.filter(content__pk=self.tuto.pk).last() - self.assertIsNotNone(new_published) # new version exists - self.assertNotEqual(old_published.pk, new_published.pk) # not the old one - self.assertEqual(new_published.publication_date, old_published.publication_date) # keep publication date - self.assertIsNotNone(new_published.update_date) # ... But is updated ! - - tuto = PublishableContent.objects.get(pk=self.tuto.pk) - self.assertEqual(tuto.public_version.pk, new_published.pk) - - def test_logical_article_pagination(self): - """Test that the past is 'left' and the future is 'right', or in other word that the good article - are given to pagination and not the opposite""" - - article1 = PublishedContentFactory(type="ARTICLE") - - # force 'article1' to be the first one by setting a publication date equals to one hour before the test - article1.public_version.publication_date = datetime.datetime.now() - datetime.timedelta(0, 1) - article1.public_version.save() - - article2 = PublishedContentFactory(type="ARTICLE") - - self.assertEqual(PublishedContent.objects.count(), 3) # both articles have been published - - # visit article 1 (so article2 is next) - result = self.client.get(reverse("article:view", kwargs={"pk": article1.pk, "slug": article1.slug})) - self.assertEqual(result.status_code, 200) - self.assertEqual(result.context["next_content"].pk, article2.public_version.pk) - self.assertIsNone(result.context["previous_content"]) - - # visit article 2 (so article1 is previous) - result = self.client.get(reverse("article:view", kwargs={"pk": article2.pk, "slug": article2.slug})) - self.assertEqual(result.status_code, 200) - self.assertEqual(result.context["previous_content"].pk, article1.public_version.pk) - self.assertIsNone(result.context["next_content"]) - - def test_validation_list_has_good_title(self): - # aka fix 3172 - tuto = PublishableContentFactory(author_list=[self.user_author], type="TUTORIAL") - self.client.force_login(self.user_author) - result = self.client.post( - reverse("validation:ask", args=[tuto.pk, tuto.slug]), - {"text": "something good", "version": tuto.sha_draft}, - follow=False, - ) - old_title = tuto.title - new_title = "a brand new title" - self.client.post( - reverse("content:edit", args=[tuto.pk, tuto.slug]), - { - "title": new_title, - "description": tuto.description, - "introduction": "a", - "conclusion": "b", - "type": "TUTORIAL", - "licence": self.licence.pk, - "subcategory": self.subcategory.pk, - "last_hash": tuto.sha_draft, - }, - follow=False, - ) - self.client.logout() - self.client.force_login(self.user_staff) - result = self.client.get(reverse("validation:list") + "?type=tuto") - self.assertIn(old_title, str(result.content)) - self.assertNotIn(new_title, str(result.content)) - - def test_public_authors_versioned(self): - published = PublishedContentFactory(author_list=[self.user_author]) - other_author = ProfileFactory() - published.authors.add(other_author.user) - published.save() - response = self.client.get(published.get_absolute_url_online()) - self.assertIn(self.user_author.username, str(response.content)) - self.assertNotIn(other_author.user.username, str(response.content)) - self.assertEqual(0, len(other_author.get_public_contents())) - - def test_unpublish_with_title_change(self): - # aka 3329 - article = PublishedContentFactory(type="ARTICLE", author_list=[self.user_author], licence=self.licence) - registered_validation = Validation( - content=article, - version=article.sha_draft, - status="ACCEPT", - comment_authors="bla", - comment_validator="bla", - date_reserve=datetime.datetime.now(), - date_proposition=datetime.datetime.now(), - date_validation=datetime.datetime.now(), - ) - registered_validation.save() - self.client.force_login(self.user_staff) - self.client.post( - reverse("content:edit", args=[article.pk, article.slug]), - { - "title": "new title so that everything explode", - "description": article.description, - "introduction": article.load_version().get_introduction(), - "conclusion": article.load_version().get_conclusion(), - "type": "ARTICLE", - "licence": article.licence.pk, - "subcategory": self.subcategory.pk, - "last_hash": article.load_version(article.sha_draft).compute_hash(), - "image": (settings.BASE_DIR / "fixtures" / "logo.png").open("rb"), - }, - follow=False, - ) - public_count = PublishedContent.objects.count() - result = self.client.post( - reverse("validation:revoke", kwargs={"pk": article.pk, "slug": article.public_version.content_public_slug}), - {"text": "This content was bad", "version": article.public_version.sha_public}, - follow=False, - ) - self.assertEqual(302, result.status_code) - self.assertEqual(public_count - 1, PublishedContent.objects.count()) - self.assertEqual("PENDING", Validation.objects.get(pk=registered_validation.pk).status) - - def test_unpublish_with_empty_subscription(self): - article = PublishedContentFactory(type="ARTICLE", author_list=[self.user_author], licence=self.licence) - registered_validation = Validation( - content=article, - version=article.sha_draft, - status="ACCEPT", - comment_authors="bla", - comment_validator="bla", - date_reserve=datetime.datetime.now(), - date_proposition=datetime.datetime.now(), - date_validation=datetime.datetime.now(), - ) - registered_validation.save() - subscriber = ProfileFactory().user - self.client.force_login(subscriber) - resp = self.client.post(reverse("content:follow-reactions", args=[article.pk]), {"follow": True}) - self.assertEqual(302, resp.status_code) - public_count = PublishedContent.objects.count() - self.client.logout() - self.client.force_login(self.user_staff) - result = self.client.post( - reverse("validation:revoke", kwargs={"pk": article.pk, "slug": article.public_version.content_public_slug}), - {"text": "This content was bad", "version": article.public_version.sha_public}, - follow=False, - ) - self.assertEqual(302, result.status_code) - self.assertEqual(public_count - 1, PublishedContent.objects.count()) - self.assertEqual(ContentReactionAnswerSubscription.objects.filter(is_active=False).count(), 1) - - def test_validation_history(self): - published = PublishedContentFactory(author_list=[self.user_author]) - self.client.force_login(self.user_author) - result = self.client.post( - reverse("content:edit", args=[published.pk, published.slug]), - { - "title": published.title, - "description": published.description, - "introduction": "crappy crap", - "conclusion": "crappy crap", - "type": "TUTORIAL", - "licence": self.licence.pk, - "subcategory": self.subcategory.pk, - "last_hash": published.load_version().compute_hash(), # good hash - }, - follow=True, - ) - self.assertEqual(result.status_code, 200) - result = self.client.post( - reverse("validation:ask", kwargs={"pk": published.pk, "slug": published.slug}), - {"text": "abcdefg", "version": published.load_version().current_version}, - follow=False, - ) - self.assertEqual(result.status_code, 302) - self.assertEqual(Validation.objects.count(), 1) - self.client.logout() - self.client.force_login(self.user_staff) - result = self.client.get(reverse("validation:list") + "?type=tuto") - self.assertIn('class="update_content"', str(result.content)) - - def test_validation_history_for_new_content(self): - publishable = PublishableContentFactory(author_list=[self.user_author]) - self.client.force_login(self.user_author) - - result = self.client.post( - reverse("validation:ask", kwargs={"pk": publishable.pk, "slug": publishable.slug}), - {"text": "abcdefg", "version": publishable.load_version().current_version}, - follow=False, - ) - self.assertEqual(result.status_code, 302) - self.assertEqual(Validation.objects.count(), 1) - self.client.logout() - self.client.force_login(self.user_staff) - result = self.client.get(reverse("validation:list") + "?type=tuto") - self.assertNotIn('class="update_content"', str(result.content)) - - def test_ask_validation_update(self): - """ - Test AskValidationView. - """ - text_validation = "La validation on vous aime !" - content = PublishableContentFactory(author_list=[self.user_author], type="ARTICLE") - content.save() - content_draft = content.load_version() - - self.assertEqual(Validation.objects.count(), 0) - - # login with user and ask validation - self.client.force_login(self.user_author) - result = self.client.post( - reverse("validation:ask", kwargs={"pk": content_draft.pk, "slug": content_draft.slug}), - {"text": text_validation, "version": content_draft.current_version}, - follow=False, - ) - self.assertEqual(result.status_code, 302) - self.assertEqual(Validation.objects.count(), 1) - - # login with staff and reserve the content - self.client.force_login(self.user_staff) - validation = Validation.objects.filter(content=content).last() - result = self.client.post( - reverse("validation:reserve", kwargs={"pk": validation.pk}), {"version": validation.version}, follow=False - ) - self.assertEqual(result.status_code, 302) - - # login with user, edit content and ask validation for update - self.client.force_login(self.user_author) - result = self.client.post( - reverse("content:edit", args=[content_draft.pk, content_draft.slug]), - { - "title": content_draft.title + "2", - "description": content_draft.description, - "introduction": content_draft.introduction, - "conclusion": content_draft.conclusion, - "type": content_draft.type, - "licence": self.licence.pk, - "subcategory": self.subcategory.pk, - "last_hash": content_draft.compute_hash(), - "image": content_draft.image or "None", - }, - follow=False, - ) - self.assertEqual(result.status_code, 302) - result = self.client.post( - reverse("validation:ask", kwargs={"pk": content_draft.pk, "slug": content_draft.slug}), - {"text": text_validation, "version": content_draft.current_version}, - follow=False, - ) - self.assertEqual(result.status_code, 302) - - # ensure the validation is effective - self.assertEqual(Validation.objects.count(), 2) - self.assertIsNotNone(Validation.objects.last().date_reserve) # issue #3432 - - def test_beta_article_closed_when_published(self): - """Test that the beta of an article is locked when the content is published""" - - text_validation = "Valide moi ce truc !" - text_publication = "Validation faite !" - - article = PublishableContentFactory(type="ARTICLE") - article.authors.add(self.user_author) - article.save() - article_draft = article.load_version() - - # login with author: - self.client.force_login(self.user_author) - - # set beta - result = self.client.post( - reverse("content:set-beta", kwargs={"pk": article.pk, "slug": article.slug}), - {"version": article_draft.current_version}, - follow=False, - ) - self.assertEqual(result.status_code, 302) - - # ask validation - self.assertEqual(Validation.objects.count(), 0) - result = self.client.post( - reverse("validation:ask", kwargs={"pk": article.pk, "slug": article.slug}), - {"text": text_validation, "version": article_draft.current_version}, - follow=False, - ) - self.assertEqual(result.status_code, 302) - - # login with staff - self.client.force_login(self.user_staff) - - # reserve the article - validation = Validation.objects.filter(content=article).last() - result = self.client.post( - reverse("validation:reserve", kwargs={"pk": validation.pk}), {"version": validation.version}, follow=False - ) - self.assertEqual(result.status_code, 302) - - # Check that the staff user doesn't have a notification for their reservation and their private topic is read. - self.assertEqual(0, len(Notification.objects.get_unread_notifications_of(self.user_staff))) - last_pm = PublishableContent.objects.get(pk=article.pk).validation_private_message - # as staff decided the action, he does not need to be notified of the new mp - self.assertFalse(is_privatetopic_unread(last_pm, self.user_staff)) - self.assertTrue(is_privatetopic_unread(last_pm, self.user_author)) - # publish the article - result = self.client.post( - reverse("validation:accept", kwargs={"pk": validation.pk}), - {"text": text_publication, "is_major": True}, - follow=False, - ) - self.assertEqual(result.status_code, 302) - beta_topic = PublishableContent.objects.get(pk=article.pk).beta_topic - self.assertIsNotNone(beta_topic) - self.assertTrue(beta_topic.is_locked) - last_message = beta_topic.last_message - self.assertIsNotNone(last_message) - - # login with author to ensure that the beta is not closed if it was already closed (at a second validation). - self.client.force_login(self.user_author) - - # ask validation - result = self.client.post( - reverse("validation:ask", kwargs={"pk": article.pk, "slug": article.slug}), - {"text": text_validation, "version": article_draft.current_version}, - follow=False, - ) - self.assertEqual(result.status_code, 302) - - # login with staff - self.client.force_login(self.user_staff) - - # reserve the article - validation = Validation.objects.filter(content=article).last() - result = self.client.post( - reverse("validation:reserve", kwargs={"pk": validation.pk}), {"version": validation.version}, follow=False - ) - self.assertEqual(result.status_code, 302) - - # publish the article - result = self.client.post( - reverse("validation:accept", kwargs={"pk": validation.pk}), - {"text": text_publication, "is_major": True}, - follow=False, - ) - self.assertEqual(result.status_code, 302) - beta_topic = PublishableContent.objects.get(pk=article.pk).beta_topic - self.assertIsNotNone(beta_topic) - self.assertTrue(beta_topic.is_locked) - self.assertEqual(beta_topic.last_message, last_message) - - def test_obsolete(self): - # check that this function is only available for staff - self.client.force_login(self.user_author) - result = self.client.post(reverse("validation:mark-obsolete", kwargs={"pk": self.tuto.pk}), follow=False) - self.assertEqual(result.status_code, 403) - # login as staff - self.client.force_login(self.user_staff) - # check that when the content is not marked as obsolete, the alert is not shown - result = self.client.get(self.tuto.get_absolute_url_online(), follow=False) - self.assertEqual(result.status_code, 200) - self.assertNotContains(result, _("Ce contenu est obsolète.")) - # now, let's mark the tutoriel as obsolete - result = self.client.post(reverse("validation:mark-obsolete", kwargs={"pk": self.tuto.pk}), follow=False) - self.assertEqual(result.status_code, 302) - # check that the alert is shown - result = self.client.get(self.tuto.get_absolute_url_online(), follow=False) - self.assertEqual(result.status_code, 200) - self.assertContains(result, _("Ce contenu est obsolète.")) - # and on a chapter - result = self.client.get(self.chapter1.get_absolute_url_online(), follow=False) - self.assertEqual(result.status_code, 200) - self.assertContains(result, _("Ce contenu est obsolète.")) - # finally, check that this alert can be hidden - result = self.client.post(reverse("validation:mark-obsolete", kwargs={"pk": self.tuto.pk}), follow=False) - self.assertEqual(result.status_code, 302) - result = self.client.get(self.tuto.get_absolute_url_online(), follow=False) - self.assertEqual(result.status_code, 200) - self.assertNotContains(result, _("Ce contenu est obsolète.")) - - def test_list_publications(self): - """Test the behavior of the publication list""" - - category_1 = CategoryFactory() - category_2 = CategoryFactory() - subcategory_1 = SubCategoryFactory(category=category_1) - subcategory_2 = SubCategoryFactory(category=category_1) - subcategory_3 = SubCategoryFactory(category=category_2) - subcategory_4 = SubCategoryFactory(category=category_2) - tag_1 = Tag(title="random") - tag_1.save() - - tuto_p_1 = PublishedContentFactory(author_list=[self.user_author]) - tuto_p_2 = PublishedContentFactory(author_list=[self.user_author]) - tuto_p_3 = PublishedContentFactory(author_list=[self.user_author]) - - article_p_1 = PublishedContentFactory(author_list=[self.user_author], type="ARTICLE") - - tuto_p_1.subcategory.add(subcategory_1) - tuto_p_1.subcategory.add(subcategory_2) - tuto_p_1.save() - - tuto_p_2.subcategory.add(subcategory_1) - tuto_p_2.subcategory.add(subcategory_2) - tuto_p_2.save() - - tuto_p_3.subcategory.add(subcategory_3) - tuto_p_3.save() - - article_p_1.subcategory.add(subcategory_4) - article_p_1.tags.add(tag_1) - article_p_1.save() - - tuto_1 = PublishedContent.objects.get(content=tuto_p_1.pk) - tuto_2 = PublishedContent.objects.get(content=tuto_p_2.pk) - tuto_3 = PublishedContent.objects.get(content=tuto_p_3.pk) - article_1 = PublishedContent.objects.get(content=article_p_1.pk) - - self.assertEqual(PublishableContent.objects.filter(type="ARTICLE").count(), 1) - self.assertEqual(PublishableContent.objects.filter(type="TUTORIAL").count(), 4) - - # 1. Publication list - result = self.client.get(reverse("publication:list")) - self.assertEqual(result.status_code, 200) - - self.assertEqual(len(result.context["last_articles"]), 1) - self.assertEqual(len(result.context["last_tutorials"]), 4) - - # 2. Category page - result = self.client.get(reverse("publication:category", kwargs={"slug": category_1.slug})) - self.assertEqual(result.status_code, 200) - - self.assertEqual(len(result.context["last_articles"]), 0) - self.assertEqual(len(result.context["last_tutorials"]), 2) - - pks = [x.pk for x in result.context["last_tutorials"]] - self.assertIn(tuto_1.pk, pks) - self.assertIn(tuto_2.pk, pks) - - result = self.client.get(reverse("publication:category", kwargs={"slug": category_2.slug})) - self.assertEqual(result.status_code, 200) - - self.assertEqual(len(result.context["last_articles"]), 1) - self.assertEqual(len(result.context["last_tutorials"]), 1) - - pks = [x.pk for x in result.context["last_tutorials"]] - self.assertIn(tuto_3.pk, pks) - - pks = [x.pk for x in result.context["last_articles"]] - self.assertIn(article_1.pk, pks) - - # 3. Subcategory page - result = self.client.get( - reverse("publication:subcategory", kwargs={"slug_category": category_1.slug, "slug": subcategory_1.slug}) - ) - - self.assertEqual(result.status_code, 200) - - self.assertEqual(len(result.context["last_articles"]), 0) - self.assertEqual(len(result.context["last_tutorials"]), 2) - - pks = [x.pk for x in result.context["last_tutorials"]] - self.assertIn(tuto_1.pk, pks) - self.assertIn(tuto_2.pk, pks) - - result = self.client.get( - reverse("publication:subcategory", kwargs={"slug_category": category_1.slug, "slug": subcategory_2.slug}) - ) - - self.assertEqual(result.status_code, 200) - - self.assertEqual(len(result.context["last_articles"]), 0) - self.assertEqual(len(result.context["last_tutorials"]), 2) - - pks = [x.pk for x in result.context["last_tutorials"]] - self.assertIn(tuto_1.pk, pks) - self.assertIn(tuto_2.pk, pks) - - result = self.client.get( - reverse("publication:subcategory", kwargs={"slug_category": category_2.slug, "slug": subcategory_3.slug}) - ) - - self.assertEqual(result.status_code, 200) - - self.assertEqual(len(result.context["last_articles"]), 0) - self.assertEqual(len(result.context["last_tutorials"]), 1) - - pks = [x.pk for x in result.context["last_tutorials"]] - self.assertIn(tuto_3.pk, pks) - - result = self.client.get( - reverse("publication:subcategory", kwargs={"slug_category": category_2.slug, "slug": subcategory_4.slug}) - ) - - self.assertEqual(result.status_code, 200) - - self.assertEqual(len(result.context["last_articles"]), 1) - self.assertEqual(len(result.context["last_tutorials"]), 0) - - pks = [x.pk for x in result.context["last_articles"]] - self.assertIn(article_1.pk, pks) - - # 4. Final page and filters - result = self.client.get(reverse("publication:list") + f"?category={category_1.slug}") - self.assertEqual(result.status_code, 200) - - self.assertEqual(len(result.context["filtered_contents"]), 2) - pks = [x.pk for x in result.context["filtered_contents"]] - self.assertIn(tuto_1.pk, pks) - self.assertIn(tuto_2.pk, pks) - - # filter by category and type - result = self.client.get(reverse("publication:list") + f"?category={category_2.slug}") - self.assertEqual(result.status_code, 200) - - self.assertEqual(len(result.context["filtered_contents"]), 2) - pks = [x.pk for x in result.context["filtered_contents"]] - self.assertIn(tuto_3.pk, pks) - self.assertIn(article_1.pk, pks) - - result = self.client.get(reverse("publication:list") + f"?category={category_2.slug}" + "&type=article") - self.assertEqual(result.status_code, 200) - - self.assertEqual(len(result.context["filtered_contents"]), 1) - pks = [x.pk for x in result.context["filtered_contents"]] - self.assertIn(article_1.pk, pks) - - result = self.client.get(reverse("publication:list") + f"?category={category_2.slug}" + "&type=tutorial") - self.assertEqual(result.status_code, 200) - - self.assertEqual(len(result.context["filtered_contents"]), 1) - pks = [x.pk for x in result.context["filtered_contents"]] - self.assertIn(tuto_3.pk, pks) - - # filter by subcategory - result = self.client.get(reverse("publication:list") + f"?subcategory={subcategory_1.slug}") - self.assertEqual(result.status_code, 200) - - self.assertEqual(len(result.context["filtered_contents"]), 2) - pks = [x.pk for x in result.context["filtered_contents"]] - self.assertIn(tuto_1.pk, pks) - self.assertIn(tuto_2.pk, pks) - - # filter by subcategory and type - result = self.client.get(reverse("publication:list") + f"?subcategory={subcategory_3.slug}") - self.assertEqual(result.status_code, 200) - - self.assertEqual(len(result.context["filtered_contents"]), 1) - pks = [x.pk for x in result.context["filtered_contents"]] - self.assertIn(tuto_3.pk, pks) - - result = self.client.get(reverse("publication:list") + f"?subcategory={subcategory_3.slug}" + "&type=article") - self.assertEqual(result.status_code, 200) - self.assertEqual(len(result.context["filtered_contents"]), 0) - - result = self.client.get(reverse("publication:list") + f"?subcategory={subcategory_3.slug}" + "&type=tutorial") - self.assertEqual(result.status_code, 200) - - self.assertEqual(len(result.context["filtered_contents"]), 1) - pks = [x.pk for x in result.context["filtered_contents"]] - self.assertIn(tuto_3.pk, pks) - - # filter by tag - result = self.client.get(reverse("publication:list") + f"?tag={tag_1.slug}" + "&type=article") - self.assertEqual(result.status_code, 200) - - self.assertEqual(len(result.context["filtered_contents"]), 1) - pks = [x.pk for x in result.context["filtered_contents"]] - self.assertIn(article_1.pk, pks) - - # 5. Everything else results in 404 - wrong_urls = [ - # not existing (sub)categories, types or tags with slug "xxx" - reverse("publication:list") + "?category=xxx", - reverse("publication:list") + "?subcategory=xxx", - reverse("publication:list") + "?type=xxx", - reverse("publication:list") + "?tag=xxx", - reverse("publication:category", kwargs={"slug": "xxx"}), - reverse("publication:subcategory", kwargs={"slug_category": category_2.slug, "slug": "xxx"}), - # subcategory_1 does not belong to category_2: - reverse("publication:subcategory", kwargs={"slug_category": category_2.slug, "slug": subcategory_1.slug}), - ] - - for url in wrong_urls: - self.assertEqual(self.client.get(url).status_code, 404, msg=url) - - def test_article_previous_link(self): - """Test the behaviour of the article previous link.""" - - article_1 = PublishedContentFactory(author_list=[self.user_author], type="ARTICLE") - article_2 = PublishedContentFactory(author_list=[self.user_author], type="ARTICLE") - article_3 = PublishedContentFactory(author_list=[self.user_author], type="ARTICLE") - article_1.save() - article_2.save() - article_3.save() - - result = self.client.get(reverse("article:view", kwargs={"pk": article_3.pk, "slug": article_3.slug})) - - self.assertEqual(result.context["previous_content"].pk, article_2.public_version.pk) - - def test_opinion_link_is_not_related_to_the_author(self): - """ - Test that the next and previous link in the opinion page take all the opinions - into accounts and not only the ones of the author. - """ - - user_1_opinion_1 = PublishedContentFactory(author_list=[self.user_author], type="OPINION") - user_2_opinion_1 = PublishedContentFactory(author_list=[self.user_guest], type="OPINION") - user_1_opinion_2 = PublishedContentFactory(author_list=[self.user_author], type="OPINION") - user_1_opinion_1.save() - user_2_opinion_1.save() - user_1_opinion_2.save() - - result = self.client.get( - reverse("opinion:view", kwargs={"pk": user_1_opinion_2.pk, "slug": user_1_opinion_2.slug}) - ) - - self.assertEqual(result.context["previous_content"].pk, user_2_opinion_1.public_version.pk) - - result = self.client.get( - reverse("opinion:view", kwargs={"pk": user_2_opinion_1.pk, "slug": user_2_opinion_1.slug}) - ) - - self.assertEqual(result.context["previous_content"].pk, user_1_opinion_1.public_version.pk) - self.assertEqual(result.context["next_content"].pk, user_1_opinion_2.public_version.pk) - - def test_author_update(self): - """Check that the author list of a content is updated when this content is updated.""" - - text_validation = "Valide moi ce truc, please !" - text_publication = "Validation faite !" - - tutorial = PublishedContentFactory( - type="TUTORIAL", author_list=[self.user_author, self.user_guest, self.user_staff] - ) - - # Remove author to check if it's correct after major update - tutorial.authors.remove(self.user_guest) - tutorial.save() - tutorial_draft = tutorial.load_version() - - # ask validation - self.client.force_login(self.user_staff) - self.client.post( - reverse("validation:ask", kwargs={"pk": tutorial.pk, "slug": tutorial.slug}), - {"text": text_validation, "version": tutorial_draft.current_version}, - follow=False, - ) - - # major update - validation = Validation.objects.filter(content=tutorial).last() - self.client.post( - reverse("validation:reserve", kwargs={"pk": validation.pk}), {"version": validation.version}, follow=False - ) - self.client.post( - reverse("validation:accept", kwargs={"pk": validation.pk}), - {"text": text_publication, "is_major": True}, - follow=False, - ) - self.assertEqual(tutorial.public_version.authors.count(), 2) - - # Remove author to check if it's correct after minor update - tutorial.authors.remove(self.user_author) - tutorial.save() - tutorial_draft = tutorial.load_version() - - # ask validation - self.client.force_login(self.user_staff) - self.client.post( - reverse("validation:ask", kwargs={"pk": tutorial.pk, "slug": tutorial.slug}), - {"text": text_validation, "version": tutorial_draft.current_version}, - follow=False, - ) - - # minor update - validation = Validation.objects.filter(content=tutorial).last() - self.client.post( - reverse("validation:reserve", kwargs={"pk": validation.pk}), {"version": validation.version}, follow=False - ) - self.client.post( - reverse("validation:accept", kwargs={"pk": validation.pk}), - {"text": text_publication, "is_major": False}, - follow=False, - ) - - self.assertEqual(tutorial.public_version.authors.count(), 1) - - def test_add_help_tuto(self): - self.client.force_login(self.user_author) - tutorial = PublishableContentFactory(author_list=[self.user_author]) - help_wanted = HelpWritingFactory() - resp = self.client.post( - reverse("content:helps-change", args=[tutorial.pk]), {"activated": True, "help_wanted": help_wanted.title} - ) - self.assertEqual(302, resp.status_code) - self.assertEqual(1, PublishableContent.objects.filter(pk=tutorial.pk).first().helps.count()) - - def test_add_help_opinion(self): - self.client.force_login(self.user_author) - tutorial = PublishableContentFactory(author_list=[self.user_author], type="OPINION") - help_wanted = HelpWritingFactory() - resp = self.client.post( - reverse("content:helps-change", args=[tutorial.pk]), {"activated": True, "help_wanted": help_wanted.title} - ) - self.assertEqual(400, resp.status_code) - self.assertEqual(0, PublishableContent.objects.filter(pk=tutorial.pk).first().helps.count()) - - def test_save_no_redirect(self): - self.client.force_login(self.user_author) - tutorial = PublishableContentFactory(author_list=[self.user_author]) - extract = ExtractFactory(db_object=tutorial, container=tutorial.load_version()) - tutorial = PublishableContent.objects.get(pk=tutorial.pk) - resp = self.client.post( - reverse("content:edit-extract", args=[tutorial.pk, tutorial.slug, extract.slug]), - { - "last_hash": extract.compute_hash(), - "text": "a brand new text", - "title": extract.title, - "msg_commit": "a commit message", - }, - HTTP_X_REQUESTED_WITH="XMLHttpRequest", - follow=False, - ) - # no redirect - self.assertEqual(200, resp.status_code) - result = loads(resp.content.decode("utf-8")) - self.assertEqual("ok", result.get("result", None)) - self.assertEqual(extract.compute_hash(), result.get("last_hash", None)) diff --git a/zds/tutorialv2/tests/tests_views/tests_published.py b/zds/tutorialv2/tests/tests_views/tests_published.py index 13fbe4821b..cc1a2998fc 100644 --- a/zds/tutorialv2/tests/tests_views/tests_published.py +++ b/zds/tutorialv2/tests/tests_views/tests_published.py @@ -1,4 +1,5 @@ import datetime +from json import loads from django.conf import settings from django.contrib.auth.models import Group @@ -29,7 +30,7 @@ from zds.tutorialv2.publication_utils import publish_content from zds.tutorialv2.tests import TutorialTestMixin from zds.utils.models import Alert, Tag, Hat -from zds.utils.tests.factories import CategoryFactory, SubCategoryFactory, LicenceFactory +from zds.utils.tests.factories import CategoryFactory, SubCategoryFactory, LicenceFactory, HelpWritingFactory from zds.utils.header_notifications import get_header_notifications from copy import deepcopy from zds import json_handler @@ -139,7 +140,7 @@ def test_public_access(self): result = self.client.post( reverse("validation:ask", kwargs={"pk": article.pk, "slug": article.slug}), - {"text": text_validation, "source": "", "version": article_draft.current_version}, + {"text": text_validation, "version": article_draft.current_version}, follow=False, ) self.assertEqual(result.status_code, 302) @@ -157,7 +158,7 @@ def test_public_access(self): # accept result = self.client.post( reverse("validation:accept", kwargs={"pk": validation.pk}), - {"text": text_publication, "is_major": True, "source": ""}, + {"text": text_publication, "is_major": True}, follow=False, ) self.assertEqual(result.status_code, 302) @@ -198,7 +199,7 @@ def test_public_access(self): # ask validation result = self.client.post( reverse("validation:ask", kwargs={"pk": midsize_tuto.pk, "slug": midsize_tuto.slug}), - {"text": text_validation, "source": "", "version": midsize_tuto_draft.current_version}, + {"text": text_validation, "version": midsize_tuto_draft.current_version}, follow=False, ) self.assertEqual(result.status_code, 302) @@ -216,7 +217,7 @@ def test_public_access(self): # accept result = self.client.post( reverse("validation:accept", kwargs={"pk": validation.pk}), - {"text": text_publication, "is_major": True, "source": ""}, + {"text": text_publication, "is_major": True}, follow=False, ) self.assertEqual(result.status_code, 302) @@ -288,7 +289,7 @@ def test_public_access(self): # ask validation result = self.client.post( reverse("validation:ask", kwargs={"pk": bigtuto.pk, "slug": bigtuto.slug}), - {"text": text_validation, "source": "", "version": bigtuto_draft.current_version}, + {"text": text_validation, "version": bigtuto_draft.current_version}, follow=False, ) self.assertEqual(result.status_code, 302) @@ -306,7 +307,7 @@ def test_public_access(self): # accept result = self.client.post( reverse("validation:accept", kwargs={"pk": validation.pk}), - {"text": text_publication, "is_major": True, "source": ""}, + {"text": text_publication, "is_major": True}, follow=False, ) self.assertEqual(result.status_code, 302) @@ -1214,7 +1215,7 @@ def test_republish_with_different_slug(self): result = self.client.post( reverse("validation:ask", kwargs={"pk": tuto.pk, "slug": tuto.slug}), - {"text": text_validation, "source": "", "version": tuto.sha_draft}, + {"text": text_validation, "version": tuto.sha_draft}, follow=False, ) self.assertEqual(result.status_code, 302) @@ -1232,7 +1233,7 @@ def test_republish_with_different_slug(self): # accept result = self.client.post( reverse("validation:accept", kwargs={"pk": validation.pk}), - {"text": text_publication, "is_major": False, "source": ""}, # minor modification (just the title) + {"text": text_publication, "is_major": False}, # minor modification (just the title) follow=False, ) self.assertEqual(result.status_code, 302) @@ -1289,7 +1290,7 @@ def test_validation_list_has_good_title(self): self.client.force_login(self.user_author) result = self.client.post( reverse("validation:ask", args=[tuto.pk, tuto.slug]), - {"text": "something good", "source": "", "version": tuto.sha_draft}, + {"text": "something good", "version": tuto.sha_draft}, follow=False, ) old_title = tuto.title @@ -1413,7 +1414,7 @@ def test_validation_history(self): self.assertEqual(result.status_code, 200) result = self.client.post( reverse("validation:ask", kwargs={"pk": published.pk, "slug": published.slug}), - {"text": "abcdefg", "source": "", "version": published.load_version().current_version}, + {"text": "abcdefg", "version": published.load_version().current_version}, follow=False, ) self.assertEqual(result.status_code, 302) @@ -1429,7 +1430,7 @@ def test_validation_history_for_new_content(self): result = self.client.post( reverse("validation:ask", kwargs={"pk": publishable.pk, "slug": publishable.slug}), - {"text": "abcdefg", "source": "", "version": publishable.load_version().current_version}, + {"text": "abcdefg", "version": publishable.load_version().current_version}, follow=False, ) self.assertEqual(result.status_code, 302) @@ -1454,7 +1455,7 @@ def test_ask_validation_update(self): self.client.force_login(self.user_author) result = self.client.post( reverse("validation:ask", kwargs={"pk": content_draft.pk, "slug": content_draft.slug}), - {"text": text_validation, "source": "", "version": content_draft.current_version}, + {"text": text_validation, "version": content_draft.current_version}, follow=False, ) self.assertEqual(result.status_code, 302) @@ -1488,7 +1489,7 @@ def test_ask_validation_update(self): self.assertEqual(result.status_code, 302) result = self.client.post( reverse("validation:ask", kwargs={"pk": content_draft.pk, "slug": content_draft.slug}), - {"text": text_validation, "source": "", "version": content_draft.current_version}, + {"text": text_validation, "version": content_draft.current_version}, follow=False, ) self.assertEqual(result.status_code, 302) @@ -1523,7 +1524,7 @@ def test_beta_article_closed_when_published(self): self.assertEqual(Validation.objects.count(), 0) result = self.client.post( reverse("validation:ask", kwargs={"pk": article.pk, "slug": article.slug}), - {"text": text_validation, "source": "", "version": article_draft.current_version}, + {"text": text_validation, "version": article_draft.current_version}, follow=False, ) self.assertEqual(result.status_code, 302) @@ -1546,7 +1547,7 @@ def test_beta_article_closed_when_published(self): # publish the article result = self.client.post( reverse("validation:accept", kwargs={"pk": validation.pk}), - {"text": text_publication, "is_major": True, "source": ""}, + {"text": text_publication, "is_major": True}, follow=False, ) self.assertEqual(result.status_code, 302) @@ -1562,7 +1563,7 @@ def test_beta_article_closed_when_published(self): # ask validation result = self.client.post( reverse("validation:ask", kwargs={"pk": article.pk, "slug": article.slug}), - {"text": text_validation, "source": "", "version": article_draft.current_version}, + {"text": text_validation, "version": article_draft.current_version}, follow=False, ) self.assertEqual(result.status_code, 302) @@ -1580,7 +1581,7 @@ def test_beta_article_closed_when_published(self): # publish the article result = self.client.post( reverse("validation:accept", kwargs={"pk": validation.pk}), - {"text": text_publication, "is_major": True, "source": ""}, + {"text": text_publication, "is_major": True}, follow=False, ) self.assertEqual(result.status_code, 302) @@ -1883,7 +1884,7 @@ def test_author_update(self): self.client.force_login(self.user_staff) self.client.post( reverse("validation:ask", kwargs={"pk": tutorial.pk, "slug": tutorial.slug}), - {"text": text_validation, "source": "", "version": tutorial_draft.current_version}, + {"text": text_validation, "version": tutorial_draft.current_version}, follow=False, ) @@ -1894,7 +1895,7 @@ def test_author_update(self): ) self.client.post( reverse("validation:accept", kwargs={"pk": validation.pk}), - {"text": text_publication, "is_major": True, "source": ""}, + {"text": text_publication, "is_major": True}, follow=False, ) self.assertEqual(tutorial.public_version.authors.count(), 2) @@ -1908,7 +1909,7 @@ def test_author_update(self): self.client.force_login(self.user_staff) self.client.post( reverse("validation:ask", kwargs={"pk": tutorial.pk, "slug": tutorial.slug}), - {"text": text_validation, "source": "", "version": tutorial_draft.current_version}, + {"text": text_validation, "version": tutorial_draft.current_version}, follow=False, ) @@ -1919,7 +1920,7 @@ def test_author_update(self): ) self.client.post( reverse("validation:accept", kwargs={"pk": validation.pk}), - {"text": text_publication, "is_major": False, "source": ""}, + {"text": text_publication, "is_major": False}, follow=False, ) @@ -2040,3 +2041,45 @@ def test_social_cards_with_image(self): self.assertEqual(result.status_code, 200) self.check_images_socials(result, "media/galleries/", self.chapter1.title, self.tuto.description) + + def test_add_help_tuto(self): + self.client.force_login(self.user_author) + tutorial = PublishableContentFactory(author_list=[self.user_author]) + help_wanted = HelpWritingFactory() + resp = self.client.post( + reverse("content:helps-change", args=[tutorial.pk]), {"activated": True, "help_wanted": help_wanted.title} + ) + self.assertEqual(302, resp.status_code) + self.assertEqual(1, PublishableContent.objects.filter(pk=tutorial.pk).first().helps.count()) + + def test_add_help_opinion(self): + self.client.force_login(self.user_author) + tutorial = PublishableContentFactory(author_list=[self.user_author], type="OPINION") + help_wanted = HelpWritingFactory() + resp = self.client.post( + reverse("content:helps-change", args=[tutorial.pk]), {"activated": True, "help_wanted": help_wanted.title} + ) + self.assertEqual(400, resp.status_code) + self.assertEqual(0, PublishableContent.objects.filter(pk=tutorial.pk).first().helps.count()) + + def test_save_no_redirect(self): + self.client.force_login(self.user_author) + tutorial = PublishableContentFactory(author_list=[self.user_author]) + extract = ExtractFactory(db_object=tutorial, container=tutorial.load_version()) + tutorial = PublishableContent.objects.get(pk=tutorial.pk) + resp = self.client.post( + reverse("content:edit-extract", args=[tutorial.pk, tutorial.slug, extract.slug]), + { + "last_hash": extract.compute_hash(), + "text": "a brand new text", + "title": extract.title, + "msg_commit": "a commit message", + }, + HTTP_X_REQUESTED_WITH="XMLHttpRequest", + follow=False, + ) + # no redirect + self.assertEqual(200, resp.status_code) + result = loads(resp.content.decode("utf-8")) + self.assertEqual("ok", result.get("result", None)) + self.assertEqual(extract.compute_hash(), result.get("last_hash", None)) diff --git a/zds/urls.py b/zds/urls.py index 2ab157e360..64ada6e5e9 100644 --- a/zds/urls.py +++ b/zds/urls.py @@ -6,7 +6,7 @@ from zds.forum.models import ForumCategory, Forum, Topic, Tag from zds.pages.views import home as home_view -from zds.member.views import MemberDetail +from zds.member.views.profile import MemberDetail from zds.tutorialv2.models.database import PublishedContent from django.conf import settings From f4f31f3299fd98c789b6e7e0ce44d1f7135f41e3 Mon Sep 17 00:00:00 2001 From: Philippe MILINK Date: Sat, 19 Feb 2022 19:27:10 +0100 Subject: [PATCH 52/53] =?UTF-8?q?Met=20=C3=A0=20jour=20EasyMDE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- yarn.lock | 60 +++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 44 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 383694ef0a..c5ee725c20 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "chartjs-adapter-moment": "1.0.0", "cssnano": "5.0.15", "del": "6.0.0", - "easymde": "2.10.2-360.0", + "easymde": "2.16.1", "gulp": "4.0.2", "gulp-concat": "2.6.1", "gulp-dart-sass": "1.0.2", diff --git a/yarn.lock b/yarn.lock index ae51348aaf..ee32ca6b37 100644 --- a/yarn.lock +++ b/yarn.lock @@ -54,6 +54,18 @@ resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== +"@types/codemirror@^5.60.4": + version "5.60.5" + resolved "https://registry.yarnpkg.com/@types/codemirror/-/codemirror-5.60.5.tgz#5b989a3b4bbe657458cf372c92b6bfda6061a2b7" + integrity sha512-TiECZmm8St5YxjFUp64LK0c8WU5bxMDt9YaAek1UqUb9swrSCoJhh92fWu1p3mTEqlHjhB5sY7OFBhWroJXZVg== + dependencies: + "@types/tern" "*" + +"@types/estree@*": + version "0.0.51" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40" + integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== + "@types/glob@^7.1.1": version "7.2.0" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb" @@ -62,6 +74,11 @@ "@types/minimatch" "*" "@types/node" "*" +"@types/marked@^4.0.1": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/marked/-/marked-4.0.2.tgz#cb2dbf10da2f41cf20bd91fb5f89b67540c282f7" + integrity sha512-auNrZ/c0w6wsM9DccwVxWHssrMDezHUAXNesdp2RQrCVCyrQbOiSq7yqdJKrUQQpw9VTm7CGYJH2A/YG7jjrjQ== + "@types/minimatch@*": version "3.0.5" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40" @@ -77,6 +94,13 @@ resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.5.tgz#75a2a8e7d8ab4b230414505d92335d1dcb53a6df" integrity sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ== +"@types/tern@*": + version "0.23.4" + resolved "https://registry.yarnpkg.com/@types/tern/-/tern-0.23.4.tgz#03926eb13dbeaf3ae0d390caf706b2643a0127fb" + integrity sha512-JAUw1iXGO1qaWwEOzxTKJZ/5JxVeON9kvGZ/osgZaJImBnyjyn0cjovPsf6FNLmyGY8Vw9DoXZCMlfMkMwHRWg== + dependencies: + "@types/estree" "*" + acorn-jsx@^5.2.0: version "5.3.1" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" @@ -902,10 +926,10 @@ codemirror-spell-checker@1.1.2: dependencies: typo-js "*" -codemirror@^5.53.2: - version "5.61.1" - resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.61.1.tgz#ccfc8a43b8fcfb8b12e8e75b5ffde48d541406e0" - integrity sha512-+D1NZjAucuzE93vJGbAaXzvoBHwp9nJZWWWF9utjv25+5AZUiah6CIlfb4ikG4MoDsFsCG8niiJH5++OO2LgIQ== +codemirror@^5.63.1: + version "5.65.1" + resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.65.1.tgz#5988a812c974c467f964bcc1a00c944e373de502" + integrity sha512-s6aac+DD+4O2u1aBmdxhB7yz2XU7tG3snOyQ05Kxifahz7hoxnfxIRHxiCSEv3TUC38dIVH8G+lZH9UWSfGQxA== collection-map@^1.0.0: version "1.0.0" @@ -1570,14 +1594,16 @@ each-props@^1.3.2: is-plain-object "^2.0.1" object.defaults "^1.1.0" -easymde@2.10.2-360.0: - version "2.10.2-360.0" - resolved "https://registry.yarnpkg.com/easymde/-/easymde-2.10.2-360.0.tgz#da4e22df3aa1916ae547d84467260fa5d28d1afb" - integrity sha512-Mbh9iYATsDHNBmVZviwmIGjJncF+wc1CPQyaovzXNWRUX/K4ToJV9a3hBZmcKeQVYRWPVp6J3cMhuEDWMyZPeg== +easymde@2.16.1: + version "2.16.1" + resolved "https://registry.yarnpkg.com/easymde/-/easymde-2.16.1.tgz#f4c2380312615cb33826f1a1fecfaa4022ff551a" + integrity sha512-FihYgjRsKfhGNk89SHSqxKLC4aJ1kfybPWW6iAmtb5GnXu+tnFPSzSaGBmk1RRlCuhFSjhF0SnIMGVPjEzkr6g== dependencies: - codemirror "^5.53.2" + "@types/codemirror" "^5.60.4" + "@types/marked" "^4.0.1" + codemirror "^5.63.1" codemirror-spell-checker "1.1.2" - marked "^1.0.0" + marked "^4.0.10" ecc-jsbn@~0.1.1: version "0.1.2" @@ -3871,10 +3897,10 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" -marked@^1.0.0: - version "1.2.9" - resolved "https://registry.yarnpkg.com/marked/-/marked-1.2.9.tgz#53786f8b05d4c01a2a5a76b7d1ec9943d29d72dc" - integrity sha512-H8lIX2SvyitGX+TRdtS06m1jHMijKN/XjfH6Ooii9fvxMlh8QdqBfBDkGUpMWH2kQNrtixjzYUa3SH8ROTgRRw== +marked@^4.0.10: + version "4.0.12" + resolved "https://registry.yarnpkg.com/marked/-/marked-4.0.12.tgz#2262a4e6fd1afd2f13557726238b69a48b982f7d" + integrity sha512-hgibXWrEDNBWgGiK18j/4lkS6ihTe9sxtV4Q1OQppb/0zzyPSzoFANBa5MfsG/zgsWklmNnhm0XACZOH/0HBiQ== matchdep@^2.0.0: version "2.0.0" @@ -6209,9 +6235,9 @@ typedarray@^0.0.6, typedarray@~0.0.5: integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= typo-js@*: - version "1.2.0" - resolved "https://registry.yarnpkg.com/typo-js/-/typo-js-1.2.0.tgz#dbe58de3a6dcbbe260b78bf290ee761b008a28e8" - integrity sha512-dELuLBVa2jvWdU/CHTKi2L/POYaRupv942k+vRsFXsM17acXesQGAiGCio82RW7fvcr7bkuD/Zj8XpUh6aPC2A== + version "1.2.1" + resolved "https://registry.yarnpkg.com/typo-js/-/typo-js-1.2.1.tgz#334a0d8c3f6c56f2f1e15fdf6c31677793cbbe9b" + integrity sha512-bTGLjbD3WqZDR3CgEFkyi9Q/SS2oM29ipXrWfDb4M74ea69QwKAECVceYpaBu0GfdnASMg9Qfl67ttB23nePHg== uglify-js@^3.1.4: version "3.13.9" From 203e0694257dc0631c7cc53fdcf2f4ac0a5a232a Mon Sep 17 00:00:00 2001 From: Arnaud-D <35631001+Arnaud-D@users.noreply.github.com> Date: Mon, 7 Mar 2022 19:36:06 +0100 Subject: [PATCH 53/53] Ajoute des tests et refactorise RemoveSuggestion (#6188) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Ajoute des tests à RemoveSuggestion * Refactorise RemoveSuggestion * Retire un TODO obsolète * Retire un print qui traîne * Retire une variable inutile * Retire un import inutile * Corrige imports --- zds/tutorialv2/forms.py | 12 +- .../tests_views/tests_removesuggestion.py | 121 ++++++++++++++++++ zds/tutorialv2/views/editorialization.py | 58 +++++---- 3 files changed, 162 insertions(+), 29 deletions(-) create mode 100644 zds/tutorialv2/tests/tests_views/tests_removesuggestion.py diff --git a/zds/tutorialv2/forms.py b/zds/tutorialv2/forms.py index 93ff9708a5..0aac241b63 100644 --- a/zds/tutorialv2/forms.py +++ b/zds/tutorialv2/forms.py @@ -11,7 +11,7 @@ from zds.utils.models import SubCategory, Licence from zds.tutorialv2.models import TYPE_CHOICES from zds.utils.models import HelpWriting -from zds.tutorialv2.models.database import PublishableContent, ContentContributionRole +from zds.tutorialv2.models.database import PublishableContent, ContentContributionRole, ContentSuggestion from django.utils.translation import gettext_lazy as _ from zds.member.models import Profile from zds.tutorialv2.utils import slugify_raise_on_invalid, InvalidSlugError @@ -1363,11 +1363,19 @@ def __init__(self, content, *args, **kwargs): class RemoveSuggestionForm(forms.Form): - pk_suggestion = forms.CharField( + pk_suggestion = forms.IntegerField( label=_("Suggestion"), required=True, + error_messages={"does_not_exist": _("La suggestion sélectionnée n'existe pas.")}, ) + def clean_pk_suggestion(self): + pk_suggestion = self.cleaned_data.get("pk_suggestion") + suggestion = ContentSuggestion.objects.filter(id=pk_suggestion).first() + if suggestion is None: + self.add_error("pk_suggestion", self.fields["pk_suggestion"].error_messages["does_not_exist"]) + return pk_suggestion + class ToggleHelpForm(forms.Form): help_wanted = forms.CharField() diff --git a/zds/tutorialv2/tests/tests_views/tests_removesuggestion.py b/zds/tutorialv2/tests/tests_views/tests_removesuggestion.py new file mode 100644 index 0000000000..fdbf1f649f --- /dev/null +++ b/zds/tutorialv2/tests/tests_views/tests_removesuggestion.py @@ -0,0 +1,121 @@ +from django.test import TestCase +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ +from django.utils.html import escape + +from zds.member.tests.factories import ProfileFactory, StaffProfileFactory +from zds.tutorialv2.tests.factories import PublishableContentFactory +from zds.tutorialv2.forms import RemoveSuggestionForm +from zds.tutorialv2.models.database import ContentSuggestion +from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents + + +@override_for_contents() +class RemoveSuggestionPermissionTests(TutorialTestMixin, TestCase): + """Test permissions and associated behaviors, such as redirections and status codes.""" + + def setUp(self): + # Create users + self.author = ProfileFactory().user + self.staff = StaffProfileFactory().user + self.outsider = ProfileFactory().user + + # Create contents and suggestion + self.content = PublishableContentFactory(author_list=[self.author]) + self.suggested_content = PublishableContentFactory() + self.suggestion = ContentSuggestion(publication=self.content, suggestion=self.suggested_content) + self.suggestion.save() + + # Get information to be reused in tests + self.form_url = reverse("content:remove-suggestion", kwargs={"pk": self.content.pk}) + self.login_url = reverse("member-login") + "?next=" + self.form_url + self.content_url = reverse("content:view", kwargs={"pk": self.content.pk, "slug": self.content.slug}) + self.form_data = {"pk_suggestion": self.suggestion.pk} + + def test_not_authenticated(self): + self.client.logout() + self.content.type = "TUTORIAL" + self.content.save() + response = self.client.post(self.form_url, self.form_data) + self.assertRedirects(response, self.login_url) + + def test_authenticated_outsider(self): + self.client.force_login(self.outsider) + self.content.type = "TUTORIAL" + self.content.save() + response = self.client.post(self.form_url, self.form_data) + self.assertEqual(response.status_code, 403) + + def test_authenticated_author(self): + self.client.force_login(self.author) + self.content.type = "TUTORIAL" + self.content.save() + response = self.client.post(self.form_url, self.form_data) + self.assertEqual(response.status_code, 403) + + def test_authenticated_staff_tutorial(self): + self.client.force_login(self.staff) + self.content.type = "TUTORIAL" + self.content.save() + response = self.client.post(self.form_url, self.form_data) + self.assertRedirects(response, self.content_url) + + def test_authenticated_staff_article(self): + self.client.force_login(self.staff) + self.content.type = "ARTICLE" + self.content.save() + response = self.client.post(self.form_url, self.form_data) + self.assertRedirects(response, self.content_url) + + def test_authenticated_staff_opinion(self): + self.client.force_login(self.staff) + self.content.type = "OPINION" + self.content.save() + response = self.client.post(self.form_url, self.form_data) + self.assertEqual(response.status_code, 403) + + +class RemoveSuggestionWorkflowTests(TutorialTestMixin, TestCase): + """Test the workflow of the form, such as validity errors and success messages.""" + + def setUp(self): + # Create users + self.staff = StaffProfileFactory().user + self.author = ProfileFactory().user + + # Create a content + self.content = PublishableContentFactory(author_list=[self.author]) + self.suggested_content_1 = PublishableContentFactory() + self.suggested_content_2 = PublishableContentFactory() + self.suggestion_1 = ContentSuggestion(publication=self.content, suggestion=self.suggested_content_1) + self.suggestion_1.save() + self.suggestion_2 = ContentSuggestion(publication=self.content, suggestion=self.suggested_content_2) + self.suggestion_2.save() + + # Get information to be reused in tests + self.form_url = reverse("content:remove-suggestion", kwargs={"pk": self.content.pk}) + self.success_message_fragment = _("Vous avez enlevé") + self.error_messages = RemoveSuggestionForm.declared_fields["pk_suggestion"].error_messages + # Log in with an authorized user to perform the tests + self.client.force_login(self.staff) + + def test_existing(self): + response = self.client.post(self.form_url, {"pk_suggestion": self.suggestion_1.pk}, follow=True) + # Check that we display correct message + self.assertContains(response, escape(self.success_message_fragment)) + # Check update of database + with self.assertRaises(ContentSuggestion.DoesNotExist): + ContentSuggestion.objects.get(pk=self.suggestion_1.pk) + ContentSuggestion.objects.get(pk=self.suggestion_2.pk) # succeeds + + def test_empty(self): + response = self.client.post(self.form_url, {"pk_suggestion": ""}, follow=True) + self.assertContains(response, escape(self.error_messages["required"])) + + def test_invalid(self): + response = self.client.post(self.form_url, {"pk_suggestion": "420"}, follow=True) # pk must not exist + self.assertContains(response, escape(self.error_messages["does_not_exist"])) + + def test_not_integer(self): + response = self.client.post(self.form_url, {"pk_suggestion": "abcd"}, follow=True) + self.assertContains(response, escape(self.error_messages["invalid"])) diff --git a/zds/tutorialv2/views/editorialization.py b/zds/tutorialv2/views/editorialization.py index 3c94beb743..c19f670eee 100644 --- a/zds/tutorialv2/views/editorialization.py +++ b/zds/tutorialv2/views/editorialization.py @@ -1,51 +1,55 @@ from django.contrib import messages +from django.contrib.auth.decorators import login_required from django.core.exceptions import PermissionDenied from django.shortcuts import get_object_or_404, redirect +from django.utils.decorators import method_decorator from django.utils.translation import gettext_lazy as _ -from zds.member.decorator import LoggedWithReadWriteHability, PermissionRequiredMixin +from zds.member.decorator import LoggedWithReadWriteHability, can_write_and_read_now, PermissionRequiredMixin from zds.tutorialv2.forms import RemoveSuggestionForm, EditContentTagsForm from zds.tutorialv2.mixins import SingleContentFormViewMixin from zds.tutorialv2.models.database import ContentSuggestion, PublishableContent -class RemoveSuggestion(LoggedWithReadWriteHability, SingleContentFormViewMixin): - +class RemoveSuggestion(PermissionRequiredMixin, SingleContentFormViewMixin): form_class = RemoveSuggestionForm + modal_form = True only_draft_version = True - authorized_for_staff = True + permissions = ["tutorialv2.change_publishablecontent"] - def form_valid(self, form): - _type = _("cet article") - if self.object.is_tutorial: - _type = _("ce tutoriel") - elif self.object.is_opinion: + @method_decorator(login_required) + @method_decorator(can_write_and_read_now) + def dispatch(self, *args, **kwargs): + if self.get_object().is_opinion: raise PermissionDenied + return super().dispatch(*args, **kwargs) + + def form_valid(self, form): + suggestion = ContentSuggestion.objects.get(pk=form.cleaned_data["pk_suggestion"]) + suggestion.delete() + messages.success(self.request, self.get_success_message(suggestion)) + return super().form_valid(form) - content_suggestion = get_object_or_404(ContentSuggestion, pk=form.cleaned_data["pk_suggestion"]) - content_suggestion.delete() + def form_invalid(self, form): + form.previous_page_url = self.get_success_url() + return super().form_invalid(form) - messages.success( - self.request, - _('Vous avez enlevé "{}" de la liste des suggestions de {}.').format( - content_suggestion.suggestion.title, _type - ), + def get_success_message(self, content_suggestion): + return _('Vous avez enlevé "{}" de la liste des suggestions de {}.').format( + content_suggestion.suggestion.title, + self.describe_type(), ) + def get_success_url(self): if self.object.public_version: - self.success_url = self.object.get_absolute_url_online() + return self.object.get_absolute_url_online() else: - self.success_url = self.object.get_absolute_url() - - return super().form_valid(form) + return self.object.get_absolute_url() - def form_invalid(self, form): - messages.error(self.request, str(_("Les suggestions sélectionnées n'existent pas."))) - if self.object.public_version: - self.success_url = self.object.get_absolute_url_online() - else: - self.success_url = self.object.get_absolute_url() - return super().form_valid(form) + def describe_type(self): + if self.object.is_tutorial: + return _("ce tutoriel") + return _("cet article") class AddSuggestion(LoggedWithReadWriteHability, PermissionRequiredMixin, SingleContentFormViewMixin):