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>
{% with ratings_listed_count=extension.ratings.listed.count %}
<div class="row">
<div class="col-md-8">
{% if my_rating and not my_rating.is_listed %}
@ -311,19 +312,20 @@
{% include "ratings/components/rating.html" with classes="mb-2" %}
{% 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>
{% endif %}
</div>
<div class="col-md-4">
{# Rating #}
{% if extension.ratings.listed.count %}
{% if ratings_listed_count %}
<section>
{% include "ratings/components/summary.html" %}
</section>
{% endif %}
</div>
</div>
{% endwith %}
</section>
{% 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."""
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)

View File

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

View File

@ -43,8 +43,29 @@
</a>
{% endif %}
{% if is_maintainer and not rating.ratingreply %}
<a href="{{ rating.get_reply_url }}" title="Reply">
<i class="i-reply"></i>
</a>
{% endif %}
</ul>
</header>
<div>{{ rating.text|markdown }}</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>

View File

@ -2,7 +2,8 @@
{% has_maintainer extension as is_maintainer %}
<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-value">
<h3>
@ -13,7 +14,7 @@
{% include "ratings/components/average.html" with score=extension.average_score %}
<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>
</div>
<div class="summary-bars">
@ -36,6 +37,7 @@
<span class="text-muted">Be the first to review.</span>
{% else %}
{% endif %}
{% endwith %}
<div class="mt-3">
{% if not is_maintainer and not my_rating %}

View File

@ -1,6 +1,5 @@
{% extends "extensions/base.html" %}
{% load i18n %}
{% load humanize %}
{% load extensions humanize i18n %}
{% block page_title %}{{ extension.name }}{% endblock page_title %}
@ -8,9 +7,11 @@
{% with latest=extension.latest_version author=extension.latest_version.file.user %}
<div class="d-flex mt-4">
<h2>
{% if extension.text_ratings_count and not score %}
{{ extension.text_ratings_count }} {% endif %}Reviews{% if score %} with {{ score|apnumber }} stars
{% with text_ratings_count=extension.text_ratings_count %}
{% if text_ratings_count and not score %}
{{ text_ratings_count }} {% endif %}Reviews{% if score %} with {{ score|apnumber }} stars
{% endif %}
{% endwith %}
</h2>
{% if score %}
<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/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 ratings.forms import AddRatingForm
from ratings.models import Rating
from ratings.models import Rating, RatingReply
import extensions.views.mixins
log = logging.getLogger(__name__)
@ -31,7 +31,18 @@ class RatingsView(extensions.views.mixins.ListedExtensionMixin, ListView):
def get_queryset(self):
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:
queryset = queryset.filter(score=self.score)
return queryset.distinct()
@ -39,6 +50,7 @@ class RatingsView(extensions.views.mixins.ListedExtensionMixin, ListView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['extension'] = self.extension
context['is_maintainer'] = self.extension.has_maintainer(self.request.user)
context['score'] = self.score
if self.request.user.is_authenticated:
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:
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}
user = self.request.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)
if user.is_moderator or user.is_superuser:
filtered_activity_types.update(