Ratings: implement replies by maintainers #181

Merged
Oleg-Komarov merged 7 commits from rating-reply into main 2024-06-11 12:35:03 +02:00
11 changed files with 143 additions and 11 deletions
Showing only changes of commit 422065f8de - Show all commits

View File

@ -302,6 +302,7 @@
</div> </div>
</div> </div>
{% with ratings_listed_count=extension.ratings.listed.count %}
<div class="row"> <div class="row">
<div class="col-md-8"> <div class="col-md-8">
{% if my_rating and not my_rating.is_listed %} {% if my_rating and not my_rating.is_listed %}
@ -311,19 +312,20 @@
{% include "ratings/components/rating.html" with classes="mb-2" %} {% include "ratings/components/rating.html" with classes="mb-2" %}
{% endfor %} {% endfor %}
{% if not extension.ratings.listed.count and not my_rating %} {% if not ratings_listed_count and not my_rating %}
<a href="{{ extension.get_rate_url }}">{% trans "Be the first to review." %}</a> <a href="{{ extension.get_rate_url }}">{% trans "Be the first to review." %}</a>
{% endif %} {% endif %}
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
{# Rating #} {# Rating #}
{% if extension.ratings.listed.count %} {% if ratings_listed_count %}
<section> <section>
{% include "ratings/components/summary.html" %} {% include "ratings/components/summary.html" %}
</section> </section>
{% endif %} {% endif %}
</div> </div>
</div> </div>
{% endwith %}
</section> </section>
{% endblock extension_reviews %} {% endblock extension_reviews %}

View File

@ -42,7 +42,10 @@ class ListedExtensionMixin:
"""Fetch a publicly listed extension by slug in the URL before dispatching the view.""" """Fetch a publicly listed extension by slug in the URL before dispatching the view."""
def dispatch(self, *args, **kwargs): def dispatch(self, *args, **kwargs):
self.extension = get_object_or_404(Extension.objects.listed, slug=self.kwargs['slug']) self.extension = get_object_or_404(
Extension.objects.listed.prefetch_related('authors', 'ratings'),
slug=self.kwargs['slug'],
)
return super().dispatch(*args, **kwargs) return super().dispatch(*args, **kwargs)

View File

@ -231,6 +231,8 @@ class ExtensionDetailView(DetailView):
'preview_set', 'preview_set',
'preview_set__file', 'preview_set__file',
'ratings', 'ratings',
'ratings__ratingreply',
'ratings__ratingreply__user',
'ratings__user', 'ratings__user',
'team', 'team',
'versions', 'versions',
@ -255,6 +257,7 @@ class ExtensionDetailView(DetailView):
context['my_rating'] = ratings.models.Rating.get_for( context['my_rating'] = ratings.models.Rating.get_for(
self.request.user.pk, self.object.pk self.request.user.pk, self.object.pk
) )
context['is_maintainer'] = self.object.has_maintainer(self.request.user)
extension = context['object'] extension = context['object']
# Add the image for "og:image" meta to the context # Add the image for "og:image" meta to the context
if extension.featured_image and extension.featured_image.is_listed: if extension.featured_image and extension.featured_image.is_listed:

View File

@ -147,6 +147,16 @@ class Rating(CreatedModifiedMixin, TrackChangesMixin, models.Model):
], ],
) )
def get_reply_url(self):
return reverse(
'ratings:reply',
args=[
self.extension.type_slug,
self.extension.slug,
self.id,
],
)
class RatingFlag(CreatedModifiedMixin, models.Model): class RatingFlag(CreatedModifiedMixin, models.Model):
SPAM = 'review_flag_reason_spam' SPAM = 'review_flag_reason_spam'

View File

