diff --git a/spirit/comment/flag/admin/templates/spirit/comment/flag/admin/detail.html b/spirit/comment/flag/admin/templates/spirit/comment/flag/admin/detail.html index 85dd49e26..46490f63e 100644 --- a/spirit/comment/flag/admin/templates/spirit/comment/flag/admin/detail.html +++ b/spirit/comment/flag/admin/templates/spirit/comment/flag/admin/detail.html @@ -16,7 +16,7 @@

{% trans "Flag" %}

{% if flag.moderator %}
{% trans "Moderated by" %}:
-
{{ flag.moderator.username }}
+
{{ flag.moderator.st.nickname }}
{% endif %} @@ -41,7 +41,7 @@

{% trans "Comment flagged" %}

- {{ flag.comment.user.username }} + {{ flag.comment.user.st.nickname }}
@@ -77,7 +77,7 @@

{% trans "Reporters" %}

- {{ f.user.username }} + {{ f.user.st.nickname }}
diff --git a/spirit/comment/flag/forms.py b/spirit/comment/flag/forms.py index fc8c85f2e..2dc48c8cc 100644 --- a/spirit/comment/flag/forms.py +++ b/spirit/comment/flag/forms.py @@ -4,7 +4,7 @@ from django import forms from django.utils.translation import ugettext_lazy as _ -from django.db import IntegrityError +from django.db import IntegrityError, transaction from django.utils import timezone from .models import Flag, CommentFlag @@ -39,8 +39,10 @@ def save(self, commit=True): self.instance.comment = self.comment try: - CommentFlag.objects.update_or_create(comment=self.comment, - defaults={'date': timezone.now(), }) + with transaction.atomic(): + CommentFlag.objects.update_or_create( + comment=self.comment, + defaults={'date': timezone.now(), }) except IntegrityError: pass diff --git a/spirit/comment/flag/templates/spirit/comment/flag/create.html b/spirit/comment/flag/templates/spirit/comment/flag/create.html index dede67be1..7ca4f8e8d 100644 --- a/spirit/comment/flag/templates/spirit/comment/flag/create.html +++ b/spirit/comment/flag/templates/spirit/comment/flag/create.html @@ -7,7 +7,7 @@ {% block content %}

- {% blocktrans trimmed with username=comment.user.username %} + {% blocktrans trimmed with username=comment.user.st.nickname %} Reporting {{ username }}'s comment {% endblocktrans %}

diff --git a/spirit/comment/history/templates/spirit/comment/history/detail.html b/spirit/comment/history/templates/spirit/comment/history/detail.html index 867134759..2ad869d4d 100644 --- a/spirit/comment/history/templates/spirit/comment/history/detail.html +++ b/spirit/comment/history/templates/spirit/comment/history/detail.html @@ -24,7 +24,7 @@

{% trans "Comment history" %}

