]> git.lizzy.rs Git - cheatdb.git/blobdiff - app/blueprints/packages/packages.py
Add last updated section to homepage
[cheatdb.git] / app / blueprints / packages / packages.py
index 700fc44caa46e3be8f52942676b6d3f767bba677..6ff6623bcfb69271bf823847b305a0d018e38c6d 100644 (file)
@@ -1,4 +1,4 @@
-# Content DB
+# ContentDB
 # Copyright (C) 2018  rubenwardy
 #
 # This program is free software: you can redistribute it and/or modify
@@ -23,7 +23,8 @@ from . import bp
 
 from app.models import *
 from app.querybuilder import QueryBuilder
-from app.tasks.importtasks import importRepoScreenshot
+from app.tasks.importtasks import importRepoScreenshot, updateMetaFromRelease
+from app.rediscache import has_key, set_key
 from app.utils import *
 
 from flask_wtf import FlaskForm
@@ -33,6 +34,8 @@ from wtforms.ext.sqlalchemy.fields import QuerySelectField, QuerySelectMultipleF
 from sqlalchemy import or_, func
 from sqlalchemy.orm import joinedload, subqueryload
 
+from celery import uuid
+
 
 @menu.register_menu(bp, ".mods", "Mods", order=11, endpoint_arguments_constructor=lambda: { 'type': 'mod' })
 @menu.register_menu(bp, ".games", "Games", order=12, endpoint_arguments_constructor=lambda: { 'type': 'game' })
@@ -49,6 +52,21 @@ def list_all():
                        joinedload(Package.media_license), \
                        subqueryload(Package.tags))
 
+       ip = request.headers.get("X-Forwarded-For") or request.remote_addr
+       if ip is not None and not is_user_bot():
+               edited = False
+               for tag in qb.tags:
+                       edited = True
+                       key = "tag/{}/{}".format(ip, tag.name)
+                       if not has_key(key):
+                               set_key(key, "true")
+                               Tag.query.filter_by(id=tag.id).update({
+                                               "views": Tag.views + 1
+                                       })
+
+               if edited:
+                       db.session.commit()
+
        if qb.lucky:
                package = query.first()
                if package:
@@ -65,11 +83,6 @@ def list_all():
        search = request.args.get("q")
        type_name = request.args.get("type")
 
-       next_url = url_for("packages.list_all", type=type_name, q=search, page=query.next_num) \
-                       if query.has_next else None
-       prev_url = url_for("packages.list_all", type=type_name, q=search, page=query.prev_num) \
-                       if query.has_prev else None
-
        authors = []
        if search:
                authors = User.query \
@@ -83,12 +96,15 @@ def list_all():
                qb.show_discarded = True
                topics = qb.buildTopicQuery().all()
 
-       tags = Tag.query.all()
+       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()
+
+       selected_tags = set(qb.tags)
+
        return render_template("packages/list.html", \
-                       title=title, packages=query.items, topics=topics, \
-                       query=search, tags=tags, type=type_name, \
-                       authors = authors, \
-                       next_url=next_url, prev_url=prev_url, page=page, page_max=query.pages, packages_count=query.total)
+                       title=title, packages=query.items, pagination=query, \
+                       query=search, tags=tags, selected_tags=selected_tags, type=type_name, \
+                       authors=authors, packages_count=query.total, topics=topics)
 
 
 def getReleases(package):
@@ -101,8 +117,6 @@ def getReleases(package):
 @bp.route("/packages/<author>/<name>/")
 @is_package_page
 def view(package):
-       clearNotifications(package.getDetailsURL())
-
        alternatives = None
        if package.type == PackageType.MOD:
                alternatives = Package.query \
@@ -152,18 +166,19 @@ def view(package):
                topic_error = "<br />".join(errors)
 
 
-       threads = Thread.query.filter_by(package_id=package.id)
+       threads = Thread.query.filter_by(package_id=package.id, review_id=None)
        if not current_user.is_authenticated:
                threads = threads.filter_by(private=False)
        elif not current_user.rank.atLeast(UserRank.EDITOR) and not current_user == package.author:
                threads = threads.filter(or_(Thread.private == False, Thread.author == current_user))
 
