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
4 changed files with 72 additions and 55 deletions
Showing only changes of commit 501b909e09 - Show all commits

View File

@ -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:

View File

@ -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)

View 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),
),
]

View File

@ -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,9 +134,6 @@ 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} reply to {instance.reply_to_id}: {instance.pk}')
else:
log.info(f'{action} rating: {instance.pk}') log.info(f'{action} rating: {instance.pk}')
def get_report_url(self): def get_report_url(self):
@ -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)