mirror of
https://github.com/minetest/contentdb.git
synced 2024-11-08 08:33:45 +01:00
Add review voting
This commit is contained in:
parent
4a1f654798
commit
fab814c46f
@ -21,8 +21,8 @@ from flask_login import current_user, login_required
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import *
|
||||
from wtforms.validators import *
|
||||
from app.models import db, PackageReview, Thread, ThreadReply, NotificationType
|
||||
from app.utils import is_package_page, addNotification, get_int_or_abort
|
||||
from app.models import db, PackageReview, Thread, ThreadReply, NotificationType, PackageReviewVote, Package
|
||||
from app.utils import is_package_page, addNotification, get_int_or_abort, isYes, is_safe_url
|
||||
from app.tasks.webhooktasks import post_discord_webhook
|
||||
|
||||
|
||||
@ -146,3 +146,43 @@ def delete_review(package):
|
||||
db.session.commit()
|
||||
|
||||
return redirect(thread.getViewURL())
|
||||
|
||||
|
||||
def handle_review_vote(package: Package, review_id: int):
|
||||
if current_user in package.maintainers:
|
||||
flash("You can't vote on the reviews on your own package!", "danger")
|
||||
return
|
||||
|
||||
review: PackageReview = PackageReview.query.get(review_id)
|
||||
if review is None or review.package != package:
|
||||
abort(404)
|
||||
|
||||
if review.author == current_user:
|
||||
flash("You can't vote on your own reviews!", "danger")
|
||||
return
|
||||
|
||||
vote = PackageReviewVote.query.filter_by(review=review, user=current_user).first()
|
||||
if vote is None:
|
||||
vote = PackageReviewVote()
|
||||
vote.review = review
|
||||
vote.user = current_user
|
||||
vote.is_positive = isYes(request.form["is_positive"])
|
||||
db.session.add(vote)
|
||||
else:
|
||||
vote.is_positive = isYes(request.form["is_positive"])
|
||||
|
||||
review.update_score()
|
||||
db.session.commit()
|
||||
|
||||
|
||||
@bp.route("/packages/<author>/<name>/review/<int:review_id>/", methods=["POST"])
|
||||
@login_required
|
||||
@is_package_page
|
||||
def review_vote(package, review_id):
|
||||
handle_review_vote(package, review_id)
|
||||
|
||||
next_url = request.args.get("r")
|
||||
if next_url and is_safe_url(next_url):
|
||||
return redirect(next_url)
|
||||
else:
|
||||
return redirect(review.thread.getViewURL())
|
||||
|
@ -15,12 +15,12 @@
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import math
|
||||
from typing import List, Optional, Tuple
|
||||
from typing import Optional
|
||||
|
||||
from flask import *
|
||||
from flask_babel import gettext
|
||||
from flask_login import current_user, login_required
|
||||
from sqlalchemy import func, and_, or_
|
||||
from sqlalchemy import func
|
||||
|
||||
from app.models import *
|
||||
from app.tasks.forumtasks import checkForumAccount
|
||||
@ -88,49 +88,51 @@ def get_user_medals(user: User) -> Tuple[List[Medal], List[Medal]]:
|
||||
# REVIEWS
|
||||
#
|
||||
|
||||
users_by_reviews = db.session.query(User.username, func.count(PackageReview.id).label("count")) \
|
||||
users_by_reviews = db.session.query(User.username, func.sum(PackageReview.score).label("karma")) \
|
||||
.select_from(User).join(PackageReview) \
|
||||
.group_by(User.username).order_by(text("count DESC")).all()
|
||||
.group_by(User.username).order_by(text("karma DESC")).all()
|
||||
try:
|
||||
review_boundary = users_by_reviews[math.floor(len(users_by_reviews) * 0.25)][1] + 1
|
||||
except IndexError:
|
||||
review_boundary = None
|
||||
users_by_reviews = [username for username, _ in users_by_reviews]
|
||||
usernames_by_reviews = [username for username, _ in users_by_reviews]
|
||||
|
||||
review_idx = None
|
||||
review_percent = None
|
||||
review_karma = 0
|
||||
try:
|
||||
review_idx = users_by_reviews.index(user.username)
|
||||
review_idx = usernames_by_reviews.index(user.username)
|
||||
review_percent = round(100 * review_idx / len(users_by_reviews), 1)
|
||||
review_karma = max(users_by_reviews[review_idx][1], 0)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if review_percent is not None and review_percent < 25:
|
||||
if review_idx == 0:
|
||||
title = gettext(u"Most reviews")
|
||||
title = gettext(u"Top reviewer")
|
||||
description = gettext(
|
||||
u"%(display_name)s has written the most reviews on ContentDB.",
|
||||
u"%(display_name)s has written the most helpful reviews on ContentDB.",
|
||||
display_name=user.display_name)
|
||||
elif review_idx <= 2:
|
||||
if review_idx == 1:
|
||||
title = gettext(u"2nd most reviews")
|
||||
title = gettext(u"2nd most helpful reviewer")
|
||||
else:
|
||||
title = gettext(u"3rd most reviews")
|
||||
title = gettext(u"3rd most helpful reviewer")
|
||||
description = gettext(
|
||||
u"This puts %(display_name)s in the top %(perc)s%%",
|
||||
display_name=user.display_name, perc=review_percent)
|
||||
else:
|
||||
title = gettext(u"Top %(perc)s%% reviewer", perc=review_percent)
|
||||
description = gettext(u"Only %(place)d users have written more reviews.", place=review_idx)
|
||||
description = gettext(u"Only %(place)d users have written more helpful reviews.", place=review_idx)
|
||||
|
||||
unlocked.append(Medal.make_unlocked(
|
||||
place_to_color(review_idx + 1), "fa-star-half-alt", title, description))
|
||||
else:
|
||||
description = gettext(u"Consider writing more reviews to get a medal.")
|
||||
description = gettext(u"Consider writing more helpful reviews to get a medal.")
|
||||
if review_idx:
|
||||
description += " " + gettext(u"You are in place %(place)s.", place=review_idx + 1)
|
||||
locked.append(Medal.make_locked(
|
||||
description, (len(user.reviews), review_boundary)))
|
||||
description, (review_karma, review_boundary)))
|
||||
|
||||
#
|
||||
# TOP PACKAGES
|
||||
|
@ -337,7 +337,8 @@ class Package(db.Model):
|
||||
threads = db.relationship("Thread", back_populates="package", order_by=db.desc("thread_created_at"),
|
||||
foreign_keys="Thread.package_id", cascade="all, delete, delete-orphan", lazy="dynamic")
|
||||
|
||||
reviews = db.relationship("PackageReview", back_populates="package", order_by=db.desc("package_review_created_at"),
|
||||
reviews = db.relationship("PackageReview", back_populates="package",
|
||||
order_by=[db.desc("package_review_score"),db.desc("package_review_created_at")],
|
||||
cascade="all, delete, delete-orphan")
|
||||
|
||||
audit_log_entries = db.relationship("AuditLogEntry", foreign_keys="AuditLogEntry.package_id",
|
||||
|
@ -15,6 +15,7 @@
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import datetime
|
||||
from typing import Tuple, List
|
||||
|
||||
from flask import url_for
|
||||
|
||||
@ -156,6 +157,16 @@ class PackageReview(db.Model):
|
||||
recommends = db.Column(db.Boolean, nullable=False)
|
||||
|
||||
thread = db.relationship("Thread", uselist=False, back_populates="review")
|
||||
votes = db.relationship("PackageReviewVote", back_populates="review")
|
||||
|
||||
score = db.Column(db.Integer, nullable=False, default=1)
|
||||
|
||||
def get_totals(self, current_user = None) -> Tuple[int,int,bool]:
|
||||
votes: List[PackageReviewVote] = self.votes
|
||||
pos = sum([ 1 for vote in votes if vote.is_positive ])
|
||||
neg = sum([ 1 for vote in votes if not vote.is_positive])
|
||||
user_vote = next(filter(lambda vote: vote.user == current_user, votes), None)
|
||||
return pos, neg, user_vote.is_positive if user_vote else None
|
||||
|
||||
def asSign(self):
|
||||
return 1 if self.recommends else -1
|
||||
@ -167,3 +178,25 @@ class PackageReview(db.Model):
|
||||
return url_for("packages.delete_review",
|
||||
author=self.package.author.username,
|
||||
name=self.package.name)
|
||||
|
||||
def getVoteUrl(self, next_url=None):
|
||||
return url_for("packages.review_vote",
|
||||
author=self.package.author.username,
|
||||
name=self.package.name,
|
||||
review_id=self.id,
|
||||
r=next_url)
|
||||
|
||||
def update_score(self):
|
||||
(pos, neg, _) = self.get_totals()
|
||||
self.score = 3 * (pos - neg) + 1
|
||||
|
||||
|
||||
class PackageReviewVote(db.Model):
|
||||
review_id = db.Column(db.Integer, db.ForeignKey("package_review.id"), primary_key=True)
|
||||
review = db.relationship("PackageReview", foreign_keys=[review_id], back_populates="votes")
|
||||
user_id = db.Column(db.Integer, db.ForeignKey("user.id"), primary_key=True)
|
||||
user = db.relationship("User", foreign_keys=[user_id], back_populates="review_votes")
|
||||
|
||||
is_positive = db.Column(db.Boolean, nullable=False)
|
||||
|
||||
created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
|
||||
|
@ -172,6 +172,7 @@ class User(db.Model, UserMixin):
|
||||
|
||||
packages = db.relationship("Package", back_populates="author", lazy="dynamic", order_by=db.asc("package_title"))
|
||||
reviews = db.relationship("PackageReview", back_populates="author", order_by=db.desc("package_review_created_at"), cascade="all, delete, delete-orphan")
|
||||
review_votes = db.relationship("PackageReviewVote", back_populates="user", cascade="all, delete, delete-orphan")
|
||||
tokens = db.relationship("APIToken", back_populates="owner", lazy="dynamic", cascade="all, delete, delete-orphan")
|
||||
threads = db.relationship("Thread", back_populates="author", lazy="dynamic", cascade="all, delete, delete-orphan")
|
||||
replies = db.relationship("ThreadReply", back_populates="author", lazy="dynamic", cascade="all, delete, delete-orphan")
|
||||
|
@ -1,6 +1,6 @@
|
||||
from . import app, utils
|
||||
from .models import Permission, Package, PackageState, PackageRelease
|
||||
from .utils import abs_url_for, url_set_query
|
||||
from .utils import abs_url_for, url_set_query, url_set_anchor
|
||||
from flask_login import current_user
|
||||
from flask_babel import format_timedelta, gettext
|
||||
from urllib.parse import urlparse
|
||||
@ -16,9 +16,8 @@ def inject_debug():
|
||||
@app.context_processor
|
||||
def inject_functions():
|
||||
check_global_perm = Permission.checkPerm
|
||||
return dict(abs_url_for=abs_url_for, url_set_query=url_set_query,
|
||||
check_global_perm=check_global_perm,
|
||||
get_headings=get_headings)
|
||||
return dict(abs_url_for=abs_url_for, url_set_query=url_set_query, url_set_anchor=url_set_anchor,
|
||||
check_global_perm=check_global_perm, get_headings=get_headings)
|
||||
|
||||
@app.context_processor
|
||||
def inject_todo():
|
||||
|
@ -152,7 +152,7 @@
|
||||
{{ _("See more") }}
|
||||
</a>
|
||||
<h2 class="my-3">{{ _("Recent Positive Reviews") }}</h2>
|
||||
{% from "macros/reviews.html" import render_reviews %}
|
||||
{% from "macros/reviews.html" import render_reviews with context %}
|
||||
{{ render_reviews(reviews, current_user, True) }}
|
||||
|
||||
|
||||
|
@ -1,7 +1,28 @@
|
||||
{% macro render_review_vote(review, current_user, next_url) %}
|
||||
{% set (positive, negative, is_positive) = review.get_totals(current_user) %}
|
||||
<form class="-group" method="post" action="{{ review.getVoteUrl(next_url) }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<div class="btn-group">
|
||||
<button class="btn {% if is_positive == true %}btn-primary{% else %}btn-secondary{% endif %}" name="is_positive" value="yes">
|
||||
<i class="fas fa-thumbs-up mr-1"></i>
|
||||
{{ _("Helpful") }}
|
||||
<span class="badge badge-light ml-1">{{ positive }}</span>
|
||||
</button>
|
||||
<button class="btn {% if is_positive == false %}btn-primary{% else %}btn-secondary{% endif %}" name="is_positive" value="no">
|
||||
<i class="fas fa-thumbs-down mr-1"></i>
|
||||
{{ _("Unhelpful") }}
|
||||
<span class="badge badge-light ml-1">{{ negative }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro render_reviews(reviews, current_user, show_package_link=False) -%}
|
||||
<ul class="comments mt-4 mb-0">
|
||||
{% for review in reviews %}
|
||||
{% set review_anchor = "review-" + (review.id | string) %}
|
||||
<li class="row my-2 mx-0">
|
||||
<a id="{{ review_anchor }}"></a>
|
||||
<div class="col-md-1 p-1">
|
||||
<a href="{{ url_for('users.profile', username=review.author.username) }}">
|
||||
<img class="img-fluid user-photo img-thumbnail img-thumbnail-1" src="{{ review.author.getProfilePicURL() }}">
|
||||
@ -44,7 +65,7 @@
|
||||
|
||||
{{ reply.comment | markdown }}
|
||||
|
||||
<p class="mt-2 mb-0">
|
||||
<div class="btn-toolbar mt-2 mb-0">
|
||||
{% if show_package_link %}
|
||||
<a class="btn btn-primary mr-1" href="{{ review.package.getURL("packages.view") }}">
|
||||
{{ _("%(title)s by %(author)s",
|
||||
@ -53,12 +74,14 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<a class="btn {% if review.thread.replies.count() > 1 %} btn-primary {% else %} btn-secondary {% endif %}"
|
||||
<a class="btn {% if review.thread.replies.count() > 1 %} btn-primary {% else %} btn-secondary {% endif %} mr-1"
|
||||
href="{{ url_for('threads.view', id=review.thread.id) }}">
|
||||
<i class="fas fa-comments mr-2"></i>
|
||||
{{ _("%(num)d comments", num=review.thread.replies.count() - 1) }}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
{{ render_review_vote(review, current_user, url_set_anchor(review_anchor)) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,5 +1,7 @@
|
||||
{% macro render_thread(thread, current_user) -%}
|
||||
|
||||
{% from "macros/reviews.html" import render_review_vote %}
|
||||
|
||||
<ul class="comments mt-4 mb-0">
|
||||
{% for r in thread.replies %}
|
||||
<li class="row my-2 mx-0">
|
||||
@ -60,6 +62,10 @@
|
||||
{% endif %}
|
||||
|
||||
{{ r.comment | markdown }}
|
||||
|
||||
{% if thread.replies[0] == r and thread.review %}
|
||||
{{ render_review_vote(thread.review, current_user, thread.getViewURL()) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -5,8 +5,8 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% from "macros/pagination.html" import render_pagination %}
|
||||
{% from "macros/reviews.html" import render_reviews %}
|
||||
{% from "macros/pagination.html" import render_pagination with context %}
|
||||
{% from "macros/reviews.html" import render_reviews with context %}
|
||||
|
||||
{{ render_pagination(pagination, url_set_query) }}
|
||||
{{ render_reviews(reviews, current_user, True) }}
|
||||
|
@ -253,7 +253,7 @@
|
||||
|
||||
<h2 id="reviews" class="mt-0">{{ _("Reviews") }}</h2>
|
||||
|
||||
{% from "macros/reviews.html" import render_reviews, render_review_form, render_review_preview %}
|
||||
{% from "macros/reviews.html" import render_reviews, render_review_form, render_review_preview with context %}
|
||||
{% if current_user.is_authenticated %}
|
||||
{% if has_review %}
|
||||
<p>
|
||||
|
@ -191,7 +191,7 @@
|
||||
|
||||
|
||||
<h2 class="my-3" id="reviews">{{ _("Reviews") }}</h2>
|
||||
{% from "macros/reviews.html" import render_reviews %}
|
||||
{% from "macros/reviews.html" import render_reviews with context %}
|
||||
{{ render_reviews(user.reviews, current_user, True) }}
|
||||
|
||||
{% endblock %}
|
||||
|
@ -40,6 +40,12 @@ def abs_url_for(path, **kwargs):
|
||||
def abs_url(path):
|
||||
return urljoin(app.config["BASE_URL"], path)
|
||||
|
||||
def url_set_anchor(anchor):
|
||||
args = MultiDict(request.args)
|
||||
dargs = dict(args.lists())
|
||||
dargs.update(request.view_args)
|
||||
return url_for(request.endpoint, **dargs) + "#" + anchor
|
||||
|
||||
def url_set_query(**kwargs):
|
||||
args = MultiDict(request.args)
|
||||
|
||||
|
38
migrations/versions/cd5ab8a01f4a_.py
Normal file
38
migrations/versions/cd5ab8a01f4a_.py
Normal file
@ -0,0 +1,38 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: cd5ab8a01f4a
|
||||
Revises: 1af840af0209
|
||||
Create Date: 2021-08-18 20:47:54.268263
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'cd5ab8a01f4a'
|
||||
down_revision = '1af840af0209'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('package_review_vote',
|
||||
sa.Column('review_id', sa.Integer(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
sa.Column('is_positive', sa.Boolean(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['review_id'], ['package_review.id'], ),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
||||
sa.PrimaryKeyConstraint('review_id', 'user_id')
|
||||
)
|
||||
op.add_column('package_review', sa.Column('score', sa.Integer(), nullable=False, server_default="1"))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('package_review', 'score')
|
||||
op.drop_table('package_review_vote')
|
||||
# ### end Alembic commands ###
|
Loading…
Reference in New Issue
Block a user