@ -43,8 +43,29 @@
</a> </a>
{% endif %} {% endif %}
{% if is_maintainer and not rating.ratingreply %}
<a href="{{ rating.get_reply_url }}" title="Reply">
<i class="i-reply"></i>
</a>
{% endif %}
</ul> </ul>
</header> </header>
<div>{{ rating.text|markdown }}</div> <div>{{ rating.text|markdown }}</div>
</div> </div>
{% if rating.ratingreply %}
<div>
<header>
<ul>
<li class="align-items-center me-auto">
<a href="{% url "extensions:by-author" user_id=rating.ratingreply.user.pk %}">{{ rating.ratingreply.user }}</a>
replied
</li>
<li>
{{ rating.ratingreply.date_created|naturaltime_compact }}
</li>
</ul>
</header>
{{ rating.ratingreply.text|markdown }}
</div>
{% endif %}
</article> </article>

View File

@ -2,7 +2,8 @@
{% has_maintainer extension as is_maintainer %} {% has_maintainer extension as is_maintainer %}
<div class="card p-3 mb-3 mt-2 mt-lg-0 ratings-summary"> <div class="card p-3 mb-3 mt-2 mt-lg-0 ratings-summary">
{% if extension.text_ratings_count %} {% with text_ratings_count=extension.text_ratings_count %}
{% if text_ratings_count %}
<div class="summary-container"> <div class="summary-container">
<div class="summary-value"> <div class="summary-value">
<h3> <h3>
@ -13,7 +14,7 @@
{% include "ratings/components/average.html" with score=extension.average_score %} {% include "ratings/components/average.html" with score=extension.average_score %}
<a class="text-muted d-block" href="{{ extension.get_ratings_url }}"> <a class="text-muted d-block" href="{{ extension.get_ratings_url }}">
<small>{{ extension.text_ratings_count }} review{{ extension.text_ratings_count | pluralize }}</small> <small>{{ text_ratings_count }} review{{ text_ratings_count | pluralize }}</small>
</a> </a>
</div> </div>
<div class="summary-bars"> <div class="summary-bars">
@ -36,6 +37,7 @@
<span class="text-muted">Be the first to review.</span> <span class="text-muted">Be the first to review.</span>
{% else %} {% else %}
{% endif %} {% endif %}
{% endwith %}
<div class="mt-3"> <div class="mt-3">
{% if not is_maintainer and not my_rating %} {% if not is_maintainer and not my_rating %}

View File

@ -1,6 +1,5 @@
{% extends "extensions/base.html" %} {% extends "extensions/base.html" %}
{% load i18n %} {% load extensions humanize i18n %}
{% load humanize %}
{% block page_title %}{{ extension.name }}{% endblock page_title %} {% block page_title %}{{ extension.name }}{% endblock page_title %}
@ -8,9 +7,11 @@
{% with latest=extension.latest_version author=extension.latest_version.file.user %} {% with latest=extension.latest_version author=extension.latest_version.file.user %}
<div class="d-flex mt-4"> <div class="d-flex mt-4">
<h2> <h2>
{% if extension.text_ratings_count and not score %} {% with text_ratings_count=extension.text_ratings_count %}
{{ extension.text_ratings_count }} {% endif %}Reviews{% if score %} with {{ score|apnumber }} stars {% if text_ratings_count and not score %}
{{ text_ratings_count }} {% endif %}Reviews{% if score %} with {{ score|apnumber }} stars
{% endif %} {% endif %}
{% endwith %}
</h2> </h2>
{% if score %} {% if score %}
<div class="ms-auto"> <div class="ms-auto">

View File

@ -0,0 +1,36 @@
{% extends "extensions/base.html" %}
{% load i18n common %}
{% block page_title %}Reply to rating{% endblock page_title %}
{% block content %}
<h2>Reply to review</h2>
<div class="row">
<div class="col-md-8">
{% include "ratings/components/rating.html" with classes="mb-2" %}
</div>
</div>
<div class="container rating-form">
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{% with form=form|add_form_classes %}
<div class="row">
<div class="col-md-8">
<div class="box p-3">
{% include "common/components/field.html" with field=form.text focus=True placeholder="Enter the text here..." %}
{% if form.non_field_errors %}
<div class="invalid-feedback">
{{ form.non_field_errors }}
</div>
{% endif %}
<button type="submit" class="btn btn-block btn-primary mt-4">
<i class="i-send"></i>
<span>{% trans 'Submit Reply' %}</span>
</button>
</div>
</div>
</div>
{% endwith %}
</form>
</div>
{% endblock content %}

