mirror of
https://github.com/minetest/contentdb.git
synced 2025-01-03 03:37:28 +01:00
parent
9d033acfff
commit
307b8f8dde
@ -18,4 +18,4 @@ from flask import Blueprint
|
|||||||
|
|
||||||
bp = Blueprint("packages", __name__)
|
bp = Blueprint("packages", __name__)
|
||||||
|
|
||||||
from . import packages, screenshots, releases
|
from . import packages, screenshots, releases, reviews
|
||||||
|
98
app/blueprints/packages/reviews.py
Normal file
98
app/blueprints/packages/reviews.py
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
# Content DB
|
||||||
|
# Copyright (C) 2020 rubenwardy
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from . import bp
|
||||||
|
|
||||||
|
from flask import *
|
||||||
|
from flask_user import *
|
||||||
|
from flask_wtf import FlaskForm
|
||||||
|
from wtforms import *
|
||||||
|
from wtforms.validators import *
|
||||||
|
from app.models import db, PackageReview, Thread, ThreadReply
|
||||||
|
from app.utils import is_package_page, triggerNotif
|
||||||
|
|
||||||
|
class ReviewForm(FlaskForm):
|
||||||
|
title = StringField("Title", [InputRequired(), Length(3,100)])
|
||||||
|
comment = TextAreaField("Comment", [InputRequired(), Length(10, 500)])
|
||||||
|
recommends = RadioField("Private", [InputRequired()], choices=[("yes", "Yes"), ("no", "No")])
|
||||||
|
submit = SubmitField("Save")
|
||||||
|
|
||||||
|
@bp.route("/packages/<author>/<name>/review/", methods=["GET", "POST"])
|
||||||
|
@login_required
|
||||||
|
@is_package_page
|
||||||
|
def review(package):
|
||||||
|
review = PackageReview.query.filter_by(package=package, author=current_user).first()
|
||||||
|
|
||||||
|
form = ReviewForm(formdata=request.form, obj=review)
|
||||||
|
|
||||||
|
# Set default values
|
||||||
|
if request.method == "GET" and review:
|
||||||
|
form.title.data = review.thread.title
|
||||||
|
form.recommends.data = "yes" if review.recommends else "no"
|
||||||
|
form.comment.data = review.thread.replies[0].comment
|
||||||
|
|
||||||
|
# Validate and submit
|
||||||
|
elif request.method == "POST" and form.validate():
|
||||||
|
was_new = False
|
||||||
|
if not review:
|
||||||
|
was_new = True
|
||||||
|
review = PackageReview()
|
||||||
|
review.package = package
|
||||||
|
review.author = current_user
|
||||||
|
db.session.add(review)
|
||||||
|
|
||||||
|
review.recommends = form.recommends.data == "yes"
|
||||||
|
|
||||||
|
thread = review.thread
|
||||||
|
if not thread:
|
||||||
|
thread = Thread()
|
||||||
|
thread.author = current_user
|
||||||
|
thread.private = False
|
||||||
|
thread.package = package
|
||||||
|
thread.review = review
|
||||||
|
db.session.add(thread)
|
||||||
|
|
||||||
|
thread.watchers.append(current_user)
|
||||||
|
|
||||||
|
reply = ThreadReply()
|
||||||
|
reply.thread = thread
|
||||||
|
reply.author = current_user
|
||||||
|
reply.comment = form.comment.data
|
||||||
|
db.session.add(reply)
|
||||||
|
|
||||||
|
thread.replies.append(reply)
|
||||||
|
else:
|
||||||
|
reply = thread.replies[0]
|
||||||
|
reply.comment = form.comment.data
|
||||||
|
|
||||||
|
thread.title = form.title.data
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
notif_msg = None
|
||||||
|
if was_new:
|
||||||
|
notif_msg = "New review '{}' on package {}".format(form.title.data, package.title)
|
||||||
|
else:
|
||||||
|
notif_msg = "Updated review '{}' on package {}".format(form.title.data, package.title)
|
||||||
|
|
||||||
|
for maintainer in package.maintainers:
|
||||||
|
triggerNotif(maintainer, current_user, notif_msg, url_for("threads.view", id=thread.id))
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return redirect(package.getDetailsURL())
|
||||||
|
|
||||||
|
return render_template("packages/review_create_edit.html", form=form, package=package)
|
@ -206,7 +206,8 @@ def new():
|
|||||||
notif_msg = None
|
notif_msg = None
|
||||||
if package is not None:
|
if package is not None:
|
||||||
notif_msg = "New thread '{}' on package {}".format(thread.title, package.title)
|
notif_msg = "New thread '{}' on package {}".format(thread.title, package.title)
|
||||||
triggerNotif(package.author, current_user, notif_msg, url_for("threads.view", id=thread.id))
|
for maintainer in package.maintainers:
|
||||||
|
triggerNotif(maintainer, current_user, notif_msg, url_for("threads.view", id=thread.id))
|
||||||
else:
|
else:
|
||||||
notif_msg = "New thread '{}'".format(thread.title)
|
notif_msg = "New thread '{}'".format(thread.title)
|
||||||
|
|
||||||
|
@ -205,9 +205,17 @@ class User(db.Model, UserMixin):
|
|||||||
raise Exception("Permission {} is not related to users".format(perm.name))
|
raise Exception("Permission {} is not related to users".format(perm.name))
|
||||||
|
|
||||||
def canCommentRL(self):
|
def canCommentRL(self):
|
||||||
|
one_min_ago = datetime.datetime.utcnow() - datetime.timedelta(minutes=1)
|
||||||
|
if ThreadReply.query.filter_by(author=self) \
|
||||||
|
.filter(ThreadReply.created_at > one_min_ago).count() >= 3:
|
||||||
|
return False
|
||||||
|
|
||||||
hour_ago = datetime.datetime.utcnow() - datetime.timedelta(hours=1)
|
hour_ago = datetime.datetime.utcnow() - datetime.timedelta(hours=1)
|
||||||
return ThreadReply.query.filter_by(author=self) \
|
if ThreadReply.query.filter_by(author=self) \
|
||||||
.filter(ThreadReply.created_at > hour_ago).count() < 4
|
.filter(ThreadReply.created_at > hour_ago).count() >= 20:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
def canOpenThreadRL(self):
|
def canOpenThreadRL(self):
|
||||||
hour_ago = datetime.datetime.utcnow() - datetime.timedelta(hours=1)
|
hour_ago = datetime.datetime.utcnow() - datetime.timedelta(hours=1)
|
||||||
@ -1063,6 +1071,9 @@ class Thread(db.Model):
|
|||||||
package_id = db.Column(db.Integer, db.ForeignKey("package.id"), nullable=True)
|
package_id = db.Column(db.Integer, db.ForeignKey("package.id"), nullable=True)
|
||||||
package = db.relationship("Package", foreign_keys=[package_id])
|
package = db.relationship("Package", foreign_keys=[package_id])
|
||||||
|
|
||||||
|
review_id = db.Column(db.Integer, db.ForeignKey("package_review.id"), nullable=True)
|
||||||
|
review = db.relationship("PackageReview", foreign_keys=[review_id])
|
||||||
|
|
||||||
author_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
|
author_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
|
||||||
title = db.Column(db.String(100), nullable=False)
|
title = db.Column(db.String(100), nullable=False)
|
||||||
private = db.Column(db.Boolean, server_default="0")
|
private = db.Column(db.Boolean, server_default="0")
|
||||||
@ -1110,6 +1121,27 @@ class ThreadReply(db.Model):
|
|||||||
created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
|
created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
|
||||||
|
|
||||||
|
|
||||||
|
class PackageReview(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
|
||||||
|
package_id = db.Column(db.Integer, db.ForeignKey("package.id"), nullable=True)
|
||||||
|
package = db.relationship("Package", foreign_keys=[package_id], backref=db.backref("reviews", lazy=True))
|
||||||
|
|
||||||
|
author_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
|
||||||
|
author = db.relationship("User", foreign_keys=[author_id], backref=db.backref("reviews", lazy=True))
|
||||||
|
|
||||||
|
recommends = db.Column(db.Boolean, nullable=False)
|
||||||
|
|
||||||
|
thread = db.relationship("Thread", uselist=False, back_populates="review")
|
||||||
|
|
||||||
|
def getEditURL(self):
|
||||||
|
return url_for("packages.edit_review",
|
||||||
|
author=self.package.author.username,
|
||||||
|
name=self.package.name,
|
||||||
|
id=self.id)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
REPO_BLACKLIST = [".zip", "mediafire.com", "dropbox.com", "weebly.com", \
|
REPO_BLACKLIST = [".zip", "mediafire.com", "dropbox.com", "weebly.com", \
|
||||||
"minetest.net", "dropboxusercontent.com", "4shared.com", \
|
"minetest.net", "dropboxusercontent.com", "4shared.com", \
|
||||||
"digitalaudioconcepts.com", "hg.intevation.org", "www.wtfpl.net", \
|
"digitalaudioconcepts.com", "hg.intevation.org", "www.wtfpl.net", \
|
||||||
|
117
app/templates/macros/reviews.html
Normal file
117
app/templates/macros/reviews.html
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
{% macro render_reviews(reviews) -%}
|
||||||
|
<ul class="comments mt-4 mb-0">
|
||||||
|
{% for review in reviews %}
|
||||||
|
<li class="row my-2 mx-0">
|
||||||
|
<div class="col-md-1 p-1">
|
||||||
|
<a href="{{ url_for('users.profile', username=review.author.username) }}">
|
||||||
|
<img class="img-responsive user-photo img-thumbnail img-thumbnail-1" src="{{ review.author.getProfilePicURL() }}">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-auto pl-1 pr-3 pt-2 text-center" style=" font-size: 200%;">
|
||||||
|
{% if review.recommends %}
|
||||||
|
<i class="fas fa-thumbs-up" style="color:#6f6;"></i>
|
||||||
|
{% else %}
|
||||||
|
<i class="fas fa-thumbs-down" style="color:#f66;"></i>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if review.thread %}
|
||||||
|
{% set reply = review.thread.replies[0] %}
|
||||||
|
<div class="col pr-0">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<a class="author {{ reply.author.rank.name }}"
|
||||||
|
href="{{ url_for('users.profile', username=reply.author.username) }}">
|
||||||
|
{{ reply.author.display_name }}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a name="reply-{{ reply.id }}" class="text-muted float-right"
|
||||||
|
href="{{ url_for('threads.view', id=review.thread.id) }}#reply-{{ reply.id }}">
|
||||||
|
{{ reply.created_at | datetime }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
<p>
|
||||||
|
<strong>{{ review.thread.title }}</strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{{ reply.comment | markdown }}
|
||||||
|
|
||||||
|
<a class="btn btn-primary" 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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
|
||||||
|
{% macro render_review_form(package, current_user) -%}
|
||||||
|
<div class="card mt-0 mb-4 ">
|
||||||
|
<div class="card-header">
|
||||||
|
{{ _("Review") }}
|
||||||
|
</div>
|
||||||
|
<form method="post" action="{{ url_for('packages.review', author=package.author.username, name=package.name) }}" class="card-body">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||||
|
<p>
|
||||||
|
{{ _("Do you recommend this %(type)s?", type=package.type.value | lower) }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="btn-group btn-group-toggle" data-toggle="buttons">
|
||||||
|
<label class="btn btn-primary">
|
||||||
|
<i class="fas fa-thumbs-up mr-2"></i>
|
||||||
|
<input type="radio" name="recommends" id="yes" autocomplete="off"> {{ _("Yes") }}
|
||||||
|
</label>
|
||||||
|
<label class="btn btn-primary">
|
||||||
|
<i class="fas fa-thumbs-down mr-2"></i>
|
||||||
|
<input type="radio" name="recommends" id="no" autocomplete="off"> {{ _("No") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="mt-4 mb-3">
|
||||||
|
{{ _("Why or why not? Try to be constructive") }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="title">{{ _("Title") }}</label>
|
||||||
|
<input class="form-control" id="title" name="title" required="" type="text">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<textarea class="form-control markdown" required maxlength=500 name="comment"></textarea><br />
|
||||||
|
<input class="btn btn-primary" type="submit" value="{{ _('Post Review') }}" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
|
||||||
|
{% macro render_review_preview(package, current_user) -%}
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||||
|
<div class="card mt-0 mb-4 ">
|
||||||
|
<div class="card-header">
|
||||||
|
{{ _("Review") }}
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p>
|
||||||
|
{{ _("Do you recommend this %(type)s?", type=package.type.value | lower) }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% set review_url = url_for('packages.review', author=package.author.username, name=package.name) %}
|
||||||
|
|
||||||
|
<div class="btn-group">
|
||||||
|
<a class="btn btn-primary" href="{{ url_for('user.login', r=review_url) }}">
|
||||||
|
<i class="fas fa-thumbs-up mr-2"></i>
|
||||||
|
{{ _("Yes") }}
|
||||||
|
</a>
|
||||||
|
<a class="btn btn-primary" href="{{ url_for('user.login', r=review_url) }}">
|
||||||
|
<i class="fas fa-thumbs-down mr-2"></i>
|
||||||
|
{{ _("No") }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
45
app/templates/packages/review_create_edit.html
Normal file
45
app/templates/packages/review_create_edit.html
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
{{ _("Review") }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h1>{{ _("Review") }}</h1>
|
||||||
|
|
||||||
|
{% from "macros/forms.html" import render_field, render_submit_field, render_radio_field %}
|
||||||
|
<form method="POST" action="" enctype="multipart/form-data">
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
<div class="row mt-0 mb-4 comments mx-0">
|
||||||
|
<div class="col-md-1 p-1">
|
||||||
|
<img class="img-responsive user-photo img-thumbnail img-thumbnail-1" src="{{ current_user.getProfilePicURL() }}">
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header {{ current_user.rank.name }}">
|
||||||
|
{{ current_user.display_name }}
|
||||||
|
<a name="reply"></a>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p>
|
||||||
|
{{ _("Do you recommend this %(type)s?", type=package.type.value | lower) }}
|
||||||
|
</p>
|
||||||
|
{{ render_radio_field(form.recommends) }}
|
||||||
|
|
||||||
|
<p class="mt-4 mb-3">
|
||||||
|
{{ _("Why or why not? Try to be constructive") }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{{ render_field(form.title) }}
|
||||||
|
{{ render_field(form.comment, label="", class_="m-0", fieldclass="form-control markdown") }} <br />
|
||||||
|
{{ render_submit_field(form.submit) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -459,6 +459,16 @@
|
|||||||
|
|
||||||
<div style="clear: both;"></div>
|
<div style="clear: both;"></div>
|
||||||
|
|
||||||
|
<h3>Ratings and Reviews</h3>
|
||||||
|
|
||||||
|
{% from "macros/reviews.html" import render_reviews, render_review_form, render_review_preview %}
|
||||||
|
{% if current_user.is_authenticated %}
|
||||||
|
{{ render_review_form(package, current_user) }}
|
||||||
|
{% else %}
|
||||||
|
{{ render_review_preview(package) }}
|
||||||
|
{% endif %}
|
||||||
|
{{ render_reviews(package.reviews) }}
|
||||||
|
|
||||||
{#
|
{#
|
||||||
{% if current_user.is_authenticated or requests %}
|
{% if current_user.is_authenticated or requests %}
|
||||||
<h3>Edit Requests</h3>
|
<h3>Edit Requests</h3>
|
||||||
|
@ -19,12 +19,21 @@ Threads
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<h1>{% if thread.private %}🔒 {% endif %}{{ thread.title }}</h1>
|
<h1>
|
||||||
|
{% if thread.review %}
|
||||||
{% if thread.package or current_user.is_authenticated %}
|
{% if thread.review.recommends %}
|
||||||
{% if thread.package %}
|
<i class="fas fa-thumbs-up mr-2" style="color:#6f6;"></i>
|
||||||
<p>Package: <a href="{{ thread.package.getDetailsURL() }}">{{ thread.package.title }}</a></p>
|
{% else %}
|
||||||
|
<i class="fas fa-thumbs-down mr-2" style="color:#f66;"></i>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if thread.private %}🔒 {% endif %}{{ thread.title }}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{% if thread.package %}
|
||||||
|
<p>
|
||||||
|
Package: <a href="{{ thread.package.getDetailsURL() }}">{{ thread.package.title }}</a>
|
||||||
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if thread.private %}
|
{% if thread.private %}
|
||||||
|
40
migrations/versions/4f2e19bc2a27_.py
Normal file
40
migrations/versions/4f2e19bc2a27_.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 4f2e19bc2a27
|
||||||
|
Revises: dd27f1311a90
|
||||||
|
Create Date: 2020-07-09 00:35:35.066719
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '4f2e19bc2a27'
|
||||||
|
down_revision = 'dd27f1311a90'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('package_review',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('package_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('author_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('recommends', sa.Boolean(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['author_id'], ['user.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['package_id'], ['package.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.add_column('thread', sa.Column('review_id', sa.Integer(), nullable=True))
|
||||||
|
op.create_foreign_key(None, 'thread', 'package_review', ['review_id'], ['id'])
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_constraint(None, 'thread', type_='foreignkey')
|
||||||
|
op.drop_column('thread', 'review_id')
|
||||||
|
op.drop_table('package_review')
|
||||||
|
# ### end Alembic commands ###
|
Loading…
Reference in New Issue
Block a user