Ratings: implement replies by maintainers #181
@ -349,7 +349,6 @@ class Extension(CreatedModifiedMixin, TrackChangesMixin, models.Model):
|
||||
return (
|
||||
not self.has_maintainer(user)
|
||||
and not self.ratings.filter(
|
||||
reply_to=None,
|
||||
user_id=user.pk,
|
||||
).exists()
|
||||
)
|
||||
@ -401,13 +400,7 @@ class Extension(CreatedModifiedMixin, TrackChangesMixin, models.Model):
|
||||
|
||||
@property
|
||||
def text_ratings_count(self) -> int:
|
||||
return len(
|
||||
[
|
||||
r
|
||||
for r in self.ratings.all()
|
||||
if r.text is not None and r.is_listed and r.reply_to is None
|
||||
]
|
||||
)
|
||||
return len([r for r in self.ratings.all() if r.text is not None and r.is_listed])
|
||||
|
||||
@property
|
||||
def total_ratings_count(self) -> int:
|
||||
|
@ -23,22 +23,7 @@ class RatingTypeFilter(admin.SimpleListFilter):
|
||||
The second element is the human-readable name for
|
||||
the option that will appear in the right sidebar.
|
||||
"""
|
||||
return (
|
||||
('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
|
||||
return (('rating', 'User Rating'),)
|
||||
|
||||
|
||||
class RatingAdmin(admin.ModelAdmin):
|
||||
@ -48,7 +33,6 @@ class RatingAdmin(admin.ModelAdmin):
|
||||
'extension',
|
||||
'version',
|
||||
'user',
|
||||
'reply_to',
|
||||
)
|
||||
readonly_fields = (
|
||||
'date_created',
|
||||
@ -66,19 +50,17 @@ class RatingAdmin(admin.ModelAdmin):
|
||||
'user',
|
||||
'ip_address',
|
||||
'score',
|
||||
'is_reply',
|
||||
'status',
|
||||
'truncated_text',
|
||||
)
|
||||
list_filter = ('status', RatingTypeFilter, 'score')
|
||||
actions = ('delete_selected',)
|
||||
list_select_related = ('user',) # For extension/reply_to see get_queryset()
|
||||
list_select_related = ('user',)
|
||||
|
||||
def get_queryset(self, request):
|
||||
base_qs = Rating.objects.all()
|
||||
return base_qs.prefetch_related(
|
||||
Prefetch('version', queryset=Version.objects.all()),
|
||||
Prefetch('reply_to', queryset=base_qs),
|
||||
)
|
||||
|
||||
def has_add_permission(self, request):
|
||||
@ -87,11 +69,5 @@ class RatingAdmin(admin.ModelAdmin):
|
||||
def truncated_text(self, obj):
|
||||
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)
|
||||
|
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
|
||||
@property
|
||||
def listed(self):
|
||||
return self.filter(status=self.model.STATUSES.APPROVED, reply_to__isnull=True)
|
||||
return self.filter(status=self.model.STATUSES.APPROVED)
|
||||
|
||||
@property
|
||||
def unlisted(self):
|
||||
return self.exclude(status=self.models.STATUSES.APPROVED, reply_to__isnull=True)
|
||||
return self.exclude(status=self.models.STATUSES.APPROVED)
|
||||
|
||||
@property
|
||||
def listed_texts(self):
|
||||
@ -41,12 +41,6 @@ class Rating(CreatedModifiedMixin, TrackChangesMixin, models.Model):
|
||||
'extensions.Version', 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)
|
||||
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=('user',), name='rating_user_idx'),
|
||||
models.Index(
|
||||
fields=('reply_to', 'is_latest', 'date_created'),
|
||||
fields=('is_latest', 'date_created'),
|
||||
name='rating_latest_idx',
|
||||
),
|
||||
models.Index(fields=('ip_address',), name='rating_ip_address_idx'),
|
||||
]
|
||||
constraints = [
|
||||
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):
|
||||
"""Get rating left by a given user for a given extension."""
|
||||
return cls.objects.filter(
|
||||
reply_to=None,
|
||||
user_id=user_id,
|
||||
extension_id=extension_id,
|
||||
).first()
|
||||
@ -131,12 +124,6 @@ class Rating(CreatedModifiedMixin, TrackChangesMixin, models.Model):
|
||||
# user_responsible=user.
|
||||
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):
|
||||
if kwargs.get('raw'):
|
||||
return
|
||||
@ -147,10 +134,7 @@ class Rating(CreatedModifiedMixin, TrackChangesMixin, models.Model):
|
||||
# when manipulating a Rating that indicates a real user made that
|
||||
# change.
|
||||
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):
|
||||
return reverse(
|
||||
@ -190,3 +174,9 @@ class RatingFlag(CreatedModifiedMixin, models.Model):
|
||||
constraints = [
|
||||
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