diff --git a/app/blueprints/api/endpoints.py b/app/blueprints/api/endpoints.py index c810d994..b4760fcb 100644 --- a/app/blueprints/api/endpoints.py +++ b/app/blueprints/api/endpoints.py @@ -283,7 +283,7 @@ def markdown(): def list_all_releases(): query = PackageRelease.query.filter_by(approved=True) \ .filter(PackageRelease.package.has(state=PackageState.APPROVED)) \ - .order_by(db.desc(PackageRelease.releaseDate)) + .order_by(db.desc(PackageRelease.created_at)) if "author" in request.args: author = User.query.filter_by(username=request.args["author"]).first() @@ -333,7 +333,7 @@ def create_release(token, package): if option not in data: error(400, option + " is required in the POST data") - return api_create_vcs_release(token, package, data["title"], data["ref"]) + return api_create_vcs_release(token, package, data["title"], data["title"], data.get("release_notes"), data["ref"]) elif request.files: file = request.files.get("file") @@ -342,7 +342,7 @@ def create_release(token, package): commit_hash = data.get("commit") - return api_create_zip_release(token, package, data["title"], file, None, None, "API", commit_hash) + return api_create_zip_release(token, package, data["title"], data["title"], data.get("release_notes"), file, None, None, "API", commit_hash) else: error(400, "Unknown release-creation method. Specify the method or provide a file.") @@ -622,7 +622,7 @@ def homepage(): updated = db.session.query(Package).select_from(PackageRelease).join(Package) \ .filter_by(state=PackageState.APPROVED) \ - .order_by(db.desc(PackageRelease.releaseDate)) \ + .order_by(db.desc(PackageRelease.created_at)) \ .limit(20).all() updated = updated[:4] diff --git a/app/blueprints/api/support.py b/app/blueprints/api/support.py index 7f0d9550..8ddaff4c 100644 --- a/app/blueprints/api/support.py +++ b/app/blueprints/api/support.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . - +from typing import Optional from flask import jsonify, abort, make_response, url_for, current_app from app.logic.packages import do_edit_package @@ -38,14 +38,14 @@ def guard(f): return ret -def api_create_vcs_release(token: APIToken, package: Package, title: str, ref: str, +def api_create_vcs_release(token: APIToken, package: Package, name: str, title: Optional[str], release_notes: Optional[str], ref: str, min_v: MinetestRelease = None, max_v: MinetestRelease = None, reason="API"): if not token.can_operate_on_package(package): error(403, "API token does not have access to the package") reason += ", token=" + token.name - rel = guard(do_create_vcs_release)(token.owner, package, title, ref, min_v, max_v, reason) + rel = guard(do_create_vcs_release)(token.owner, package, name, title, release_notes, ref, min_v, max_v, reason) return jsonify({ "success": True, @@ -54,14 +54,14 @@ def api_create_vcs_release(token: APIToken, package: Package, title: str, ref: s }) -def api_create_zip_release(token: APIToken, package: Package, title: str, file, +def api_create_zip_release(token: APIToken, package: Package, name: str, title: Optional[str], release_notes: Optional[str], file, min_v: MinetestRelease = None, max_v: MinetestRelease = None, reason="API", commit_hash: str = None): if not token.can_operate_on_package(package): error(403, "API token does not have access to the package") reason += ", token=" + token.name - rel = guard(do_create_zip_release)(token.owner, package, title, file, min_v, max_v, reason, commit_hash) + rel = guard(do_create_zip_release)(token.owner, package, name, title, release_notes, file, min_v, max_v, reason, commit_hash) return jsonify({ "success": True, diff --git a/app/blueprints/homepage/__init__.py b/app/blueprints/homepage/__init__.py index 5049ebc5..c84ccca7 100644 --- a/app/blueprints/homepage/__init__.py +++ b/app/blueprints/homepage/__init__.py @@ -105,7 +105,7 @@ def home(): recent_releases_query = ( db.session.query( Package.id, - func.max(PackageRelease.releaseDate).label("max_created_at") + func.max(PackageRelease.created_at).label("max_created_at") ) .join(PackageRelease, Package.releases) .group_by(Package.id) diff --git a/app/blueprints/packages/game_hub.py b/app/blueprints/packages/game_hub.py index bc64040e..b402342c 100644 --- a/app/blueprints/packages/game_hub.py +++ b/app/blueprints/packages/game_hub.py @@ -44,7 +44,7 @@ def game_hub(package: Package): updated = db.session.query(Package).select_from(PackageRelease).join(Package) \ .filter(Package.supported_games.any(game=package, supports=True), Package.state==PackageState.APPROVED) \ - .order_by(db.desc(PackageRelease.releaseDate)) \ + .order_by(db.desc(PackageRelease.created_at)) \ .limit(20).all() updated = updated[:4] diff --git a/app/blueprints/packages/releases.py b/app/blueprints/packages/releases.py index 3ce0958f..4d613783 100644 --- a/app/blueprints/packages/releases.py +++ b/app/blueprints/packages/releases.py @@ -20,6 +20,7 @@ from flask_babel import lazy_gettext, gettext from flask_login import login_required, current_user from flask_wtf import FlaskForm from wtforms import StringField, SubmitField, BooleanField, RadioField, FileField +from wtforms.fields.simple import TextAreaField from wtforms.validators import InputRequired, Length, Optional from wtforms_sqlalchemy.fields import QuerySelectField @@ -28,7 +29,7 @@ from app.models import Package, db, User, PackageState, Permission, UserRank, Pa PackageRelease, PackageUpdateTrigger, PackageUpdateConfig from app.rediscache import has_key, set_temp_key, make_download_key from app.tasks.importtasks import check_update_config -from app.utils import is_user_bot, is_package_page, nonempty_or_none +from app.utils import is_user_bot, is_package_page, nonempty_or_none, normalize_line_endings from . import bp, get_package_tabs @@ -51,19 +52,25 @@ def get_mt_releases(is_max): class CreatePackageReleaseForm(FlaskForm): - title = StringField(lazy_gettext("Title"), [InputRequired(), Length(1, 30)]) - uploadOpt = RadioField(lazy_gettext("Method"), choices=[("upload", lazy_gettext("File Upload"))], default="upload") - vcsLabel = StringField(lazy_gettext("Git reference (ie: commit hash, branch, or tag)"), default=None) + name = StringField(lazy_gettext("Name"), [InputRequired(), Length(1, 30)]) + title = StringField(lazy_gettext("Title"), [Optional(), Length(1, 100)], filters=[nonempty_or_none]) + release_notes = TextAreaField(lazy_gettext("Release Notes"), [Optional(), Length(1, 100)], + filters=[nonempty_or_none, normalize_line_endings]) + upload_mode = RadioField(lazy_gettext("Method"), choices=[("upload", lazy_gettext("File Upload"))], default="upload") + vcs_label = StringField(lazy_gettext("Git reference (ie: commit hash, branch, or tag)"), default=None) file_upload = FileField(lazy_gettext("File Upload")) min_rel = QuerySelectField(lazy_gettext("Minimum Minetest Version"), [InputRequired()], query_factory=lambda: get_mt_releases(False), get_pk=lambda a: a.id, get_label=lambda a: a.name) - max_rel = QuerySelectField(lazy_gettext("Maximum Minetest Version"), [InputRequired()], + max_rel = QuerySelectField(lazy_gettext("Maximum Minetest Version"), [InputRequired()], query_factory=lambda: get_mt_releases(True), get_pk=lambda a: a.id, get_label=lambda a: a.name) - submit = SubmitField(lazy_gettext("Save")) + submit = SubmitField(lazy_gettext("Save")) class EditPackageReleaseForm(FlaskForm): - title = StringField(lazy_gettext("Title"), [InputRequired(), Length(1, 30)]) + name = StringField(lazy_gettext("Name"), [InputRequired(), Length(1, 30)]) + title = StringField(lazy_gettext("Title"), [Optional(), Length(1, 30)], filters=[nonempty_or_none]) + release_notes = TextAreaField(lazy_gettext("Release Notes"), [Optional(), Length(1, 100)], + filters=[nonempty_or_none, normalize_line_endings]) url = StringField(lazy_gettext("URL"), [Optional()]) task_id = StringField(lazy_gettext("Task ID"), filters = [lambda x: x or None]) approved = BooleanField(lazy_gettext("Is Approved")) @@ -88,21 +95,21 @@ def create_release(package): # Initial form class from post data and default data form = CreatePackageReleaseForm() if package.repo is not None: - form["uploadOpt"].choices = [("vcs", gettext("Import from Git")), ("upload", gettext("Upload .zip file"))] + form.upload_mode.choices = [("vcs", gettext("Import from Git")), ("upload", gettext("Upload .zip file"))] if request.method == "GET": - form["uploadOpt"].data = "vcs" - form.vcsLabel.data = request.args.get("ref") + form.upload_mode.data = "vcs" + form.vcs_label.data = request.args.get("ref") if request.method == "GET": form.title.data = request.args.get("title") if form.validate_on_submit(): try: - if form["uploadOpt"].data == "vcs": - rel = do_create_vcs_release(current_user, package, form.title.data, - form.vcsLabel.data, form.min_rel.data.get_actual(), form.max_rel.data.get_actual()) + if form.upload_mode.data == "vcs": + rel = do_create_vcs_release(current_user, package, form.name.data, form.title.data, form.release_notes.data, + form.vcs_label.data, form.min_rel.data.get_actual(), form.max_rel.data.get_actual()) else: - rel = do_create_zip_release(current_user, package, form.title.data, + rel = do_create_zip_release(current_user, package, form.name.data, form.title.data, form.release_notes.data, form.file_upload.data, form.min_rel.data.get_actual(), form.max_rel.data.get_actual()) return redirect(url_for("tasks.check", id=rel.task_id, r=rel.get_edit_url())) except LogicError as e: diff --git a/app/blueprints/vcs/github.py b/app/blueprints/vcs/github.py index 553fa66a..d35dc7fe 100644 --- a/app/blueprints/vcs/github.py +++ b/app/blueprints/vcs/github.py @@ -192,7 +192,7 @@ def github_webhook(): if package.releases.filter_by(commit_hash=ref).count() > 0: return - return api_create_vcs_release(token, package, title, ref, reason="Webhook") + return api_create_vcs_release(token, package, title, title, None, ref, reason="Webhook") return jsonify({ "success": False, diff --git a/app/blueprints/vcs/gitlab.py b/app/blueprints/vcs/gitlab.py index f5e9aa8e..c37a36d4 100644 --- a/app/blueprints/vcs/gitlab.py +++ b/app/blueprints/vcs/gitlab.py @@ -68,7 +68,7 @@ def webhook_impl(): if package.releases.filter_by(commit_hash=ref).count() > 0: continue - return api_create_vcs_release(token, package, title, ref, reason="Webhook") + return api_create_vcs_release(token, package, title, title, None, ref, reason="Webhook") return jsonify({ "success": False, diff --git a/app/flatpages/help/api.md b/app/flatpages/help/api.md index 347028f8..c517f2c7 100644 --- a/app/flatpages/help/api.md +++ b/app/flatpages/help/api.md @@ -224,7 +224,9 @@ Format query parameters: * `maintainer`: Filter by maintainer * Returns array of release dictionaries with keys: * `id`: release ID + * `name`: short release name * `title`: human-readable title + * `release_notes`: string or null, what's new in this release * `release_date`: Date released * `url`: download URL * `commit`: commit hash or null @@ -248,6 +250,7 @@ Format query parameters: * Requires authentication. * Body can be JSON or multipart form data. Zip uploads must be multipart form data. * `title`: human-readable name of the release. + * `release_notes`: string or null, what's new in this release. * For Git release creation: * `method`: must be `git`. * `ref`: (Optional) git reference, eg: `master`. diff --git a/app/logic/releases.py b/app/logic/releases.py index 54a611cc..38167d32 100644 --- a/app/logic/releases.py +++ b/app/logic/releases.py @@ -16,6 +16,7 @@ import datetime import re +from typing import Optional from celery import uuid from flask_babel import lazy_gettext @@ -32,18 +33,20 @@ def check_can_create_release(user: User, package: Package): raise LogicError(403, lazy_gettext("You don't have permission to make releases")) five_minutes_ago = datetime.datetime.now() - datetime.timedelta(minutes=5) - count = package.releases.filter(PackageRelease.releaseDate > five_minutes_ago).count() + count = package.releases.filter(PackageRelease.created_at > five_minutes_ago).count() if count >= 5: raise LogicError(429, lazy_gettext("You've created too many releases for this package in the last 5 minutes, please wait before trying again")) -def do_create_vcs_release(user: User, package: Package, title: str, ref: str, +def do_create_vcs_release(user: User, package: Package, name: str, title: Optional[str], release_notes: Optional[str], ref: str, min_v: MinetestRelease = None, max_v: MinetestRelease = None, reason: str = None): check_can_create_release(user, package) rel = PackageRelease() rel.package = package - rel.title = title + rel.name = name + rel.title = title or name + rel.release_notes = release_notes rel.url = "" rel.task_id = uuid() rel.min_rel = min_v @@ -63,7 +66,7 @@ def do_create_vcs_release(user: User, package: Package, title: str, ref: str, return rel -def do_create_zip_release(user: User, package: Package, title: str, file, +def do_create_zip_release(user: User, package: Package, name: str, title: Optional[str], release_notes: Optional[str], file, min_v: MinetestRelease = None, max_v: MinetestRelease = None, reason: str = None, commit_hash: str = None): check_can_create_release(user, package) @@ -77,7 +80,9 @@ def do_create_zip_release(user: User, package: Package, title: str, file, rel = PackageRelease() rel.package = package - rel.title = title + rel.name = name + rel.title = title or name + rel.release_notes = release_notes rel.url = uploaded_url rel.task_id = uuid() rel.commit_hash = commit_hash diff --git a/app/models/packages.py b/app/models/packages.py index 018646eb..c54a2b4a 100644 --- a/app/models/packages.py +++ b/app/models/packages.py @@ -474,7 +474,7 @@ class Package(db.Model): content_warnings = db.relationship("ContentWarning", secondary=ContentWarnings, back_populates="packages") releases = db.relationship("PackageRelease", back_populates="package", - lazy="dynamic", order_by=db.desc("package_release_releaseDate"), cascade="all, delete, delete-orphan") + lazy="dynamic", order_by=db.desc("package_release_created_at"), cascade="all, delete, delete-orphan") screenshots = db.relationship("PackageScreenshot", back_populates="package", foreign_keys="PackageScreenshot.package_id", lazy="dynamic", order_by=db.asc("package_screenshot_order"), cascade="all, delete, delete-orphan") @@ -1085,13 +1085,15 @@ class PackageRelease(db.Model): package_id = db.Column(db.Integer, db.ForeignKey("package.id")) package = db.relationship("Package", back_populates="releases", foreign_keys=[package_id]) + name = db.Column(db.String(30), nullable=False) title = db.Column(db.String(100), nullable=False) - releaseDate = db.Column(db.DateTime, nullable=False) + created_at = db.Column(db.DateTime, 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) downloads = db.Column(db.Integer, nullable=False, default=0) + release_notes = db.Column(db.UnicodeText, nullable=True, default=None) min_rel_id = db.Column(db.Integer, db.ForeignKey("minetest_release.id"), nullable=True, server_default=None) min_rel = db.relationship("MinetestRelease", foreign_keys=[min_rel_id]) @@ -1126,9 +1128,11 @@ class PackageRelease(db.Model): def as_dict(self): return { "id": self.id, + "name": self.name, "title": self.title, + "release_notes": self.release_notes, "url": self.url if self.url != "" else None, - "release_date": self.releaseDate.isoformat(), + "release_date": self.created_at.isoformat(), "commit": self.commit_hash, "downloads": self.downloads, "min_minetest_version": self.min_rel and self.min_rel.as_dict(), @@ -1141,7 +1145,7 @@ class PackageRelease(db.Model): "id": self.id, "title": self.title, "url": self.url if self.url != "" else None, - "release_date": self.releaseDate.isoformat(), + "release_date": self.created_at.isoformat(), "commit": self.commit_hash, "downloads": self.downloads, "min_minetest_version": self.min_rel and self.min_rel.as_dict(), @@ -1169,7 +1173,7 @@ class PackageRelease(db.Model): id=self.id) def __init__(self): - self.releaseDate = datetime.datetime.now() + self.created_at = datetime.datetime.now() def get_download_filename(self): return f"{self.package.name}_{self.id}.zip" diff --git a/app/public/static/js/release_new.js b/app/public/static/js/release_new.js index a4bab14d..82bbf47f 100644 --- a/app/public/static/js/release_new.js +++ b/app/public/static/js/release_new.js @@ -5,15 +5,15 @@ window.addEventListener("load", () => { function check_opt() { - if (document.querySelector("input[name='uploadOpt']:checked").value === "vcs") { + if (document.querySelector("input[name='upload_mode']:checked").value === "vcs") { document.getElementById("file_upload").parentElement.classList.add("d-none"); - document.getElementById("vcsLabel").parentElement.classList.remove("d-none"); + document.getElementById("vcs_label").parentElement.classList.remove("d-none"); } else { document.getElementById("file_upload").parentElement.classList.remove("d-none"); - document.getElementById("vcsLabel").parentElement.classList.add("d-none"); + document.getElementById("vcs_label").parentElement.classList.add("d-none"); } } - document.querySelectorAll("input[name='uploadOpt']").forEach(x => x.addEventListener("change", check_opt)); + document.querySelectorAll("input[name='upload_mode']").forEach(x => x.addEventListener("change", check_opt)); check_opt(); }); diff --git a/app/querybuilder.py b/app/querybuilder.py index 8195ab8b..9fb5ef1b 100644 --- a/app/querybuilder.py +++ b/app/querybuilder.py @@ -343,7 +343,7 @@ class QueryBuilder: elif self.order_by == "approved_at" or self.order_by == "date": to_order = Package.approved_at elif self.order_by == "last_release": - to_order = PackageRelease.releaseDate + to_order = PackageRelease.created_at else: abort(400) diff --git a/app/templates/macros/releases.html b/app/templates/macros/releases.html index 338c21bc..d7b3f40e 100644 --- a/app/templates/macros/releases.html +++ b/app/templates/macros/releases.html @@ -20,7 +20,7 @@ [{{ rel.commit_hash | truncate(5, end='') }}] {% endif %} - {{ _("created %(date)s", date=rel.releaseDate | date) }}. + {{ _("created %(date)s", date=rel.created_at | date) }}. {% endfor %} @@ -50,7 +50,7 @@ [{{ rel.commit_hash | truncate(5, end='') }}] {% endif %} - {{ _("created %(date)s", date=rel.releaseDate | date) }}. + {{ _("created %(date)s", date=rel.created_at | date) }}. {% endif %} @@ -96,7 +96,7 @@ [{{ rel.commit_hash | truncate(5, end='') }}] {% endif %} - {{ _("created %(date)s", date=rel.releaseDate | date) }}. + {{ _("created %(date)s", date=rel.created_at | date) }}. {% if (package.check_perm(current_user, "MAKE_RELEASE") or rel.check_perm(current_user, "APPROVE_RELEASE")) and rel.task_id %} diff --git a/app/templates/packages/release_edit.html b/app/templates/packages/release_edit.html index 4dfab624..b66807bd 100644 --- a/app/templates/packages/release_edit.html +++ b/app/templates/packages/release_edit.html @@ -12,9 +12,17 @@ {{ form.hidden_tag() }} {% if package.check_perm(current_user, "MAKE_RELEASE") %} - {{ render_field(form.title) }} + {{ render_field(form.name, hint=_("Release short name. Eg: 1.0.0 or 2018-05-28")) }} + {{ render_field(form.title, hint=_("Human-readable name. Eg: 1.0.0 - The Trains Update")) }} + {{ render_field(form.release_notes) }} {% else %} - {{ _("Title") }}: {{ release.title }} +

+ {{ _("Name") }}: {{ release.name }}
+ {{ _("Title") }}: {{ release.title }} +

+

+ {{ release.release_notes }} +

{% endif %} {% if package.check_perm(current_user, "CHANGE_RELEASE_URL") %} diff --git a/app/templates/packages/release_new.html b/app/templates/packages/release_new.html index 21d02810..6ac0ad7c 100644 --- a/app/templates/packages/release_new.html +++ b/app/templates/packages/release_new.html @@ -39,15 +39,16 @@

{{ _("1. Name release") }}

- {{ render_field(form.title, placeholder=_("Human readable. Eg: 1.0.0 or 2018-05-28")) }} + {{ render_field(form.name, hint=_("Release short name. Eg: 1.0.0 or 2018-05-28")) }} + {{ render_field(form.title, hint=_("Human-readable name. Eg: 1.0.0 - The Trains Update")) }} + {{ render_field(form.release_notes) }}

{{ _("2. Set the content") }}

-

{{ _("Method") }}

- {{ render_radio_field(form.uploadOpt) }} + {{ render_radio_field(form.upload_mode) }} {% if package.repo %} - {{ render_field(form.vcsLabel, placeholder=_("Leave blank to use default branch"), class_="mt-3", + {{ render_field(form.vcs_label, placeholder=_("Leave blank to use default branch"), class_="mt-3", pattern="[A-Za-z0-9/._-]+") }} {% endif %} @@ -99,5 +100,5 @@ {% block scriptextra %} - + {% endblock %}