Add Featured packages and protected tags

This commit is contained in:
rubenwardy 2021-07-22 12:13:16 +01:00
parent ce2bb3abad
commit 148ece162c
10 changed files with 143 additions and 12 deletions

@ -44,6 +44,7 @@ class TagForm(FlaskForm):
title = StringField("Title", [InputRequired(), Length(3,100)]) title = StringField("Title", [InputRequired(), Length(3,100)])
description = TextAreaField("Description", [Optional(), Length(0, 500)]) description = TextAreaField("Description", [Optional(), Length(0, 500)])
name = StringField("Name", [Optional(), Length(1, 20), Regexp("^[a-z0-9_]", 0, "Lower case letters (a-z), digits (0-9), and underscores (_) only")]) name = StringField("Name", [Optional(), Length(1, 20), Regexp("^[a-z0-9_]", 0, "Lower case letters (a-z), digits (0-9), and underscores (_) only")])
is_protected = BooleanField("Is Protected")
submit = SubmitField("Save") submit = SubmitField("Save")
@bp.route("/tags/new/", methods=["GET", "POST"]) @bp.route("/tags/new/", methods=["GET", "POST"])
@ -59,14 +60,16 @@ def create_edit_tag(name=None):
if not Permission.checkPerm(current_user, Permission.EDIT_TAGS if tag else Permission.CREATE_TAG): if not Permission.checkPerm(current_user, Permission.EDIT_TAGS if tag else Permission.CREATE_TAG):
abort(403) abort(403)
form = TagForm(formdata=request.form, obj=tag) form = TagForm( obj=tag)
if form.validate_on_submit(): if form.validate_on_submit():
if tag is None: if tag is None:
tag = Tag(form.title.data) tag = Tag(form.title.data)
tag.description = form.description.data tag.description = form.description.data
tag.is_protected = form.is_protected.data
db.session.add(tag) db.session.add(tag)
else: else:
form.populate_obj(tag) form.populate_obj(tag)
db.session.commit() db.session.commit()
if Permission.EDIT_TAGS.check(current_user): if Permission.EDIT_TAGS.check(current_user):

