From 14faae3fd16520859d0ca6b371a86c54e91c6756 Mon Sep 17 00:00:00 2001 From: rubenwardy Date: Fri, 24 Jan 2020 20:21:40 +0000 Subject: [PATCH] Add API to create releases --- app/blueprints/api/endpoints.py | 30 ++++++++++++++++++++++++- app/blueprints/api/support.py | 40 +++++++++++++++++++++++++++++++++ app/flatpages/help/api.md | 13 +++++++++++ app/models.py | 18 ++++++++++++--- 4 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 app/blueprints/api/support.py diff --git a/app/blueprints/api/endpoints.py b/app/blueprints/api/endpoints.py index 266bf93..bd17bb5 100644 --- a/app/blueprints/api/endpoints.py +++ b/app/blueprints/api/endpoints.py @@ -19,6 +19,7 @@ from flask import * from flask_user import * from . import bp from .auth import is_api_authd +from .support import error, handleCreateRelease from app import csrf from app.models import * from app.utils import is_package_page @@ -71,6 +72,13 @@ def package_dependencies(package): return jsonify(ret) +@bp.route("/api/packages///releases/") +@is_package_page +def list_releases(package): + releases = package.releases.filter_by(approved=True).all() + return jsonify([ rel.getAsDictionary() for rel in releases ]) + + @bp.route("/api/topics/") def topics(): qb = QueryBuilder(request.args) @@ -113,5 +121,25 @@ def whoami(token): @bp.route("/api/markdown/", methods=["POST"]) @csrf.exempt -def clean_markdown(): +def markdown(): return render_markdown(request.data.decode("utf-8")) + + +@bp.route("/api/packages///releases/new/", methods=["POST"]) +@csrf.exempt +@is_package_page +@is_api_authd +def create_release(token, package): + json = request.json + if json is None: + return error(400, "JSON post data is required") + + for option in ["method", "title", "ref"]: + if json.get(option) is None: + return error(400, option + " is required in the POST data") + + + if json["method"].lower() != "vcs": + return error(400, "Release-creation methods other than VCS are not supported") + + return handleCreateRelease(token, package, json["title"], json["ref"]) diff --git a/app/blueprints/api/support.py b/app/blueprints/api/support.py new file mode 100644 index 0000000..0adf3db --- /dev/null +++ b/app/blueprints/api/support.py @@ -0,0 +1,40 @@ +from app.models import PackageRelease, db, Permission +from app.tasks.importtasks import makeVCSRelease +from celery import uuid +from flask import jsonify, make_response, url_for +import datetime + + +def error(status, message): + return make_response(jsonify({ "success": False, "error": message }), status) + + +def handleCreateRelease(token, package, title, ref): + if not token.canOperateOnPackage(package): + return error(403, "API token does not have access to the package") + + if not package.checkPerm(token.owner, Permission.MAKE_RELEASE): + return error(403, "Permission denied. Missing MAKE_RELEASE permission") + + five_minutes_ago = datetime.datetime.now() - datetime.timedelta(minutes=5) + count = package.releases.filter(PackageRelease.releaseDate > five_minutes_ago).count() + if count >= 2: + return error(429, "Too many requests, please wait before trying again") + + rel = PackageRelease() + rel.package = package + rel.title = title + rel.url = "" + rel.task_id = uuid() + rel.min_rel = None + rel.max_rel = None + db.session.add(rel) + db.session.commit() + + makeVCSRelease.apply_async((rel.id, ref), task_id=rel.task_id) + + return jsonify({ + "success": True, + "task": url_for("tasks.check", id=rel.task_id), + "release": rel.getAsDictionary() + }) diff --git a/app/flatpages/help/api.md b/app/flatpages/help/api.md index 95e23d2..c836153 100644 --- a/app/flatpages/help/api.md +++ b/app/flatpages/help/api.md @@ -23,6 +23,19 @@ You can use the `/api/whoami` to check authentication. * GET `/api/packages/` - See [Package Queries](#package-queries) * GET `/api/packages///` +### Releases + +* GET `/api/packages///releases/` +* POST `/api/packages///releases/` + * Requires authentication. + * `title`: human-readable name of the release. + * `method`: Must be `vcs`. + * `min_protocol`: minimum Minetest protocol version. See [Minetest](#minetest). + * `min_protocol`: maximum Minetest protocol version. See [Minetest](#minetest). + * If `vcs` release-creation method: + * `ref` - git reference. + + ### Topics * GET `/api/topics/` - Supports [Package Queries](#package-queries), and the following two options: diff --git a/app/models.py b/app/models.py index 1849075..5eff2dd 100644 --- a/app/models.py +++ b/app/models.py @@ -522,7 +522,7 @@ class Package(db.Model): "short_description": self.short_desc, "desc": self.desc, "type": self.type.toName(), - "created_at": self.created_at, + "created_at": self.created_at.isoformat(), "license": self.license.name, "media_license": self.media_license.name, @@ -773,6 +773,18 @@ class PackageRelease(db.Model): # If the release is approved, then the task_id must be null and the url must be present CK_approval_valid = db.CheckConstraint("not approved OR (task_id IS NULL AND (url = '') IS NOT FALSE)") + def getAsDictionary(self): + return { + "id": self.id, + "title": self.title, + "url": self.url if self.url != "" else None, + "release_date": self.releaseDate.isoformat(), + "commit": self.commit_hash, + "downloads": self.downloads, + "min_protocol": self.min_rel and self.min_rel.protocol, + "max_protocol": self.max_rel and self.max_rel.protocol + } + def getEditURL(self): return url_for("packages.edit_release", author=self.package.author.username, @@ -875,10 +887,10 @@ class APIToken(db.Model): package = db.relationship("Package", foreign_keys=[package_id]) def canOperateOnPackage(self, package): - if self.package and self.package != None: + if self.package and self.package != package: return False - return package.owner == self.owner + return package.author == self.owner class EditRequest(db.Model):