- {{ c.comment_fk.user.username }} + {{ c.comment_fk.user.st.nickname }}
{% endspaceless %} diff --git a/spirit/core/tests/tests_markdown.py b/spirit/core/tests/tests_markdown.py index 4cafcfcf4..0aef1cc29 100644 --- a/spirit/core/tests/tests_markdown.py +++ b/spirit/core/tests/tests_markdown.py @@ -69,13 +69,14 @@ def test_markdown_mentions(self): """ comment = "@nitely, @esteban,@áéíóú, @fakeone" comment_md = Markdown().render(comment) - self.assertEqual(comment_md, '

@nitely, ' - '@esteban,' - '@áéíóú, ' - '@fakeone

' % - (self.user.st.get_absolute_url(), - self.user2.st.get_absolute_url(), - self.user3.st.get_absolute_url())) + self.assertEqual(comment_md, + '

@nitely, ' + '@esteban,' + '@áéíóú, ' + '@fakeone

' % + (self.user.st.get_absolute_url(), + self.user2.st.get_absolute_url(), + self.user3.st.get_absolute_url())) @override_settings(ST_MENTIONS_PER_COMMENT=2) def test_markdown_mentions_limit(self): @@ -94,8 +95,29 @@ def test_markdown_mentions_dict(self): md = Markdown() md.render(comment) # mentions get dynamically added on MentionifyExtension - self.assertDictEqual(md.get_mentions(), {'nitely': self.user, - 'esteban': self.user2}) + self.assertDictEqual(md.get_mentions(), { + 'nitely': self.user, + 'esteban': self.user2}) + + @override_settings(ST_CASE_INSENSITIVE_USERNAMES=True) + def test_markdown_mentions_dict_ci(self): + """ + markdown mentions dict case-insensitive + """ + comment = "@NiTely, @EsTebaN, @nitEly, @NiteLy" + md = Markdown() + md.render(comment) + self.assertDictEqual(md.get_mentions(), { + 'nitely': self.user, + 'esteban': self.user2}) + + @override_settings(ST_CASE_INSENSITIVE_USERNAMES=False) + def test_markdown_mentions_dict_ci_off(self): + comment = "@NiTely, @esteban, @nitEly, @NiteLy" + md = Markdown() + md.render(comment) + self.assertDictEqual(md.get_mentions(), { + 'esteban': self.user2}) def test_markdown_emoji(self): """ @@ -115,7 +137,9 @@ def test_markdown_quote(self): """ comment = "text\nnew line" quote = quotify(comment, self.user) - self.assertListEqual(quote.splitlines(), ("> @%s said:\n> text\n> new line\n\n" % self.user.username).splitlines()) + self.assertListEqual( + quote.splitlines(), + ("> @%s said:\n> text\n> new line\n\n" % self.user.st.nickname).splitlines()) @override_settings(LANGUAGE_CODE='en') def test_markdown_quote_header_language(self): @@ -127,7 +151,9 @@ def test_markdown_quote_header_language(self): quote = quotify(comment, self.user) with translation.override('es'): - self.assertListEqual(quote.splitlines(), ("> @%s said:\n> \n\n" % self.user.username).splitlines()) + self.assertListEqual( + quote.splitlines(), + ("> @%s said:\n> \n\n" % self.user.st.nickname).splitlines()) @override_settings(LANGUAGE_CODE='en') def test_markdown_quote_no_polls(self): @@ -145,7 +171,9 @@ def test_markdown_quote_no_polls(self): "2. opt 2\n" \ "[/poll]" quote = quotify(comment, self.user) - self.assertListEqual(quote.splitlines(), ("> @%s said:\n> foo\n> \n> bar\n\n" % self.user.username).splitlines()) + self.assertListEqual( + quote.splitlines(), + ("> @%s said:\n> foo\n> \n> bar\n\n" % self.user.username).splitlines()) def test_markdown_image(self): """ diff --git a/spirit/core/tests/utils.py b/spirit/core/tests/utils.py index 6abe0223e..aed3dfed8 100644 --- a/spirit/core/tests/utils.py +++ b/spirit/core/tests/utils.py @@ -75,7 +75,9 @@ def create_comment(**kwargs): def login(test_case_instance, user=None, password=None): user = user or test_case_instance.user password = password or "bar" - login_successful = test_case_instance.client.login(username=user.username, password=password) + login_successful = test_case_instance.client.login( + username=user.username, + password=password) test_case_instance.assertTrue(login_successful) diff --git a/spirit/core/utils/markdown/inline.py b/spirit/core/utils/markdown/inline.py index ef281f5a4..816851cc8 100644 --- a/spirit/core/utils/markdown/inline.py +++ b/spirit/core/utils/markdown/inline.py @@ -65,25 +65,33 @@ def output_emoji(self, m): def output_mention(self, m): username = m.group('username') + if settings.ST_CASE_INSENSITIVE_USERNAMES: + username = username.lower() + # Already mentioned? if username in self.mentions: user = self.mentions[username] - return self.renderer.mention(username, user.st.get_absolute_url()) + return self.renderer.mention( + user.st.nickname, + user.st.get_absolute_url()) # Mentions limiter + # We increase this before doing the query to avoid abuses + # i.e adding 1K invalid usernames won't make 1K queries if self._mention_count >= settings.ST_MENTIONS_PER_COMMENT: return m.group(0) - - # We increase this before doing the query to avoid abuses self._mention_count += 1 # New mention try: - user = User.objects\ - .select_related('st')\ - .get(username=username) + user = ( + User.objects + .select_related('st') + .get(username=username)) except User.DoesNotExist: return m.group(0) self.mentions[username] = user - return self.renderer.mention(username, user.st.get_absolute_url()) + return self.renderer.mention( + user.st.nickname, + user.st.get_absolute_url()) diff --git a/spirit/core/utils/widgets.py b/spirit/core/utils/widgets.py index 95cf72565..ebdfa8f38 100644 --- a/spirit/core/utils/widgets.py +++ b/spirit/core/utils/widgets.py @@ -11,9 +11,6 @@ class MultipleInput(forms.TextInput): TextInput widget for input multiple *raw* choices """ - def __init__(self, *args, **kwargs): - super(MultipleInput, self).__init__(*args, **kwargs) - def render(self, name, value, *args, **kwargs): if value: value = ','.join(force_text(v) for v in value) @@ -28,3 +25,21 @@ def value_from_datadict(self, data, files, name, *args, **kwargs): if value: return [v.strip() for v in value.split(',')] + + +class CIMultipleInput(MultipleInput): + """Case-Insensitive ``MultipleInput`` widget""" + + def value_from_datadict(self, *args, **kwargs): + value = super(CIMultipleInput, self).value_from_datadict(*args, **kwargs) + if value: + return [v.lower() for v in value] + + +class CITextInput(forms.TextInput): + """Case-Insensitive ``TextInput`` widget""" + + def value_from_datadict(self, *args, **kwargs): + value = super(CITextInput, self).value_from_datadict(*args, **kwargs) + if value: + return value.lower() diff --git a/spirit/topic/notification/managers.py b/spirit/topic/notification/managers.py index 4b7c09012..f86a55389 100644 --- a/spirit/topic/notification/managers.py +++ b/spirit/topic/notification/managers.py @@ -9,26 +9,32 @@ class TopicNotificationQuerySet(models.QuerySet): def unremoved(self): - return self.filter(Q(topic__category__parent=None) | Q(topic__category__parent__is_removed=False), - topic__category__is_removed=False, - topic__is_removed=False) + return self.filter( + Q(topic__category__parent=None) | + Q(topic__category__parent__is_removed=False), + topic__category__is_removed=False, + topic__is_removed=False) def unread(self): return self.filter(is_read=False) def _access(self, user): - return self.filter(Q(topic__category__is_private=False) | Q(topic__topics_private__user=user), - user=user) + return self.filter( + Q(topic__category__is_private=False) | + Q(topic__topics_private__user=user), + user=user) def for_access(self, user): - return self.unremoved()\ - ._access(user=user)\ - .exclude(action=0) # Undefined action + return ( + self.unremoved() + ._access(user=user) + .exclude(action=0)) # Undefined action def read(self, user): # returns updated rows count (int) - return self.filter(user=user)\ - .update(is_read=True) + return ( + self.filter(user=user) + .update(is_read=True)) def with_related_data(self): return self.select_related('comment__user__st', 'topic') diff --git a/spirit/topic/notification/templates/spirit/topic/notification/_render_list.html b/spirit/topic/notification/templates/spirit/topic/notification/_render_list.html index b9d73e4cc..db0ebd941 100644 --- a/spirit/topic/notification/templates/spirit/topic/notification/_render_list.html +++ b/spirit/topic/notification/templates/spirit/topic/notification/_render_list.html @@ -6,11 +6,11 @@ {% url "spirit:comment:find" pk=n.comment.pk as url_topic %} {% if n.is_comment %} - {% blocktrans trimmed with username=n.comment.user.username topic_title=n.topic.title %} + {% blocktrans trimmed with username=n.comment.user.st.nickname topic_title=n.topic.title %} {{ username }} has commented on {{ topic_title }} {% endblocktrans %} {% elif n.is_mention %} - {% blocktrans trimmed with username=n.comment.user.username topic_title=n.topic.title %} + {% blocktrans trimmed with username=n.comment.user.st.nickname topic_title=n.topic.title %} {{ username }} has mention you on {{ topic_title }} {% endblocktrans %} {% else %} diff --git a/spirit/topic/notification/views.py b/spirit/topic/notification/views.py index bead7cf96..49efbf9db 100644 --- a/spirit/topic/notification/views.py +++ b/spirit/topic/notification/views.py @@ -65,7 +65,7 @@ def index_ajax(request): notifications = [ { - 'user': escape(n.comment.user.username), + 'user': escape(n.comment.user.st.nickname), 'action': n.action, 'title': escape(n.topic.title), 'url': n.get_absolute_url(), diff --git a/spirit/topic/private/forms.py b/spirit/topic/private/forms.py index 0719827ac..ded613656 100644 --- a/spirit/topic/private/forms.py +++ b/spirit/topic/private/forms.py @@ -9,7 +9,10 @@ from ...core.conf import settings from ...core import utils -from ...core.utils.widgets import MultipleInput +from ...core.utils.widgets import ( + MultipleInput, + CIMultipleInput, + CITextInput) from ...topic.models import Topic from ...category.models import Category from .models import TopicPrivate @@ -61,19 +64,27 @@ def save(self, commit=True): return super(TopicForPrivateForm, self).save(commit) +def cx_multiple_input(*args, **kwargs): + if settings.ST_CASE_INSENSITIVE_USERNAMES: + return CIMultipleInput(*args, **kwargs) + return MultipleInput(*args, **kwargs) + + class TopicPrivateManyForm(forms.Form): # Only good for create users = forms.ModelMultipleChoiceField( label=_("Invite users"), queryset=User.objects.all(), - to_field_name=User.USERNAME_FIELD, - widget=MultipleInput(attrs={'placeholder': _("user1, user2, ...")})) + to_field_name=User.USERNAME_FIELD) def __init__(self, user=None, topic=None, *args, **kwargs): super(TopicPrivateManyForm, self).__init__(*args, **kwargs) self.user = user self.topic = topic + # Make it dynamic for testing + self.fields['users'].widget = cx_multiple_input( + attrs={'placeholder': _("user1, user2, ...")}) def clean_users(self): users = set(self.cleaned_data['users']) @@ -91,21 +102,31 @@ def get_users(self): def save_m2m(self): users = self.cleaned_data['users'] # Since the topic was just created this should not raise an exception - return TopicPrivate.objects.bulk_create([TopicPrivate(user=user, topic=self.topic) - for user in users]) + return TopicPrivate.objects.bulk_create( + [TopicPrivate(user=user, topic=self.topic) + for user in users]) + + +def cx_text_input(*args, **kwargs): + if settings.ST_CASE_INSENSITIVE_USERNAMES: + return CITextInput(*args, **kwargs) + return forms.TextInput(*args, **kwargs) class TopicPrivateInviteForm(forms.ModelForm): # Only good for create - user = forms.ModelChoiceField(queryset=User.objects.all(), - to_field_name=User.USERNAME_FIELD, - widget=forms.TextInput(attrs={'placeholder': _("username"), }), - label=_("Invite user")) + user = forms.ModelChoiceField( + queryset=User.objects.all(), + to_field_name=User.USERNAME_FIELD, + label=_("Invite user")) def __init__(self, topic=None, *args, **kwargs): super(TopicPrivateInviteForm, self).__init__(*args, **kwargs) self.topic = topic + # Make it dynamic for testing + self.fields['user'].widget = cx_text_input( + attrs={'placeholder': _("username")}) class Meta: model = TopicPrivate @@ -114,13 +135,14 @@ class Meta: def clean_user(self): user = self.cleaned_data['user'] - private = TopicPrivate.objects.filter(user=user, - topic=self.topic) + private = TopicPrivate.objects.filter( + user=user, topic=self.topic) if private.exists(): # Do this since some of the unique_together fields are excluded. - raise forms.ValidationError(_("%(username)s is already a participant") % - {'username': getattr(user, user.USERNAME_FIELD), }) + raise forms.ValidationError( + _("%(username)s is already a participant") % + {'username': user.st.nickname}) return user @@ -148,13 +170,14 @@ class Meta: def clean(self): cleaned_data = super(TopicPrivateJoinForm, self).clean() - private = TopicPrivate.objects.filter(user=self.user, - topic=self.topic) + private = TopicPrivate.objects.filter( + user=self.user, topic=self.topic) if private.exists(): # Do this since some of the unique_together fields are excluded. - raise forms.ValidationError(_("%(username)s is already a participant") % - {'username': getattr(self.user, self.user.USERNAME_FIELD), }) + raise forms.ValidationError( + _("%(username)s is already a participant") % + {'username': self.user.st.nickname}) return cleaned_data diff --git a/spirit/topic/private/templates/spirit/topic/private/delete.html b/spirit/topic/private/templates/spirit/topic/private/delete.html index e9893b898..9cdc447c7 100644 --- a/spirit/topic/private/templates/spirit/topic/private/delete.html +++ b/spirit/topic/private/templates/spirit/topic/private/delete.html @@ -7,7 +7,7 @@ {% block content %}

- {% blocktrans trimmed with username=topic_private.user.username topic_title=topic_private.topic.title %} + {% blocktrans trimmed with username=topic_private.user.st.nickname topic_title=topic_private.topic.title %} Removing {{ username }}'s from {{ topic_title }} {% endblocktrans %}

diff --git a/spirit/topic/private/templates/spirit/topic/private/detail.html b/spirit/topic/private/templates/spirit/topic/private/detail.html index ad5fbb89c..655de8b53 100644 --- a/spirit/topic/private/templates/spirit/topic/private/detail.html +++ b/spirit/topic/private/templates/spirit/topic/private/detail.html @@ -24,7 +24,7 @@

{{ topic.title }}

{% spaceless %} {% for tp in topic.topics_private.all %}