@ -364,6 +364,8 @@ def homepage():
query = Package.query.filter_by(state=PackageState.APPROVED) query = Package.query.filter_by(state=PackageState.APPROVED)
count = query.count() count = query.count()
featured = Package.query.filter(Package.tags.any(name="featured")).order_by(
func.random()).limit(6).all()
new = query.order_by(db.desc(Package.approved_at)).limit(4).all() new = query.order_by(db.desc(Package.approved_at)).limit(4).all()
pop_mod = query.filter_by(type=PackageType.MOD).order_by(db.desc(Package.score)).limit(8).all() pop_mod = query.filter_by(type=PackageType.MOD).order_by(db.desc(Package.score)).limit(8).all()
pop_gam = query.filter_by(type=PackageType.GAME).order_by(db.desc(Package.score)).limit(8).all() pop_gam = query.filter_by(type=PackageType.GAME).order_by(db.desc(Package.score)).limit(8).all()
@ -386,6 +388,7 @@ def homepage():
return { return {
"count": count, "count": count,
"downloads": downloads, "downloads": downloads,
"featured": featured,
"new": mapPackages(new), "new": mapPackages(new),
"updated": mapPackages(updated), "updated": mapPackages(updated),
"pop_mod": mapPackages(pop_mod), "pop_mod": mapPackages(pop_mod),

@ -18,6 +18,8 @@ def home():
query = Package.query.filter_by(state=PackageState.APPROVED) query = Package.query.filter_by(state=PackageState.APPROVED)
count = query.count() count = query.count()
featured = Package.query.filter(Package.tags.any(name="featured")).order_by(func.random()).limit(6).all()
new = join(query.order_by(db.desc(Package.approved_at))).limit(4).all() new = join(query.order_by(db.desc(Package.approved_at))).limit(4).all()
pop_mod = join(query.filter_by(type=PackageType.MOD).order_by(db.desc(Package.score))).limit(8).all() pop_mod = join(query.filter_by(type=PackageType.MOD).order_by(db.desc(Package.score))).limit(8).all()
pop_gam = join(query.filter_by(type=PackageType.GAME).order_by(db.desc(Package.score))).limit(8).all() pop_gam = join(query.filter_by(type=PackageType.GAME).order_by(db.desc(Package.score))).limit(8).all()
@ -39,5 +41,5 @@ def home():
tags = db.session.query(func.count(Tags.c.tag_id), Tag) \ tags = db.session.query(func.count(Tags.c.tag_id), Tag) \
.select_from(Tag).outerjoin(Tags).group_by(Tag.id).order_by(db.asc(Tag.title)).all() .select_from(Tag).outerjoin(Tags).group_by(Tag.id).order_by(db.asc(Tag.title)).all()
return render_template("index.html", count=count, downloads=downloads, tags=tags, return render_template("index.html", count=count, downloads=downloads, tags=tags, featured=featured,
new=new, updated=updated, pop_mod=pop_mod, pop_txp=pop_txp, pop_gam=pop_gam, high_reviewed=high_reviewed, reviews=reviews) new=new, updated=updated, pop_mod=pop_mod, pop_txp=pop_txp, pop_gam=pop_gam, high_reviewed=high_reviewed, reviews=reviews)

@ -19,7 +19,7 @@ import re
import validators import validators
from app.logic.LogicError import LogicError from app.logic.LogicError import LogicError
from app.models import User, Package, PackageType, MetaPackage, Tag, ContentWarning, db, Permission, AuditSeverity, License from app.models import User, Package, PackageType, MetaPackage, Tag, ContentWarning, db, Permission, AuditSeverity, License, UserRank
from app.utils import addAuditLog from app.utils import addAuditLog
@ -134,14 +134,19 @@ def do_edit_package(user: User, package: Package, was_new: bool, data: dict, rea
package.provides.append(m) package.provides.append(m)
if "tags" in data: if "tags" in data:
old_tags = package.tags
package.tags.clear() package.tags.clear()
for tag_id in data["tags"]: for tag_id in data["tags"]:
if is_int(tag_id): if is_int(tag_id):
package.tags.append(Tag.query.get(tag_id)) tag = Tag.query.get(tag_id)
else: else:
tag = Tag.query.filter_by(name=tag_id).first() tag = Tag.query.filter_by(name=tag_id).first()
if tag is None: if tag is None:
raise LogicError(400, "Unknown tag: " + tag_id) raise LogicError(400, "Unknown tag: " + tag_id)
if tag.is_protected and tag not in old_tags and not user.rank.atLeast(UserRank.EDITOR):
raise LogicError(400, f"Unable to add protected tag {tag.title} to package")
package.tags.append(tag) package.tags.append(tag)
if "content_warnings" in data: if "content_warnings" in data:

@ -744,6 +744,7 @@ class Tag(db.Model):
backgroundColor = db.Column(db.String(6), nullable=False) backgroundColor = db.Column(db.String(6), nullable=False)
textColor = db.Column(db.String(6), nullable=False) textColor = db.Column(db.String(6), nullable=False)
views = db.Column(db.Integer, nullable=False, default=0) views = db.Column(db.Integer, nullable=False, default=0)
is_protected = db.Column(db.Boolean, nullable=False, default=False)
packages = db.relationship("Package", back_populates="tags", secondary=Tags) packages = db.relationship("Package", back_populates="tags", secondary=Tags)

@ -173,4 +173,12 @@ pre code {
} }
} }
.fs-2 {
font-size: calc(1.325rem + .9vw) !important;
}
.text-shadow {
text-shadow: 3px 3px 3px rgba(10,10,10,0.2);
}
@import "dracula.scss"; @import "dracula.scss";

