Ratings: implement replies by maintainers #181
@ -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 %}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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'
|
||||||
|
@ -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>
|
||||||
|
@ -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 %}
|
||||||
|
@ -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">
|
||||||
|
36
ratings/templates/ratings/ratingreply_form.html
Normal file
36
ratings/templates/ratings/ratingreply_form.html
Normal 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 %}
|
@ -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',
|
||||||
|
),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -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()
|
||||||
|
@ -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(
|
||||||
|
Loading…
Reference in New Issue
Block a user