From f27fd7678d522468507a5378333a5b4767dfb9d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Lente?= Date: Thu, 2 May 2024 13:06:50 +0200 Subject: [PATCH 01/33] Refactor: Wrap js theme into an IIFE to avoid global namespace pollution --- common/static/common/scripts/app.js | 172 ++++++++++++++-------------- 1 file changed, 87 insertions(+), 85 deletions(-) diff --git a/common/static/common/scripts/app.js b/common/static/common/scripts/app.js index c8449332..09a3c3e8 100644 --- a/common/static/common/scripts/app.js +++ b/common/static/common/scripts/app.js @@ -1,99 +1,101 @@ -// Create function btnBack -function btnBack() { - const btnBack = document.querySelectorAll('.js-btn-back'); +(function() { + // Create function btnBack + function btnBack() { + const btnBack = document.querySelectorAll('.js-btn-back'); - btnBack.forEach(function(item) { - item.addEventListener('click', function(e) { - e.preventDefault(); - window.history.back(); + btnBack.forEach(function(item) { + item.addEventListener('click', function(e) { + e.preventDefault(); + window.history.back(); + }); }); - }); -} - -// Create finction commentForm -function commentForm() { - const commentForm = document.querySelector('.js-comment-form'); - if (!commentForm) { - return; } - const commentFormSelect = commentForm.querySelector('select'); - if (!commentFormSelect) { - return; - } - - // Create event comment form select change - commentFormSelect.addEventListener('change', function(e) { - let value = e.target.value; - let verb = 'Comment'; - const activitySubmitButton = document.getElementById('activity-submit'); - activitySubmitButton.classList.remove('btn-success', 'btn-warning'); - - // Hide or show comment form msg on change - if (value == 'AWC') { - verb = 'Set as Awaiting Changes'; - activitySubmitButton.classList.add('btn-warning'); - } else if (value == 'AWR') { - verb = 'Set as Awaiting Review'; - } else if (value == 'APR') { - verb = 'Approve!'; - activitySubmitButton.classList.add('btn-success'); - } - - activitySubmitButton.querySelector('span').textContent = verb; - }); -} - -// Create function copyInstallUrl -function copyInstallUrl() { - function init() { - // Create variables - const btnInstall = document.querySelector('.js-btn-install'); - const btnInstallAction = document.querySelector('.js-btn-install-action'); - const btnInstallGroup = document.querySelector('.js-btn-install-group'); - const btnInstallDrag = document.querySelector('.js-btn-install-drag'); - const btnInstallDragGroup = document.querySelector('.js-btn-install-drag-group'); - - if (btnInstall == null) { + // Create finction commentForm + function commentForm() { + const commentForm = document.querySelector('.js-comment-form'); + if (!commentForm) { return; } - // Get data install URL - const btnInstallUrl = btnInstall.getAttribute('data-install-url'); + const commentFormSelect = commentForm.querySelector('select'); + if (!commentFormSelect) { + return; + } - btnInstall.addEventListener('click', function() { - // Hide btnInstallGroup - btnInstallGroup.classList.add('d-none'); + // Create event comment form select change + commentFormSelect.addEventListener('change', function(e) { + let value = e.target.value; + let verb = 'Comment'; + const activitySubmitButton = document.getElementById('activity-submit'); + activitySubmitButton.classList.remove('btn-success', 'btn-warning'); - // Show btnInstallAction - btnInstallAction.classList.add('show'); - }); + // Hide or show comment form msg on change + if (value == 'AWC') { + verb = 'Set as Awaiting Changes'; + activitySubmitButton.classList.add('btn-warning'); + } else if (value == 'AWR') { + verb = 'Set as Awaiting Review'; + } else if (value == 'APR') { + verb = 'Approve!'; + activitySubmitButton.classList.add('btn-success'); + } - // Drag btnInstallUrl - btnInstallDrag.addEventListener('dragstart', function(e) { - // Set data install URL to be transferred during drag - e.dataTransfer.setData('text/plain', btnInstallUrl); - - // Set drag area active - btnInstallDragGroup.classList.add('opacity-50'); - }); - - // Undrag btnInstallUrl - btnInstallDrag.addEventListener('dragend', function() { - // Set drag area inactive - btnInstallDragGroup.classList.remove('opacity-50'); + activitySubmitButton.querySelector('span').textContent = verb; }); } - init(); -} -// Create function init -function init() { - btnBack(); - commentForm(); - copyInstallUrl(); -} + // Create function copyInstallUrl + function copyInstallUrl() { + function init() { + // Create variables + const btnInstall = document.querySelector('.js-btn-install'); + const btnInstallAction = document.querySelector('.js-btn-install-action'); + const btnInstallGroup = document.querySelector('.js-btn-install-group'); + const btnInstallDrag = document.querySelector('.js-btn-install-drag'); + const btnInstallDragGroup = document.querySelector('.js-btn-install-drag-group'); -document.addEventListener('DOMContentLoaded', function() { - init(); -}); + if (btnInstall == null) { + return; + } + + // Get data install URL + const btnInstallUrl = btnInstall.getAttribute('data-install-url'); + + btnInstall.addEventListener('click', function() { + // Hide btnInstallGroup + btnInstallGroup.classList.add('d-none'); + + // Show btnInstallAction + btnInstallAction.classList.add('show'); + }); + + // Drag btnInstallUrl + btnInstallDrag.addEventListener('dragstart', function(e) { + // Set data install URL to be transferred during drag + e.dataTransfer.setData('text/plain', btnInstallUrl); + + // Set drag area active + btnInstallDragGroup.classList.add('opacity-50'); + }); + + // Undrag btnInstallUrl + btnInstallDrag.addEventListener('dragend', function() { + // Set drag area inactive + btnInstallDragGroup.classList.remove('opacity-50'); + }); + } + + init(); + } + // Create function init + function init() { + btnBack(); + commentForm(); + copyInstallUrl(); + } + + document.addEventListener('DOMContentLoaded', function() { + init(); + }); +}()) -- 2.30.2 From 4b274407c5fba0a3c5162a70b73ce9bf2799f682 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Lente?= Date: Thu, 2 May 2024 14:33:51 +0200 Subject: [PATCH 02/33] UI: Add js app function agreeWithTerms Add JavaScript app function agreeWith terms to enable button submit if agree with terms button is checked. --- common/static/common/scripts/app.js | 17 +++++++++++++++++ common/static/common/styles/_button.sass | 4 ++++ common/static/common/styles/main.sass | 1 + extensions/templates/extensions/submit.html | 2 +- 4 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 common/static/common/styles/_button.sass diff --git a/common/static/common/scripts/app.js b/common/static/common/scripts/app.js index 09a3c3e8..0738a13f 100644 --- a/common/static/common/scripts/app.js +++ b/common/static/common/scripts/app.js @@ -1,4 +1,20 @@ (function() { + // Create function agreeWithTerms + function agreeWithTerms() { + const agreeWithTermsInput = document.querySelector('#id_agreed_with_terms'); + + agreeWithTermsInput.addEventListener('change', function(e) { + const agreeWithTermsBtnSubmit = document.querySelector('.js-agree-with-terms-btn-submit'); + + // Check if checkbox is checked + if (e.target.checked == true) { + agreeWithTermsBtnSubmit.removeAttribute('disabled'); + } else { + agreeWithTermsBtnSubmit.setAttribute('disabled', true); + } + }); + } + // Create function btnBack function btnBack() { const btnBack = document.querySelectorAll('.js-btn-back'); @@ -90,6 +106,7 @@ } // Create function init function init() { + agreeWithTerms(); btnBack(); commentForm(); copyInstallUrl(); diff --git a/common/static/common/styles/_button.sass b/common/static/common/styles/_button.sass new file mode 100644 index 00000000..f26799f7 --- /dev/null +++ b/common/static/common/styles/_button.sass @@ -0,0 +1,4 @@ +button, +.btn + &[type=submit] + transition: opacity var(--transition-speed) diff --git a/common/static/common/styles/main.sass b/common/static/common/styles/main.sass index 03bf4a51..e7ab28dc 100644 --- a/common/static/common/styles/main.sass +++ b/common/static/common/styles/main.sass @@ -18,6 +18,7 @@ $container-width: map-get($container-max-widths, 'xl') @import '_alert.sass' @import '_badge.sass' @import '_box.sass' +@import '_button.sass' @import '_cards.sass' @import '_code.sass' @import '_comments.sass' diff --git a/extensions/templates/extensions/submit.html b/extensions/templates/extensions/submit.html index ef34e00d..763dcb38 100644 --- a/extensions/templates/extensions/submit.html +++ b/extensions/templates/extensions/submit.html @@ -70,7 +70,7 @@
- - - {% endif %} - -{% if notification_list %} - {% for notification in notification_list %} -
-
+

{% trans 'Notifications' %}

- {{ notification.action.timestamp | naturaltime_compact }} - - {{ notification.action.actor }} - - - {{ notification.action.verb }} - - {{ notification.action.target }} - - - - {% if not notification.read_at %} -
- {% csrf_token %} - -
+ {% if notification_list %} +
+ {% if user|unread_notification_count %} +
+ +
{% endif %} - +
+ + + {% for notification in notification_list %} + + + + + + {% endfor %} + +
+ {{ notification.action.timestamp | naturaltime_compact }} + + {{ notification.action.actor }} {{ notification.action.verb }} {{ notification.action.target }} + + +
- {% endfor %} -{% else %} -

- {% trans 'You have no notifications' %} -

-{% endif %} + {% else %} +

+ {% trans 'You have no notifications' %} +

