From e786f7369d8a632dd3295b96d4c52ef2ba3a37c2 Mon Sep 17 00:00:00 2001 From: Arnaud-D <35631001+Arnaud-D@users.noreply.github.com> Date: Sun, 20 Feb 2022 08:47:16 +0100 Subject: [PATCH 1/3] Scinde des gros fichiers du module member --- 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 | 1923 ----------------- .../tests/tests_views/tests_published.py | 42 +- zds/urls.py | 2 +- 26 files changed, 3701 insertions(+), 5331 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..e85ca3c3bb --- /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.factories import ProfileFactory, StaffProfileFactory, UserFactory +from zds.member.models import Profile +from zds.forum.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..348cfbd80d --- /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.factories import ProfileFactory, StaffProfileFactory, UserFactory +from zds.member.models import NewEmailProvider, BannedEmailProvider, TokenRegister +from zds.forum.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..2dae508d62 --- /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.factories import ProfileFactory, StaffProfileFactory, UserFactory +from zds.forum.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..f0c6cfa530 --- /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.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..2a3f9bead6 --- /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.factories import ForumCategoryFactory, ForumFactory +from zds.member.views.moderation import member_from_ip +from zds.member.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..d08e968454 --- /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.factories import ProfileFactory, StaffProfileFactory, UserFactory +from zds.member.models import TokenForgotPassword +from zds.forum.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..9b0aeb5bed --- /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.factories import ( + ProfileFactory, + StaffProfileFactory, + UserFactory, + DevProfileFactory, +) +from zds.member.models import Profile +from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents +from zds.forum.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..54398d1c52 --- /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.factories import ProfileFactory, UserFactory, StaffProfileFactory +from zds.mp.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.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.models import Topic, Post +from zds.gallery.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..6fd95bcf0f --- /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.factories import ProfileFactory, StaffProfileFactory, UserFactory + +from zds.mp.models import PrivateTopic +from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents +from zds.forum.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..bb1302acd4 100644 --- a/zds/tutorialv2/tests/tests_views/tests_content.py +++ b/zds/tutorialv2/tests/tests_views/tests_content.py @@ -3581,1926 +3581,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..6591e1c50f 100644 --- a/zds/tutorialv2/tests/tests_views/tests_published.py +++ b/zds/tutorialv2/tests/tests_views/tests_published.py @@ -139,7 +139,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 +157,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 +198,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 +216,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 +288,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 +306,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 +1214,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 +1232,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 +1289,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 +1413,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 +1429,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 +1454,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 +1488,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 +1523,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 +1546,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 +1562,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 +1580,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 +1883,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 +1894,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 +1908,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 +1919,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, ) 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 f96d51857cd8da0f2c208db4a83be6fef5352e0f Mon Sep 17 00:00:00 2001 From: Arnaud-D <35631001+Arnaud-D@users.noreply.github.com> Date: Sat, 5 Mar 2022 12:48:41 +0100 Subject: [PATCH 2/3] Fix imports --- zds/member/tests/views/tests_admin.py | 4 ++-- zds/member/tests/views/tests_emailproviders.py | 4 ++-- zds/member/tests/views/tests_hats.py | 4 ++-- zds/member/tests/views/tests_login.py | 2 +- zds/member/tests/views/tests_moderation.py | 4 ++-- zds/member/tests/views/tests_password_recovery.py | 4 ++-- zds/member/tests/views/tests_profile.py | 4 ++-- zds/member/tests/views/tests_register.py | 10 +++++----- zds/member/tests/views/tests_reports.py | 4 ++-- 9 files changed, 20 insertions(+), 20 deletions(-) diff --git a/zds/member/tests/views/tests_admin.py b/zds/member/tests/views/tests_admin.py index e85ca3c3bb..0d60d4fb45 100644 --- a/zds/member/tests/views/tests_admin.py +++ b/zds/member/tests/views/tests_admin.py @@ -4,9 +4,9 @@ from django.test import TestCase from zds.notification.models import TopicAnswerSubscription -from zds.member.factories import ProfileFactory, StaffProfileFactory, UserFactory +from zds.member.tests.factories import ProfileFactory, StaffProfileFactory, UserFactory from zds.member.models import Profile -from zds.forum.factories import ForumCategoryFactory, ForumFactory, TopicFactory +from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory, TopicFactory class MemberTests(TestCase): diff --git a/zds/member/tests/views/tests_emailproviders.py b/zds/member/tests/views/tests_emailproviders.py index 348cfbd80d..a3039ab422 100644 --- a/zds/member/tests/views/tests_emailproviders.py +++ b/zds/member/tests/views/tests_emailproviders.py @@ -3,9 +3,9 @@ from django.urls import reverse from django.test import TestCase -from zds.member.factories import ProfileFactory, StaffProfileFactory, UserFactory +from zds.member.tests.factories import ProfileFactory, StaffProfileFactory, UserFactory from zds.member.models import NewEmailProvider, BannedEmailProvider, TokenRegister -from zds.forum.factories import ForumCategoryFactory, ForumFactory +from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory class EmailProvidersTests(TestCase): diff --git a/zds/member/tests/views/tests_hats.py b/zds/member/tests/views/tests_hats.py index 2dae508d62..05b87b0fab 100644 --- a/zds/member/tests/views/tests_hats.py +++ b/zds/member/tests/views/tests_hats.py @@ -4,8 +4,8 @@ from django.test import TestCase from django.utils.translation import gettext_lazy as _ -from zds.member.factories import ProfileFactory, StaffProfileFactory, UserFactory -from zds.forum.factories import ForumCategoryFactory, ForumFactory +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 diff --git a/zds/member/tests/views/tests_login.py b/zds/member/tests/views/tests_login.py index f0c6cfa530..f56c00e321 100644 --- a/zds/member/tests/views/tests_login.py +++ b/zds/member/tests/views/tests_login.py @@ -2,7 +2,7 @@ from django.test import TestCase from django.utils.translation import gettext_lazy as _ -from zds.member.factories import ProfileFactory, NonAsciiProfileFactory +from zds.member.tests.factories import ProfileFactory, NonAsciiProfileFactory class MemberTests(TestCase): diff --git a/zds/member/tests/views/tests_moderation.py b/zds/member/tests/views/tests_moderation.py index 2a3f9bead6..ae1f8a71d9 100644 --- a/zds/member/tests/views/tests_moderation.py +++ b/zds/member/tests/views/tests_moderation.py @@ -8,9 +8,9 @@ from django.test import TestCase -from zds.forum.factories import ForumCategoryFactory, ForumFactory +from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory from zds.member.views.moderation import member_from_ip -from zds.member.factories import ProfileFactory, StaffProfileFactory, UserFactory +from zds.member.tests.factories import ProfileFactory, StaffProfileFactory, UserFactory from zds.member.models import Profile, Ban, KarmaNote diff --git a/zds/member/tests/views/tests_password_recovery.py b/zds/member/tests/views/tests_password_recovery.py index d08e968454..0dd342247f 100644 --- a/zds/member/tests/views/tests_password_recovery.py +++ b/zds/member/tests/views/tests_password_recovery.py @@ -4,9 +4,9 @@ from django.urls import reverse from django.test import TestCase -from zds.member.factories import ProfileFactory, StaffProfileFactory, UserFactory +from zds.member.tests.factories import ProfileFactory, StaffProfileFactory, UserFactory from zds.member.models import TokenForgotPassword -from zds.forum.factories import ForumCategoryFactory, ForumFactory +from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory class MemberTests(TestCase): diff --git a/zds/member/tests/views/tests_profile.py b/zds/member/tests/views/tests_profile.py index 9b0aeb5bed..a8f115bdac 100644 --- a/zds/member/tests/views/tests_profile.py +++ b/zds/member/tests/views/tests_profile.py @@ -3,7 +3,7 @@ from django.urls import reverse from django.test import TestCase -from zds.member.factories import ( +from zds.member.tests.factories import ( ProfileFactory, StaffProfileFactory, UserFactory, @@ -11,7 +11,7 @@ ) from zds.member.models import Profile from zds.tutorialv2.tests import TutorialTestMixin, override_for_contents -from zds.forum.factories import ForumCategoryFactory, ForumFactory +from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory @override_for_contents() diff --git a/zds/member/tests/views/tests_register.py b/zds/member/tests/views/tests_register.py index 54398d1c52..2d3d9040a0 100644 --- a/zds/member/tests/views/tests_register.py +++ b/zds/member/tests/views/tests_register.py @@ -13,17 +13,17 @@ from django.utils.html import escape from django.test import TestCase, override_settings -from zds.member.factories import ProfileFactory, UserFactory, StaffProfileFactory -from zds.mp.factories import PrivateTopicFactory, PrivatePostFactory +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.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.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.tests.factories import GalleryFactory, UserGalleryFactory from zds.gallery.models import Gallery, UserGallery from zds.utils.models import CommentVote diff --git a/zds/member/tests/views/tests_reports.py b/zds/member/tests/views/tests_reports.py index 6fd95bcf0f..c5d8aaac1e 100644 --- a/zds/member/tests/views/tests_reports.py +++ b/zds/member/tests/views/tests_reports.py @@ -3,11 +3,11 @@ from django.urls import reverse from django.test import TestCase -from zds.member.factories import ProfileFactory, StaffProfileFactory, UserFactory +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.factories import ForumCategoryFactory, ForumFactory +from zds.forum.tests.factories import ForumCategoryFactory, ForumFactory from zds.utils.models import Alert From 2fa53c4c11694a2058744b45712eb77c580f7c61 Mon Sep 17 00:00:00 2001 From: Arnaud-D <35631001+Arnaud-D@users.noreply.github.com> Date: Sun, 6 Mar 2022 22:35:57 +0100 Subject: [PATCH 3/3] =?UTF-8?q?Remets=20en=20place=20quelques=20tests=20in?= =?UTF-8?q?justement=20=C3=A9limin=C3=A9s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tests/tests_views/tests_content.py | 12 ++--- .../tests/tests_views/tests_published.py | 45 ++++++++++++++++++- 2 files changed, 47 insertions(+), 10 deletions(-) diff --git a/zds/tutorialv2/tests/tests_views/tests_content.py b/zds/tutorialv2/tests/tests_views/tests_content.py index bb1302acd4..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 diff --git a/zds/tutorialv2/tests/tests_views/tests_published.py b/zds/tutorialv2/tests/tests_views/tests_published.py index 6591e1c50f..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 @@ -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))