From 14a67b99baeb08cf04db71e9857684013ed79303 Mon Sep 17 00:00:00 2001 From: rubenwardy Date: Tue, 15 Dec 2020 19:05:29 +0000 Subject: [PATCH] Add package update configuration for polling --- app/blueprints/packages/releases.py | 33 ++++++++- app/models/packages.py | 47 +++++++++++- app/models/threads.py | 2 +- app/models/users.py | 7 +- app/public/static/bot_avatar.png | Bin 0 -> 2182 bytes app/scss/comments.scss | 6 ++ app/scss/components.scss | 4 + app/tasks/__init__.py | 5 ++ app/tasks/importtasks.py | 86 +++++++++++++++++++++- app/templates/macros/threads.html | 14 +++- app/templates/packages/update_config.html | 29 ++++++++ app/templates/users/list.html | 2 + app/templates/users/profile.html | 2 +- app/utils.py | 27 +++++++ migrations/versions/105d4c740ad6_.py | 36 +++++++++ migrations/versions/886c92dc6eaa_.py | 35 +++++++++ utils/create_migration.sh | 2 +- 17 files changed, 327 insertions(+), 10 deletions(-) create mode 100644 app/public/static/bot_avatar.png create mode 100644 app/templates/packages/update_config.html create mode 100644 migrations/versions/105d4c740ad6_.py create mode 100644 migrations/versions/886c92dc6eaa_.py diff --git a/app/blueprints/packages/releases.py b/app/blueprints/packages/releases.py index 57a2d6a..96db569 100644 --- a/app/blueprints/packages/releases.py +++ b/app/blueprints/packages/releases.py @@ -24,7 +24,7 @@ from wtforms.ext.sqlalchemy.fields import QuerySelectField from wtforms.validators import * from app.rediscache import has_key, set_key, make_download_key -from app.tasks.importtasks import makeVCSRelease, checkZipRelease +from app.tasks.importtasks import makeVCSRelease, checkZipRelease, updateMetaFromRelease, check_for_updates from app.utils import * from . import bp @@ -247,3 +247,34 @@ def delete_release(package, id): db.session.commit() return redirect(package.getDetailsURL()) + + +class PackageUpdateConfigFrom(FlaskForm): + trigger = SelectField("Trigger", [InputRequired()], choices=PackageUpdateTrigger.choices(), coerce=PackageUpdateTrigger.coerce, + default=PackageUpdateTrigger.COMMIT) + make_release = BooleanField("Create Release") + submit = SubmitField("Save") + + +@bp.route("/packages///update-config/", methods=["GET", "POST"]) +@login_required +@is_package_page +def update_config(package): + package.update_config = package.update_config or PackageUpdateConfig() + + if not package.checkPerm(current_user, Permission.MAKE_RELEASE): + return redirect(package.getDetailsURL()) + + form = PackageUpdateConfigFrom(obj=package.update_config) + if form.validate_on_submit(): + flash("Changed update configuration", "success") + form.populate_obj(package.update_config) + db.session.add(package.update_config) + + check_for_updates.delay() + + db.session.commit() + + return redirect(package.getDetailsURL()) + + return render_template("packages/update_config.html", package=package, form=form) diff --git a/app/models/packages.py b/app/models/packages.py index 2048aa7..e42f9ff 100644 --- a/app/models/packages.py +++ b/app/models/packages.py @@ -317,7 +317,7 @@ class Package(db.Model): maintainers = db.relationship("User", secondary=maintainers) threads = db.relationship("Thread", back_populates="package", order_by=db.desc("thread_created_at"), - foreign_keys="Thread.package_id", cascade="all, delete, delete-orphan") + 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"), cascade="all, delete, delete-orphan") @@ -331,6 +331,9 @@ class Package(db.Model): tokens = db.relationship("APIToken", foreign_keys="APIToken.package_id", back_populates="package", cascade="all, delete, delete-orphan") + update_config = db.relationship("PackageUpdateConfig", uselist=False, back_populates="package", + cascade="all, delete, delete-orphan") + def __init__(self, package=None): if package is None: return @@ -507,6 +510,10 @@ class Package(db.Model): return url_for("packages.bulk_change_release", author=self.author.username, name=self.name) + def getUpdateConfigURL(self): + return url_for("packages.update_config", + author=self.author.username, name=self.name) + def getDownloadURL(self): return url_for("packages.download", author=self.author.username, name=self.name) @@ -790,7 +797,7 @@ class PackageRelease(db.Model): title = db.Column(db.String(100), nullable=False) releaseDate = db.Column(db.DateTime, nullable=False) - url = db.Column(db.String(200), nullable=False) + url = db.Column(db.String(200), nullable=False, default="") approved = db.Column(db.Boolean, nullable=False, default=False) task_id = db.Column(db.String(37), nullable=True) commit_hash = db.Column(db.String(41), nullable=True, default=None) @@ -903,3 +910,39 @@ class PackageScreenshot(db.Model): def getThumbnailURL(self, level=2): return self.url.replace("/uploads/", "/thumbnails/{:d}/".format(level)) + + +class PackageUpdateTrigger(enum.Enum): + COMMIT = "New Commit" + TAG = "New Tag" + + def toName(self): + return self.name.lower() + + def __str__(self): + return self.name + + @classmethod + def get(cls, name): + try: + return PackageUpdateTrigger[name.upper()] + except KeyError: + return None + + @classmethod + def choices(cls): + return [(choice, choice.value) for choice in cls] + + @classmethod + def coerce(cls, item): + return item if type(item) == PackageUpdateTrigger else PackageUpdateTrigger[item] + + +class PackageUpdateConfig(db.Model): + package_id = db.Column(db.Integer, db.ForeignKey("package.id"), primary_key=True) + package = db.relationship("Package", back_populates="update_config", foreign_keys=[package_id]) + + last_commit = db.Column(db.String(41), nullable=True, default=None) + + trigger = db.Column(db.Enum(PackageUpdateTrigger), nullable=False, default=PackageUpdateTrigger.COMMIT) + make_release = db.Column(db.Boolean, nullable=False, default=False) diff --git a/app/models/threads.py b/app/models/threads.py index 12b3c5d..95e01f7 100644 --- a/app/models/threads.py +++ b/app/models/threads.py @@ -56,7 +56,7 @@ class Thread(db.Model): watchers = db.relationship("User", secondary=watchers, backref="watching") def getViewURL(self): - return url_for("threads.view", id=self.id) + return url_for("threads.view", id=self.id, _external=False) def getSubscribeURL(self): return url_for("threads.subscribe", id=self.id) diff --git a/app/models/users.py b/app/models/users.py index d40d0e9..63f5e52 100644 --- a/app/models/users.py +++ b/app/models/users.py @@ -32,8 +32,9 @@ class UserRank(enum.Enum): MEMBER = 3 TRUSTED_MEMBER = 4 EDITOR = 5 - MODERATOR = 6 - ADMIN = 7 + BOT = 6 + MODERATOR = 7 + ADMIN = 8 def atLeast(self, min): return self.value >= min.value @@ -192,6 +193,8 @@ class User(db.Model, UserMixin): def getProfilePicURL(self): if self.profile_pic: return self.profile_pic + elif self.rank == UserRank.BOT: + return "/static/bot_avatar.png" else: return gravatar(self.email or "") diff --git a/app/public/static/bot_avatar.png b/app/public/static/bot_avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..96b94d28baa822fde5d102cff33cb3be36981bea GIT binary patch literal 2182 zcmZ`)c{CL28y?$WY$J1{jL0RzFtWr%W#8BA`;cW^VTO<`gBfea{1|KYP_9g5xh31+ zk}XEEl#!wAAwrVwbWf+-Kfm*x_xqmnyyrdV`Of=(&zE9pZomUN4FUiFJVu7P){Ha# zdvE|5dtIm2WdMLF5v8MJX{4hg7K{n9zrZ&~C7 zhSBfaN^9dRWgAyLFe`$}C_`%(DBEOwf@qp8yP&VDhzpz67E90bF{3d(<0E$`KmLU- z^cC7{)SEVzM135}?geYY6IU>OB#a9O7RzD9`RS$ zO7&_N>?i_R!>~TH=yrlg z^)2Ru60B_70$DXK--5a(0>iK-He=J)GOE^@axZmmWt}Z-ko}AT;h48#_2;r#@PXLl zCcB`Gn7fw&r&eWdvOuBC3wi@?`w`*p;X&VuQTzKW1J;;Aa-@dQX zw(2Yf!x?Po7{ch_k!#5|pF_NN}OD1PyI>P5R+2`p!JZy>g@7*~s z2EkhjX(KssXCZhnNJpBx44z2&7chvQ83wQ!9f3U4x01bvks-&>ZCDFCpuV$djp(421S@tC9 zGUxR^qP&zC2gJwSLgqIJHN+v}P|$eAar|x*$?Q~bcDkob{O*XaYfQJ0#uv`W=uIuZ zKPuo85PND~A+MJC(AHDF((F{ao(w1{6o`E*v@S1-`%HLu9YVYki!Bcr&z4#23DGcV z?@Sq-)7m8jkG2ZMCI>G|4K!?K1`3ZoFLdC(qTB+15+St17dXa28oKfqE?V5v9GNPM z(v=FLGT+gax~+$j^pZzr8?)_vGi^9jp%r_MhnL#OyyvgVOhu|3r%#Y?iD@KgN75uH z(x8*YbKVnthDV=WuUe%m4C&X$S*N~M;ne`TfV>_z`bQ&QeSA}6)!`z4%3Vmk@OtGM zY0!mwK-mIy%ceIgnw}jto`mH(E^t*ONrLo|QJO*VC>0dymr+pE!27jE_9(Fiy_G{C zTA8G!$%4cb%`W}G;%&>kZ}s~{x?*bhKr}#mA}9TrNO+WEBCaZeXv{5&OfwLq*n|(! zoMyjl@Ajq$^YFMp+vCbYN-AdkU+*l%i*?Quw~njl#P^m}F=JnOos<40mLZr*x=^lhfmrwI=yDr47)9`|Wiplv?`lN-(-j5d89?0r&x_rHLHh|^|+oLMQF4_Ah4AZ#mB z*Sh<3kTp~N!(I>Zy)Q7T*x{r2`Gqog6u6Lge&SWt>ViAzBK)Gr1v3puRjRd2>Qu$+ zn$+EzXIKW{+3>4ui8yUE*d9Hkx9Jt5Lg-9d2u`LtztD#0VeguG1R_GEd&itLAUzG`iwfq5od_|22c5 z#MMu55zViNnZkQ*mwH;Co?716S7vc0cO2UbO?Gk?y8(MAZa+JwBvUz*{;+&Q4gQKl zM@UBK%PY15yOl7Uyab!uNY*kQoi22Qx%KJapI?ALCtu=>$HjxwCh!YO9dQ?ffuFBK z22+yjfE3-?#Y%ewXNxj-QXlz=ZAvhj{&!h3cyxTdlSBDwuE%hTg!X^anLdLmGVy%1 zmY5TM6(*bB)
- {{ r.author.display_name }} + + {% if r.author in thread.package.maintainers %} + + {{ _("Maintainer") }} + + {% endif %} + {% if r.author.rank == r.author.rank.BOT %} + + {{ r.author.rank.getTitle() }} + + {% endif %} + {{ r.created_at | datetime }} diff --git a/app/templates/packages/update_config.html b/app/templates/packages/update_config.html new file mode 100644 index 0000000..6431efc --- /dev/null +++ b/app/templates/packages/update_config.html @@ -0,0 +1,29 @@ +{% extends "base.html" %} + +{% block title %} + Configure Update Detection | {{ package.title }} +{% endblock %} + +{% block content %} +