+ {% endif %} {% endblock content %} -- 2.30.2 From 1c80c59176550398403b2a3d47081a6d23726516 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Lente?= Date: Fri, 3 May 2024 10:33:28 +0200 Subject: [PATCH 05/33] Chore: Update git submodule assets_shared --- assets_shared | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets_shared b/assets_shared index 9b684395..66c63dc8 160000 --- a/assets_shared +++ b/assets_shared @@ -1 +1 @@ -Subproject commit 9b6843953bd147ff7b93ee8bbab7c1df0f16fdc3 +Subproject commit 66c63dc8c67f7aae12bcfc2bc57d494edde5a35c -- 2.30.2 From 566368a4216018cdb3c8cbf8f7dea3d87dd23b03 Mon Sep 17 00:00:00 2001 From: Anna Sirota Date: Thu, 2 May 2024 14:04:22 +0200 Subject: [PATCH 06/33] Basic email template for notifications (#96) Based on the proposal https://projects.blender.org/infrastructure/extensions-website/issues/93 Email subjects change to the following: ``` Add-on approved: "Genuinely Disgusting" New comment on Add-on "Genuinely Disgusting" Add-on rated: "Genuinely Disgusting" Add-on reported: "Genuinely Disgusting" Add-on rating reported: "Genuinely Disgusting" Add-on changes requested: "Genuinely Disgusting" Add-on review requested: "Genuinely Disgusting" ``` Notification emails can also be viewed in the admin `Email previews` section (with fake objects constructed and passed into email template contexts). Indicating why the email was sent is not in this change because we currently don't store why **precisely** (can only guess it's either because of `moderators` group or extension follow, the state of either can change after the notification was generated). This can be part of the `Notification` records however, after that the reason can be included in the email. Reviewed-on: https://projects.blender.org/infrastructure/extensions-website/pulls/96 Reviewed-by: Oleg-Komarov --- common/tests/factories/notifications.py | 66 ++++++++ common/tests/factories/reviewers.py | 8 + common/tests/factories/users.py | 4 +- emails/admin.py | 35 ++-- emails/templates/emails/base.html | 109 ++++++++++-- emails/templates/emails/base.txt | 6 - .../emails/components/new_activity_action | 21 +++ emails/templates/emails/email.html | 2 +- emails/templates/emails/email_base.html | 95 ----------- emails/templates/emails/new_activity.html | 24 +++ .../templates/emails/new_activity_subject.txt | 21 +++ emails/util.py | 40 ++--- .../commands/send_notification_emails.py | 15 +- notifications/models.py | 51 +++++- notifications/tests/test_follow_logic.py | 156 +++++++++++++++++- utils.py | 49 ++++++ 16 files changed, 527 insertions(+), 175 deletions(-) create mode 100644 common/tests/factories/reviewers.py create mode 100644 emails/templates/emails/components/new_activity_action delete mode 100644 emails/templates/emails/email_base.html create mode 100644 emails/templates/emails/new_activity.html create mode 100644 emails/templates/emails/new_activity_subject.txt diff --git a/common/tests/factories/notifications.py b/common/tests/factories/notifications.py index 9d6e75dc..bebd2a25 100644 --- a/common/tests/factories/notifications.py +++ b/common/tests/factories/notifications.py @@ -1,8 +1,19 @@ from django.contrib.contenttypes.models import ContentType from factory.django import DjangoModelFactory +from faker import Faker +import actstream.models import factory +from common.tests.factories.extensions import ExtensionFactory, RatingFactory +from common.tests.factories.reviewers import ApprovalActivityFactory +from common.tests.factories.users import UserFactory +from constants.activity import Verb +import notifications.models +import reviewers.models + + RELATION_ALLOWED_MODELS = [] +fake = Faker() def generic_foreign_key_id_for_type_factory(generic_relation_type_field): @@ -17,3 +28,58 @@ def generic_foreign_key_id_for_type_factory(generic_relation_type_field): class ContentTypeFactory(DjangoModelFactory): class Meta: model = ContentType + + +class ActionFactory(DjangoModelFactory): + class Meta: + model = actstream.models.Action + + +class NotificationFactory(DjangoModelFactory): + class Meta: + model = notifications.models.Notification + + action = factory.SubFactory(ActionFactory) + + +def construct_fake_notifications() -> list['NotificationFactory']: + """Construct notifications of known types without persisting them in the DB.""" + fake_extension = ExtensionFactory.build(slug='test') + verb_to_action_object = { + Verb.APPROVED: ApprovalActivityFactory.build( + extension=fake_extension, + type=reviewers.models.ApprovalActivity.ActivityType.APPROVED, + message=fake.paragraph(nb_sentences=1), + ), + Verb.COMMENTED: ApprovalActivityFactory.build( + extension=fake_extension, + type=reviewers.models.ApprovalActivity.ActivityType.COMMENT, + message=fake.paragraph(nb_sentences=1), + ), + Verb.RATED_EXTENSION: RatingFactory.build( + text=fake.paragraph(nb_sentences=2), + ), + Verb.REPORTED_EXTENSION: None, # TODO: fake action_object + Verb.REPORTED_RATING: None, # TODO: fake action_object + Verb.REQUESTED_CHANGES: ApprovalActivityFactory.build( + extension=fake_extension, + type=reviewers.models.ApprovalActivity.ActivityType.AWAITING_CHANGES, + message=fake.paragraph(nb_sentences=1), + ), + Verb.REQUESTED_REVIEW: ApprovalActivityFactory.build( + extension=fake_extension, + type=reviewers.models.ApprovalActivity.ActivityType.AWAITING_REVIEW, + message=fake.paragraph(nb_sentences=1), + ), + } + fake_notifications = [ + NotificationFactory.build( + recipient=UserFactory.build(), + action__actor=UserFactory.build(), + action__target=fake_extension, + action__verb=verb, + action__action_object=action_object, + ) + for verb, action_object in verb_to_action_object.items() + ] + return fake_notifications diff --git a/common/tests/factories/reviewers.py b/common/tests/factories/reviewers.py new file mode 100644 index 00000000..422ec877 --- /dev/null +++ b/common/tests/factories/reviewers.py @@ -0,0 +1,8 @@ +from factory.django import DjangoModelFactory + +from reviewers.models import ApprovalActivity + + +class ApprovalActivityFactory(DjangoModelFactory): + class Meta: + model = ApprovalActivity diff --git a/common/tests/factories/users.py b/common/tests/factories/users.py index 5925e1c4..0dcc241f 100644 --- a/common/tests/factories/users.py +++ b/common/tests/factories/users.py @@ -45,8 +45,8 @@ class UserFactory(DjangoModelFactory): oauth_info = factory.RelatedFactory(OAuthUserInfoFactory, factory_related_name='user') -def create_moderator(): - user = UserFactory() +def create_moderator(**kwargs): + user = UserFactory(**kwargs) moderators = Group.objects.get(name='moderators') user.groups.add(moderators) return user diff --git a/emails/admin.py b/emails/admin.py index f47d67fd..04773609 100644 --- a/emails/admin.py +++ b/emails/admin.py @@ -9,7 +9,8 @@ from django.utils.safestring import mark_safe import django.core.mail from emails.models import Email -from emails.util import construct_email, get_template_context +from emails.util import construct_email +from common.tests.factories.notifications import construct_fake_notifications logger = logging.getLogger(__name__) User = get_user_model() @@ -92,16 +93,15 @@ class EmailPreviewAdmin(NoAddDeleteMixin, EmailAdmin): def _get_email_sent_message(self, obj): return f'Sent a test email "{obj.subject}" to {obj.to} from {obj.from_email}' - def get_object(self, request, object_id, from_field=None): - """Construct the Email on th fly from known subscription email templates.""" - context = { - 'user': User(), - **get_template_context(), - } - mail_name = object_id + def get_object(self, request, object_id, from_field=None, fake_context=None): + """Construct the Email on the fly from known email templates.""" + if not fake_context: + fake_context = self._get_emails_with_fake_context(request) + context = fake_context[object_id] + mail_name = context.get('template', object_id) email_body_html, email_body_txt, subject = construct_email(mail_name, context) return EmailPreview( - id=mail_name, + id=object_id, subject=subject, from_email=settings.DEFAULT_FROM_EMAIL, reply_to=settings.DEFAULT_REPLY_TO_EMAIL, @@ -109,10 +109,23 @@ class EmailPreviewAdmin(NoAddDeleteMixin, EmailAdmin): message=email_body_txt, ) + def _get_emails_with_fake_context(self, request): + email_with_fake_context = {'feedback': {}} + + fake_notifications = construct_fake_notifications() + for fake_notification in fake_notifications: + mail_name = fake_notification.original_template_name + email_with_fake_context[mail_name] = { + 'template': fake_notification.template_name, + **fake_notification.get_template_context(), + } + return email_with_fake_context + def _get_emails_list(self, request): emails = [] - for mail_name in ('feedback',): - emails.append(self.get_object(request, object_id=mail_name)) + fake_context = self._get_emails_with_fake_context(request) + for mail_name in fake_context: + emails.append(self.get_object(request, object_id=mail_name, fake_context=fake_context)) return emails def _changeform_view(self, request, object_id, form_url, extra_context): diff --git a/emails/templates/emails/base.html b/emails/templates/emails/base.html index 70a1e5f9..f672b4b0 100644 --- a/emails/templates/emails/base.html +++ b/emails/templates/emails/base.html @@ -1,16 +1,97 @@ -{% extends "emails/email_base.html" %} +{% spaceless %} + + + + + +
-{% block header_logo %} - {# have a title instead of the logo with the remote image #} -
{{ subject }}
-{% endblock header_logo %} +
+
+ {% block header_logo %} + {# have a title instead of the logo with the remote image #} +
{{ subject }}
+ {% endblock %} +
+
+
+
-{% block body %} -

Dear {% firstof user.full_name user.email %},

- {% block content %}{% endblock content %} -

- --
- Kind regards,
- Blender Extensions Team -

-{% endblock body %} + {% block body %}{% block content %}{% endblock content %}{% endblock body %} + +
+ +
+ + +{% endspaceless %} diff --git a/emails/templates/emails/base.txt b/emails/templates/emails/base.txt index d72945ad..839e285f 100644 --- a/emails/templates/emails/base.txt +++ b/emails/templates/emails/base.txt @@ -1,9 +1,3 @@ -Dear {% firstof user.full_name user.email %}, {% block content %}{% endblock content %} Manage your profile: {{ profile_url }} - --- -Kind regards, - -Blender Extensions Team diff --git a/emails/templates/emails/components/new_activity_action b/emails/templates/emails/components/new_activity_action new file mode 100644 index 00000000..fd0263e1 --- /dev/null +++ b/emails/templates/emails/components/new_activity_action @@ -0,0 +1,21 @@ +{% spaceless %}{% load i18n %} + {% with target_type=target.get_type_display what=target|safe someone=action.actor verb=action.verb %} + {% if verb == Verb.APPROVED %} + {% blocktrans %}{{ someone }} {{ verb }} {{ what }}{% endblocktrans %} + {% elif action.verb == Verb.COMMENTED %} + {% blocktrans %}{{ someone }} {{ verb }} on {{ what }}{% endblocktrans %} + {% elif verb == Verb.RATED_EXTENSION %} + {% blocktrans %}{{ someone }} {{ verb }} {{ what }}{% endblocktrans %} + {% elif verb == Verb.REPORTED_EXTENSION %} + {% blocktrans %}{{ someone }} reported {{ what }}{% endblocktrans %} + {% elif verb == Verb.REPORTED_RATING %} + {% blocktrans %}{{ someone }} {{ verb }} of {{ what }}{% endblocktrans %} + {% elif verb == Verb.REQUESTED_CHANGES %} + {% blocktrans %}{{ someone }} {{ verb }} on {{ what }}{% endblocktrans %} + {% elif verb == Verb.REQUESTED_REVIEW %} + {% blocktrans %}{{ someone }} {{ verb }} of {{ what }}{% endblocktrans %} + {% else %} + {% blocktrans %}{{ someone }} {{ verb }} {{ what }}{% endblocktrans %} + {% endif %} + {% endwith %} +{% endspaceless %} diff --git a/emails/templates/emails/email.html b/emails/templates/emails/email.html index c61f8a0d..1706718a 100644 --- a/emails/templates/emails/email.html +++ b/emails/templates/emails/email.html @@ -1,4 +1,4 @@ -{% extends "emails/email_base.html" %} +{% extends "emails/base.html" %} {% block body %} {{ email.html_message|safe }} diff --git a/emails/templates/emails/email_base.html b/emails/templates/emails/email_base.html deleted file mode 100644 index 7e8d33de..00000000 --- a/emails/templates/emails/email_base.html +++ /dev/null @@ -1,95 +0,0 @@ - - - - - -
- -
-
- {% block header_logo %} - - - {% endblock %} -
-
-
-
- - {% block body %}{% endblock body %} - -
- -
- - diff --git a/emails/templates/emails/new_activity.html b/emails/templates/emails/new_activity.html new file mode 100644 index 00000000..0c5b3dea --- /dev/null +++ b/emails/templates/emails/new_activity.html @@ -0,0 +1,24 @@ +{% extends "emails/base.html" %} +{% block content %}{% spaceless %} +
+ {% include "emails/components/new_activity_action" %}{% if quoted_message %}:{% endif %} + {% if quoted_message %} +

+ {{ quoted_message|truncatewords:15|truncatechars:140 }} +

+ {% endif %} +
+

View it at Blender Extensions

+

+ Read all notifications at {{ notifications_url }} +

+ {# TODO: store follow flags on Notifications, otherwise it's impossible to tell why this email is sent #} + {% comment %} + You are receiving this email because you are a moderator subscribed to notification emails. + You are receiving this email because you are subscribed to notifications on this extension. + {% endcomment %} +{% endspaceless %}{% endblock content %} + +{% block footer_links %}{% spaceless %} + Unsubscribe by adjusting your preferences at {{ profile_url }} +{% endspaceless %}{% endblock footer_links %} diff --git a/emails/templates/emails/new_activity_subject.txt b/emails/templates/emails/new_activity_subject.txt new file mode 100644 index 00000000..667a20a0 --- /dev/null +++ b/emails/templates/emails/new_activity_subject.txt @@ -0,0 +1,21 @@ +{% spaceless %}{% load i18n %} +{% with target_type=target.get_type_display what=target|safe name=target.name someone=action.actor verb=action.verb %} +{% if verb == Verb.APPROVED %} + {% blocktrans %}{{ target_type }} approved: "{{ name }}"{% endblocktrans %} +{% elif verb == Verb.COMMENTED %} + {% blocktrans %}New comment on {{ what }}{% endblocktrans %} +{% elif verb == Verb.RATED_EXTENSION %} + {% blocktrans %}{{ target_type }} rated: "{{ name }}"{% endblocktrans %} +{% elif verb == Verb.REPORTED_EXTENSION %} + {% blocktrans %}{{ target_type }} reported: "{{ name }}"{% endblocktrans %} +{% elif verb == Verb.REPORTED_RATING %} + {% blocktrans %}{{ target_type }} rating reported: "{{ name }}"{% endblocktrans %} +{% elif verb == Verb.REQUESTED_CHANGES %} + {% blocktrans %}{{ target_type }} changes requested: "{{ name }}"{% endblocktrans %} +{% elif verb == Verb.REQUESTED_REVIEW %} + {% blocktrans %}{{ target_type }} review requested: "{{ name }}"{% endblocktrans %} +{% else %} + {% blocktrans %}{{ someone }} {{ verb }} on {{ what }}{% endblocktrans %} +{% endif %} +{% endwith %} +{% endspaceless %} diff --git a/emails/util.py b/emails/util.py index 16d991c2..b1e4ed7b 100644 --- a/emails/util.py +++ b/emails/util.py @@ -1,35 +1,17 @@ """Utilities for rendering email templates.""" -from typing import List, Optional, Tuple, Dict, Any +from typing import List, Tuple, Dict, Any import logging from django.conf import settings -from django.contrib.sites.shortcuts import get_current_site -from django.template import loader from django.core.mail import get_connection, EmailMultiAlternatives -from django.urls import reverse +from django.template import loader, TemplateDoesNotExist + +from utils import absolute_url, html_to_text logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) -def _get_site_url(): - domain = get_current_site(None).domain - return f'https://{domain}' - - -def absolute_url( - view_name: str, args: Optional[tuple] = None, kwargs: Optional[dict] = None -) -> str: - """Same as django.urls.reverse() but then as absolute URL. - - For simplicity this assumes HTTPS is used. - """ - from urllib.parse import urljoin - - relative_url = reverse(view_name, args=args, kwargs=kwargs) - return urljoin(_get_site_url(), relative_url) - - def is_noreply(email: str) -> bool: """Return True if the email address is a no-reply address.""" return email.startswith('noreply@') or email.startswith('no-reply@') @@ -38,8 +20,9 @@ def is_noreply(email: str) -> bool: def get_template_context() -> Dict[str, str]: """Return additional context for use in an email template.""" return { - 'site_url': _get_site_url(), - # 'profile_url': absolute_url('profile_update'), + 'site_url': absolute_url('extensions:home'), + 'profile_url': absolute_url('users:my-profile'), + 'notifications_url': absolute_url('notifications:notifications'), 'DEFAULT_REPLY_TO_EMAIL': settings.DEFAULT_REPLY_TO_EMAIL, } @@ -47,8 +30,11 @@ def get_template_context() -> Dict[str, str]: def construct_email(email_name: str, context: Dict[str, Any]) -> Tuple[str, str, str]: """Construct an email message. + If plain text template is not found, text version will be generated from the HTML of the email. + :return: tuple (html, text, subject) """ + context.update(**get_template_context()) base_path = 'emails' subj_tmpl, html_tmpl, txt_tmpl = ( f'{base_path}/{email_name}_subject.txt', @@ -60,7 +46,11 @@ def construct_email(email_name: str, context: Dict[str, Any]) -> Tuple[str, str, context['subject'] = subject.strip() email_body_html = loader.render_to_string(html_tmpl, context) - email_body_txt = loader.render_to_string(txt_tmpl, context) + try: + email_body_txt = loader.render_to_string(txt_tmpl, context) + except TemplateDoesNotExist: + # Generate plain text content from the HTML one + email_body_txt = html_to_text(email_body_html) return email_body_html, email_body_txt, context['subject'] diff --git a/notifications/management/commands/send_notification_emails.py b/notifications/management/commands/send_notification_emails.py index 971a9898..e7f5e7a9 100644 --- a/notifications/management/commands/send_notification_emails.py +++ b/notifications/management/commands/send_notification_emails.py @@ -1,11 +1,10 @@ """Send user notifications as emails, at most once delivery.""" import logging -from django.conf import settings -from django.core.mail import send_mail from django.core.management.base import BaseCommand from django.utils import timezone +from emails.util import construct_and_send_email from notifications.models import Notification logger = logging.getLogger(__name__) @@ -36,12 +35,6 @@ class Command(BaseCommand): def send_notification_email(notification): - # TODO construct a proper phrase, depending on the verb, - # possibly share a template with NotificationsView - subject, message = notification.format_email() - send_mail( - subject, - message, - settings.DEFAULT_FROM_EMAIL, - [notification.recipient.email], - ) + template_name = notification.template_name + context = notification.get_template_context() + construct_and_send_email(template_name, context, recipient_list=[notification.recipient.email]) diff --git a/notifications/models.py b/notifications/models.py index 124d45b4..a0ca69b6 100644 --- a/notifications/models.py +++ b/notifications/models.py @@ -1,11 +1,15 @@ +import logging + from actstream.models import Action from django.contrib.auth import get_user_model from django.db import models +from django.template import loader, TemplateDoesNotExist from constants.activity import Verb from utils import absolutify User = get_user_model() +logger = logging.getLogger(__name__) class Notification(models.Model): @@ -30,12 +34,49 @@ class Notification(models.Model): ] unique_together = ['recipient', 'action'] - def format_email(self): + @property + def original_template_name(self) -> str: + """Template name constructed from action type, target and so on. + + If we want to override email template for this specific notification, + this is the name of the template that should be created. + """ action = self.action - subject = f'New Activity: {action.actor} {action.verb} {action.target}' - url = self.get_absolute_url() - mesage = f'{action.actor} {action.verb} {action.target}: {url}' - return (subject, mesage) + action_object = action.action_object + target_name = action.target.__class__.__name__.lower() + action_object_name = action_object.__class__.__name__.lower() if action_object else '' + return f"new_activity_{action_object_name}_{target_name}_{action.verb.replace(' ', '_')}" + + @property + def template_name(self) -> str: + """Template name to be used for constructing notification email.""" + default_name = 'new_activity' + name = self.original_template_name + args = {'name': name, 'default_name': default_name} + + try: + loader.get_template(f'emails/{name}.html') + except TemplateDoesNotExist: + logger.warning('Template %(name)s does not exist, using %(default_name)s', args) + return default_name + return name + + def get_template_context(self): + """Return template context to be used in the notification email.""" + action = self.action + action_object = action.action_object + quoted_message = getattr(action_object, 'message', getattr(action_object, 'text', '')) + context = { + 'Verb': Verb, + 'action': action, + 'action_object': action_object, + 'notification': self, + 'quoted_message': quoted_message, + 'target': action.target, + 'url': self.get_absolute_url(), + 'user': self.recipient, + } + return context def get_absolute_url(self): if self.action.verb == Verb.RATED_EXTENSION: diff --git a/notifications/tests/test_follow_logic.py b/notifications/tests/test_follow_logic.py index ca381dcb..0bab4142 100644 --- a/notifications/tests/test_follow_logic.py +++ b/notifications/tests/test_follow_logic.py @@ -1,7 +1,10 @@ from pathlib import Path +from django.core import mail +from django.core.management import call_command from django.test import TestCase from django.urls import reverse +from django.utils import timezone from common.tests.factories.extensions import create_approved_version, create_version from common.tests.factories.files import FileFactory @@ -17,7 +20,9 @@ class TestTasks(TestCase): fixtures = ['dev', 'licenses'] def test_ratings(self): - extension = create_approved_version(ratings=[]).extension + extension = create_approved_version( + ratings=[], file__user__confirmed_email_at=timezone.now() + ).extension author = extension.authors.first() notification_nr = Notification.objects.filter(recipient=author).count() some_user = UserFactory() @@ -29,14 +34,35 @@ class TestTasks(TestCase): new_notification_nr = Notification.objects.filter(recipient=author).count() self.assertEqual(new_notification_nr, notification_nr + 1) + # Call the command that sends notification emails + call_command('send_notification_emails') + + # Test that one message has been sent. + self.assertEqual(len(mail.outbox), 1) + + email = mail.outbox[0] + self.assertEqual(email.subject, f'Add-on rated: "{extension.name}"') + self.assertEqual(email.to, [author.email]) + email_text = email.body + self.maxDiff = None + expected_text = expected_rated_text.format(**locals()) + self.assertEqual(email_text, expected_text) + + # Check that most important action and relevant URL are in the HTML version + email_html = email.alternatives[0][0] + self.assertIn(' rated ', email_html) + self.assertIn( + f'https://extensions.local:8111/add-ons/{extension.slug}/reviews/', email_html + ) + def test_abuse(self): extension = create_approved_version(ratings=[]).extension - moderator = create_moderator() + moderator = create_moderator(confirmed_email_at=timezone.now()) notification_nr = Notification.objects.filter(recipient=moderator).count() some_user = UserFactory() self.client.force_login(some_user) url = extension.get_report_url() - self.client.post( + response = self.client.post( url, { 'message': 'test message', @@ -44,11 +70,31 @@ class TestTasks(TestCase): 'version': '', }, ) + self.assertEqual(response.status_code, 302) + report_url = response['Location'] new_notification_nr = Notification.objects.filter(recipient=moderator).count() self.assertEqual(new_notification_nr, notification_nr + 1) + call_command('send_notification_emails') + + # Test that one message has been sent. + self.assertEqual(len(mail.outbox), 1) + + email = mail.outbox[0] + self.assertEqual(email.subject, f'Add-on reported: "{extension.name}"') + self.assertEqual(email.to, [moderator.email]) + email_text = email.body + self.maxDiff = None + expected_text = expected_abuse_report_text.format(**locals()) + self.assertEqual(email_text, expected_text) + + # Check that most important action and relevant URL are in the HTML version + email_html = email.alternatives[0][0] + self.assertIn(report_url, email_html) + self.assertIn(' reported ', email_html) + def test_new_extension_submitted(self): - moderator = create_moderator() + moderator = create_moderator(confirmed_email_at=timezone.now()) notification_nr = Notification.objects.filter(recipient=moderator).count() some_user = UserFactory() file_data = { @@ -117,10 +163,29 @@ class TestTasks(TestCase): new_notification_nr = Notification.objects.filter(recipient=moderator).count() self.assertEqual(new_notification_nr, notification_nr + 1) + call_command('send_notification_emails') + + # Test that one message has been sent. + self.assertEqual(len(mail.outbox), 1) + + email = mail.outbox[0] + extension = file.version.extension + self.assertEqual(email.subject, 'Add-on review requested: "Edit Breakdown"') + self.assertEqual(email.to, [moderator.email]) + email_text = email.body + self.maxDiff = None + expected_text = expected_review_requested_text.format(**locals()) + self.assertEqual(email_text, expected_text) + + # Check that most important action and relevant URL are in the HTML version + email_html = email.alternatives[0][0] + self.assertIn(' requested review of ', email_html) + self.assertIn(f'https://extensions.local:8111/approval-queue/{extension.slug}/', email_html) + def test_approval_queue_activity(self): extension = create_approved_version(ratings=[]).extension author = extension.authors.first() - moderator = create_moderator() + moderator = create_moderator(confirmed_email_at=timezone.now()) some_user = UserFactory() notification_nrs = {} for user in [author, moderator, some_user]: @@ -136,7 +201,88 @@ class TestTasks(TestCase): self.assertEqual(new_notification_nrs[moderator.pk], notification_nrs[moderator.pk] + 1) self.assertEqual(new_notification_nrs[some_user.pk], notification_nrs[some_user.pk] + 1) + call_command('send_notification_emails') + + # Test that one message has been sent (only moderator has email confirmed here). + self.assertEqual(len(mail.outbox), 1) + + email = mail.outbox[0] + self.assertEqual(email.subject, f'New comment on Add-on "{extension.name}"') + email_text = email.body + expected_text = expected_new_comment_text.format(**locals()) + self.maxDiff = None + self.assertEqual(email_text, expected_text) + + # Check that most important action and relevant URL are in the HTML version + email_html = email.alternatives[0][0] + self.assertIn(' commented on ', email_html) + self.assertIn(f'https://extensions.local:8111/approval-queue/{extension.slug}/', email_html) + def _leave_a_comment(self, user, extension, text): self.client.force_login(user) url = reverse('reviewers:approval-comment', args=[extension.slug]) self.client.post(url, {'type': ApprovalActivity.ActivityType.COMMENT, 'message': text}) + + # TODO: test for notifications about a reported rating + # TODO: test for notifications about extension approved by moderators + + +expected_abuse_report_text = """Add-on reported: "{extension.name}" +{some_user.full_name} reported Add-on "{extension.name}" +: + +“test message” + +https://extensions.local:8111{report_url} +Read all notifications at https://extensions.local:8111/notifications/ + + +Unsubscribe by adjusting your preferences at https://extensions.local:8111/settings/profile/ + +https://extensions.local:8111/ +""" + +expected_new_comment_text = """New comment on Add-on "{extension.name}" +{some_user.full_name} commented on Add-on "{extension.name}" +: + +“this is bad” + +https://extensions.local:8111/approval-queue/{extension.slug}/ +Read all notifications at https://extensions.local:8111/notifications/ + + +Unsubscribe by adjusting your preferences at https://extensions.local:8111/settings/profile/ + +https://extensions.local:8111/ +""" + +expected_rated_text = """Add-on rated: "{extension.name}" +{some_user.full_name} rated extension Add-on "{extension.name}" +: + +“rating text” + +https://extensions.local:8111/add-ons/{extension.slug}/reviews/ +Read all notifications at https://extensions.local:8111/notifications/ + + +Unsubscribe by adjusting your preferences at https://extensions.local:8111/settings/profile/ + +https://extensions.local:8111/ +""" + +expected_review_requested_text = """Add-on review requested: "Edit Breakdown" +{some_user.full_name} requested review of Add-on "Edit Breakdown" +: + +“Extension is ready for initial review” + +https://extensions.local:8111/approval-queue/edit-breakdown/ +Read all notifications at https://extensions.local:8111/notifications/ + + +Unsubscribe by adjusting your preferences at https://extensions.local:8111/settings/profile/ + +https://extensions.local:8111/ +""" diff --git a/utils.py b/utils.py index 64008585..e628f63c 100644 --- a/utils.py +++ b/utils.py @@ -1,3 +1,4 @@ +from html.parser import HTMLParser from typing import Optional from urllib.parse import urljoin import datetime @@ -20,6 +21,7 @@ from django.core.exceptions import ValidationError from django.core.validators import validate_ipv46_address from django.http import HttpRequest from django.http.response import HttpResponseRedirectBase +from django.urls import reverse from django.utils.encoding import force_bytes, force_str from django.utils.http import _urlparse import django.utils.text @@ -189,3 +191,50 @@ def absolutify(url: str, request=None) -> str: proto = 'http' if settings.DEBUG else 'https' domain = get_current_site(request).domain return urljoin(f'{proto}://{domain}', url) + + +def absolute_url( + view_name: str, args: Optional[tuple] = None, kwargs: Optional[dict] = None +) -> str: + """Same as django.urls.reverse() but returned as an absolute URL.""" + relative_url = reverse(view_name, args=args, kwargs=kwargs) + return absolutify(relative_url) + + +class HTMLFilter(HTMLParser): + skip_text_of = ('a', 'style') + text = '' + skip_tag_text = False + + def handle_starttag(self, tag, attrs): + if tag in self.skip_text_of: + self.skip_tag_text = True + for name, value in attrs: + if name == 'href': + self.skip_tag_text = True + self.text += value + if tag in ('quote', 'q'): + self.text += '“' + + def handle_endtag(self, tag): + if tag in self.skip_text_of: + self.skip_tag_text = False + if tag in ('quote', 'q'): + self.text += '”\n\n' + + def handle_data(self, data): + if self.skip_tag_text: + return + self.text += data + + +def html_to_text(data: str) -> str: + f = HTMLFilter() + f.feed(data) + lines = [_.lstrip(' \t') for _ in f.text.split('\n')] + skip_empty = 0 + for line in lines: + if not re.match(r'^\s*$', line): + break + skip_empty += 1 + return '\n'.join(lines[skip_empty:]) -- 2.30.2 From 6a720cd7002a908da87242dbfe9ab8b09c65aadd Mon Sep 17 00:00:00 2001 From: Anna Sirota Date: Thu, 2 May 2024 14:16:53 +0200 Subject: [PATCH 07/33] Deps: install faker and factory in prod too (used in the admin) --- requirements.txt | 2 ++ requirements_dev.txt | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index bb0497b9..2f4b7263 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,6 +25,8 @@ django-waffle==4.1.0 djangorestframework==3.14.0 drf-spectacular==0.27.1 drf-spectacular-sidecar==2024.2.1 +factory-boy==3.2.1 +Faker==13.15.1 frozenlist==1.3.0 geoip2==4.6.0 h11==0.13.0 diff --git a/requirements_dev.txt b/requirements_dev.txt index e589676c..81ae9645 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -3,8 +3,6 @@ cfgv==3.3.1 coverage==6.4.4 distlib==0.3.5 django-debug-toolbar==4.2.0 -factory-boy==3.2.1 -Faker==13.15.1 filelock==3.7.1 identify==2.5.2 mdgen==0.1.10 -- 2.30.2 From 5e26f4696961a9d31f3a216b45132d57380bf280 Mon Sep 17 00:00:00 2001 From: Anna Sirota Date: Thu, 2 May 2024 14:18:08 +0200 Subject: [PATCH 08/33] Revert "Deps: install faker and factory in prod too (used in the admin)" This reverts commit bddbcd3ad95e1c27421e2db3b3ee58aa9928fa03. --- requirements.txt | 2 -- requirements_dev.txt | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 2f4b7263..bb0497b9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,8 +25,6 @@ django-waffle==4.1.0 djangorestframework==3.14.0 drf-spectacular==0.27.1 drf-spectacular-sidecar==2024.2.1 -factory-boy==3.2.1 -Faker==13.15.1 frozenlist==1.3.0 geoip2==4.6.0 h11==0.13.0 diff --git a/requirements_dev.txt b/requirements_dev.txt index 81ae9645..e589676c 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -3,6 +3,8 @@ cfgv==3.3.1 coverage==6.4.4 distlib==0.3.5 django-debug-toolbar==4.2.0 +factory-boy==3.2.1 +Faker==13.15.1 filelock==3.7.1 identify==2.5.2 mdgen==0.1.10 -- 2.30.2 From 3d239ab4268d3564414b0fd24706c84d3670a668 Mon Sep 17 00:00:00 2001 From: Anna Sirota Date: Thu, 2 May 2024 14:20:22 +0200 Subject: [PATCH 09/33] Admin: only show notification email previews in DEBUG --- emails/admin.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/emails/admin.py b/emails/admin.py index 04773609..38251cc9 100644 --- a/emails/admin.py +++ b/emails/admin.py @@ -10,7 +10,6 @@ import django.core.mail from emails.models import Email from emails.util import construct_email -from common.tests.factories.notifications import construct_fake_notifications logger = logging.getLogger(__name__) User = get_user_model() @@ -112,13 +111,16 @@ class EmailPreviewAdmin(NoAddDeleteMixin, EmailAdmin): def _get_emails_with_fake_context(self, request): email_with_fake_context = {'feedback': {}} - fake_notifications = construct_fake_notifications() - for fake_notification in fake_notifications: - mail_name = fake_notification.original_template_name - email_with_fake_context[mail_name] = { - 'template': fake_notification.template_name, - **fake_notification.get_template_context(), - } + if settings.DEBUG: + from common.tests.factories.notifications import construct_fake_notifications + + fake_notifications = construct_fake_notifications() + for fake_notification in fake_notifications: + mail_name = fake_notification.original_template_name + email_with_fake_context[mail_name] = { + 'template': fake_notification.template_name, + **fake_notification.get_template_context(), + } return email_with_fake_context def _get_emails_list(self, request): -- 2.30.2 From 4c5c0bf0bce51082b7878ca34908a01ea8098ab3 Mon Sep 17 00:00:00 2001 From: Anna Sirota Date: Thu, 2 May 2024 18:24:05 +0200 Subject: [PATCH 10/33] Forms: remove cols,rows; add classes and placeholder in the template This should help with UI improvements listed in https://projects.blender.org/infrastructure/extensions-website/issues/105 --- common/templates/common/components/field.html | 5 +++- common/templatetags/common.py | 30 +++++++++++++++++++ .../templates/extensions/manage/update.html | 2 +- 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/common/templates/common/components/field.html b/common/templates/common/components/field.html index 1b3eb5fc..765c701d 100644 --- a/common/templates/common/components/field.html +++ b/common/templates/common/components/field.html @@ -1,5 +1,7 @@ +{% load common %} {% spaceless %} - {% with type=field.field.widget.input_type %} + {% with type=field.field.widget.input_type classes=classes|default:"" placeholder=placeholder|default:"" %} + {% with field=field|remove_cols_rows|add_classes:classes|set_placeholder:placeholder %} {% firstof label field.label as label %} {% comment %} Checkboxes {% endcomment %} @@ -43,4 +45,5 @@
{{ field.errors }}
{% endif %} {% endwith %} + {% endwith %} {% endspaceless %} diff --git a/common/templatetags/common.py b/common/templatetags/common.py index 86e708df..9482dfd6 100644 --- a/common/templatetags/common.py +++ b/common/templatetags/common.py @@ -2,6 +2,7 @@ from urllib.parse import urlparse import json import logging +from django.forms.boundfield import BoundField from django.template import Library, loader from django.template.defaultfilters import stringfilter from django.utils.safestring import mark_safe @@ -166,3 +167,32 @@ def replace(value, old_char_new_char): @register.filter(name='unread_notification_count') def unread_notification_count(user): return Notification.objects.filter(recipient=user, read_at__isnull=True).count() + + +@register.filter(name='add_classes') +def add_classes(bound_field: BoundField, classes: str): + """Add classes to the `class` attribute of the given form field's widget. + + Expects a string of space-separated classes. + """ + class_value = bound_field.field.widget.attrs.get('class', '') + bound_field.field.widget.attrs['class'] = class_value + f' {classes}' + return bound_field + + +@register.filter(name='set_placeholder') +def set_placeholder(bound_field: BoundField, placeholder: str): + """Set `placeholder` attribute of the given form field's widget.""" + bound_field.field.widget.attrs['placeholder'] = placeholder + return bound_field + + +@register.filter(name='remove_cols_rows') +def remove_cols_rows(bound_field: BoundField): + """Removes cols and rows attributes from the form field's widget. + + We'd rather go the CSS route when it comes to styling textareas. + """ + bound_field.field.widget.attrs.pop('cols', None) + bound_field.field.widget.attrs.pop('rows', None) + return bound_field diff --git a/extensions/templates/extensions/manage/update.html b/extensions/templates/extensions/manage/update.html index b29e1b12..b19bfd9b 100644 --- a/extensions/templates/extensions/manage/update.html +++ b/extensions/templates/extensions/manage/update.html @@ -30,7 +30,7 @@
- {% include "common/components/field.html" with field=form.description label="Description" %} + {% include "common/components/field.html" with field=form.description label="Description" classes="one two three" placeholder="Describe this extension" %}
-- 2.30.2 From 9fa54ff9ae51d4aad49baa3a5f914c12359aaa76 Mon Sep 17 00:00:00 2001 From: Oleg Komarov Date: Thu, 2 May 2024 19:25:22 +0200 Subject: [PATCH 11/33] Convert to draft (Closes #98) --- extensions/forms.py | 11 +++++++ .../templates/extensions/manage/update.html | 11 ++++++- extensions/tests/test_update.py | 29 ++++++++++++++++++- extensions/views/manage.py | 21 ++++++++++++-- 4 files changed, 68 insertions(+), 4 deletions(-) diff --git a/extensions/forms.py b/extensions/forms.py index 99bd56f1..4587737a 100644 --- a/extensions/forms.py +++ b/extensions/forms.py @@ -111,6 +111,17 @@ class ExtensionUpdateForm(forms.ModelForm): 'support', ) + def clean(self): + super().clean() + if ( + 'convert_to_draft' in self.data + and self.instance.status != self.instance.STATUSES.AWAITING_REVIEW + ): + self.add_error( + None, 'An extension can be converted to draft only while it is Awating Review' + ) + return self.cleaned_data + class ExtensionDeleteForm(forms.ModelForm): class Meta: diff --git a/extensions/templates/extensions/manage/update.html b/extensions/templates/extensions/manage/update.html index b19bfd9b..3bd23ed7 100644 --- a/extensions/templates/extensions/manage/update.html +++ b/extensions/templates/extensions/manage/update.html @@ -97,13 +97,22 @@
- + {% if extension.status == extension.STATUSES.AWAITING_REVIEW %} + + {% endif %} +
diff --git a/extensions/tests/test_update.py b/extensions/tests/test_update.py index 53192920..bf13c2a6 100644 --- a/extensions/tests/test_update.py +++ b/extensions/tests/test_update.py @@ -2,9 +2,11 @@ from pathlib import Path from django.test import TestCase -from common.tests.factories.extensions import create_approved_version +from common.tests.factories.extensions import create_approved_version, create_version from common.tests.utils import _get_all_form_errors +from extensions.models import Extension from files.models import File +from reviewers.models import ApprovalActivity TEST_FILES_DIR = Path(__file__).resolve().parent / 'files' POST_DATA = { @@ -280,3 +282,28 @@ class UpdateTest(TestCase): response.context['add_preview_formset'].forms[0].errors, {'source': ['Choose a JPEG, PNG or WebP image, or an MP4 video']}, ) + + def test_convert_to_draft(self): + version = create_version(extension__status=Extension.STATUSES.AWAITING_REVIEW) + extension = version.extension + url = extension.get_manage_url() + user = extension.authors.first() + self.client.force_login(user) + response = self.client.get(url) + self.assertContains(response, 'convert_to_draft') + response2 = self.client.post( + url, + { + **POST_DATA, + 'convert_to_draft': '', + }, + ) + self.assertEqual(response2.status_code, 302) + extension.refresh_from_db() + self.assertEqual(extension.status, extension.STATUSES.INCOMPLETE) + self.assertEqual( + extension.review_activity.last().type, ApprovalActivity.ActivityType.AWAITING_CHANGES + ) + response3 = self.client.get(url) + self.assertEqual(response3.status_code, 302) + self.assertEqual(response3['Location'], extension.get_draft_url()) diff --git a/extensions/views/manage.py b/extensions/views/manage.py index e9619d9b..bffb961d 100644 --- a/extensions/views/manage.py +++ b/extensions/views/manage.py @@ -3,7 +3,7 @@ from django import forms from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin from django.contrib.messages.views import SuccessMessageMixin from django.db import transaction -from django.shortcuts import get_object_or_404, reverse +from django.shortcuts import get_object_or_404, redirect, reverse from django.utils.translation import gettext_lazy as _ from django.views.generic import DetailView, ListView from django.views.generic.edit import CreateView, UpdateView, DeleteView, FormView @@ -109,6 +109,14 @@ class UpdateExtensionView( template_name = 'extensions/manage/update.html' form_class = ExtensionUpdateForm success_message = "Updated successfully" + msg_converted_to_draft = _('Converted to Draft') + + def get(self, request, *args, **kwargs): + extension = self.extension + if extension.status == extension.STATUSES.INCOMPLETE: + return redirect('extensions:draft', slug=extension.slug, type_slug=extension.type_slug) + else: + return super().get(request, *args, **kwargs) def get_success_url(self): self.object.refresh_from_db() @@ -147,6 +155,15 @@ class UpdateExtensionView( @transaction.atomic def form_valid(self, form): + if 'convert_to_draft' in self.request.POST: + form.instance.status = form.instance.STATUSES.INCOMPLETE + form.save() + ApprovalActivity( + user=self.request.user, + extension=form.instance, + type=ApprovalActivity.ActivityType.AWAITING_CHANGES, + message=self.msg_converted_to_draft, + ).save() edit_preview_formset = EditPreviewFormSet( self.request.POST, self.request.FILES, instance=self.object ) @@ -357,7 +374,7 @@ class DraftExtensionView( ): template_name = 'extensions/draft_finalise.html' form_class = VersionForm - msg_awaiting_review = _('Extension is ready for initial review') + msg_awaiting_review = _('Ready for review') @property def success_message(self) -> str: -- 2.30.2 From 9715c5a8b9e05eecfeebcd836af96274df4f1025 Mon Sep 17 00:00:00 2001 From: Oleg Komarov Date: Fri, 3 May 2024 12:04:59 +0200 Subject: [PATCH 12/33] Approval queue: sort by status (#89) --- reviewers/views.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/reviewers/views.py b/reviewers/views.py index 5e05d62a..404cfd71 100644 --- a/reviewers/views.py +++ b/reviewers/views.py @@ -1,3 +1,4 @@ +from collections import defaultdict import logging from django.contrib.auth.mixins import LoginRequiredMixin @@ -12,10 +13,11 @@ from reviewers.models import ApprovalActivity log = logging.getLogger(__name__) +# the ordering of this list determines the order of rows in approval queue listing STATUS_CHANGE_TYPES = [ - ApprovalActivity.ActivityType.APPROVED, - ApprovalActivity.ActivityType.AWAITING_CHANGES, ApprovalActivity.ActivityType.AWAITING_REVIEW, + ApprovalActivity.ActivityType.AWAITING_CHANGES, + ApprovalActivity.ActivityType.APPROVED, ] @@ -36,13 +38,13 @@ class ApprovalQueueView(ListView): .all() ) by_extension = {} - result = [] + by_date_created = [] for item in qs: extension = item.extension stats = by_extension.get(extension, None) if not stats: # this check guarantees that we add a record only once per extension, - # and iterating over qs we get result also ordered by item.date_created + # and iterating over qs we get by_date_created also ordered by item.date_created stats = { 'count': 0, 'extension': extension, @@ -50,12 +52,19 @@ class ApprovalQueueView(ListView): 'last_type_display': None, } by_extension[extension] = stats - result.append(stats) + by_date_created.append(stats) stats['count'] += 1 if not stats.get('last_activity', None): stats['last_activity'] = item if not stats.get('last_type_display', None) and item.type in STATUS_CHANGE_TYPES: - stats['last_type_display'] = item.get_type_display + stats['last_type_display'] = item.get_type_display() + stats['type'] = item.type + groupped_by_type = defaultdict(list) + for stats in by_date_created: + groupped_by_type[stats['type']].append(stats) + result = [] + for type in STATUS_CHANGE_TYPES: + result.extend(groupped_by_type[type]) return result template_name = 'reviewers/extensions_review_list.html' -- 2.30.2 From 84ba9c7c9f93a418dd49eae23391e2d74456d7db Mon Sep 17 00:00:00 2001 From: Oleg Komarov Date: Fri, 3 May 2024 12:31:09 +0200 Subject: [PATCH 13/33] Fix KeyError: 'type' for extensions without meaningful statuses in approval queue Normally this shouldn't happen, but due to an incomplete data migration #94 we currently have extensions that have only CMT items and miss the AWR one. --- reviewers/views.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/reviewers/views.py b/reviewers/views.py index 404cfd71..9270462f 100644 --- a/reviewers/views.py +++ b/reviewers/views.py @@ -50,6 +50,7 @@ class ApprovalQueueView(ListView): 'extension': extension, 'last_activity': None, 'last_type_display': None, + 'type': None, } by_extension[extension] = stats by_date_created.append(stats) @@ -61,7 +62,8 @@ class ApprovalQueueView(ListView): stats['type'] = item.type groupped_by_type = defaultdict(list) for stats in by_date_created: - groupped_by_type[stats['type']].append(stats) + type = stats['type'] or ApprovalActivity.ActivityType.AWAITING_REVIEW + groupped_by_type[type].append(stats) result = [] for type in STATUS_CHANGE_TYPES: result.extend(groupped_by_type[type]) -- 2.30.2 From 9cb16ac16bc207c4549b163421e834e25e60cd17 Mon Sep 17 00:00:00 2001 From: Oleg Komarov Date: Fri, 3 May 2024 13:00:03 +0200 Subject: [PATCH 14/33] Report rating: remove version field from the form --- abuse/forms.py | 8 +++++++- abuse/urls.py | 2 +- abuse/views.py | 8 ++++---- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/abuse/forms.py b/abuse/forms.py index ee955fb2..3e7a4d0c 100644 --- a/abuse/forms.py +++ b/abuse/forms.py @@ -10,7 +10,7 @@ import abuse.models logger = logging.getLogger(__name__) -class ReportForm(forms.ModelForm): +class ReportExtensionForm(forms.ModelForm): class Meta: model = abuse.models.AbuseReport fields = ('reason', 'version', 'message') @@ -35,3 +35,9 @@ class ReportForm(forms.ModelForm): return compare.version(self.cleaned_data[field]) except ValidationError as e: self.add_error(field, forms.ValidationError([e.message], code='invalid')) + + +class ReportRatingForm(forms.ModelForm): + class Meta: + model = abuse.models.AbuseReport + fields = ('reason', 'message') diff --git a/abuse/urls.py b/abuse/urls.py index 44a7b6d5..dd963d01 100644 --- a/abuse/urls.py +++ b/abuse/urls.py @@ -18,7 +18,7 @@ urlpatterns = [ ), path( '///report/', - views.ReportReviewView.as_view(), + views.ReportRatingView.as_view(), name='report-ratings', ), ], diff --git a/abuse/views.py b/abuse/views.py index d9fdd27a..d36d12ea 100644 --- a/abuse/views.py +++ b/abuse/views.py @@ -7,7 +7,7 @@ from django.views.generic.list import ListView from django.views.generic.edit import CreateView from django.shortcuts import get_object_or_404, redirect -from .forms import ReportForm +from .forms import ReportExtensionForm, ReportRatingForm from constants.base import ABUSE_TYPE_EXTENSION, ABUSE_TYPE_RATING from abuse.models import AbuseReport from ratings.models import Rating @@ -40,7 +40,7 @@ class ReportExtensionView( CreateView, ): model = AbuseReport - form_class = ReportForm + form_class = ReportExtensionForm def get(self, request, *args, **kwargs): extension = get_object_or_404(Extension.objects.listed, slug=self.kwargs['slug']) @@ -69,14 +69,14 @@ class ReportExtensionView( return self.object.get_absolute_url() -class ReportReviewView( +class ReportRatingView( LoginRequiredMixin, extensions.views.mixins.ListedExtensionMixin, UserPassesTestMixin, CreateView, ): model = AbuseReport - form_class = ReportForm + form_class = ReportRatingForm def test_func(self) -> bool: # TODO: best to redirect to existing report or show a friendly message -- 2.30.2 From 47fcc56480c8a29ff8c74fcc2bf1df93def59963 Mon Sep 17 00:00:00 2001 From: Oleg Komarov Date: Fri, 3 May 2024 14:38:32 +0200 Subject: [PATCH 15/33] Report compatibility issue: link to a prefilled comment (#97) --- .../extensions/components/blender_version.html | 1 + extensions/templates/extensions/version_list.html | 2 +- reviewers/views.py | 10 +++++++++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/extensions/templates/extensions/components/blender_version.html b/extensions/templates/extensions/components/blender_version.html index 71d874b3..c7830ffe 100644 --- a/extensions/templates/extensions/components/blender_version.html +++ b/extensions/templates/extensions/components/blender_version.html @@ -25,4 +25,5 @@ {% else %} {% trans 'and newer' %} {% endif %} + {% endif %} diff --git a/extensions/templates/extensions/version_list.html b/extensions/templates/extensions/version_list.html index ef2cad78..b9ac29da 100644 --- a/extensions/templates/extensions/version_list.html +++ b/extensions/templates/extensions/version_list.html @@ -55,7 +55,7 @@
-
Compatibility
+
{% trans 'Compatibility' %}
{% include "extensions/components/blender_version.html" with version=version %}
diff --git a/reviewers/views.py b/reviewers/views.py index 9270462f..efa2b8d4 100644 --- a/reviewers/views.py +++ b/reviewers/views.py @@ -82,7 +82,15 @@ class ExtensionsApprovalDetailView(DetailView): ctx['status_change_types'] = STATUS_CHANGE_TYPES if self.request.user.is_authenticated: - form = ctx['comment_form'] = CommentForm() + initial = {} + if 'report_compatibility_issue' in self.request.GET: + version = self.request.GET.get('version') + initial['message'] = ( + f'Compatibility range for version {version} is outdated\n' + 'Platform: ...\n' + 'Version of Blender where the add-on is **no longer** working: ?????' + ) + form = ctx['comment_form'] = CommentForm(initial=initial) # anyone can comment filtered_activity_types = {ApprovalActivity.ActivityType.COMMENT} user = self.request.user -- 2.30.2 From 10078a42af8dcdabffc837de160198473d459e62 Mon Sep 17 00:00:00 2001 From: Oleg Komarov Date: Fri, 3 May 2024 15:04:11 +0200 Subject: [PATCH 16/33] Add test for blender_version_max in manifest (#97) --- .../tests/files/edit_breakdown-0.1.0.zip | Bin 53959 -> 53969 bytes extensions/tests/test_submit.py | 5 +++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/extensions/tests/files/edit_breakdown-0.1.0.zip b/extensions/tests/files/edit_breakdown-0.1.0.zip index e8d5cb817a2674bbca274f46a212b15856138227..71850b3b2cfccbb53c1fc6e73c628c64dbc6a6fe 100644 GIT binary patch delta 348 zcmX@Ul=o_u@l#%g?dSs_&m0&?uoZ_s_lG?_!@% z&^`7|__0T0v_B93f^DI}S699GxBAzvRppVQwPiD^1nOY9X{QXGfmcrpW8J$x^2oo z;nk~s`(_%PDcotCK6}^ALu*eJ&$yB0`OoDSTYxtslgMVV)8_1G!4h@JMH5Sq1bDNu S0fUV}2nZLjGcZV>2Jrwjcb7u| delta 322 zcmcb(l==8l<_-5x@*Y|i5^-alDxBx{;wL5G z%~ikG-9NPURPdaelA6DqelZ4kGct)Viva!4z`&jmyjknCE<0MVq+D`g0R_qAHJ9Ys J1W$uh0sySolx6?` diff --git a/extensions/tests/test_submit.py b/extensions/tests/test_submit.py index 617b053a..92accc4f 100644 --- a/extensions/tests/test_submit.py +++ b/extensions/tests/test_submit.py @@ -23,11 +23,12 @@ EXPECTED_EXTENSION_DATA = { 'name': 'Edit Breakdown', 'version': '0.1.0', 'blender_version_min': '4.2.0', + 'blender_version_max': '4.3.0', 'type': 'add-on', 'schema_version': "1.0.0", }, - 'file_hash': 'sha256:4f3664940fc41641c7136a909270a024bbcfb2f8523a06a0d22f85c459b0b1ae', - 'size_bytes': 53959, + 'file_hash': 'sha256:28313858b9be34e6ecd15a63e28f626fb914dbdcc74c6d21c6536c9fad9de426', + 'size_bytes': 53969, 'tags': ['Sequencer'], 'version_str': '0.1.0', 'slug': 'edit-breakdown', -- 2.30.2 From def0943c441611ceb96033a8f55362911f0ad31a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Lente?= Date: Fri, 3 May 2024 18:20:42 +0200 Subject: [PATCH 17/33] UI: Add templates components form fields placeholders Add templates components form fields placeholders. Placeholder texts' are as specific as possible based on templates' architecture (so that shared fields have generic placeholder texts). --- extensions/templates/extensions/draft_finalise.html | 4 ++-- .../templates/extensions/manage/components/add_previews.html | 2 +- extensions/templates/extensions/manage/update.html | 4 ++-- ratings/templates/ratings/rating_form.html | 2 +- reviewers/templates/reviewers/extensions_review_detail.html | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/extensions/templates/extensions/draft_finalise.html b/extensions/templates/extensions/draft_finalise.html index b69f3d01..62a1c036 100644 --- a/extensions/templates/extensions/draft_finalise.html +++ b/extensions/templates/extensions/draft_finalise.html @@ -45,7 +45,7 @@ {# TODO: fix handling of tags #}
- {% include "common/components/field.html" %} + {% include "common/components/field.html" with placeholder="Enter the text here..." %}
{% endif %} @@ -55,7 +55,7 @@

{% trans 'Initial Version' %}

- {% include "common/components/field.html" with field=form.release_notes %} + {% include "common/components/field.html" with field=form.release_notes placeholder="Add the release notes..." %}
diff --git a/extensions/templates/extensions/manage/components/add_previews.html b/extensions/templates/extensions/manage/components/add_previews.html index bec7c145..02618c6d 100644 --- a/extensions/templates/extensions/manage/components/add_previews.html +++ b/extensions/templates/extensions/manage/components/add_previews.html @@ -17,7 +17,7 @@
- {% include "common/components/field.html" with field=inlineform.caption label='Caption' %} + {% include "common/components/field.html" with field=inlineform.caption label='Caption' placeholder="Describe the preview" %}
{% include "common/components/field.html" with field=inlineform.source label='File' %} diff --git a/extensions/templates/extensions/manage/update.html b/extensions/templates/extensions/manage/update.html index 3bd23ed7..ab45775b 100644 --- a/extensions/templates/extensions/manage/update.html +++ b/extensions/templates/extensions/manage/update.html @@ -30,11 +30,11 @@
- {% include "common/components/field.html" with field=form.description label="Description" classes="one two three" placeholder="Describe this extension" %} + {% include "common/components/field.html" with field=form.description label="Description" placeholder="Describe the extension..." %}
- {% include "common/components/field.html" with field=form.support %} + {% include "common/components/field.html" with field=form.support placeholder="https://example.com" %}
diff --git a/ratings/templates/ratings/rating_form.html b/ratings/templates/ratings/rating_form.html index 3358b2b3..b850db50 100644 --- a/ratings/templates/ratings/rating_form.html +++ b/ratings/templates/ratings/rating_form.html @@ -14,7 +14,7 @@
- {% include "common/components/field.html" with field=form.text focus=True %} + {% include "common/components/field.html" with field=form.text focus=True placeholder="Enter the text here..." %} {% if form.non_field_errors %}
diff --git a/reviewers/templates/reviewers/extensions_review_detail.html b/reviewers/templates/reviewers/extensions_review_detail.html index 5ebca8d1..bccabdbb 100644 --- a/reviewers/templates/reviewers/extensions_review_detail.html +++ b/reviewers/templates/reviewers/extensions_review_detail.html @@ -145,7 +145,7 @@ {% csrf_token %} {% with form=comment_form|add_form_classes %} - {% include "common/components/field.html" with field=form.message %} + {% include "common/components/field.html" with field=form.message placeholder="Enter the text here..." %}
-- 2.30.2 From 2a1b11c245c45f1b960a57e22b2413c14233deed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Lente?= Date: Fri, 3 May 2024 18:21:42 +0200 Subject: [PATCH 18/33] UI: Improve js app function agreeWithTerms selector specificity --- common/static/common/scripts/app.js | 2 +- extensions/templates/extensions/submit.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/common/static/common/scripts/app.js b/common/static/common/scripts/app.js index 0738a13f..d24be203 100644 --- a/common/static/common/scripts/app.js +++ b/common/static/common/scripts/app.js @@ -1,7 +1,7 @@ (function() { // Create function agreeWithTerms function agreeWithTerms() { - const agreeWithTermsInput = document.querySelector('#id_agreed_with_terms'); + const agreeWithTermsInput = document.querySelector('.js-agree-with-terms-input'); agreeWithTermsInput.addEventListener('change', function(e) { const agreeWithTermsBtnSubmit = document.querySelector('.js-agree-with-terms-btn-submit'); diff --git a/extensions/templates/extensions/submit.html b/extensions/templates/extensions/submit.html index 763dcb38..08522afe 100644 --- a/extensions/templates/extensions/submit.html +++ b/extensions/templates/extensions/submit.html @@ -66,7 +66,7 @@
- {% include "common/components/field.html" with field=form.agreed_with_terms %} + {% include "common/components/field.html" with field=form.agreed_with_terms classes="js-agree-with-terms-input" %}
-- 2.30.2 From f1a2692ec1a59eaa01ca353630bb3fa29efd050f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Lente?= Date: Fri, 3 May 2024 18:26:29 +0200 Subject: [PATCH 19/33] Fix: Fix js app funcion agreeWithTerms console errors on other pages --- common/static/common/scripts/app.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/common/static/common/scripts/app.js b/common/static/common/scripts/app.js index d24be203..72bb527c 100644 --- a/common/static/common/scripts/app.js +++ b/common/static/common/scripts/app.js @@ -3,6 +3,11 @@ function agreeWithTerms() { const agreeWithTermsInput = document.querySelector('.js-agree-with-terms-input'); + if (!agreeWithTermsInput) { + // Stop function execution if agreeWithTermsInput is not present + return; + } + agreeWithTermsInput.addEventListener('change', function(e) { const agreeWithTermsBtnSubmit = document.querySelector('.js-agree-with-terms-btn-submit'); -- 2.30.2 From 66a5950a4d7c2ae152ba63d1bfb6d7df672f1ca8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Lente?= Date: Fri, 3 May 2024 18:59:11 +0200 Subject: [PATCH 20/33] UI: Improve template components box and cards paddings consistency --- common/static/common/styles/_extension.sass | 7 ------- common/static/common/styles/_utilities.sass | 3 +++ extensions/templates/extensions/detail.html | 2 +- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/common/static/common/styles/_extension.sass b/common/static/common/styles/_extension.sass index a41d30d3..d4566b9e 100644 --- a/common/static/common/styles/_extension.sass +++ b/common/static/common/styles/_extension.sass @@ -80,13 +80,6 @@ .ext-detail-tagline +margin(2, bottom) -.ext-detail-description - +padding(4) - +style-rich-text - - pre - +margin(3, bottom) - .ext-detail-info dd color: var(--color-text) diff --git a/common/static/common/styles/_utilities.sass b/common/static/common/styles/_utilities.sass index 9762587c..b0da3e10 100644 --- a/common/static/common/styles/_utilities.sass +++ b/common/static/common/styles/_utilities.sass @@ -37,6 +37,9 @@ .show opacity: 1 +.style-rich-text + +style-rich-text + .text-accent color: var(--color-accent) diff --git a/extensions/templates/extensions/detail.html b/extensions/templates/extensions/detail.html index 977f356b..c2a64a01 100644 --- a/extensions/templates/extensions/detail.html +++ b/extensions/templates/extensions/detail.html @@ -21,7 +21,7 @@ {% block extension_description %} {% if extension.description %}
-
+
{{ extension.description|markdown }}
-- 2.30.2 From e0a900a745511add319eae6ae02c013ed5742846 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Lente?= Date: Fri, 3 May 2024 19:23:52 +0200 Subject: [PATCH 21/33] UI: Add template new_version_finalise field release notes placeholder --- extensions/templates/extensions/new_version_finalise.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/templates/extensions/new_version_finalise.html b/extensions/templates/extensions/new_version_finalise.html index bc16c1ae..13881e18 100644 --- a/extensions/templates/extensions/new_version_finalise.html +++ b/extensions/templates/extensions/new_version_finalise.html @@ -25,7 +25,7 @@
- {% include "common/components/field.html" with field=form.release_notes %} + {% include "common/components/field.html" with field=form.release_notes placeholder="Add the release notes..." %}
{% if form.non_field_errors or form.file.errors %} -- 2.30.2 From a443c098ad18ad14e8f687413d8a38f392409836 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Lente?= Date: Fri, 3 May 2024 19:30:11 +0200 Subject: [PATCH 22/33] UI: Improve template component blender_version input layout --- .../templates/extensions/components/blender_version.html | 6 +++--- .../extensions/components/extension_edit_detail_card.html | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/extensions/templates/extensions/components/blender_version.html b/extensions/templates/extensions/components/blender_version.html index c7830ffe..7e13ba03 100644 --- a/extensions/templates/extensions/components/blender_version.html +++ b/extensions/templates/extensions/components/blender_version.html @@ -4,10 +4,10 @@ href="https://www.blender.org/download/releases/{{ version.blender_version_min|version_without_patch|replace:".,-" }}/" title="{{ version.blender_version_min }}">Blender {{ version.blender_version_min|version_without_patch }} {% if is_editable %} - — - — + diff --git a/extensions/templates/extensions/components/extension_edit_detail_card.html b/extensions/templates/extensions/components/extension_edit_detail_card.html index c6d60f63..a29579f8 100644 --- a/extensions/templates/extensions/components/extension_edit_detail_card.html +++ b/extensions/templates/extensions/components/extension_edit_detail_card.html @@ -76,7 +76,9 @@
{% trans 'Compatibility' %}
-
{% include "extensions/components/blender_version.html" with version=version is_editable=is_editable form=form %}
+
+ {% include "extensions/components/blender_version.html" with version=version is_editable=is_editable form=form %} +
-- 2.30.2 From 14abe5d1bf32730e4d14424c3a2eb9ab8336009c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Lente?= Date: Mon, 6 May 2024 11:43:01 +0200 Subject: [PATCH 23/33] UI: Improve style form control file sizing --- common/static/common/styles/_forms.sass | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/common/static/common/styles/_forms.sass b/common/static/common/styles/_forms.sass index 0cf8ddc9..e12d3efe 100644 --- a/common/static/common/styles/_forms.sass +++ b/common/static/common/styles/_forms.sass @@ -11,7 +11,8 @@ .form-control &[type="file"] - height: calc(var(--spacer) * 2.5) + // TODO: @web-assets improve component style + height: calc(var(--spacer) * 3.5) /* Override Tagger's styling. */ .was-validated .form-control:invalid, -- 2.30.2 From 5bd05da3c964ba45f766ed01522edfb432b0272a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Lente?= Date: Mon, 6 May 2024 11:50:01 +0200 Subject: [PATCH 24/33] UI: Improve style forms invalid-feedback list indenting --- common/static/common/styles/_forms.sass | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/common/static/common/styles/_forms.sass b/common/static/common/styles/_forms.sass index e12d3efe..c0070d25 100644 --- a/common/static/common/styles/_forms.sass +++ b/common/static/common/styles/_forms.sass @@ -14,6 +14,10 @@ // TODO: @web-assets improve component style height: calc(var(--spacer) * 3.5) +.invalid-feedback + ul + +padding(3, left) + /* Override Tagger's styling. */ .was-validated .form-control:invalid, .form-control.is-invalid -- 2.30.2 From 0303de2f138c1f10e24b9cf323a2fb969e6ec5b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Lente?= Date: Mon, 6 May 2024 12:00:16 +0200 Subject: [PATCH 25/33] UI: Add style component notifications is-read overrides --- common/static/common/styles/_notifications.sass | 8 ++++++++ common/static/common/styles/main.sass | 1 + 2 files changed, 9 insertions(+) create mode 100644 common/static/common/styles/_notifications.sass diff --git a/common/static/common/styles/_notifications.sass b/common/static/common/styles/_notifications.sass new file mode 100644 index 00000000..eafa4750 --- /dev/null +++ b/common/static/common/styles/_notifications.sass @@ -0,0 +1,8 @@ +// TODO: remove style if 'mark as unread' is implemented +.notifications-item + &.is-read + .dropdown-toggle + color: var(--color-text-secondary) !important + + &:hover + cursor: default diff --git a/common/static/common/styles/main.sass b/common/static/common/styles/main.sass index e7ab28dc..98707cd0 100644 --- a/common/static/common/styles/main.sass +++ b/common/static/common/styles/main.sass @@ -29,6 +29,7 @@ $container-width: map-get($container-max-widths, 'xl') @import '_hero.sass' @import '_list.sass' @import '_navigation_global.sass' +@import '_notifications.sass' @import '_table.sass' @import 'ratings/static/ratings/styles/_review.sass' @import 'ratings/static/ratings/styles/_stars.sass' -- 2.30.2 From 67f98f3e6ed1d4767706ad35528474c7c8d08894 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Lente?= Date: Mon, 6 May 2024 14:17:05 +0200 Subject: [PATCH 26/33] UI: Add component notifications dropdown item View user --- common/static/common/styles/_extension.sass | 4 ++++ common/static/common/styles/_notifications.sass | 17 +++++++++++------ .../notifications/notification_list.html | 5 ++++- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/common/static/common/styles/_extension.sass b/common/static/common/styles/_extension.sass index d4566b9e..61506ffd 100644 --- a/common/static/common/styles/_extension.sass +++ b/common/static/common/styles/_extension.sass @@ -371,3 +371,7 @@ @extend .dropdown-divider +margin(0, top) + +.dropdown-item + &a + +padding(3, x) diff --git a/common/static/common/styles/_notifications.sass b/common/static/common/styles/_notifications.sass index eafa4750..2cf28acd 100644 --- a/common/static/common/styles/_notifications.sass +++ b/common/static/common/styles/_notifications.sass @@ -1,8 +1,13 @@ // TODO: remove style if 'mark as unread' is implemented -.notifications-item - &.is-read - .dropdown-toggle - color: var(--color-text-secondary) !important +// .notifications-item +// &.is-read +// .dropdown-toggle +// color: var(--color-text-secondary) !important +// +// &:hover +// cursor: default - &:hover - cursor: default +.notifications-item + .dropdown-item + padding-left: var(--spacer) !important + padding-right: var(--spacer) !important diff --git a/notifications/templates/notifications/notification_list.html b/notifications/templates/notifications/notification_list.html index 2e93b570..ab88f461 100644 --- a/notifications/templates/notifications/notification_list.html +++ b/notifications/templates/notifications/notification_list.html @@ -23,7 +23,7 @@ {{ notification.action.timestamp | naturaltime_compact }} - {{ notification.action.actor }} {{ notification.action.verb }} {{ notification.action.target }} + {{ notification.action.actor }} {{ notification.action.verb }} {{ notification.action.target }} -- 2.30.2 From 8859fb4af307f270647a48495dfe4ebc239c4916 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Lente?= Date: Mon, 6 May 2024 14:20:07 +0200 Subject: [PATCH 27/33] Chore: Add template notification_list todo back-end add notification detail link --- notifications/templates/notifications/notification_list.html | 1 + 1 file changed, 1 insertion(+) diff --git a/notifications/templates/notifications/notification_list.html b/notifications/templates/notifications/notification_list.html index ab88f461..a59fd7c5 100644 --- a/notifications/templates/notifications/notification_list.html +++ b/notifications/templates/notifications/notification_list.html @@ -23,6 +23,7 @@ {{ notification.action.timestamp | naturaltime_compact }} + {# TODO: @back-end add link to action target ID (so that link works as an anchor link) #} {{ notification.action.actor }} {{ notification.action.verb }} {{ notification.action.target }} -- 2.30.2 From e31955d146814683d3899c7899a11a83d1891f86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Lente?= Date: Mon, 6 May 2024 14:21:40 +0200 Subject: [PATCH 28/33] Fix: Fix add template notification_list form btn mar all as read csrf token --- notifications/templates/notifications/notification_list.html | 1 + 1 file changed, 1 insertion(+) diff --git a/notifications/templates/notifications/notification_list.html b/notifications/templates/notifications/notification_list.html index a59fd7c5..8a794a15 100644 --- a/notifications/templates/notifications/notification_list.html +++ b/notifications/templates/notifications/notification_list.html @@ -11,6 +11,7 @@
{% if user|unread_notification_count %}
+ {% csrf_token %}
{% endif %} -- 2.30.2 From 4b907e6c80c61ac9fa0b91d728099e410816f96f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Lente?= Date: Mon, 6 May 2024 14:30:37 +0200 Subject: [PATCH 29/33] Fix: Fix js app function commentFormSelect btn state changes --- common/static/common/scripts/app.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/common/static/common/scripts/app.js b/common/static/common/scripts/app.js index 72bb527c..fd09c59d 100644 --- a/common/static/common/scripts/app.js +++ b/common/static/common/scripts/app.js @@ -49,7 +49,7 @@ let value = e.target.value; let verb = 'Comment'; const activitySubmitButton = document.getElementById('activity-submit'); - activitySubmitButton.classList.remove('btn-success', 'btn-warning'); + activitySubmitButton.classList.remove('btn-primary', 'btn-success', 'btn-warning'); // Hide or show comment form msg on change if (value == 'AWC') { @@ -60,6 +60,8 @@ } else if (value == 'APR') { verb = 'Approve!'; activitySubmitButton.classList.add('btn-success'); + } else { + activitySubmitButton.classList.add('btn-primary'); } activitySubmitButton.querySelector('span').textContent = verb; -- 2.30.2 From 3d884a37f783c6de12c01ba9de7b6c475792210b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Lente?= Date: Mon, 6 May 2024 15:05:58 +0200 Subject: [PATCH 30/33] Chore: Update git submodule web-assets to v2.0.0-alpha.9 --- assets_shared | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets_shared b/assets_shared index 66c63dc8..17202dce 160000 --- a/assets_shared +++ b/assets_shared @@ -1 +1 @@ -Subproject commit 66c63dc8c67f7aae12bcfc2bc57d494edde5a35c +Subproject commit 17202dce8fa90182ccdb891fc23ca04efea782b3 -- 2.30.2 From 401cdac634b4a6f893319a81291133654e05fcf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Lente?= Date: Mon, 6 May 2024 15:52:29 +0200 Subject: [PATCH 31/33] Fix: Fix template notification_list link absolute url --- notifications/templates/notifications/notification_list.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notifications/templates/notifications/notification_list.html b/notifications/templates/notifications/notification_list.html index 8a794a15..73e2d460 100644 --- a/notifications/templates/notifications/notification_list.html +++ b/notifications/templates/notifications/notification_list.html @@ -25,7 +25,7 @@ {# TODO: @back-end add link to action target ID (so that link works as an anchor link) #} - {{ notification.action.actor }} {{ notification.action.verb }} {{ notification.action.target }} + {{ notification.action.actor }} {{ notification.action.verb }} {{ notification.action.target }} {% if form.non_field_errors %} -- 2.30.2