rating and comment system complete

This commit is contained in:
2026-08-05 12:25:59 -05:00
parent 26e9179be4
commit aed6a7c24d
9 changed files with 407 additions and 1 deletions
+132
View File
@@ -19,11 +19,13 @@ from django.utils.text import slugify
from common.markdown import render_markdown
from .forms import ContributorForm, ProjectForm, VersionForm
from .models import (
Comment,
FileIndex,
Project,
ProjectAsset,
ProjectContributor,
ProjectDraft,
Rating,
Tag,
TagCategory,
TagList,
@@ -259,6 +261,10 @@ def browse(request):
projects = projects.annotate(_downloads=Sum('versions__downloads')).order_by(
F('_downloads').desc(nulls_last=True)
)
elif sort == 'most_liked':
projects = projects.order_by('-rating_score')
elif sort == 'most_voted':
projects = projects.order_by('-rating_count')
elif sort == 'name':
projects = projects.order_by('title')
else:
@@ -373,6 +379,21 @@ def project_detail(request, slug):
if loader and loader not in mod_loaders:
mod_loaders.append(loader)
# Ratings + forum.
rating_agg = project.ratings.aggregate(
positive=Count('pk', filter=Q(value=True)),
total=Count('pk'),
)
rating_positive = rating_agg['positive'] or 0
rating_total = rating_agg['total'] or 0
rating_negative = rating_total - rating_positive
rating_percent = round(rating_positive / rating_total * 100) if rating_total else 0
user_rating = None
if request.user.is_authenticated:
user_rating = project.ratings.filter(user=request.user).values_list('value', flat=True).first()
comments = project.comments.select_related('user').order_by('-created_at')[:200]
can_comment = request.user.is_authenticated
autoload_plugins_json = ''
if plugins_manifest:
members = [
@@ -397,6 +418,15 @@ def project_detail(request, slug):
'mod_manifest': mod_manifest,
'mod_loaders': mod_loaders,
'mod_count': mod_count,
'rating_stats': {
'positive': rating_positive,
'negative': rating_negative,
'total': rating_total,
'percent': rating_percent,
},
'user_rating': user_rating,
'comments': comments,
'can_comment': can_comment,
'autoload_plugins_json': autoload_plugins_json,
'anim_stats': anim_stats,
}
@@ -1311,6 +1341,108 @@ def contributors(request, slug):
})
# ---------- Ratings & Forum ----------
def refresh_rating_fields(project):
"""Recompute Project.rating_score (net) + rating_count from its votes."""
agg = project.ratings.aggregate(
up=Count('pk', filter=Q(value=True)),
total=Count('pk'),
)
up = agg['up'] or 0
total = agg['total'] or 0
project.rating_count = total
project.rating_score = 2 * up - total
project.save(update_fields=['rating_score', 'rating_count'])
def _require_rating_user(request, project):
"""Redirect guests to login; return None when allowed."""
if request.user.is_authenticated:
return None
return redirect('{}?next={}'.format(
reverse('profiles:login'),
request.path,
))
def rate_project(request, slug):
"""Set / change / clear the current user's vote on a project."""
project = get_object_or_404(Project, slug=slug)
if (login_redirect := _require_rating_user(request, project)) is not None:
return login_redirect
if request.method != 'POST':
return redirect('library:project_detail', slug=slug)
value_raw = request.POST.get('value', '').strip()
if value_raw in ('1', 'up'):
value = True
elif value_raw in ('0', 'down'):
value = False
else:
value = None # clear the vote
rating, created = Rating.objects.get_or_create(
project=project, user=request.user, defaults={'value': value if value is not None else True},
)
if value is None:
rating.delete()
messages.success(request, 'Your rating was removed.')
elif created or rating.value != value:
rating.value = value
rating.save(update_fields=['value', 'updated_at'])
messages.success(request, 'Your rating was updated.')
else:
messages.info(request, 'Your rating is unchanged.')
refresh_rating_fields(project)
return redirect(reverse('library:project_detail', args=[slug]) + '#rating')
def add_comment(request, slug):
project = get_object_or_404(Project, slug=slug)
if (login_redirect := _require_rating_user(request, project)) is not None:
return login_redirect
if request.method == 'POST':
body = request.POST.get('body', '').strip()
if body:
Comment.objects.create(project=project, user=request.user, body=body)
messages.success(request, 'Comment added.')
else:
messages.error(request, 'Comment cannot be empty.')
return redirect(reverse('library:project_detail', args=[slug]) + '#forum')
def edit_comment(request, slug, comment_id):
project = get_object_or_404(Project, slug=slug)
if (login_redirect := _require_rating_user(request, project)) is not None:
return login_redirect
comment = get_object_or_404(Comment, pk=comment_id, project=project)
if comment.user_id != request.user.pk and not request.user.is_staff:
return HttpResponseForbidden('You can only edit your own comments.')
if request.method == 'POST':
body = request.POST.get('body', '').strip()
if body:
comment.body = body
comment.save(update_fields=['body', 'updated_at'])
messages.success(request, 'Comment updated.')
else:
messages.error(request, 'Comment cannot be empty.')
return redirect(reverse('library:project_detail', args=[slug]) + '#forum')
def delete_comment(request, slug, comment_id):
project = get_object_or_404(Project, slug=slug)
if (login_redirect := _require_rating_user(request, project)) is not None:
return login_redirect
comment = get_object_or_404(Comment, pk=comment_id, project=project)
if comment.user_id != request.user.pk and not request.user.is_staff:
return HttpResponseForbidden('You can only delete your own comments.')
if request.method == 'POST':
comment.delete()
messages.success(request, 'Comment deleted.')
return redirect(reverse('library:project_detail', args=[slug]) + '#forum')
# ---------- API (autocomplete) ----------
def api_tags_autocomplete(request):