From 6a674c3c79437c7b4829947c67dd7f14b32c1941 Mon Sep 17 00:00:00 2001 From: rubenwardy Date: Fri, 17 Jul 2020 20:48:51 +0100 Subject: [PATCH] Add Content Warnings --- app/blueprints/admin/__init__.py | 2 +- app/blueprints/admin/warningseditor.py | 59 +++++++++++++++++++++++++ app/blueprints/packages/packages.py | 39 +++++++++------- app/models.py | 25 +++++++++++ app/templates/admin/list.html | 1 + app/templates/admin/warnings/edit.html | 26 +++++++++++ app/templates/admin/warnings/list.html | 52 ++++++++++++++++++++++ app/templates/packages/create_edit.html | 1 + app/templates/packages/view.html | 7 +++ migrations/versions/b370c3eb4227_.py | 56 +++++++++++++++++++++++ 10 files changed, 251 insertions(+), 17 deletions(-) create mode 100644 app/blueprints/admin/warningseditor.py create mode 100644 app/templates/admin/warnings/edit.html create mode 100644 app/templates/admin/warnings/list.html create mode 100644 migrations/versions/b370c3eb4227_.py diff --git a/app/blueprints/admin/__init__.py b/app/blueprints/admin/__init__.py index c48f97c..1aa06c3 100644 --- a/app/blueprints/admin/__init__.py +++ b/app/blueprints/admin/__init__.py @@ -19,4 +19,4 @@ from flask import Blueprint bp = Blueprint("admin", __name__) -from . import admin, licenseseditor, tagseditor, versioneditor, audit +from . import admin, audit, licenseseditor, tagseditor, versioneditor, warningseditor diff --git a/app/blueprints/admin/warningseditor.py b/app/blueprints/admin/warningseditor.py new file mode 100644 index 0000000..418d052 --- /dev/null +++ b/app/blueprints/admin/warningseditor.py @@ -0,0 +1,59 @@ +# ContentDB +# 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 . + + +from flask import * +from flask_user import * +from . import bp +from app.models import * +from flask_wtf import FlaskForm +from wtforms import * +from wtforms.validators import * +from app.utils import rank_required + +@bp.route("/admin/warnings/") +@rank_required(UserRank.ADMIN) +def warning_list(): + return render_template("admin/warnings/list.html", warnings=ContentWarning.query.order_by(db.asc(ContentWarning.title)).all()) + +class WarningForm(FlaskForm): + title = StringField("Title", [InputRequired(), Length(3,100)]) + name = StringField("Name", [Optional(), Length(1, 20), Regexp("^[a-z0-9_]", 0, "Lower case letters (a-z), digits (0-9), and underscores (_) only")]) + description = TextAreaField("Description", [InputRequired(), Length(0, 500)]) + submit = SubmitField("Save") + +@bp.route("/admin/warnings/new/", methods=["GET", "POST"]) +@bp.route("/admin/warnings//edit/", methods=["GET", "POST"]) +@rank_required(UserRank.ADMIN) +def create_edit_warning(name=None): + warning = None + if name is not None: + warning = ContentWarning.query.filter_by(name=name).first() + if warning is None: + abort(404) + + form = WarningForm(formdata=request.form, obj=warning) + if request.method == "POST" and form.validate(): + if warning is None: + warning = ContentWarning(form.title.data, form.description.data) + db.session.add(warning) + else: + form.populate_obj(warning) + db.session.commit() + + return redirect(url_for("admin.warning_list")) + + return render_template("admin/warnings/edit.html", warning=warning, form=form) diff --git a/app/blueprints/packages/packages.py b/app/blueprints/packages/packages.py index 716e79e..5a775b2 100644 --- a/app/blueprints/packages/packages.py +++ b/app/blueprints/packages/packages.py @@ -198,22 +198,24 @@ def download(package): class PackageForm(FlaskForm): - name = StringField("Name (Technical)", [InputRequired(), Length(1, 100), Regexp("^[a-z0-9_]+$", 0, "Lower case letters (a-z), digits (0-9), and underscores (_) only")]) - title = StringField("Title (Human-readable)", [InputRequired(), Length(3, 100)]) - short_desc = StringField("Short Description (Plaintext)", [InputRequired(), Length(1,200)]) - desc = TextAreaField("Long Description (Markdown)", [Optional(), Length(0,10000)]) - type = SelectField("Type", [InputRequired()], choices=PackageType.choices(), coerce=PackageType.coerce, default=PackageType.MOD) - license = QuerySelectField("License", [DataRequired()], allow_blank=True, query_factory=lambda: License.query.order_by(db.asc(License.name)), get_pk=lambda a: a.id, get_label=lambda a: a.name) - media_license = QuerySelectField("Media License", [DataRequired()], allow_blank=True, query_factory=lambda: License.query.order_by(db.asc(License.name)), get_pk=lambda a: a.id, get_label=lambda a: a.name) - provides_str = StringField("Provides (mods included in package)", [Optional()]) - tags = QuerySelectMultipleField('Tags', query_factory=lambda: Tag.query.order_by(db.asc(Tag.name)), get_pk=lambda a: a.id, get_label=lambda a: a.title) - harddep_str = StringField("Hard Dependencies", [Optional()]) - softdep_str = StringField("Soft Dependencies", [Optional()]) - repo = StringField("VCS Repository URL", [Optional(), URL()], filters = [lambda x: x or None]) - website = StringField("Website URL", [Optional(), URL()], filters = [lambda x: x or None]) - issueTracker = StringField("Issue Tracker URL", [Optional(), URL()], filters = [lambda x: x or None]) - forums = IntegerField("Forum Topic ID", [Optional(), NumberRange(0,999999)]) - submit = SubmitField("Save") + name = StringField("Name (Technical)", [InputRequired(), Length(1, 100), Regexp("^[a-z0-9_]+$", 0, "Lower case letters (a-z), digits (0-9), and underscores (_) only")]) + title = StringField("Title (Human-readable)", [InputRequired(), Length(3, 100)]) + short_desc = StringField("Short Description (Plaintext)", [InputRequired(), Length(1,200)]) + desc = TextAreaField("Long Description (Markdown)", [Optional(), Length(0,10000)]) + type = SelectField("Type", [InputRequired()], choices=PackageType.choices(), coerce=PackageType.coerce, default=PackageType.MOD) + license = QuerySelectField("License", [DataRequired()], allow_blank=True, query_factory=lambda: License.query.order_by(db.asc(License.name)), get_pk=lambda a: a.id, get_label=lambda a: a.name) + media_license = QuerySelectField("Media License", [DataRequired()], allow_blank=True, query_factory=lambda: License.query.order_by(db.asc(License.name)), get_pk=lambda a: a.id, get_label=lambda a: a.name) + provides_str = StringField("Provides (mods included in package)", [Optional()]) + tags = QuerySelectMultipleField('Tags', query_factory=lambda: Tag.query.order_by(db.asc(Tag.name)), get_pk=lambda a: a.id, get_label=lambda a: a.title) + content_warnings = QuerySelectMultipleField('Content Warnings', query_factory=lambda: ContentWarning.query.order_by(db.asc(ContentWarning.name)), get_pk=lambda a: a.id, get_label=lambda a: a.title) + harddep_str = StringField("Hard Dependencies", [Optional()]) + softdep_str = StringField("Soft Dependencies", [Optional()]) + repo = StringField("VCS Repository URL", [Optional(), URL()], filters = [lambda x: x or None]) + website = StringField("Website URL", [Optional(), URL()], filters = [lambda x: x or None]) + issueTracker = StringField("Issue Tracker URL", [Optional(), URL()], filters = [lambda x: x or None]) + forums = IntegerField("Forum Topic ID", [Optional(), NumberRange(0,999999)]) + submit = SubmitField("Save") + @bp.route("/packages/new/", methods=["GET", "POST"]) @bp.route("/packages///edit/", methods=["GET", "POST"]) @@ -259,6 +261,7 @@ def create_edit(author=None, name=None): form.softdep_str.data = ",".join([str(x) for x in package.getSortedOptionalDependencies() ]) form.provides_str.data = MetaPackage.ListToSpec(package.provides) form.tags.data = list(package.tags) + form.content_warnings.data = list(package.content_warnings) if request.method == "POST" and form.validate(): wasNew = False @@ -320,6 +323,10 @@ def create_edit(author=None, name=None): for tag in form.tags.raw_data: package.tags.append(Tag.query.get(tag)) + package.content_warnings.clear() + for warning in form.content_warnings.raw_data: + package.content_warnings.append(ContentWarning.query.get(warning)) + db.session.commit() # save next_url = package.getDetailsURL() diff --git a/app/models.py b/app/models.py index e6ff6f0..b2dbcc0 100644 --- a/app/models.py +++ b/app/models.py @@ -358,6 +358,11 @@ Tags = db.Table("tags", db.Column("package_id", db.Integer, db.ForeignKey("package.id"), primary_key=True) ) +ContentWarnings = db.Table("content_warnings", + db.Column("content_warning_id", db.Integer, db.ForeignKey("content_warning.id"), primary_key=True), + db.Column("package_id", db.Integer, db.ForeignKey("package.id"), primary_key=True) +) + maintainers = db.Table("maintainers", db.Column("user_id", db.Integer, db.ForeignKey("user.id"), primary_key=True), db.Column("package_id", db.Integer, db.ForeignKey("package.id"), primary_key=True) @@ -488,6 +493,9 @@ class Package(db.Model): tags = db.relationship("Tag", secondary=Tags, lazy="select", backref=db.backref("packages", lazy=True)) + content_warnings = db.relationship("ContentWarning", secondary=ContentWarnings, lazy="select", + backref=db.backref("packages", lazy=True)) + releases = db.relationship("PackageRelease", backref="package", lazy="dynamic", order_by=db.desc("package_release_releaseDate")) @@ -816,6 +824,23 @@ class MetaPackage(db.Model): return retval + +class ContentWarning(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(100), unique=True, nullable=False) + title = db.Column(db.String(100), nullable=False) + description = db.Column(db.String(500), nullable=False) + + def __init__(self, title, description=""): + self.title = title + self.description = description + + import re + regex = re.compile("[^a-z_]") + self.name = regex.sub("", self.title.lower().replace(" ", "_")) + + + class Tag(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(100), unique=True, nullable=False) diff --git a/app/templates/admin/list.html b/app/templates/admin/list.html index 75fb61e..211c86d 100644 --- a/app/templates/admin/list.html +++ b/app/templates/admin/list.html @@ -12,6 +12,7 @@ Tag Editor License Editor Version Editor + Warning Editor Sign in as another user diff --git a/app/templates/admin/warnings/edit.html b/app/templates/admin/warnings/edit.html new file mode 100644 index 0000000..2e6e71a --- /dev/null +++ b/app/templates/admin/warnings/edit.html @@ -0,0 +1,26 @@ +{% extends "base.html" %} + +{% block title %} + {% if warning %} + Edit {{ warning.title }} + {% else %} + New warning + {% endif %} +{% endblock %} + +{% block content %} + New Warning + Back to list + + {% from "macros/forms.html" import render_field, render_submit_field %} +
+ {{ form.hidden_tag() }} + + {{ render_field(form.title) }} + {{ render_field(form.description) }} + {% if warning %} + {{ render_field(form.name) }} + {% endif %} + {{ render_submit_field(form.submit) }} +
+{% endblock %} diff --git a/app/templates/admin/warnings/list.html b/app/templates/admin/warnings/list.html new file mode 100644 index 0000000..96a299c --- /dev/null +++ b/app/templates/admin/warnings/list.html @@ -0,0 +1,52 @@ +{% extends "base.html" %} + +{% block title %} +{{ _("Warnings") }} +{% endblock %} + +{% block content %} + {{ _("New Warning") }} + +

