Skip to content

Commit

Permalink
Merge branch 'dev' into n+1-sql-queries
Browse files Browse the repository at this point in the history
  • Loading branch information
Arnaud-D committed Aug 27, 2023
2 parents b8b0e5e + b5c6af7 commit 73c52e8
Show file tree
Hide file tree
Showing 23 changed files with 8,407 additions and 74 deletions.
13 changes: 7 additions & 6 deletions doc/source/front-end/template-tags.rst
Original file line number Diff line number Diff line change
Expand Up @@ -86,23 +86,24 @@ Ce filtre formate une date au format ``DateTime`` destiné à être affiché sur

Ce filtre effectue la même chose que ``format_date`` mais à destination des ``tooltip``.

``humane_time``
---------------
``date_from_timestamp``
-----------------------

Formate une date au format *Nombre de seconde depuis Epoch* en un élément lisible. Ainsi :
Convertit une date au format *Nombre de seconde depuis Epoch* en un objet
accepté par les autres filtres de ce module. Ainsi :

.. sourcecode:: html+django

{% load date %}
{{ date_epoch|humane_time }}
{{ date_epoch|date_from_timestamp|format_date }}

sera rendu :

.. sourcecode:: text

jeudi 01 janvier 1970 à 00h00
jeudi 01 janvier 1970 à 00h02

…si le contenu de ``date_epoch`` était de ``42``.
…si le contenu de ``date_epoch`` était de ``122``.

``from_elasticsearch_date``
---------------------------
Expand Down
4 changes: 2 additions & 2 deletions templates/tutorialv2/view/history.html
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ <h2 class="subtitle">
{% endif %}
</td>
<td>
{{ commit.authored_date|humane_time }}
{{ commit.authored_date|date_from_timestamp|format_date }}
</td>
<td>
<a href="{% url "content:view" content.pk content.slug %}?version={{ commit.hexsha }}" >
Expand Down Expand Up @@ -161,7 +161,7 @@ <h2 class="subtitle">
{% trans "mettre à jour" %}
{% endif %}
{% endcaptureas %}
{% blocktrans with action=action date_version=commit.authored_date|humane_time content_title=content.title %}
{% blocktrans with action=action date_version=commit.authored_date|date_from_timestamp|format_date content_title=content.title %}
Êtes-vous certain de vouloir <strong>{{ action }}</strong> la bêta pour le contenu
"<em>{{ content_title }}</em>" dans sa version de {{ date_version }} ?
{% endblocktrans %}
Expand Down
17 changes: 12 additions & 5 deletions zds/forum/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,7 @@ class TopicForm(forms.Form, FieldValidatorMixin):
label=_("Tag(s) séparés par une virgule (exemple: python,django,web)"),
max_length=64,
required=False,
widget=forms.TextInput(
attrs={"data-autocomplete": '{ "type": "multiple", "fieldname": "title", "url": "/api/tags/?search=%s" }'}
),
widget=forms.TextInput(),
)