{{ _("Configure Update Detection") }}

+ +

+ {{ _("ContentDB will poll your Git repository at 2am UTC every day.") }} + {{ _("You should consider using webhooks or the API for faster rollouts.") }} +

+ + {% from "macros/forms.html" import render_field, render_submit_field, render_checkbox_field %} +
+ {{ form.hidden_tag() }} + +

Triggers

+ {{ render_field(form.trigger) }} + +

Actions

+ {{ render_checkbox_field(form.make_release) }} + +

+ {{ render_submit_field(form.submit) }} +

+
+{% endblock %} diff --git a/app/templates/users/list.html b/app/templates/users/list.html index 90f6227..bfe07fe 100644 --- a/app/templates/users/list.html +++ b/app/templates/users/list.html @@ -38,6 +38,8 @@ {% elif user.rank == user.rank.EDITOR %} + {% elif user.rank == user.rank.BOT %} + {% else %} {% endif %} diff --git a/app/templates/users/profile.html b/app/templates/users/profile.html index 3c13ac4..03a4b96 100644 --- a/app/templates/users/profile.html +++ b/app/templates/users/profile.html @@ -23,7 +23,7 @@
{% endif %} -

+

{{ user.display_name }}

diff --git a/app/utils.py b/app/utils.py index 515f6dc..d8d5874 100644 --- a/app/utils.py +++ b/app/utils.py @@ -253,3 +253,30 @@ def nonEmptyOrNone(str): return None return str + + +def post_system_thread(package: Package, title: str, message: str): + system_user = User.query.filter_by(username="ContentDB").first() + assert system_user + + thread = package.threads.filter_by(author=system_user).first() + if not thread: + thread = Thread() + thread.package = package + thread.title = "System Notifications" + thread.author = system_user + thread.private = True + thread.watchers.append(package.author) + db.session.add(thread) + db.session.flush() + + reply = ThreadReply() + reply.thread = thread + reply.author = system_user + reply.comment = "# {}\n\n{}".format(title, message) + db.session.add(reply) + + addNotification(thread.watchers, system_user, NotificationType.THREAD_REPLY, + title, thread.getViewURL(), thread.package) + + thread.replies.append(reply) diff --git a/migrations/versions/105d4c740ad6_.py b/migrations/versions/105d4c740ad6_.py new file mode 100644 index 0000000..c55c280 --- /dev/null +++ b/migrations/versions/105d4c740ad6_.py @@ -0,0 +1,36 @@ +"""empty message + +Revision ID: 105d4c740ad6 +Revises: 886c92dc6eaa +Create Date: 2020-12-15 17:28:56.559801 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +from sqlalchemy import orm +from app.models import User, UserRank + +revision = '105d4c740ad6' +down_revision = '886c92dc6eaa' +branch_labels = None +depends_on = None + + +def upgrade(): + op.execute("COMMIT") + op.execute("ALTER TYPE userrank ADD VALUE 'BOT' AFTER 'EDITOR'") + + conn = op.get_bind() + system_user = User("ContentDB", active=False) + system_user.rank = UserRank.BOT + + session = orm.Session(bind=conn) + session.add(system_user) + session.commit() + + +def downgrade(): + pass diff --git a/migrations/versions/886c92dc6eaa_.py b/migrations/versions/886c92dc6eaa_.py new file mode 100644 index 0000000..aaacb46 --- /dev/null +++ b/migrations/versions/886c92dc6eaa_.py @@ -0,0 +1,35 @@ +"""empty message + +Revision ID: 886c92dc6eaa +Revises: 8d22def23c8b +Create Date: 2020-12-15 16:38:54.114559 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = '886c92dc6eaa' +down_revision = '8d22def23c8b' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('package_update_config', + sa.Column('package_id', sa.Integer(), nullable=False), + sa.Column('last_commit', sa.String(length=41), nullable=True), + sa.Column('trigger', sa.Enum('COMMIT', 'TAG', name='packageupdatetrigger'), nullable=False), + sa.Column('make_release', sa.Boolean(), nullable=False), + sa.ForeignKeyConstraint(['package_id'], ['package.id'], ), + sa.PrimaryKeyConstraint('package_id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('package_update_config') + # ### end Alembic commands ### diff --git a/utils/create_migration.sh b/utils/create_migration.sh index 4c73937..59aaea0 100755 --- a/utils/create_migration.sh +++ b/utils/create_migration.sh @@ -2,7 +2,7 @@ # Create a database migration, and copy it back to the host. -docker exec contentdb_app_1 sh -c "FLASK_CONFIG=../config.cfg FLASK_APP=app/__init__.py flask db migrate" +docker exec contentdb_app_1 sh -c "FLASK_CONFIG=../config.cfg FLASK_APP=app/__init__.py flask db revision" docker exec -u root contentdb_app_1 sh -c "cp /home/cdb/migrations/versions/* /source/migrations/versions/" USER=$(whoami)