+       has_review = current_user.is_authenticated and PackageReview.query.filter_by(package=package, author=current_user).count() > 0
 
        return render_template("packages/view.html", \
                        package=package, releases=releases, requests=requests, \
                        alternatives=alternatives, similar_topics=similar_topics, \
                        review_thread=review_thread, topic_error=topic_error, topic_error_lvl=topic_error_lvl, \
-                       threads=threads.all())
+                       threads=threads.all(), has_review=has_review)
 
 
 @bp.route("/packages/<author>/<name>/download/")
@@ -182,23 +197,31 @@ def download(package):
                return redirect(release.getDownloadURL(), code=302)
 
 
+def makeLabel(obj):
+       if obj.description:
+               return "{}: {}".format(obj.title, obj.description)
+       else:
+               return obj.title
+
 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=makeLabel)
+       content_warnings = QuerySelectMultipleField('Content Warnings', query_factory=lambda: ContentWarning.query.order_by(db.asc(ContentWarning.name)), get_pk=lambda a: a.id, get_label=makeLabel)
+       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/<author>/<name>/edit/", methods=["GET", "POST"])
@@ -243,6 +266,8 @@ def create_edit(author=None, name=None):
                        form.harddep_str.data  = ",".join([str(x) for x in package.getSortedHardDependencies() ])
                        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
@@ -257,6 +282,7 @@ def create_edit(author=None, name=None):
 
                        package = Package()
                        package.author = author
+                       package.maintainers.append(author)
                        wasNew = True
 
                elif package.approved and package.name != form.name.data and \
@@ -265,12 +291,17 @@ def create_edit(author=None, name=None):
                        return redirect(url_for("packages.create_edit", author=author, name=name))
 
                else:
-                       triggerNotif(package.author, current_user,
-                                       "{} edited".format(package.title), package.getDetailsURL())
+                       msg = "Edited {}".format(package.title)
+
+                       addNotification(package.maintainers, current_user,
+                                       msg, package.getDetailsURL(), package)
+
+                       severity = AuditSeverity.NORMAL if current_user in package.maintainers else AuditSeverity.EDITOR
+                       addAuditLog(severity, current_user, msg, package.getDetailsURL(), package)
 
                form.populate_obj(package) # copy to row
 
-               if package.type== PackageType.TXP:
+               if package.type == PackageType.TXP:
                        package.license = package.media_license
 
                mpackage_cache = {}
@@ -298,6 +329,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()
@@ -332,13 +367,16 @@ def approve(package):
 
        else:
                package.approved = True
+               package.approved_at = datetime.datetime.now()
 
                screenshots = PackageScreenshot.query.filter_by(package=package, approved=False).all()
                for s in screenshots:
                        s.approved = True
 
-               triggerNotif(package.author, current_user,
-                               "{} approved".format(package.title), package.getDetailsURL())
+               msg = "Approved {}".format(package.title)
+               addNotification(package.maintainers, current_user, msg, package.getDetailsURL(), package)
+               severity = AuditSeverity.NORMAL if current_user == package.author else AuditSeverity.EDITOR
+               addAuditLog(severity, current_user, msg, package.getDetailsURL(), package)
                db.session.commit()
 
        return redirect(package.getDetailsURL())
@@ -359,8 +397,9 @@ def remove(package):
                package.soft_deleted = True
 
                url = url_for("users.profile", username=package.author.username)
-               triggerNotif(package.author, current_user,
-                               "{} deleted".format(package.title), url)
+               msg = "Deleted {}".format(package.title)
+               addNotification(package.maintainers, current_user, msg, url, package)
+               addAuditLog(AuditSeverity.EDITOR, current_user, msg, url)
                db.session.commit()
 
                flash("Deleted package", "success")
@@ -373,8 +412,10 @@ def remove(package):
 
                package.approved = False
 
-               triggerNotif(package.author, current_user,
-                               "{} deleted".format(package.title), package.getDetailsURL())
+               msg = "Unapproved {}".format(package.title)
+               addNotification(package.maintainers, current_user, msg, package.getDetailsURL(), package)
+               addAuditLog(AuditSeverity.EDITOR, current_user, msg, package.getDetailsURL(), package)
+
                db.session.commit()
 
                flash("Unapproved package", "success")
