Ratings: implement replies by maintainers #181
@ -349,7 +349,6 @@ class Extension(CreatedModifiedMixin, TrackChangesMixin, models.Model):
|
|||||||
return (
|
return (
|
||||||
not self.has_maintainer(user)
|
not self.has_maintainer(user)
|
||||||
and not self.ratings.filter(
|
and not self.ratings.filter(
|
||||||
reply_to=None,
|
|
||||||
user_id=user.pk,
|
user_id=user.pk,
|
||||||
).exists()
|
).exists()
|
||||||
)
|
)
|
||||||
@ -401,13 +400,7 @@ class Extension(CreatedModifiedMixin, TrackChangesMixin, models.Model):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def text_ratings_count(self) -> int:
|
def text_ratings_count(self) -> int:
|
||||||
return len(
|
return len([r for r in self.ratings.all() if r.text is not None and r.is_listed])
|
||||||
[
|
|
||||||
r
|
|
||||||
for r in self.ratings.all()
|
|
||||||
if r.text is not None and r.is_listed and r.reply_to is None
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def total_ratings_count(self) -> int:
|
def total_ratings_count(self) -> int:
|
||||||
|
@ -23,22 +23,7 @@ class RatingTypeFilter(admin.SimpleListFilter):
|
|||||||
The second element is the human-readable name for
|
The second element is the human-readable name for
|
||||||
the option that will appear in the right sidebar.
|
the option that will appear in the right sidebar.
|
||||||
"""
|
"""
|
||||||
return (
|
return (('rating', 'User Rating'),)
|
||||||
('rating', 'User Rating'),
|
|
||||||
('reply', 'Developer/Admin Reply'),
|
|
||||||
)
|
|
||||||
|
|
||||||
def queryset(self, request, queryset):
|
|
||||||
"""Return the filtered queryset.
|
|
||||||
|
|
||||||
Filter based on the value provided in the query string
|
|
||||||
and retrievable via `self.value()`.
|
|
||||||
"""
|
|
||||||
if self.value() == 'rating':
|
|
||||||
return queryset.filter(reply_to__isnull=True)
|
|
||||||
elif self.value() == 'reply':
|
|
||||||
return queryset.filter(reply_to__isnull=False)
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
|
|
||||||
class RatingAdmin(admin.ModelAdmin):
|
class RatingAdmin(admin.ModelAdmin):
|
||||||
@ -48,7 +33,6 @@ class RatingAdmin(admin.ModelAdmin):
|
|||||||
'extension',
|
'extension',
|
||||||
'version',
|
'version',
|
||||||
'user',
|
'user',
|
||||||
'reply_to',
|
|
||||||
)
|
)
|
||||||
readonly_fields = (
|
readonly_fields = (
|
||||||
'date_created',
|
'date_created',
|
||||||
@ -66,19 +50,17 @@ class RatingAdmin(admin.ModelAdmin):
|
|||||||
'user',
|
'user',
|
||||||
'ip_address',
|
'ip_address',
|
||||||
'score',
|
'score',
|
||||||
'is_reply',
|
|
||||||
'status',
|
'status',
|
||||||
'truncated_text',
|
'truncated_text',
|
||||||
)
|
)
|
||||||
list_filter = ('status', RatingTypeFilter, 'score')
|
list_filter = ('status', RatingTypeFilter, 'score')
|
||||||
actions = ('delete_selected',)
|
actions = ('delete_selected',)
|
||||||
list_select_related = ('user',) # For extension/reply_to see get_queryset()
|
list_select_related = ('user',)
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
base_qs = Rating.objects.all()
|
base_qs = Rating.objects.all()
|
||||||
return base_qs.prefetch_related(
|
return base_qs.prefetch_related(
|
||||||
Prefetch('version', queryset=Version.objects.all()),
|
Prefetch('version', queryset=Version.objects.all()),
|
||||||
Prefetch('reply_to', queryset=base_qs),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def has_add_permission(self, request):
|
def has_add_permission(self, request):
|
||||||
@ -87,11 +69,5 @@ class RatingAdmin(admin.ModelAdmin):
|
|||||||
def truncated_text(self, obj):
|
def truncated_text(self, obj):
|
||||||
return truncatechars(obj.text, 140) if obj.text else ''
|
return truncatechars(obj.text, 140) if obj.text else ''
|
||||||
|
|
||||||
def is_reply(self, obj):
|
|
||||||
return bool(obj.reply_to)
|
|
||||||
|
|
||||||
is_reply.boolean = True
|
|
||||||
is_reply.admin_order_field = 'reply_to'
|
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(Rating, RatingAdmin)
|
admin.site.register(Rating, RatingAdmin)
|
||||||
|
58
ratings/migrations/0007_ratingreply_and_more.py
Normal file
58
ratings/migrations/0007_ratingreply_and_more.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
# Generated by Django 4.2.11 on 2024-06-10 14:01
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('ratings', '0006_alter_ratingflag_user'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='RatingReply',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('date_created', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('date_modified', models.DateTimeField(auto_now=True)),
|
||||||
|
('text', models.TextField(null=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.RemoveConstraint(
|
||||||
|
model_name='rating',
|
||||||
|
name='rating_one_review_per_user_key',
|
||||||
|
),
|
||||||
|
migrations.RemoveIndex(
|
||||||
|
model_name='rating',
|
||||||
|
name='rating_latest_idx',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='rating',
|
||||||
|
name='reply_to',
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='rating',
|
||||||
|
index=models.Index(fields=['is_latest', 'date_created'], name='rating_latest_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='rating',
|
||||||
|
constraint=models.UniqueConstraint(fields=('version', 'user'), name='rating_one_review_per_user_key'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='ratingreply',
|
||||||
|
name='rating',
|
||||||
|
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='ratings.rating'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='ratingreply',
|
||||||
|
name='user',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
]
|
@ -17,11 +17,11 @@ class RatingManager(models.Manager):
|
|||||||
# TODO: figure out how to retrieve reviews "annotated" with replies, if any
|
# TODO: figure out how to retrieve reviews "annotated" with replies, if any
|
||||||
@property
|
@property
|
||||||
def listed(self):
|
def listed(self):
|
||||||
return self.filter(status=self.model.STATUSES.APPROVED, reply_to__isnull=True)
|
return self.filter(status=self.model.STATUSES.APPROVED)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unlisted(self):
|
def unlisted(self):
|
||||||
return self.exclude(status=self.models.STATUSES.APPROVED, reply_to__isnull=True)
|
return self.exclude(status=self.models.STATUSES.APPROVED)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def listed_texts(self):
|
def listed_texts(self):
|
||||||
@ -41,12 +41,6 @@ class Rating(CreatedModifiedMixin, TrackChangesMixin, models.Model):
|
|||||||
'extensions.Version', related_name='ratings', on_delete=models.CASCADE
|
'extensions.Version', related_name='ratings', on_delete=models.CASCADE
|
||||||
)
|
)
|
||||||
user = models.ForeignKey(User, related_name='ratings', on_delete=models.CASCADE)
|
user = models.ForeignKey(User, related_name='ratings', on_delete=models.CASCADE)
|
||||||
reply_to = models.OneToOneField(
|
|
||||||
'self',
|
|
||||||
null=True,
|
|
||||||
related_name='reply',
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
)
|
|
||||||
|
|
||||||
score = models.PositiveSmallIntegerField(null=True, choices=SCORES)
|
score = models.PositiveSmallIntegerField(null=True, choices=SCORES)
|
||||||
text = models.TextField(null=True)
|
text = models.TextField(null=True)
|
||||||
@ -74,14 +68,14 @@ class Rating(CreatedModifiedMixin, TrackChangesMixin, models.Model):
|
|||||||
models.Index(fields=('version',), name='rating_version_id'),
|
models.Index(fields=('version',), name='rating_version_id'),
|
||||||
models.Index(fields=('user',), name='rating_user_idx'),
|
models.Index(fields=('user',), name='rating_user_idx'),
|
||||||
models.Index(
|
models.Index(
|
||||||
fields=('reply_to', 'is_latest', 'date_created'),
|
fields=('is_latest', 'date_created'),
|
||||||
name='rating_latest_idx',
|
name='rating_latest_idx',
|
||||||
),
|
),
|
||||||
models.Index(fields=('ip_address',), name='rating_ip_address_idx'),
|
models.Index(fields=('ip_address',), name='rating_ip_address_idx'),
|
||||||
]
|
]
|
||||||
constraints = [
|
constraints = [
|
||||||
models.UniqueConstraint(
|
models.UniqueConstraint(
|
||||||
fields=('version', 'user', 'reply_to'), name='rating_one_review_per_user_key'
|
fields=('version', 'user'), name='rating_one_review_per_user_key'
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -98,7 +92,6 @@ class Rating(CreatedModifiedMixin, TrackChangesMixin, models.Model):
|
|||||||
def get_for(cls, user_id: int, extension_id: int):
|
def get_for(cls, user_id: int, extension_id: int):
|
||||||
"""Get rating left by a given user for a given extension."""
|
"""Get rating left by a given user for a given extension."""
|
||||||
return cls.objects.filter(
|
return cls.objects.filter(
|
||||||
reply_to=None,
|
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
extension_id=extension_id,
|
extension_id=extension_id,
|
||||||
).first()
|
).first()
|
||||||
@ -131,12 +124,6 @@ class Rating(CreatedModifiedMixin, TrackChangesMixin, models.Model):
|
|||||||
# user_responsible=user.
|
# user_responsible=user.
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_replies(cls, ratings):
|
|
||||||
ratings = [r.id for r in ratings]
|
|
||||||
qs = Rating.objects.filter(reply_to__in=ratings)
|
|
||||||
return {r.reply_to_id: r for r in qs}
|
|
||||||
|
|
||||||
def post_save(sender, instance, created, **kwargs):
|
def post_save(sender, instance, created, **kwargs):
|
||||||
if kwargs.get('raw'):
|
if kwargs.get('raw'):
|
||||||
return
|
return
|
||||||
@ -147,10 +134,7 @@ class Rating(CreatedModifiedMixin, TrackChangesMixin, models.Model):
|
|||||||
# when manipulating a Rating that indicates a real user made that
|
# when manipulating a Rating that indicates a real user made that
|
||||||
# change.
|
# change.
|
||||||
action = 'New' if created else 'Edited'
|
action = 'New' if created else 'Edited'
|
||||||
if instance.reply_to:
|
log.info(f'{action} rating: {instance.pk}')
|
||||||
log.info(f'{action} reply to {instance.reply_to_id}: {instance.pk}')
|
|
||||||
else:
|
|
||||||
log.info(f'{action} rating: {instance.pk}')
|
|
||||||
|
|
||||||
def get_report_url(self):
|
def get_report_url(self):
|
||||||
return reverse(
|
return reverse(
|
||||||
@ -190,3 +174,9 @@ class RatingFlag(CreatedModifiedMixin, models.Model):
|
|||||||
constraints = [
|
constraints = [
|
||||||
models.UniqueConstraint(fields=('rating', 'user'), name='ratingflag_review_user_key')
|
models.UniqueConstraint(fields=('rating', 'user'), name='ratingflag_review_user_key')
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class RatingReply(CreatedModifiedMixin, models.Model):
|
||||||
|
rating = models.OneToOneField(Rating, on_delete=models.CASCADE)
|
||||||
|
user = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)
|
||||||
|
text = models.TextField(null=True)
|
||||||
|
Loading…
Reference in New Issue
Block a user