From 90aeb6e1a749b181db11751e261f7e99d7a3dd96 Mon Sep 17 00:00:00 2001 From: rubenwardy Date: Tue, 2 Feb 2021 22:47:46 +0000 Subject: [PATCH] Add missing fields to packages API --- app/blueprints/packages/packages.py | 28 ++++++++------- app/flatpages/help/api.md | 53 ++++++++++++++++++++++++----- app/logic/packages.py | 19 +++++++---- app/models/packages.py | 7 ++++ 4 files changed, 80 insertions(+), 27 deletions(-) diff --git a/app/blueprints/packages/packages.py b/app/blueprints/packages/packages.py index 53d636a..13e857c 100644 --- a/app/blueprints/packages/packages.py +++ b/app/blueprints/packages/packages.py @@ -218,19 +218,23 @@ def makeLabel(obj): 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) + title = StringField("Title (Human-readable)", [InputRequired(), Length(3, 100)]) + name = StringField("Name (Technical)", [InputRequired(), Length(1, 100), Regexp("^[a-z0-9_]+$", 0, "Lower case letters (a-z), digits (0-9), and underscores (_) only")]) + short_desc = StringField("Short Description (Plaintext)", [InputRequired(), Length(1,200)]) + 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) + 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) + + desc = TextAreaField("Long Description (Markdown)", [Optional(), Length(0,10000)]) + 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") @@ -301,15 +305,15 @@ def create_edit(author=None, name=None): try: do_edit_package(current_user, package, wasNew, { - "name": form.name.data, - "title": form.title.data, - "short_desc": form.short_desc.data, - "desc": form.desc.data, "type": form.type.data, - "license": form.license.data, - "media_license": form.media_license.data, + "title": form.title.data, + "name": form.name.data, + "short_desc": form.short_desc.data, "tags": form.tags.raw_data, "content_warnings": form.content_warnings.raw_data, + "license": form.license.data, + "media_license": form.media_license.data, + "desc": form.desc.data, "repo": form.repo.data, "website": form.website.data, "issueTracker": form.issueTracker.data, diff --git a/app/flatpages/help/api.md b/app/flatpages/help/api.md index b56b1e7..962c2ed 100644 --- a/app/flatpages/help/api.md +++ b/app/flatpages/help/api.md @@ -1,12 +1,29 @@ title: API +## Responses and Error Handling + +If there is an error, the response will be JSON similar to the following with a non-200 status code: + +```json +{ + "success": false, + "error": "The error message" +} +``` + +Successful GET requests will return the resource's information directly as a JSON response. + +Other successful results will return a dictionary with `success` equaling true, and +often other keys with information. + + ## Authentication -Not all endpoints require authentication. -Authentication is done using Bearer tokens: +Not all endpoints require authentication, but it is done using Bearer tokens: ```bash -curl -H "Authorization: Bearer YOURTOKEN" https://content.minetest.net/api/whoami/ +curl https://content.minetest.net/api/whoami/ \ + -H "Authorization: Bearer YOURTOKEN" ``` Tokens can be attained by visiting [Settings > API Tokens](/user/tokens/). @@ -23,19 +40,37 @@ Tokens can be attained by visiting [Settings > API Tokens](/user/tokens/). * See [Package Queries](#package-queries) * GET `/api/packages///` (Read) * PUT `/api/packages///` (Update) - * JSON dictionary with any of these keys (all are optional): - * `title`: Human-readable title. - * `short_description` - * `desc` + * JSON dictionary with any of these keys (all are optional, null to delete Nullables): * `type`: One of `GAME`, `MOD`, `TXP`. + * `title`: Human-readable title. + * `name`: Technical name (needs permission if already approved). + * `short_description` + * `tags`: List of tag names, see [misc](#misc). + * `content_Warnings`: List of content warning names, see [misc](#misc). * `license`: A license name. - * `media_license`: A license name. + * `media_license`: A license name. + * `desc`: Long markdown description. * `repo`: Git repo URL. * `website`: Website URL. * `issue_tracker`: Issue tracker URL. + * `forums`: forum topic ID. * GET `/api/packages///dependencies/` * If query argument `only_hard` is present, only hard deps will be returned. +Examples: + +```bash +# Edit packages +curl -X PUT http://localhost:5123/api/packages/username/name/ \ + -H "Authorization: Bearer YOURTOKEN" -H "Content-Type: application/json" \ + -d '{ "title": "Foo bar", "tags": ["pvp", "survival"], "license": "wtfpl" }' + +# Remove website URL +curl -X PUT http://localhost:5123/api/packages/username/name/ \ + -H "Authorization: Bearer YOURTOKEN" -H "Content-Type: application/json" \ + -d '{ "website": null }' +``` + ### Package Queries Example: @@ -93,7 +128,7 @@ Examples: # Create release from Git curl -X POST https://content.minetest.net/api/packages/username/name/releases/new/ \ -H "Authorization: Bearer YOURTOKEN" -H "Content-Type: application/json" \ - -d "{\"method\": \"git\", \"title\": \"My Release\", \"ref\": \"master\" }" + -d '{ "method": "git", "title": "My Release", "ref": "master" }' # Create release from zip upload curl -X POST https://content.minetest.net/api/packages/username/name/releases/new/ \ diff --git a/app/logic/packages.py b/app/logic/packages.py index 40b55a9..c00f290 100644 --- a/app/logic/packages.py +++ b/app/logic/packages.py @@ -39,14 +39,18 @@ def get_license(name): name_re = re.compile("^[a-z0-9_]+$") -TYPES = { - "name": str, +any = "?" +ALLOWED_FIELDS = { + "type": any, "title": str, + "name": str, "short_description": str, "short_desc": str, - "desc": str, "tags": list, "content_warnings": list, + "license": any, + "media_license": any, + "desc": str, "repo": str, "website": str, "issue_tracker": str, @@ -63,9 +67,12 @@ def is_int(val): def validate(data: dict): - for key, typ in TYPES.items(): - if data.get(key) is not None: - check(isinstance(data[key], typ), key + " must be a " + typ.__name__) + for key, value in data.items(): + if value is not None: + typ = ALLOWED_FIELDS.get(key) + check(typ is not None, key + " is not a known field") + if typ != any: + check(isinstance(value, typ), key + " must be a " + typ.__name__) if "name" in data: name = data["name"] diff --git a/app/models/packages.py b/app/models/packages.py index ac1bbfb..9e743a7 100644 --- a/app/models/packages.py +++ b/app/models/packages.py @@ -400,6 +400,10 @@ class Package(db.Model): release = self.getDownloadRelease(version=version) return { "author": self.author.username, + "maintainers": [x.username for x in self.maintainers], + + "state": self.state.name, + "name": self.name, "title": self.title, "short_description": self.short_desc, @@ -415,6 +419,9 @@ class Package(db.Model): "issue_tracker": self.issueTracker, "forums": self.forums, + "tags": [x.name for x in self.tags], + "content_warnings": [x.name for x in self.content_warnings], + "provides": [x.name for x in self.provides], "thumbnail": (base_url + tnurl) if tnurl is not None else None, "screenshots": [base_url + ss.url for ss in self.screenshots],