@ -70,6 +70,23 @@ def getMeta(urlstr, author):
return result return result
def get_edit_data_from_dir(dir: str):
data = {}
for path in [os.path.join(dir, ".cdb.json"), os.path.join(dir, ".cdb", "meta.json")]:
if os.path.isfile(path):
with open(path, "r") as f:
data = json.loads(f.read())
break
for path in [os.path.join(dir, ".cdb.md"), os.path.join(dir, ".cdb", "long_description.md")]:
if os.path.isfile(path):
with open(path, "r") as f:
data["long_description"] = f.read().replace("\r\n", "\n")
break
return data
def postReleaseCheckUpdate(self, release: PackageRelease, path): def postReleaseCheckUpdate(self, release: PackageRelease, path):
try: try:
tree = build_tree(path, expected_type=ContentType[release.package.type.name], tree = build_tree(path, expected_type=ContentType[release.package.type.name],
@ -117,13 +134,11 @@ def postReleaseCheckUpdate(self, release: PackageRelease, path):
release.max_rel = MinetestRelease.get(tree.meta["max_minetest_version"], None) release.max_rel = MinetestRelease.get(tree.meta["max_minetest_version"], None)
try: try:
with open(os.path.join(tree.baseDir, ".cdb.json"), "r") as f: data = get_edit_data_from_dir(tree.baseDir)
data = json.loads(f.read()) if data != {}: # Not sure if this will actually work to check not empty, probably not
do_edit_package(package.author, package, False, data, "Post release hook") do_edit_package(package.author, package, False, data, "Post release hook")
except LogicError as e: except LogicError as e:
raise TaskError(e.message) raise TaskError(e.message)
except IOError:
pass
return tree return tree

@ -12,7 +12,7 @@
<a class="btn btn-primary float-right" href="{{ url_for('admin.create_edit_tag') }}">New Tag</a> <a class="btn btn-primary float-right" href="{{ url_for('admin.create_edit_tag') }}">New Tag</a>
<a class="btn btn-secondary mb-4" href="{{ url_for('admin.tag_list') }}">Back to list</a> <a class="btn btn-secondary mb-4" href="{{ url_for('admin.tag_list') }}">Back to list</a>
{% from "macros/forms.html" import render_field, render_submit_field %} {% from "macros/forms.html" import render_field, render_submit_field, render_checkbox_field %}
<form method="POST" action="" enctype="multipart/form-data"> <form method="POST" action="" enctype="multipart/form-data">
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
@ -21,6 +21,10 @@
{% if tag %} {% if tag %}
{{ render_field(form.name) }} {{ render_field(form.name) }}
{% endif %} {% endif %}
<div class="form-group my-5">
{{ render_checkbox_field(form.is_protected) }}
<small class="form-text text-muted">Whether non-Editors can add this tag to packages</small>
</div>
{{ render_submit_field(form.submit) }} {{ render_submit_field(form.submit) }}
</form> </form>
{% endblock %} {% endblock %}

@ -21,7 +21,69 @@
{% block content %} {% block content %}
{% from "macros/packagegridtile.html" import render_pkggrid %} {% from "macros/packagegridtile.html" import render_pkggrid %}
<div id="featuredCarousel" class="carousel slide mb-5" data-ride="carousel">
<ol class="carousel-indicators">
{% for package in featured %}
<li data-target="#featuredCarousel" data-slide-to="{{ loop.index - 1 }}" {% if loop.index == 1 %}class="active"{% endif %}></li>
{% endfor %}
</ol>
<div class="carousel-inner">
{% for package in featured %}
{% set cover_image = package.cover_image.url or package.getMainScreenshotURL() %}
{% set tags = package.tags | sort(attribute="views", reverse=True) %}
<div class="carousel-item {% if loop.index == 1 %}active{% endif %}">
<a href="{{ package.getDetailsURL() }}">
<div class="embed-responsive embed-responsive-16by9">
<img class="embed-responsive-item" src="{{ cover_image }}"
alt="{{ _('%(title)s by %(author)s', title=package.title, author=package.author.display_name) }}">
</div>
<div class="carousel-caption d-none d-md-block text-shadow">
<h3 class="mt-0 mb-3 fs-2">
{{ _('<strong>%(title)s</strong> by %(author)s', title=package.title, author=package.author.display_name) }}
</h3>
<p>
{{ package.short_desc }}
</p>
<div>
<span class="mr-2">
{{ package.type.value }}
</span>
{% for warning in package.content_warnings %}
<span class="badge badge-warning" title="{{ warning.description }}">
<i class="fas fa-exclamation-circle" style="margin-right: 0.3em;"></i>
{{ warning.title }}
</span>
{% endfor %}
{% for t in tags[:3] %}
{% if t.name != "featured" %}
<span class="badge badge-primary" title="{{ t.description or '' }}">
{{ t.title }}
</span>
{% endif %}
{% endfor %}
<span class="btn" title="{{ _("Reviews") }}">
<i class="fas fa-star-half-alt"></i>
<span class="count">
+{{ package.reviews | selectattr("recommends") | list | length }}
/
-{{ package.reviews | rejectattr("recommends") | list | length }}
</span>
</span>
</div>
</div>
</a>
</div>
{% endfor %}
</div>
<a class="carousel-control-prev" href="#featuredCarousel" role="button" data-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="sr-only">{{ _("Previous") }}</span>
</a>
<a class="carousel-control-next" href="#featuredCarousel" role="button" data-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="sr-only">{{ _("Next") }}</span>
</a>
</div>
<a href="{{ url_for('packages.list_all', sort='approved_at', order='desc') }}" class="btn btn-secondary float-right"> <a href="{{ url_for('packages.list_all', sort='approved_at', order='desc') }}" class="btn btn-secondary float-right">
{{ _("See more") }} {{ _("See more") }}
@ -76,7 +138,7 @@
<a href="{{ url_for('packages.list_all', sort='reviews', order='desc') }}" class="btn btn-secondary float-right"> <a href="{{ url_for('packages.list_all', sort='reviews', order='desc') }}" class="btn btn-secondary float-right">
{{ _("See more") }} {{ _("See more") }}
</a> </a>
<h2 class="my-3">{{ _("Top Reviewed") }}</h2> <h2 class="my-3">{{ _("Highest Reviewed") }}</h2>
{{ render_pkggrid(high_reviewed) }} {{ render_pkggrid(high_reviewed) }}

@ -0,0 +1,28 @@
"""empty message
Revision ID: d4262fb15b37
Revises: 8ee3cf3fb312
Create Date: 2021-07-22 10:59:03.217264
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = 'd4262fb15b37'
down_revision = '8ee3cf3fb312'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('tag', sa.Column('is_protected', sa.Boolean(), nullable=False, server_default="false"))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('tag', 'is_protected')
# ### end Alembic commands ###