{{ _("Warnings") }}

+ +

+ Also see Package Flags. +

+ +
+
+
+
+ {{ _("Name") }} +
+ +
+ {{ _("Description") }} +
+ +
+ {{ _("Packages") }} +
+
+
+ + {% for t in warnings %} + +
+
+ {{ t.title }} +
+ +
+ {{ t.description }} +
+ +
+ {{ t.packages | count }} +
+
+
+ {% endfor %} +
+{% endblock %} diff --git a/app/templates/packages/create_edit.html b/app/templates/packages/create_edit.html index 3e9277e..44280bc 100644 --- a/app/templates/packages/create_edit.html +++ b/app/templates/packages/create_edit.html @@ -66,6 +66,7 @@ {{ render_field(form.short_desc, class_="pkg_meta") }} {{ render_multiselect_field(form.tags, class_="pkg_meta") }} + {{ render_multiselect_field(form.content_warnings, class_="pkg_meta") }}
{{ render_field(form.license, class_="not_txp col-sm-6") }} {{ render_field(form.media_license, class_="col-sm-6") }} diff --git a/app/templates/packages/view.html b/app/templates/packages/view.html index 00eea2e..f2c22e3 100644 --- a/app/templates/packages/view.html +++ b/app/templates/packages/view.html @@ -47,6 +47,13 @@ {{ package_warning }} {% endif %} + {% for warning in package.content_warnings %} + + + {{ warning.title }} + + {% endfor %} {% for t in package.tags %} {{ t.title }} diff --git a/migrations/versions/b370c3eb4227_.py b/migrations/versions/b370c3eb4227_.py new file mode 100644 index 0000000..4377db5 --- /dev/null +++ b/migrations/versions/b370c3eb4227_.py @@ -0,0 +1,56 @@ +"""empty message + +Revision ID: b370c3eb4227 +Revises: c5e4213721dd +Create Date: 2020-07-17 19:22:15.267179 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy import orm +from app.models import ContentWarning + + +# revision identifiers, used by Alembic. +revision = 'b370c3eb4227' +down_revision = 'c5e4213721dd' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('content_warning', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=100), nullable=False), + sa.Column('title', sa.String(length=100), nullable=False), + sa.Column('description', sa.String(length=500), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name') + ) + op.create_table('content_warnings', + sa.Column('content_warning_id', sa.Integer(), nullable=False), + sa.Column('package_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['content_warning_id'], ['content_warning.id'], ), + sa.ForeignKeyConstraint(['package_id'], ['package.id'], ), + sa.PrimaryKeyConstraint('content_warning_id', 'package_id') + ) + + bind = op.get_bind() + session = orm.Session(bind=bind) + + session.add(ContentWarning("Violence", "Non-cartoon violence")) + session.add(ContentWarning("Drugs", "Drugs or alcohol")) + session.add(ContentWarning("Bad Language")) + session.add(ContentWarning("Gambling")) + session.add(ContentWarning("Horror")) + session.commit() + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('content_warnings') + op.drop_table('content_warning') + # ### end Alembic commands ###