Add release notes and long titles to releases

Fixes #492 and fixes #480
This commit is contained in:
rubenwardy 2024-06-22 15:18:58 +01:00
parent 4147e5edc7
commit 019cd66033
15 changed files with 80 additions and 52 deletions

@ -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]

@ -14,7 +14,7 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
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,

@ -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)

@ -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]

@ -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,9 +52,12 @@ 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)
@ -63,7 +67,10 @@ class CreatePackageReleaseForm(FlaskForm):
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:

@ -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,

@ -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,

@ -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`.

@ -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

@ -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"

@ -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();
});

@ -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)

@ -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) }}.
</small>
</a>
{% 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) }}.
</small>
</a>
{% 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) }}.
</small>
{% if (package.check_perm(current_user, "MAKE_RELEASE") or rel.check_perm(current_user, "APPROVE_RELEASE")) and rel.task_id %}
<a href="{{ url_for('tasks.check', id=rel.task_id, r=package.get_url('packages.view')) }}">

@ -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 %}
<p>
{{ _("Name") }}: {{ release.name }}<br>
{{ _("Title") }}: {{ release.title }}
</p>
<p>
{{ release.release_notes }}
</p>
{% endif %}
{% if package.check_perm(current_user, "CHANGE_RELEASE_URL") %}

@ -39,15 +39,16 @@
<h3>{{ _("1. Name release") }}</h3>
{{ 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) }}
<h3 class="mt-5">{{ _("2. Set the content") }}</h3>
<p class="mb-0">{{ _("Method") }}</p>
{{ 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 %}
<script src="/static/js/release_minmax.js?v=2"></script>
<script src="/static/js/release_new.js"></script>
<script src="/static/js/release_new.js?v=2"></script>
{% endblock %}