text = forms.CharField(
Expand All @@ -44,6 +42,15 @@ class TopicForm(forms.Form, FieldValidatorMixin):

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

self.fields["tags"].widget.attrs.update(
{
"data-autocomplete": '{ "type": "multiple", "fieldname": "title", "url": "'
+ reverse("api:utils:tags-list")
+ '?search=%s" }'
}
)

self.helper = FormHelper()
self.helper.form_class = "content-wrapper"
self.helper.form_method = "post"
Expand All @@ -53,11 +60,11 @@ def __init__(self, *args, **kwargs):
Field("subtitle", autocomplete="off"),
Field("tags"),
HTML(
"""<div id="topic-suggest" style="display:none;" url="/rechercher/sujets-similaires/">
"""<div id="topic-suggest" style="display:none;" url="{}">
<label>{}</label>
<div id="topic-result-container" data-neither="{}"></div>
</div>""".format(
_("Sujets similaires au vôtre :"), _("Aucun résultat")
reverse("search:similar"), _("Sujets similaires au vôtre :"), _("Aucun résultat")
)
),
CommonLayoutEditor(),
Expand Down
31 changes: 31 additions & 0 deletions zds/forum/tests/tests_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from django.contrib.auth.models import Group
from django.test import TestCase

from zds.forum.tests.factories import create_category_and_forum
from zds.forum.utils import get_authorized_forums_pk
from zds.member.tests.factories import ProfileFactory, StaffProfileFactory


class GetAuthorizedForumsTests(TestCase):
def test_get_authorized_forums_pk(self):
user = ProfileFactory().user
staff = StaffProfileFactory().user

# 1. Create a hidden forum belonging to a hidden staff group:
group = Group.objects.create(name="Les illuminatis anonymes de ZdS")
_, hidden_forum = create_category_and_forum(group)

staff.groups.add(group)
staff.save()

# 2. Create a public forum:
_, public_forum = create_category_and_forum()

# 3. Regular user can access only the public forum:
self.assertEqual(get_authorized_forums_pk(user), [public_forum.pk])

# 4. Staff user can access all forums:
self.assertEqual(sorted(get_authorized_forums_pk(staff)), sorted([hidden_forum.pk, public_forum.pk]))

# 5. By default, only public forums are available:
self.assertEqual(get_authorized_forums_pk(None), [public_forum.pk])
19 changes: 18 additions & 1 deletion zds/forum/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from django.views.generic import CreateView
from django.views.generic.detail import SingleObjectMixin
from django.utils.translation import gettext as _
from zds.forum.models import Topic, Post
from zds.forum.models import Forum, Topic, Post
from zds.member.views import get_client_ip
from zds.utils.misc import contains_utf8mb4
from zds.utils.mixins import QuoteMixin
Expand Down Expand Up @@ -198,3 +198,20 @@ def post(self, request, *args, **kwargs):

def create_forum(self, form_class, **kwargs):
raise NotImplementedError("`create_forum()` must be implemented.")


def get_authorized_forums_pk(user):
"""
Find forums the user is allowed to visit.
:param user: concerned user.
:return: pk of authorized forums
"""
forums_pub = Forum.objects.filter(groups__isnull=True).all()
if user and user.is_authenticated:
forums_private = Forum.objects.filter(groups__isnull=False, groups__in=user.groups.all()).all()
list_forums = list(forums_pub | forums_private)
else:
list_forums = list(forums_pub)

return [f.pk for f in list_forums]
5 changes: 2 additions & 3 deletions zds/member/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -468,9 +468,8 @@ def find_username_skeleton(username):
skeleton = ""
for ch in username:
homoglyph = hg.Homoglyphs(languages={"fr"}, strategy=hg.STRATEGY_LOAD).to_ascii(ch)
if len(homoglyph) > 0:
if homoglyph[0].strip() != "":
skeleton += homoglyph[0]
if len(homoglyph) > 0 and homoglyph[0].strip() != "":
skeleton += homoglyph[0]
return skeleton.lower()


Expand Down
11 changes: 11 additions & 0 deletions zds/member/tests/tests_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,17 @@ def test_username_slash_register_form(self):
form = RegisterForm(data=data)
self.assertFalse(form.is_valid())

def test_username_character_null(self):
ProfileFactory()
data = {
"email": "test@gmail.com",
"username": "foo\x00bar",
"password": "ZePassword",
"password_confirm": "ZePassword",
}
form = RegisterForm(data=data)
self.assertFalse(form.is_valid())


class UnregisterFormTest(TestCase):
"""
Expand Down
12 changes: 10 additions & 2 deletions zds/member/validators.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.core.validators import EmailValidator
from django.core.validators import EmailValidator, ProhibitNullCharactersValidator
from django.utils.encoding import force_str
from django.utils.translation import gettext_lazy as _

Expand Down Expand Up @@ -77,12 +77,20 @@ def validate_zds_username(value, check_username_available=True):
:param value: value to validate (str or None)
:return:
"""

# If the character \x00 is in the username, the homoglyphs library called
# in Profile.find_username_skeleton() will raise a ValueError (the bug has
# been reported: https://github.com/yamatt/homoglyphs/issues/6). To prevent
# this, we call this validator which will raise a ValidationError if \x00 is
# in the username.
ProhibitNullCharactersValidator()(value)

msg = None
user_count = User.objects.filter(username=value).count()
skeleton_user_count = Profile.objects.filter(username_skeleton=Profile.find_username_skeleton(value)).count()
if "," in value:
msg = _("Le nom d'utilisateur ne peut contenir de virgules")
if "/" in value:
elif "/" in value:
msg = _("Le nom d'utilisateur ne peut contenir de barres obliques")
elif contains_utf8mb4(value):
msg = _("Le nom d'utilisateur ne peut pas contenir des caractères utf8mb4")
Expand Down
9 changes: 7 additions & 2 deletions zds/mp/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,8 @@ class PrivateTopicForm(forms.Form, ParticipantsStringValidator, TitleValidator,
label=_("Participants"),
widget=forms.TextInput(
attrs={
"placeholder": _("Les participants doivent " "être séparés par une virgule."),
"placeholder": _("Les participants doivent être séparés par une virgule."),
"required": "required",
"data-autocomplete": '{ "type": "multiple", "url": "/api/membres/?search=%s" }',
}
),
)
Expand All @@ -40,6 +39,12 @@ class PrivateTopicForm(forms.Form, ParticipantsStringValidator, TitleValidator,

def __init__(self, username, *args, **kwargs):
super().__init__(*args, **kwargs)

self.fields["participants"].widget.attrs.update(
{
"data-autocomplete": '{ "type": "multiple", "url": "' + reverse("api:member:list") + '?search=%s" }',
}
)
self.helper = FormHelper()
self.helper.form_class = "content-wrapper"
self.helper.form_method = "post"
Expand Down
2 changes: 1 addition & 1 deletion zds/notification/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class NotificationAdmin(admin.ModelAdmin):

list_display = ("subscription", "pubdate", "is_read", "is_dead", "sender")
list_filter = ("is_read", "is_dead")
search_fields = ("subscription__user__username, sender__username", "url", "title")
search_fields = ("subscription__user__username", "sender__username", "url", "title")
raw_id_fields = ("subscription", "sender")


Expand Down
17 changes: 17 additions & 0 deletions zds/notification/managers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
from django.db import models
Expand Down Expand Up @@ -157,6 +158,22 @@ def deactivate_subscriptions(self, user, _object):
subscription.save(update_fields=["is_active"])


class NewPublicationSubscriptionManager(SubscriptionManager):
def get_objects_followed_by(self, user):
"""
Gets objects followed by the given user.
:param user: concerned user.
:type user: django.contrib.auth.models.User
:return: All objects followed by given user.
"""
user_list = self.filter(
user=user, is_active=True, content_type=ContentType.objects.get_for_model(User)
).values_list("object_id", flat=True)

return User.objects.filter(id__in=user_list)


class NewTopicSubscriptionManager(SubscriptionManager):
def mark_read_everybody_at(self, topic):
"""
Expand Down
3 changes: 2 additions & 1 deletion zds/notification/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
SubscriptionManager,
TopicFollowedManager,
TopicAnswerSubscriptionManager,
NewPublicationSubscriptionManager,
NewTopicSubscriptionManager,
)
from zds.utils.misc import convert_camel_to_underscore
Expand Down Expand Up @@ -349,7 +350,7 @@ class NewPublicationSubscription(Subscription, MultipleNotificationsMixin):
"""

module = _("Contenu")
objects = SubscriptionManager()
objects = NewPublicationSubscriptionManager()

def __str__(self):
return _('<Abonnement du membre "{0}" aux nouvelles publications de l\'utilisateur #{1}>').format(
Expand Down
21 changes: 14 additions & 7 deletions zds/notification/receivers.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,13 +157,20 @@ def mark_content_reactions_read(sender, *, instance, user=None, target, **__):
subscription = ContentReactionAnswerSubscription.objects.get_existing(user, instance, is_active=True)
if subscription:
subscription.mark_notification_read()
elif target == PublishableContent:
authors = list(instance.authors.all())
for author in authors:
subscription = NewPublicationSubscription.objects.get_existing(user, author)
# a subscription has to be handled only if it is active OR if it was triggered from the publication
# event that creates an "autosubscribe" which is immediately deactivated.
if subscription and (subscription.is_active or subscription.user in authors):
elif target == PublishableContent and user is not None:
# We cannot use the list of authors of the content, because the user we
# are subscribed to may have left the authorship of the content (see issue #5544).
followed_users = list(NewPublicationSubscription.objects.get_objects_followed_by(user))
if user not in followed_users:
# When a content is published, their authors are subscribed for
# notifications of their own publications, but these subscriptions
# are not activated (see receiver for signal content_published).
# Since followed_users contains only active subscriptions, current
# user should not be in it, so we add it manually:
followed_users.append(user)
for followed_user in followed_users:
subscription = NewPublicationSubscription.objects.get_existing(user, followed_user)
if subscription:
subscription.mark_notification_read(content=instance)


Expand Down
Loading

0 comments on commit 73c52e8

Please sign in to comment.