@@ -382,3 +423,104 @@ def remove(package):
                return redirect(package.getDetailsURL())
        else:
                abort(400)
+
+
+
+class PackageMaintainersForm(FlaskForm):
+       maintainers_str  = StringField("Maintainers (Comma-separated)", [Optional()])
+       submit        = SubmitField("Save")
+
+
+@bp.route("/packages/<author>/<name>/edit-maintainers/", methods=["GET", "POST"])
+@login_required
+@is_package_page
+def edit_maintainers(package):
+       if not package.checkPerm(current_user, Permission.EDIT_MAINTAINERS):
+               flash("You do not have permission to edit maintainers", "danger")
+               return redirect(package.getDetailsURL())
+
+       form = PackageMaintainersForm(formdata=request.form)
+       if request.method == "GET":
+               form.maintainers_str.data = ", ".join([ x.username for x in package.maintainers if x != package.author ])
+
+       if request.method == "POST" and form.validate():
+               usernames = [x.strip().lower() for x in form.maintainers_str.data.split(",")]
+               users = User.query.filter(func.lower(User.username).in_(usernames)).all()
+
+               for user in users:
+                       if not user in package.maintainers:
+                               addNotification(user, current_user,
+                                               "Added you as a maintainer of {}".format(package.title), package.getDetailsURL(), package)
+
+               for user in package.maintainers:
+                       if user != package.author and not user in users:
+                               addNotification(user, current_user,
+                                               "Removed you as a maintainer of {}".format(package.title), package.getDetailsURL(), package)
+
+               package.maintainers.clear()
+               package.maintainers.extend(users)
+               if package.author not in package.maintainers:
+                       package.maintainers.append(package.author)
+
+               msg = "Edited {} maintainers".format(package.title)
+               addNotification(package.author, current_user, msg, package.getDetailsURL(), package)
+               severity = AuditSeverity.NORMAL if current_user == package.author else AuditSeverity.MODERATION
+               addAuditLog(severity, current_user, msg, package.getDetailsURL(), package)
+
+               db.session.commit()
+
+               return redirect(package.getDetailsURL())
+
+       users = User.query.filter(User.rank >= UserRank.NEW_MEMBER).order_by(db.asc(User.username)).all()
+
+       return render_template("packages/edit_maintainers.html", \
+                       package=package, form=form, users=users)
+
+
+@bp.route("/packages/<author>/<name>/remove-self-maintainer/", methods=["POST"])
+@login_required
+@is_package_page
+def remove_self_maintainers(package):
+       if not current_user in package.maintainers:
+               flash("You are not a maintainer", "danger")
+
+       elif current_user == package.author:
+               flash("Package owners cannot remove themselves as maintainers", "danger")
+
+       else:
+               package.maintainers.remove(current_user)
+
+               addNotification(package.author, current_user,
+                               "Removed themself as a maintainer of {}".format(package.title), package.getDetailsURL(), package)
+
+               db.session.commit()
+
+       return redirect(package.getDetailsURL())
+
+
+@bp.route("/packages/<author>/<name>/import-meta/", methods=["POST"])
+@login_required
+@is_package_page
+def update_from_release(package):
+       if not package.checkPerm(current_user, Permission.REIMPORT_META):
+               flash("You don't have permission to reimport meta", "danger")
+               return redirect(package.getDetailsURL())
+
+       release = package.releases.first()
+       if not release:
+               flash("Release needed", "danger")
+               return redirect(package.getDetailsURL())
+
+       msg = "Updated meta from latest release"
+       addNotification(package.maintainers, current_user,
+                       msg, package.getDetailsURL(), package)
+       severity = AuditSeverity.NORMAL if current_user in package.maintainers else AuditSeverity.EDITOR
+       addAuditLog(severity, current_user, msg, package.getDetailsURL(), package)
+
+       db.session.commit()
+
+       task_id = uuid()
+       zippath = release.url.replace("/uploads/", app.config["UPLOAD_DIR"])
+       updateMetaFromRelease.apply_async((release.id, zippath), task_id=task_id)
+
+       return redirect(url_for("tasks.check", id=task_id, r=package.getEditURL()))