View File

@ -11,6 +11,11 @@ urlpatterns = [
[ [
path('<slug:slug>/reviews/', views.RatingsView.as_view(), name='for-extension'), path('<slug:slug>/reviews/', views.RatingsView.as_view(), name='for-extension'),
path('<slug:slug>/reviews/new/', views.AddRatingView.as_view(), name='new'), path('<slug:slug>/reviews/new/', views.AddRatingView.as_view(), name='new'),
path(
'<slug:slug>/reviews/<int:pk>/reply/',
views.AddRatingReplyView.as_view(),
name='reply',
),
] ]
), ),
) )

View File

@ -5,7 +5,7 @@ from django.views.generic.edit import CreateView
from django.views.generic.list import ListView from django.views.generic.list import ListView
from ratings.forms import AddRatingForm from ratings.forms import AddRatingForm
from ratings.models import Rating from ratings.models import Rating, RatingReply
import extensions.views.mixins import extensions.views.mixins
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -31,7 +31,18 @@ class RatingsView(extensions.views.mixins.ListedExtensionMixin, ListView):
def get_queryset(self): def get_queryset(self):
self._set_score_filter() self._set_score_filter()
queryset = super().get_queryset().filter(extension_id=self.extension.pk) queryset = (
super()
.get_queryset()
.filter(extension_id=self.extension.pk)
.select_related(
'extension',
'ratingreply',
'ratingreply__user',
'user',
'version',
)
)
if self.score: if self.score:
queryset = queryset.filter(score=self.score) queryset = queryset.filter(score=self.score)
return queryset.distinct() return queryset.distinct()
@ -39,6 +50,7 @@ class RatingsView(extensions.views.mixins.ListedExtensionMixin, ListView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['extension'] = self.extension context['extension'] = self.extension
context['is_maintainer'] = self.extension.has_maintainer(self.request.user)
context['score'] = self.score context['score'] = self.score
if self.request.user.is_authenticated: if self.request.user.is_authenticated:
context['my_rating'] = Rating.get_for(self.request.user.pk, self.extension.pk) context['my_rating'] = Rating.get_for(self.request.user.pk, self.extension.pk)
@ -77,3 +89,39 @@ class AddRatingView(
def get_success_url(self) -> str: def get_success_url(self) -> str:
return self.extension.get_ratings_url() return self.extension.get_ratings_url()
class AddRatingReplyView(
extensions.views.mixins.ListedExtensionMixin,
UserPassesTestMixin,
LoginRequiredMixin,
CreateView,
):
model = RatingReply
fields = ('text',)
def get_rating(self):
return Rating.objects.filter(pk=self.kwargs['pk'], extension=self.extension).first()
def test_func(self) -> bool:
# Check that user is replying to a rating for their exension and this is the first reply
rating = self.get_rating()
return (
rating
and not hasattr(rating, 'ratingreply')
and self.extension.has_maintainer(self.request.user)
)
def form_valid(self, form):
form.instance.rating = self.get_rating()
form.instance.user = self.request.user
return super().form_valid(form)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['extension'] = self.extension
context['rating'] = self.get_rating()
return context
def get_success_url(self) -> str:
return self.extension.get_ratings_url()

View File

@ -106,6 +106,7 @@ class ExtensionsApprovalDetailView(DetailView):
filtered_activity_types = {ApprovalActivity.ActivityType.COMMENT} filtered_activity_types = {ApprovalActivity.ActivityType.COMMENT}
user = self.request.user user = self.request.user
if self.object.has_maintainer(user): if self.object.has_maintainer(user):
ctx['is_maintainer'] = self.object.has_maintainer(self.request.user)
filtered_activity_types.add(ApprovalActivity.ActivityType.AWAITING_REVIEW) filtered_activity_types.add(ApprovalActivity.ActivityType.AWAITING_REVIEW)
if user.is_moderator or user.is_superuser: if user.is_moderator or user.is_superuser:
filtered_activity_types.update( filtered_activity_types.update(