Compare commits

...

43 Commits

Author SHA1 Message Date
rubenwardy 8ad066409c Fix notification digest issue 2022-02-11 17:17:11 +00:00
rubenwardy 4ac8949c3a Disable Celery concurrency, to see if it fixes the session issue 2022-02-10 19:24:37 +00:00
rubenwardy 83b2cf48d4 Fix audit crash 2022-02-10 02:20:12 +00:00
rubenwardy 2bbb117eac Small fixes 2022-02-09 19:14:08 +00:00
rubenwardy f61112a8d7 Add ability for moderators to convert reviews into threads 2022-02-09 12:47:36 +00:00
rubenwardy 3566b030c5 Attempt to fix package session issues 2022-02-08 10:40:20 +00:00
rubenwardy 2d54fe4ed7 Fix issues with Package sets by adding a PackageSet class 2022-02-07 18:10:43 +00:00
rubenwardy 7fdd2cc7c9 Fix small typos in dev_intro 2022-02-06 23:08:58 +00:00
rubenwardy 81a85cbbe5 Update dev intro 2022-02-06 22:58:13 +00:00
rubenwardy 4902436b6b Add start of developer's intro 2022-02-06 22:07:22 +00:00
rubenwardy b82bcb0af9 Disable 'Submit for Approval' when release is broken 2022-02-04 13:36:26 +00:00
rubenwardy eeea5d004a Fix "specific" typo 2022-02-03 17:04:09 +00:00
rubenwardy 97ee0a9f85 Fix crash on game support update 2022-02-03 17:02:01 +00:00
rubenwardy 958f92fd63 Add single API to upload cover image 2022-02-02 01:29:14 +00:00
rubenwardy dfef268b05 Fix docs on cover-image 2022-02-02 01:21:33 +00:00
rubenwardy e7d2f09eb4 Add cover_image to screenshot response in API 2022-02-02 01:20:11 +00:00
rubenwardy 5bb9012655 Add game support to API 2022-02-02 01:11:44 +00:00
rubenwardy a291b2cd6f Add cover_image API
Fixes #360
2022-02-02 01:08:01 +00:00
rubenwardy ead077fb92 Metapackages: Split up "provided" into multiple subheadings 2022-02-02 00:56:56 +00:00
rubenwardy 1c9d6ac865 Rename "Supported Games" to "Compatible Games" 2022-02-02 00:29:16 +00:00
rubenwardy d098ee9dff Run game support update_all on unapproved mods too 2022-02-02 00:25:59 +00:00
rubenwardy b8d95dd222 Add disclaimer to Supported Games section on package pages 2022-02-01 21:54:11 +00:00
rubenwardy 7c93db95a3 Add community hub to list game content 2022-02-01 21:22:28 +00:00
rubenwardy d529634b7f Add game support detection
Part of #232
2022-02-01 20:56:43 +00:00
Y.W 765b5603c1
Translated using Weblate (Chinese (Simplified))
Currently translated at 28.1% (205 of 727 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 28.1% (205 of 727 strings)

Co-authored-by: Y.W <y5nw@outlook.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/zh_Hans/
Translation: Minetest/ContentDB
2022-02-01 15:55:32 +01:00
Gao Tiesuan eec39a3fc5
Translated using Weblate (Chinese (Simplified))
Currently translated at 28.1% (205 of 727 strings)

Co-authored-by: Gao Tiesuan <yepifoas@666email.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/zh_Hans/
Translation: Minetest/ContentDB
2022-02-01 15:55:32 +01:00
Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi 72f66530aa
Translated using Weblate (Malay)
Currently translated at 100.0% (727 of 727 strings)

Co-authored-by: Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi <translation@mnh48.moe>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ms/
Translation: Minetest/ContentDB
2022-02-01 15:55:32 +01:00
Nikita Epifanov 99ee1cfc7e
Translated using Weblate (Russian)
Currently translated at 95.4% (694 of 727 strings)

Co-authored-by: Nikita Epifanov <nikgreens@protonmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ru/
Translation: Minetest/ContentDB
2022-02-01 15:55:32 +01:00
rubenwardy f8e82b63e3 Revert "Limit visibility of unapproved packages to maintainers and approvers" and "Fix 404 on packages when not logged in"
This reverts commit 85a178d90e.
This reverts commit 727db52c19.
2022-02-01 14:54:09 +00:00
rubenwardy afdf06b3f6 Remove confusing min/max version text 2022-01-30 19:27:21 +00:00
rubenwardy d21a86587f Fix content_flags documentation 2022-01-30 19:24:33 +00:00
rubenwardy 38071165d1 Add welcome dialog API 2022-01-30 03:35:32 +00:00
rubenwardy 1cfc152d3b Fix crash on needs tags in user todo 2022-01-29 20:23:15 +00:00
rubenwardy 2db2f61992 Enable Russian language 2022-01-29 20:23:00 +00:00
Balázs Kovács 4543f6ca39
Translated using Weblate (Hungarian)
Currently translated at 18.9% (138 of 727 strings)

Co-authored-by: Balázs Kovács <kovacs.balazs.ktk@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/hu/
Translation: Minetest/ContentDB
2022-01-29 20:51:41 +01:00
Nikita Epifanov f8d518300d
Translated using Weblate (Russian)
Currently translated at 95.4% (694 of 727 strings)

Co-authored-by: Nikita Epifanov <nikgreens@protonmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ru/
Translation: Minetest/ContentDB
2022-01-29 20:51:41 +01:00
Andrij Mizyk 347e214944
Translated using Weblate (Ukrainian)
Currently translated at 6.1% (45 of 727 strings)

Added translation using Weblate (Ukrainian)

Co-authored-by: Andrij Mizyk <andmizyk@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/uk/
Translation: Minetest/ContentDB
2022-01-29 20:51:41 +01:00
Mikitko 99b4d8e084
Translated using Weblate (Russian)
Currently translated at 93.9% (683 of 727 strings)

Co-authored-by: Mikitko <rudzik8@protonmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ru/
Translation: Minetest/ContentDB
2022-01-29 20:51:41 +01:00
Nikita Epifanov 313cab6b2d
Translated using Weblate (Russian)
Currently translated at 93.8% (682 of 727 strings)

Co-authored-by: Nikita Epifanov <nikgreens@protonmail.com>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ru/
Translation: Minetest/ContentDB
2022-01-29 20:51:41 +01:00
debiankaios 494559cfd7
Translated using Weblate (German)
Currently translated at 100.0% (727 of 727 strings)

Translated using Weblate (German)

Currently translated at 97.7% (711 of 727 strings)

Co-authored-by: debiankaios <info@debiankaios.de>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/de/
Translation: Minetest/ContentDB
2022-01-29 20:51:41 +01:00
Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi e3326aa0f1
Translated using Weblate (Malay)
Currently translated at 100.0% (727 of 727 strings)

Co-authored-by: Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi <translation@mnh48.moe>
Translate-URL: https://hosted.weblate.org/projects/minetest/contentdb/ms/
Translation: Minetest/ContentDB
2022-01-29 20:51:41 +01:00
rubenwardy bdd3ab4360 Add is_protected and views to Tags API 2022-01-29 19:26:55 +00:00
rubenwardy 4f9ec2e8a4 Fix attempting to set protected tag in API dropping other tags 2022-01-29 19:25:02 +00:00
49 changed files with 5001 additions and 761 deletions

View File

@ -4,7 +4,9 @@
Content database for Minetest mods, games, and more.\
Developed by rubenwardy, license AGPLv3.0+.
See [Getting Started](docs/getting_started.md).
See [Getting Started](docs/getting_started.md) for setting up a development/prodiction environment.
See [Developer Intro](docs/dev_intro.md) for an overview of the code organisation.
## How-tos

View File

@ -39,6 +39,7 @@ app.config["LANGUAGES"] = {
"fr": "Français",
"id": "Bahasa Indonesia",
"ms": "Bahasa Melayu",
"ru": "русский язык",
}
app.config.from_pyfile(os.environ["FLASK_CONFIG"])

View File

@ -16,15 +16,18 @@
import os
import sys
from typing import List
import requests
from celery import group
from flask import redirect, url_for, flash, current_app
from flask import redirect, url_for, flash, current_app, jsonify
from sqlalchemy import or_, and_
from app.logic.game_support import GameSupportResolver
from app.models import PackageRelease, db, Package, PackageState, PackageScreenshot, MetaPackage, User, \
NotificationType, PackageUpdateConfig, License, UserRank, PackageType
NotificationType, PackageUpdateConfig, License, UserRank, PackageType, PackageGameSupport
from app.tasks.emails import send_pending_digests
from app.tasks.forumtasks import importTopicList, checkAllForumAccounts
from app.tasks.importtasks import importRepoScreenshot, checkZipRelease, check_for_updates
from app.utils import addNotification, get_system_user
@ -321,3 +324,15 @@ def update_screenshot_sizes():
screenshot.height = height
db.session.commit()
@action("Detect game support")
def detect_game_support():
resolver = GameSupportResolver()
resolver.update_all()
db.session.commit()
@action("Send pending notif digests")
def do_send_pending_digests():
send_pending_digests.delay()

View File

@ -39,7 +39,7 @@ def audit():
return render_template("admin/audit.html", log=pagination.items, pagination=pagination)
@bp.route("/admin/audit/<int:id>/")
@bp.route("/admin/audit/<int:id_>/")
@rank_required(UserRank.MODERATOR)
def audit_view(id_):
entry = AuditLogEntry.query.get(id_)

View File

@ -13,13 +13,14 @@
#
# 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/>.
import math
from typing import List
import flask_sqlalchemy
from flask import request, jsonify, current_app
from flask_login import current_user, login_required
from sqlalchemy.orm import subqueryload, joinedload
from sqlalchemy.orm import joinedload
from sqlalchemy.sql.expression import func
from app import csrf
@ -30,7 +31,8 @@ from app.querybuilder import QueryBuilder
from app.utils import is_package_page, get_int_or_abort, url_set_query, abs_url, isYes
from . import bp
from .auth import is_api_authd
from .support import error, api_create_vcs_release, api_create_zip_release, api_create_screenshot, api_order_screenshots, api_edit_package
from .support import error, api_create_vcs_release, api_create_zip_release, api_create_screenshot, \
api_order_screenshots, api_edit_package, api_set_cover_image
from functools import wraps
@ -302,7 +304,7 @@ def create_screenshot(token: APIToken, package: Package):
if file is None:
error(400, "Missing 'file' in multipart body")
return api_create_screenshot(token, package, data["title"], file)
return api_create_screenshot(token, package, data["title"], file, isYes(data.get("is_cover_image")))
@bp.route("/api/packages/<author>/<name>/screenshots/<int:id>/")
@ -355,7 +357,7 @@ def order_screenshots(token: APIToken, package: Package):
error(401, "Authentication needed")
if not package.checkPerm(token.owner, Permission.ADD_SCREENSHOTS):
error(403, "You do not have the permission to delete screenshots")
error(403, "You do not have the permission to change screenshots")
if not token.canOperateOnPackage(package):
error(403, "API token does not have access to the package")
@ -367,6 +369,28 @@ def order_screenshots(token: APIToken, package: Package):
return api_order_screenshots(token, package, request.json)
@bp.route("/api/packages/<author>/<name>/screenshots/cover-image/", methods=["POST"])
@csrf.exempt
@is_package_page
@is_api_authd
@cors_allowed
def set_cover_image(token: APIToken, package: Package):
if not token:
error(401, "Authentication needed")
if not package.checkPerm(token.owner, Permission.ADD_SCREENSHOTS):
error(403, "You do not have the permission to change screenshots")
if not token.canOperateOnPackage(package):
error(403, "API token does not have access to the package")
json = request.json
if json is None or not isinstance(json, dict) or "cover_image" not in json:
error(400, "Expected body to be an object with cover_image as a key")
return api_set_cover_image(token, package, request.json["cover_image"])
@bp.route("/api/packages/<author>/<name>/reviews/")
@is_package_page
@cors_allowed
@ -477,6 +501,26 @@ def homepage():
})
@bp.route("/api/welcome/v1/")
@cors_allowed
def welcome_v1():
featured = Package.query \
.filter(Package.type == PackageType.GAME, Package.state == PackageState.APPROVED,
Package.tags.any(name="featured")) \
.order_by(func.random()) \
.limit(5).all()
mtg = Package.query.filter(Package.author.has(username="Minetest"), Package.name == "minetest_game").one()
featured.insert(2, mtg)
def map_packages(packages: List[Package]):
return [pkg.getAsDictionaryShort(current_app.config["BASE_URL"]) for pkg in packages]
return jsonify({
"featured": map_packages(featured),
})
@bp.route("/api/minetest_versions/")
@cors_allowed
def versions():

View File

@ -19,7 +19,7 @@ from flask import jsonify, abort, make_response, url_for, current_app
from app.logic.packages import do_edit_package
from app.logic.releases import LogicError, do_create_vcs_release, do_create_zip_release
from app.logic.screenshots import do_create_screenshot, do_order_screenshots
from app.logic.screenshots import do_create_screenshot, do_order_screenshots, do_set_cover_image
from app.models import APIToken, Package, MinetestRelease, PackageScreenshot
@ -69,13 +69,13 @@ def api_create_zip_release(token: APIToken, package: Package, title: str, file,
})
def api_create_screenshot(token: APIToken, package: Package, title: str, file, reason="API"):
def api_create_screenshot(token: APIToken, package: Package, title: str, file, is_cover_image: bool, reason="API"):
if not token.canOperateOnPackage(package):
error(403, "API token does not have access to the package")
reason += ", token=" + token.name
ss : PackageScreenshot = guard(do_create_screenshot)(token.owner, package, title, file, reason)
ss : PackageScreenshot = guard(do_create_screenshot)(token.owner, package, title, file, is_cover_image, reason)
return jsonify({
"success": True,
@ -94,6 +94,17 @@ def api_order_screenshots(token: APIToken, package: Package, order: [any]):
})
def api_set_cover_image(token: APIToken, package: Package, cover_image):
if not token.canOperateOnPackage(package):
error(403, "API token does not have access to the package")
guard(do_set_cover_image)(token.owner, package, cover_image)
return jsonify({
"success": True
})
def api_edit_package(token: APIToken, package: Package, data: dict, reason: str = "API"):
if not token.canOperateOnPackage(package):
error(403, "API token does not have access to the package")

View File

@ -53,12 +53,11 @@ def view(name):
.filter(Dependency.optional==True, Package.state==PackageState.APPROVED) \
.all()
similar_topics = None
if mpackage.packages.filter_by(state=PackageState.APPROVED).count() == 0:
similar_topics = ForumTopic.query \
.filter_by(name=name) \
.order_by(db.asc(ForumTopic.name), db.asc(ForumTopic.title)) \
.all()
similar_topics = ForumTopic.query \
.filter_by(name=name) \
.filter(~ db.exists().where(Package.forums == ForumTopic.topic_id)) \
.order_by(db.asc(ForumTopic.name), db.asc(ForumTopic.title)) \
.all()
return render_template("metapackages/view.html", mpackage=mpackage,
dependers=dependers, optional_dependers=optional_dependers,

View File

@ -65,4 +65,4 @@ def get_package_tabs(user: User, package: Package):
]
from . import packages, screenshots, releases, reviews
from . import packages, screenshots, releases, reviews, game_hub

View File

@ -0,0 +1,54 @@
# ContentDB
# Copyright (C) 2022 rubenwardy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# 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 flask import render_template, abort
from sqlalchemy.orm import joinedload
from . import bp
from app.utils import is_package_page
from ...models import Package, PackageType, PackageState, db, PackageRelease
@bp.route("/packages/<author>/<name>/hub/")
@is_package_page
def game_hub(package: Package):
if package.type != PackageType.GAME:
abort(404)
def join(query):
return query.options(
joinedload(Package.license),
joinedload(Package.media_license))
query = Package.query.filter(Package.supported_games.any(game=package), Package.state==PackageState.APPROVED)
count = query.count()
new = join(query.order_by(db.desc(Package.approved_at))).limit(4).all()
pop_mod = join(query.filter_by(type=PackageType.MOD).order_by(db.desc(Package.score))).limit(8).all()
pop_gam = join(query.filter_by(type=PackageType.GAME).order_by(db.desc(Package.score))).limit(8).all()
pop_txp = join(query.filter_by(type=PackageType.TXP).order_by(db.desc(Package.score))).limit(8).all()
high_reviewed = join(query.order_by(db.desc(Package.score - Package.score_downloads))) \
.filter(Package.reviews.any()).limit(4).all()
updated = db.session.query(Package).select_from(PackageRelease).join(Package) \
.filter(Package.supported_games.any(game=package), Package.state==PackageState.APPROVED) \
.order_by(db.desc(PackageRelease.releaseDate)) \
.limit(20).all()
updated = updated[:4]
return render_template("packages/game_hub.html", package=package, count=count,
new=new, updated=updated, pop_mod=pop_mod, pop_txp=pop_txp, pop_gam=pop_gam,
high_reviewed=high_reviewed)

View File

@ -115,9 +115,6 @@ def getReleases(package):
@bp.route("/packages/<author>/<name>/")
@is_package_page
def view(package):
if not package.checkPerm(current_user, Permission.SEE_PACKAGE):
abort(404)
show_similar = not package.approved and (
current_user in package.maintainers or
package.checkPerm(current_user, Permission.APPROVE_NEW))
@ -208,9 +205,6 @@ def shield(package, type):
@bp.route("/packages/<author>/<name>/download/")
@is_package_page
def download(package):
if not package.checkPerm(current_user, Permission.SEE_PACKAGE):
abort(404)
release = package.getDownloadRelease()
if release is None:
@ -593,9 +587,6 @@ def alias_create_edit(package: Package, alias_id: int = None):
@login_required
@is_package_page
def share(package):
if not package.checkPerm(current_user, Permission.SEE_PACKAGE):
abort(404)
return render_template("packages/share.html", package=package,
tabs=get_package_tabs(current_user, package), current_tab="share")
@ -603,9 +594,6 @@ def share(package):
@bp.route("/packages/<author>/<name>/similar/")
@is_package_page
def similar(package):
if not package.checkPerm(current_user, Permission.SEE_PACKAGE):
abort(404)
packages_modnames = {}
for metapackage in package.provides:
packages_modnames[metapackage] = Package.query.filter(Package.id != package.id,

View File

@ -33,9 +33,6 @@ from . import bp, get_package_tabs
@bp.route("/packages/<author>/<name>/releases/", methods=["GET", "POST"])
@is_package_page
def list_releases(package):
if not package.checkPerm(current_user, Permission.SEE_PACKAGE):
abort(404)
return render_template("packages/releases_list.html",
package=package,
tabs=get_package_tabs(current_user, package), current_tab="releases")
@ -111,9 +108,6 @@ def create_release(package):
@bp.route("/packages/<author>/<name>/releases/<id>/download/")
@is_package_page
def download_release(package, id):
if not package.checkPerm(current_user, Permission.SEE_PACKAGE):
abort(404)
release = PackageRelease.query.get(id)
if release is None or release.package != package:
abort(404)

View File

@ -25,8 +25,8 @@ from flask_wtf import FlaskForm
from wtforms import *
from wtforms.validators import *
from app.models import db, PackageReview, Thread, ThreadReply, NotificationType, PackageReviewVote, Package, UserRank, \
Permission
from app.utils import is_package_page, addNotification, get_int_or_abort, isYes, is_safe_url, rank_required
Permission, AuditSeverity
from app.utils import is_package_page, addNotification, get_int_or_abort, isYes, is_safe_url, rank_required, addAuditLog
from app.tasks.webhooktasks import post_discord_webhook
@ -54,9 +54,6 @@ def review(package):
flash(gettext("You can't review your own package!"), "danger")
return redirect(package.getURL("packages.view"))
if not package.checkPerm(current_user, Permission.SEE_PACKAGE):
abort(404)
review = PackageReview.query.filter_by(package=package, author=current_user).first()
form = ReviewForm(formdata=request.form, obj=review)
@ -129,14 +126,19 @@ def review(package):
form=form, package=package, review=review)
@bp.route("/packages/<author>/<name>/review/delete/", methods=["POST"])
@bp.route("/packages/<author>/<name>/reviews/<reviewer>/delete/", methods=["POST"])
@login_required
@is_package_page
def delete_review(package):
review = PackageReview.query.filter_by(package=package, author=current_user).first()
def delete_review(package, reviewer):
review = PackageReview.query \
.filter(PackageReview.package == package, PackageReview.author.has(username=reviewer)) \
.first()
if review is None or review.package != package:
abort(404)
if not review.checkPerm(current_user, Permission.DELETE_REVIEW):
abort(403)
thread = review.thread
reply = ThreadReply()
@ -147,10 +149,17 @@ def delete_review(package):
thread.review = None
msg = "Converted review by {} to thread".format(review.author.display_name)
addAuditLog(AuditSeverity.MODERATION if current_user.username != reviewer else AuditSeverity.NORMAL,
current_user, msg, thread.getViewURL(), thread.package)
notif_msg = "Deleted review '{}', comments were kept as a thread".format(thread.title)
addNotification(package.maintainers, current_user, NotificationType.OTHER, notif_msg, url_for("threads.view", id=thread.id), package)
db.session.delete(review)
package.recalcScore()
db.session.commit()
return redirect(thread.getViewURL())
@ -228,4 +237,4 @@ def review_votes(package):
user_biases_info.sort(key=lambda x: -abs(x.balance))
return render_template("packages/review_votes.html", form=form, package=package, reviews=package.reviews,
user_biases=user_biases_info)
user_biases=user_biases_info)

View File

@ -87,7 +87,7 @@ def create_screenshot(package):
form = CreateScreenshotForm()
if form.validate_on_submit():
try:
do_create_screenshot(current_user, package, form.title.data, form.fileUpload.data)
do_create_screenshot(current_user, package, form.title.data, form.fileUpload.data, False)
return redirect(package.getURL("packages.screenshots"))
except LogicError as e:
flash(e.message, "danger")

View File

@ -185,8 +185,8 @@ def view_user(username=None):
.all()
needs_tags = user.maintained_packages \
.filter(Package.state != PackageState.DELETED) \
.filter_by(tags=None).order_by(db.asc(Package.title)).all()
.filter(Package.state != PackageState.DELETED, Package.tags==None) \
.order_by(db.asc(Package.title)).all()
return render_template("todo/user.html", current_tab="user", user=user,
unapproved_packages=unapproved_packages, outdated_packages=outdated_packages,

View File

@ -90,6 +90,7 @@ Tokens can be attained by visiting [Settings > API Tokens](/user/tokens/).
* `issue_tracker`: Issue tracker URL.
* `forums`: forum topic ID.
* `video_url`: URL to a video.
* `game_support`: Array of game support information objects. Not currently documented, as subject to change.
* GET `/api/packages/<username>/<name>/dependencies/`
* Returns dependencies, with suggested candidates
* If query argument `only_hard` is present, only hard deps will be returned.
@ -225,6 +226,7 @@ curl -X DELETE https://content.minetest.net/api/packages/username/name/releases/
* `url`: absolute URL to screenshot.
* `created_at`: ISO time.
* `order`: Number used in ordering.
* `is_cover_image`: true for cover image.
* GET `/api/packages/<username>/<name>/screenshots/<id>/` (Read)
* Returns screenshot dictionary like above.
* POST `/api/packages/<username>/<name>/screenshots/new/` (Create)
@ -232,12 +234,16 @@ curl -X DELETE https://content.minetest.net/api/packages/username/name/releases/
* Body is multipart form data.
* `title`: human-readable name for the screenshot, shown as a caption and alt text.
* `file`: multipart file to upload, like `<input type=file>`.
* `is_cover_image`: set cover image to this.
* DELETE `/api/packages/<username>/<name>/screenshots/<id>/` (Delete)
* Requires authentication.
* Deletes screenshot.
* POST `/api/packages/<username>/<name>/screenshots/order/`
* Requires authentication.
* Body is a JSON array containing the screenshot IDs in their order.
* POST `/api/packages/<username>/<name>/screenshots/cover-image/`
* Requires authentication.
* Body is a JSON dictionary with "cover_image" containing the screenshot ID.
Currently, to get a different size of thumbnail you can replace the number in `/thumbnails/1/` with any number from 1-3.
The resolutions returned may change in the future, and we may move to a more capable thumbnail generation.
@ -249,6 +255,11 @@ Examples:
curl -X POST https://content.minetest.net/api/packages/username/name/screenshots/new/ \
-H "Authorization: Bearer YOURTOKEN" \
-F title="My Release" -F file=@path/to/screnshot.png
# Create screenshot and set it as the cover image
curl -X POST https://content.minetest.net/api/packages/username/name/screenshots/new/ \
-H "Authorization: Bearer YOURTOKEN" \
-F title="My Release" -F file=@path/to/screnshot.png -F is_cover_image="true"
# Delete screenshot
curl -X DELETE https://content.minetest.net/api/packages/username/name/screenshots/3/ \
@ -258,6 +269,11 @@ curl -X DELETE https://content.minetest.net/api/packages/username/name/screensho
curl -X POST https://content.minetest.net/api/packages/username/name/screenshots/order/ \
-H "Authorization: Bearer YOURTOKEN" -H "Content-Type: application/json" \
-d "[13, 2, 5, 7]"
# Set cover image
curl -X POST https://content.minetest.net/api/packages/username/name/screenshots/cover-image/ \
-H "Authorization: Bearer YOURTOKEN" -H "Content-Type: application/json" \
-d "{ 'cover_image': 123 }"
```
@ -330,9 +346,11 @@ Supported query parameters:
### Tags
* GET `/api/tags/` ([View](/api/tags/)): List of:
* `name`: technical name
* `title`: human-readable title
* `description`: tag description or null
* `name`: technical name.
* `title`: human-readable title.
* `description`: tag description or null.
* `is_protected`: boolean, whether the tag is protected (can only be set by Editors in the web interface).
* `views`: number of views of this tag.
### Content Warnings
@ -376,3 +394,5 @@ Supported query parameters:
* `pop_txp`: popular textures
* `pop_game`: popular games
* `high_reviewed`: highest reviewed
* GET `/api/welcome/v1/` ([View](/api/welcome/v1/)) - in-menu welcome dialog. Experimental (may change without warning)
* `featured`: featured games

View File

@ -25,8 +25,8 @@ A flag can be:
There are also two meta-flags, which are designed so that we can change how different platforms filter the package list
without making a release.
* `android_default`: currently same as `*, deprecated`. Hides all content warnings, WIP packages, and deprecated packages
* `desktop_default`: currently same as `deprecated`. Hides all WIP and deprecated packages
* `android_default`: currently same as `*, deprecated`. Hides all content warnings and deprecated packages
* `desktop_default`: currently same as `deprecated`. Hides deprecated packages
## Content Warnings

188
app/logic/game_support.py Normal file
View File

@ -0,0 +1,188 @@
# ContentDB
# Copyright (C) 2022 rubenwardy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# 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/>.
import sys
from typing import List, Dict, Optional, Iterator, Iterable
from app.logic.LogicError import LogicError
from app.models import Package, MetaPackage, PackageType, PackageState, PackageGameSupport, db
"""
get_game_support(package):
if package is a game:
return [ package ]
for all hard dependencies:
support = support AND get_meta_package_support(dep)
return support
get_meta_package_support(meta):
for package implementing meta package:
support = support OR get_game_support(package)
return support
"""
minetest_game_mods = {
"beds", "boats", "bucket", "carts", "default", "dungeon_loot", "env_sounds", "fire", "flowers",
"give_initial_stuff", "map", "player_api", "sethome", "spawn", "tnt", "walls", "wool",
"binoculars", "bones", "butterflies", "creative", "doors", "dye", "farming", "fireflies", "game_commands",
"keys", "mtg_craftguide", "screwdriver", "sfinv", "stairs", "vessels", "weather", "xpanes",
}
mtg_mod_blacklist = {
"repixture", "tutorial", "runorfall", "realtest_mt5", "mevo", "xaenvironment",
"survivethedays"
}
class PackageSet:
packages: Dict[str, Package]
def __init__(self, packages: Optional[Iterable[Package]] = None):
self.packages = {}
if packages:
self.update(packages)
def update(self, packages: Iterable[Package]):
for package in packages:
key = package.getId()
if key not in self.packages:
self.packages[key] = package
def intersection_update(self, other):
keys = set(self.packages.keys())
keys.difference_update(set(other.packages.keys()))
for key in keys:
del self.packages[key]
def __len__(self):
return len(self.packages)
def __iter__(self):
return self.packages.values().__iter__()
class GameSupportResolver:
checked_packages = set()
checked_metapackages = set()
resolved_packages: Dict[str, PackageSet] = {}
resolved_metapackages: Dict[str, PackageSet] = {}
def resolve_for_meta_package(self, meta: MetaPackage, history: List[str]) -> PackageSet:
print(f"Resolving for {meta.name}", file=sys.stderr)
key = meta.name
if key in self.resolved_metapackages:
return self.resolved_metapackages.get(key)
if key in self.checked_metapackages:
print(f"Error, cycle found: {','.join(history)}", file=sys.stderr)
return PackageSet()
self.checked_metapackages.add(key)
retval = PackageSet()
for package in meta.packages:
if package.state != PackageState.APPROVED:
continue
if meta.name in minetest_game_mods and package.name in mtg_mod_blacklist:
continue
ret = self.resolve(package, history)
if len(ret) == 0:
retval = PackageSet()
break
retval.update(ret)
self.resolved_metapackages[key] = retval
return retval
def resolve(self, package: Package, history: List[str]) -> PackageSet:
db.session.merge(package)
key = package.getId()
print(f"Resolving for {key}", file=sys.stderr)
history = history.copy()
history.append(key)
if package.type == PackageType.GAME:
return PackageSet([package])
if key in self.resolved_packages:
return self.resolved_packages.get(key)
if key in self.checked_packages:
print(f"Error, cycle found: {','.join(history)}", file=sys.stderr)
return PackageSet()
self.checked_packages.add(key)
if package.type != PackageType.MOD:
raise LogicError(500, "Got non-mod")
retval = PackageSet()
for dep in package.dependencies.filter_by(optional=False).all():
ret = self.resolve_for_meta_package(dep.meta_package, history)
if len(ret) == 0:
continue
elif len(retval) == 0:
retval.update(ret)
else:
retval.intersection_update(ret)
if len(retval) == 0:
raise LogicError(500, f"Detected game support contradiction, {key} may not be compatible with any games")
self.resolved_packages[key] = retval
return retval
def update_all(self) -> None:
for package in Package.query.filter(Package.type == PackageType.MOD, Package.state != PackageState.DELETED).all():
retval = self.resolve(package, [])
for game in retval:
support = PackageGameSupport(package, game)
db.session.add(support)
def update(self, package: Package) -> None:
previous_supported: Dict[str, PackageGameSupport] = {}
for support in package.supported_games.all():
previous_supported[support.game.getId()] = support
retval = self.resolve(package, [])
for game in retval:
assert game
lookup = previous_supported.pop(game.getId(), None)
if lookup is None:
support = PackageGameSupport(package, game)
db.session.add(support)
elif lookup.confidence == 0:
lookup.supports = True
db.session.merge(lookup)
for game, support in previous_supported.items():
if support.confidence == 0:
db.session.remove(support)

View File

@ -159,7 +159,7 @@ def do_edit_package(user: User, package: Package, was_new: bool, was_web: bool,
raise LogicError(400, "Unknown tag: " + tag_id)
if not was_web and tag.is_protected:
break
continue
if tag.is_protected and tag not in old_tags and not user.rank.atLeast(UserRank.EDITOR):
raise LogicError(400, lazy_gettext("Unable to add protected tag %(title)s to package", title=tag.title))

View File

@ -9,7 +9,7 @@ from app.utils import addNotification, addAuditLog
from app.utils.image import get_image_size
def do_create_screenshot(user: User, package: Package, title: str, file, reason: str = None):
def do_create_screenshot(user: User, package: Package, title: str, file, is_cover_image: bool, reason: str = None):
thirty_minutes_ago = datetime.datetime.now() - datetime.timedelta(minutes=30)
count = package.screenshots.filter(PackageScreenshot.created_at > thirty_minutes_ago).count()
if count >= 20:
@ -47,6 +47,10 @@ def do_create_screenshot(user: User, package: Package, title: str, file, reason:
db.session.commit()
if is_cover_image:
package.cover_image = ss
db.session.commit()
return ss
@ -66,3 +70,18 @@ def do_order_screenshots(_user: User, package: Package, order: [any]):
raise LogicError(400, "Invalid id, not a number: {}".format(json.dumps(ss_id)))
db.session.commit()
def do_set_cover_image(_user: User, package: Package, cover_image):
try:
cover_image = int(cover_image)
except (ValueError, TypeError) as e:
raise LogicError(400, "Invalid id, not a number: {}".format(json.dumps(cover_image)))
for screenshot in package.screenshots.all():
if screenshot.id == cover_image:
package.cover_image = screenshot
db.session.commit()
return
raise LogicError(400, "Unable to find screenshot")

View File

@ -344,6 +344,25 @@ class Dependency(db.Model):
return retval
class PackageGameSupport(db.Model):
id = db.Column(db.Integer, primary_key=True)
package_id = db.Column(db.Integer, db.ForeignKey("package.id"), nullable=False)
package = db.relationship("Package", foreign_keys=[package_id])
game_id = db.Column(db.Integer, db.ForeignKey("package.id"), nullable=False)
game = db.relationship("Package", foreign_keys=[game_id])
supports = db.Column(db.Boolean, nullable=False, default=True)
confidence = db.Column(db.Integer, nullable=False, default=1)
__table_args__ = (db.UniqueConstraint("game_id", "package_id", name="_package_game_support_uc"),)
def __init__(self, package, game):
self.package = package
self.game = game
class Package(db.Model):
query_class = PackageQuery
@ -396,6 +415,12 @@ class Package(db.Model):
dependencies = db.relationship("Dependency", back_populates="depender", lazy="dynamic", foreign_keys=[Dependency.depender_id])
supported_games = db.relationship("PackageGameSupport", back_populates="package", lazy="dynamic",
foreign_keys=[PackageGameSupport.package_id])
game_supported_mods = db.relationship("PackageGameSupport", back_populates="game", lazy="dynamic",
foreign_keys=[PackageGameSupport.game_id])
tags = db.relationship("Tag", secondary=Tags, back_populates="packages")
content_warnings = db.relationship("ContentWarning", secondary=ContentWarnings, back_populates="packages")
@ -450,6 +475,14 @@ class Package(db.Model):
for e in PackagePropertyKey:
setattr(self, e.name, getattr(package, e.name))
@classmethod
def get_by_key(cls, key):
parts = key.split("/")
if len(parts) != 2:
return None
return Package.query.filter(Package.name == parts[1], Package.author.has(username=parts[0])).first()
def getId(self):
return "{}/{}".format(self.author.username, self.name)
@ -471,6 +504,11 @@ class Package(db.Model):
def getSortedOptionalDependencies(self):
return self.getSortedDependencies(False)
def getSortedSupportedGames(self):
supported = self.supported_games.all()
supported.sort(key=lambda x: -x.game.score)
return supported
def getAsDictionaryKey(self):
return {
"name": self.name,
@ -542,7 +580,15 @@ class Package(db.Model):
"release": release and release.id,
"score": round(self.score * 10) / 10,
"downloads": self.downloads
"downloads": self.downloads,
"game_support": [
{
"supports": support.supports,
"confidence": support.confidence,
"game": support.game.getAsDictionaryShort(base_url, version)
} for support in self.supported_games.all()
]
}
def getThumbnailOrPlaceholder(self, level=2):
@ -599,7 +645,7 @@ class Package(db.Model):
def checkPerm(self, user, perm):
if not user.is_authenticated:
return perm == Permission.SEE_PACKAGE and self.state == PackageState.APPROVED
return False
if type(perm) == str:
perm = Permission[perm]
@ -610,10 +656,7 @@ class Package(db.Model):
isMaintainer = isOwner or user.rank.atLeast(UserRank.EDITOR) or user in self.maintainers
isApprover = user.rank.atLeast(UserRank.APPROVER)
if perm == Permission.SEE_PACKAGE:
return self.state == PackageState.APPROVED or isMaintainer or isApprover
elif perm == Permission.CREATE_THREAD:
if perm == Permission.CREATE_THREAD:
return user.rank.atLeast(UserRank.MEMBER)
# Members can edit their own packages, and editors can edit any packages
@ -687,7 +730,8 @@ class Package(db.Model):
needsScreenshot = \
(self.type == self.type.GAME or self.type == self.type.TXP) and \
self.screenshots.count() == 0
return self.releases.count() > 0 and not needsScreenshot
return self.releases.filter(PackageRelease.task_id.is_(None)).count() > 0 and not needsScreenshot
elif state == PackageState.CHANGES_NEEDED:
return self.checkPerm(user, Permission.APPROVE_NEW)
@ -818,7 +862,13 @@ class Tag(db.Model):
def getAsDictionary(self):
description = self.description if self.description != "" else None
return { "name": self.name, "title": self.title, "description": description }
return {
"name": self.name,
"title": self.title,
"description": description,
"is_protected": self.is_protected,
"views": self.views,
}
class MinetestRelease(db.Model):
@ -1042,8 +1092,11 @@ class PackageScreenshot(db.Model):
"order": self.order,
"title": self.title,
"url": base_url + self.url,
"width": self.width,
"height": self.height,
"approved": self.approved,
"created_at": self.created_at.isoformat(),
"is_cover_image": self.package.cover_image == self,
}

View File

@ -200,7 +200,8 @@ class PackageReview(db.Model):
def getDeleteURL(self):
return url_for("packages.delete_review",
author=self.package.author.username,
name=self.package.name)
name=self.package.name,
reviewer=self.author.username)
def getVoteUrl(self, next_url=None):
return url_for("packages.review_vote",
@ -213,6 +214,20 @@ class PackageReview(db.Model):
(pos, neg, _) = self.get_totals()
self.score = 3 * (pos - neg) + 1
def checkPerm(self, user, perm):
if not user.is_authenticated:
return False
if type(perm) == str:
perm = Permission[perm]
elif type(perm) != Permission:
raise Exception("Unknown permission given to PackageReview.checkPerm()")
if perm == Permission.DELETE_REVIEW:
return user == self.author or user.rank.atLeast(UserRank.MODERATOR)
else:
raise Exception("Permission {} is not related to reviews".format(perm.name))
class PackageReviewVote(db.Model):
review_id = db.Column(db.Integer, db.ForeignKey("package_review.id"), primary_key=True)

View File

@ -59,7 +59,6 @@ class UserRank(enum.Enum):
class Permission(enum.Enum):
SEE_PACKAGE = "SEE_PACKAGE"
EDIT_PACKAGE = "EDIT_PACKAGE"
DELETE_PACKAGE = "DELETE_PACKAGE"
CHANGE_AUTHOR = "CHANGE_AUTHOR"
@ -87,6 +86,7 @@ class Permission(enum.Enum):
TOPIC_DISCARD = "TOPIC_DISCARD"
CREATE_TOKEN = "CREATE_TOKEN"
EDIT_MAINTAINERS = "EDIT_MAINTAINERS"
DELETE_REVIEW = "DELETE_REVIEW"
CHANGE_PROFILE_URLS = "CHANGE_PROFILE_URLS"
CHANGE_DISPLAY_NAME = "CHANGE_DISPLAY_NAME"

View File

@ -1,3 +1,6 @@
// @author rubenwardy
// @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later
$("textarea.markdown").each(function() {
async function render(plainText, preview) {
const response = await fetch(new Request("/api/markdown/", {

View File

@ -1,3 +1,6 @@
// @author rubenwardy
// @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later
const min = $("#min_rel");
const max = $("#max_rel");
const none = $("#min_rel option:first-child").attr("value");

View File

@ -1,3 +1,6 @@
// @author rubenwardy
// @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later
$(".topic-discard").click(function() {
const ele = $(this);
const tid = ele.attr("data-tid");

View File

@ -1,3 +1,6 @@
// @author rubenwardy
// @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later
document.querySelectorAll(".video-embed").forEach(ele => {
try {
const href = ele.getAttribute("href");

View File

@ -75,6 +75,10 @@ class QueryBuilder:
if self.search is not None and self.search.strip() == "":
self.search = None
self.game = args.get("game")
if self.game:
self.game = Package.get_by_key(self.game)
def setSortIfNone(self, name, dir="desc"):
if self.order_by is None:
self.order_by = name
@ -132,6 +136,9 @@ class QueryBuilder:
query = query.filter_by(author=author)
if self.game:
query = query.filter(Package.supported_games.any(game=self.game))
for tag in self.tags:
query = query.filter(Package.tags.any(Tag.id == tag.id))

View File

@ -13,6 +13,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/>.
import json
import os, shutil, gitdb
from zipfile import ZipFile
@ -22,10 +23,11 @@ from kombu import uuid
from app.models import *
from app.tasks import celery, TaskError
from app.utils import randomString, post_bot_message, addSystemNotification, addSystemAuditLog, get_system_user
from app.utils import randomString, post_bot_message, addSystemNotification, addSystemAuditLog
from app.utils.git import clone_repo, get_latest_tag, get_latest_commit, get_temp_dir
from .minetestcheck import build_tree, MinetestCheckError, ContentType
from ..logic.LogicError import LogicError
from ..logic.game_support import GameSupportResolver
from ..logic.packages import do_edit_package, ALIASES
from ..utils.image import get_image_size
@ -113,6 +115,11 @@ def postReleaseCheckUpdate(self, release: PackageRelease, path):
for meta in getMetaPackages(optional_depends):
db.session.add(Dependency(package, meta=meta, optional=True))
# Update game supports
if package.type == PackageType.MOD:
resolver = GameSupportResolver()
resolver.update(package)
# Update min/max
if tree.meta.get("min_minetest_version"):
release.min_rel = MinetestRelease.get(tree.meta["min_minetest_version"], None)

View File

@ -2,7 +2,7 @@
{% block content %}
{% for title, group in notifications | select("package") | groupby("package.title") %}
{% for title, group in notifications | selectattr("package") | groupby("package.title") %}
<h2>
{{ title }}
</h2>
@ -17,20 +17,23 @@
</ul>
{% endfor %}
{% for group in notifications | reject("package") %}
{% set other_notifications = notifications | selectattr("package", "none") %}
{% if other_notifications %}
<h2>
{{ _("Other Notifications") }}
</h2>
<ul>
{% for notification in group %}
{% for notification in other_notifications %}
<li>
<a href="{{ notification.url | abs_url }}">{{ notification.title }}</a> -
{{ _("from %(username)s.", username=notification.causer.username) }}
</li>
{% endfor %}
</ul>
{% endfor %}
{% endif %}
<p style="margin-top: 3em;">
<a class="btn" href="{{ abs_url_for('notifications.list_all') }}">
{{ _("View Notifications") }}

View File

@ -3,7 +3,7 @@
{% for entry in log %}
<a class="list-group-item list-group-item-action"
{% if entry.description and current_user.rank.atLeast(current_user.rank.MODERATOR) %}
href="{{ url_for('admin.audit_view', id=entry.id) }}">
href="{{ url_for('admin.audit_view', id_=entry.id) }}">
{% else %}
href="{{ entry.url }}">
{% endif %}

View File

@ -14,19 +14,24 @@
</div>
{% set level = "warning" %}
{% if package.releases.count() == 0 %}
{% if package.releases.filter_by(task_id=None).count() == 0 %}
{% set message %}
{% if package.checkPerm(current_user, "MAKE_RELEASE") %}
{% if package.update_config %}
<a class="btn btn-sm btn-warning float-right" href="{{ package.getURL("packages.create_release") }}">
{{ _("Create first release") }}
<a class="btn btn-sm btn-warning float-right" href="{{ package.getURL('packages.create_release') }}">
{{ _("Create release") }}
</a>
{% else %}
<a class="btn btn-sm btn-warning float-right" href="{{ package.getURL("packages.setup_releases") }}">
<a class="btn btn-sm btn-warning float-right" href="{{ package.getURL('packages.setup_releases') }}">
{{ _("Set up releases") }}
</a>
{% endif %}
{{ _("You need to create a release before this package can be approved.") }}
{% if package.releases.count() == 0 %}
{{ _("You need to create a release before this package can be approved.") }}
{% else %}
{{ _("Release is still importing, or has an error.") }}
{% endif %}
{% else %}
{{ _("A release is required before this package can be approved.") }}
{% endif %}

View File

@ -4,18 +4,21 @@
{{ mpackage.name }} - {{ _("Meta Packages") }}
{% endblock %}
{% from "macros/packagegridtile.html" import render_pkggrid %}
{% block content %}
<h1>{{ _("Meta Package \"%(name)s\"", name=mpackage.name) }}</h1>
<h2>{{ _("Provided By") }}</h2>
{% from "macros/packagegridtile.html" import render_pkggrid %}
{{ render_pkggrid(mpackage.packages.filter_by(state="APPROVED").all()) }}
<h3>{{ _("Games") }}</h3>
{{ render_pkggrid(mpackage.packages.filter_by(type="GAME", state="APPROVED").all()) }}
<h3>{{ _("Mods") }}</h3>
{{ render_pkggrid(mpackage.packages.filter_by(type="MOD", state="APPROVED").all()) }}
{% if similar_topics %}
<p>
{{ _("Unfortunately, this isn't on ContentDB yet! Here's some forum topic(s):") }}
</p>
<h3>{{ _("Forum Topics") }}</h3>
<ul>
{% for t in similar_topics %}
<li>

View File

@ -0,0 +1,57 @@
{% extends "base.html" %}
{% block title %}
{{ _("Community Hub") }} -
{{ _('%(title)s by %(author)s', title=package.title, author=package.author.display_name) }}
{% endblock %}
{% block headextra %}
<meta name="og:title" content="{{ self.title() }}"/>
<meta name="og:description" content="{{ _('Mods for %(title)s', title=package.title) }}"/>
<meta name="description" content="{{ _('Mods for %(title)s', title=package.title) }}"/>
<meta name="og:url" content="{{ package.getURL('packages.game_hub', absolute=True) }}"/>
{% if package.getMainScreenshotURL() %}
<meta name="og:image" content="{{ package.getMainScreenshotURL(absolute=True) }}"/>
{% endif %}
{% endblock %}
{% block content %}
{% from "macros/packagegridtile.html" import render_pkggrid %}
<h1 class="mb-5">
{{ _("Community Hub") }} -
<a href="{{ package.getURL('packages.view') }}">
{{ _('%(title)s by %(author)s', title=package.title, author=package.author.display_name) }}
</a>
</h1>
<a href="{{ url_for('packages.list_all', sort='approved_at', order='desc', game=package.getId()) }}" class="btn btn-secondary float-right">
{{ _("See more") }}
</a>
<h2 class="my-3">{{ _("Recently Added") }}</h2>
{{ render_pkggrid(new) }}
<a href="{{ url_for('packages.list_all', sort='last_release', order='desc', game=package.getId()) }}" class="btn btn-secondary float-right">
{{ _("See more") }}
</a>
<h2 class="my-3">{{ _("Recently Updated") }}</h2>
{{ render_pkggrid(updated) }}
<a href="{{ url_for('packages.list_all', type='mod', sort='score', order='desc', game=package.getId()) }}" class="btn btn-secondary float-right">
{{ _("See more") }}
</a>
<h2 class="my-3">{{ _("Top Mods") }}</h2>
{{ render_pkggrid(pop_mod) }}
<a href="{{ url_for('packages.list_all', sort='reviews', order='desc', game=package.getId()) }}" class="btn btn-secondary float-right">
{{ _("See more") }}
</a>
<h2 class="my-3">{{ _("Highest Reviewed") }}</h2>
{{ render_pkggrid(high_reviewed) }}
{% endblock %}

View File

@ -32,8 +32,6 @@
<p class="mt-3">
{{ _("Note: Min and max versions will be used to hide the package on
platforms not within the range.") }}
{{ _("You cannot select the oldest version for min or the newest version
for max as this does not make sense - you can't predict the future.") }}
<br />
{{ _("Leave both as None if in doubt.") }}
</p>

View File

@ -62,11 +62,6 @@
{{ _("You can <a href='/help/package_config/'>set this automatically</a> in the .conf of your package.") }}
</p>
<p>
{{ _("You cannot select the oldest version for min or the newest version
for max as this does not make sense - you can't predict the future.") }}
</p>
<p class="mt-5">
{{ render_submit_field(form.submit) }}
</p>

View File

@ -82,11 +82,6 @@
<br />
{{ _("Leave both as None if in doubt.") }}
</p>
<p>
{{ _("You cannot select the oldest version for min or the newest version
for max as this does not make sense - you can't predict the future.") }}
</p>
<p class="mt-5">
{{ render_submit_field(form.submit) }}
</p>

View File

@ -322,6 +322,13 @@
{% from "macros/packagegridtile.html" import render_pkggrid %}
{{ render_pkggrid(packages_uses) }}
{% endif %}
{% if package.type == package.type.GAME %}
<h2>{{ _("Content") }}</h2>
<a href="{{ package.getURL('packages.game_hub') }}" class="btn btn-lg btn-primary">
{{ _("View content for game") }}
</a>
{% endif %}
</div>
<aside class="col-md-3 info-sidebar">
@ -371,6 +378,12 @@
</div>
{% endif %}
{% if package.type == package.type.GAME %}
<a href="{{ package.getURL('packages.game_hub') }}" class="btn btn-lg btn-block mb-4 btn-primary">
{{ _("View content for game") }}
</a>
{% endif %}
{% if package.type != package.type.TXP %}
<h3>{{ _("Dependencies") }}</h3>
<dl>
@ -419,6 +432,23 @@
</dl>
{% endif %}
{% if package.type == package.type.MOD %}
<h3>{{ _("Compatible Games") }}</h3>
{% for support in package.getSortedSupportedGames() %}
<a class="badge badge-secondary"
href="{{ support.game.getURL('packages.view') }}">
{{ _("%(title)s by %(display_name)s",
title=support.game.title, display_name=support.game.author.display_name) }}
</a>
{% else %}
{{ _("No specific game is required") }}
{% endfor %}
<p class="text-muted small mt-2 mb-0">
{{ _("This is an experimental feature.") }}
{{ _("Supported games are determined by an algorithm, and may not be correct.") }}
</p>
{% endif %}
<h3>
{{ _("Information") }}
</h3>

View File

@ -36,10 +36,16 @@
<input type="submit" class="btn btn-primary" value="{{ _('Subscribe') }}" />
</form>
{% endif %}
{% if thread and thread.checkPerm(current_user, "DELETE_THREAD") %}
{% if thread.checkPerm(current_user, "DELETE_THREAD") %}
<a href="{{ url_for('threads.delete_thread', id=thread.id) }}" class="float-right mr-2 btn btn-danger">{{ _('Delete') }}</a>
{% endif %}
{% if thread and thread.checkPerm(current_user, "LOCK_THREAD") %}
{% if thread.review and thread.review.checkPerm(current_user, "DELETE_REVIEW") and current_user.username != thread.review.author.username %}
<form method="post" action="{{ thread.review.getDeleteURL() }}" class="float-right mr-2">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="submit" class="btn btn-danger" value="{{ _('Convert to Thread') }}" />
</form>
{% endif %}
{% if thread.checkPerm(current_user, "LOCK_THREAD") %}
{% if thread.locked %}
<form method="post" action="{{ url_for('threads.set_lock', id=thread.id, lock=0) }}" class="float-right mr-2">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />

View File

@ -18,8 +18,7 @@
from functools import wraps
from flask import abort, redirect, url_for, request
from flask_login import current_user
from app.models import User, NotificationType, Package, UserRank, Notification, db, AuditSeverity, AuditLogEntry, \
ThreadReply, Thread, PackageState, PackageType, PackageAlias
from app.models import User, NotificationType, Package, UserRank, Notification, db, AuditSeverity, AuditLogEntry, ThreadReply, Thread, PackageState, PackageType, PackageAlias
def getPackageByInfo(author, name):
@ -40,15 +39,14 @@ def is_package_page(f):
if not ("author" in kwargs and "name" in kwargs):
abort(400)
author = kwargs.pop("author")
name = kwargs.pop("name")
author = kwargs["author"]
name = kwargs["name"]
package = getPackageByInfo(author, name)
if package is None:
package = getPackageByInfo(author, name + "_game")
if package and package.type == PackageType.GAME:
args = dict(kwargs)
args["author"] = author
args["name"] = name + "_game"
return redirect(url_for(request.endpoint, **args))
@ -61,6 +59,8 @@ def is_package_page(f):
abort(404)
del kwargs["author"]
del kwargs["name"]
return f(package=package, *args, **kwargs)
return decorated_function

View File

@ -30,7 +30,7 @@ services:
worker:
build: .
command: celery -A app.tasks.celery worker
command: celery -A app.tasks.celery worker --concurrency 1
env_file:
- config.env
environment:

105
docs/dev_intro.md Normal file
View File

@ -0,0 +1,105 @@
# Developer Introduction
## Overview
ContentDB is a Python [Flask](https://flask.palletsprojects.com/en/2.0.x/) webservice.
There's a PostgreSQL database, manipulated using the [SQLAlchemy ORM](https://docs.sqlalchemy.org/en/14/).
When a user makes a request, Python Flask will direct the request to a *route* in an *blueprint*.
A [blueprint](https://flask.palletsprojects.com/en/2.0.x/blueprints/) is a Flask construct to hold a set of routes.
Routes are implemented using Python, and likely to respond by using database *models* and rendering HTML *templates*.
Routes may also use functions in the `app/logic/` module, which is a directory containing reusable functions. This
allows the API, background tasks, and the front-end to reuse code.
To avoid blocking web requests, background tasks run as
[Celery](https://docs.celeryproject.org/en/stable/getting-started/introduction.html) tasks.
## Locations
### The App
The `app` directory contains the Python Flask application.
* `blueprints` contains all the Python code behind each endpoint / route.
* `templates` contains all the HTML templates used to generate responses. Each directory in here matches a directory in blueprints.
* `models` contains all the database table classes. ContentDB uses [SQLAlchemy](https://docs.sqlalchemy.org/en/14/) to interact with PostgreSQL.
* `flatpages` contains all the markdown user documentation, including `/help/`.
* `public` contains files that should be added to the web server unedited. Examples include CSS libraries, images, and JS scripts.
* `scss` contains the stylesheet files, that are compiled into CSS.
* `tasks` contains the background tasks executed by [Celery](https://docs.celeryproject.org/en/stable/getting-started/introduction.html).
* `logic` is a collection of reusable functions. For example, shared code to create a release or edit a package is here.
* `tests` contains the Unit Tests and UI tests.
* `utils` contain generic Python utilities, for example common code to manage Flask requests.
There are also a number of Python files in the `app` directory. The most important one is `querybuilder.py`,
which is used to generate SQLAlachemy queries for packages and topics.
### Supporting directories
* `migrations` contains code to manage database updates.
* `translations` contains user-maintained translations / locales.
* `utils` contains bash scripts to aid development and deployment.
## How to find stuff
Generally, you want to start by finding the endpoint and then seeing the code it calls.
Endpoints are sensibly organised in `app/blueprints`.
You can also use a file search. For example, to find the package edit endpoint, search for `"/packages/<author>/<name>/edit/"`.
## Users and Permissions
Many routes need to check whether a user can do a particular thing. Rather than hard coding this,
models tend to have a `checkPerm` function which takes a user and a `Permission`.
A permission may be something like `Permission.EDIT_PACKAGE` or `Permission.DELETE_THREAD`.
```bash
if not package.checkPerm(current_user, Permission.EDIT_PACKAGE):
abort(403)
```
## Translations
ContentDB uses [Flask-Babel](https://flask-babel.tkte.ch/) for translation. All strings need to be tagged using
a gettext function.
### Translating templates (HTML)
```html
<div class="something" title="{{ _('This is translatable now') }}">
{{ _("Please remember to do something related to this page or something") }}
</div>
```
With parameters:
```html
<p>
{{ _("Hello %(username)s, you have %(count)d new messages", username=username, count=count) }}
</p>
```
See <https://pythonhosted.org/Flask-Babel/#flask.ext.babel.Babel.localeselector> and
<https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-xiv-i18n-and-l10n>.
### Translating Python
If the text is within a request, then you can use gettext like so:
```py
flash(gettext("Some error message"), "danger")
```
If the text is global, for example as part of a python class, then you need to use lazy_gettext:
```py
class PackageForm(FlaskForm):
title = StringField(lazy_gettext("Title (Human-readable)"), [InputRequired(), Length(1, 100)])
```

View File

@ -54,3 +54,5 @@ To hot/live update CDB whilst it is running, use:
./utils/reload.sh
This will only work with python code and templates, it won't update tasks or config.
Now consider reading the [Developer Introduction](dev_intro.md).

View File

@ -0,0 +1,34 @@
"""empty message
Revision ID: e571b3498f9e
Revises: 3710e5fbbe87
Create Date: 2022-02-01 19:30:59.537512
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = 'e571b3498f9e'
down_revision = '3710e5fbbe87'
branch_labels = None
depends_on = None
def upgrade():
op.create_table('package_game_support',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('package_id', sa.Integer(), nullable=False),
sa.Column('game_id', sa.Integer(), nullable=False),
sa.Column('supports', sa.Boolean(), nullable=False),
sa.Column('confidence', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['game_id'], ['package.id'], ),
sa.ForeignKeyConstraint(['package_id'], ['package.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('game_id', 'package_id', name='_package_game_support_uc')
)
def downgrade():
op.drop_table('package_game_support')

View File

@ -8,15 +8,16 @@ msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2022-01-26 03:09+0000\n"
"PO-Revision-Date: 2022-01-23 17:15+0000\n"
"PO-Revision-Date: 2022-01-29 19:51+0000\n"
"Last-Translator: debiankaios <info@debiankaios.de>\n"
"Language-Team: German <https://hosted.weblate.org/projects/minetest/"
"contentdb/de/>\n"
"Language: de\n"
"Language-Team: German "
"<https://hosted.weblate.org/projects/minetest/contentdb/de/>\n"
"Plural-Forms: nplurals=2; plural=n != 1\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.11-dev\n"
"Generated-By: Babel 2.9.1\n"
#: app/__init__.py:102
@ -194,7 +195,7 @@ msgstr "Forumthema-ID"
#: app/blueprints/packages/packages.py:253
msgid "Video URL"
msgstr ""
msgstr "Video-URL"
#: app/blueprints/packages/packages.py:271
msgid "Unable to find that user"
@ -930,6 +931,8 @@ msgid ""
"Screenshot is too small, it should be at least %(width)s by %(height)s "
"pixels"
msgstr ""
"Der Screenshot ist zu klein, er sollte mindestens %(width)s mal %(height)s "
"Pixel groß sein"
#: app/logic/uploads.py:52
#, python-format
@ -1973,7 +1976,7 @@ msgstr "Tipp: Fügen Sie die URL eines Forumthemas ein"
#: app/templates/packages/create_edit.html:120
msgid "YouTube videos will be shown in an embed."
msgstr ""
msgstr "YouTube-Videos werden in einer Einbettung angezeigt."
#: app/templates/packages/edit_maintainers.html:4
msgid "Edit Maintainers"
@ -2392,6 +2395,8 @@ msgid ""
"The recommended resolution is 1920x1080, and screenshots must be at least"
" %(width)dx%(height)d."
msgstr ""
"Die empfohlene Auflösung ist 1920x1080, und die Screenshots müssen "
"mindestens %(width)dx%(height)d groß sein."
#: app/templates/packages/screenshots.html:11
msgid "Add Image"
@ -2403,15 +2408,15 @@ msgstr "Das oberste Bildschirmfoto wird als Vorschaubild für das Paket verwende
#: app/templates/packages/screenshots.html:39 app/templates/todo/user.html:77
msgid "Way too small"
msgstr ""
msgstr "Viel zu klein"
#: app/templates/packages/screenshots.html:43 app/templates/todo/user.html:80
msgid "Too small"
msgstr ""
msgstr "Zu klein"
#: app/templates/packages/screenshots.html:47 app/templates/todo/user.html:83
msgid "Not HD"
msgstr ""
msgstr "Nicht HD"
#: app/templates/packages/screenshots.html:53
msgid "Awaiting approval"
@ -2431,11 +2436,11 @@ msgstr "Umsortieren benötigt JavaScript."
#: app/templates/packages/screenshots.html:100
msgid "Videos"
msgstr ""
msgstr "Videos"
#: app/templates/packages/screenshots.html:102
msgid "You can set a video on the Edit Details page"
msgstr ""
msgstr "Sie können ein Video auf der Seite \"Details bearbeiten\" einstellen"
#: app/templates/packages/share.html:10
msgid "Links"
@ -2518,21 +2523,21 @@ msgstr "Herunterladen"
#: app/templates/packages/view.html:43
#, python-format
msgid "Minetest %(min)s - %(max)s"
msgstr ""
msgstr "Minetest %(min)s-%(max)s"
#: app/templates/packages/view.html:45
#, python-format
msgid "For Minetest %(min)s and above"
msgstr ""
msgstr "Für Minetest %(min)s und höher"
#: app/templates/packages/view.html:47
#, python-format
msgid "Minetest %(max)s and below"
msgstr ""
msgstr "Minetest %(max)s und niedriger"
#: app/templates/packages/view.html:67
msgid "How do I install this?"
msgstr ""
msgstr "Wie tue ich das installieren?"
#: app/templates/packages/view.html:73
msgid "No downloads available"
@ -2946,19 +2951,22 @@ msgstr ""
#: app/templates/todo/user.html:68
msgid "Small Screenshots"
msgstr ""
msgstr "Kleine Screenshots"
#: app/templates/todo/user.html:71
msgid ""
"These packages have screenshots that are too small, and should be "
"replaced."
msgstr ""
"Diese Pakete haben Screenshots, die zu klein sind und ersetzt werden sollten."
#: app/templates/todo/user.html:72
msgid ""
"Red and orange are screenshots below the limit, and grey screenshots are "
"below the recommended resolution."
msgstr ""
"Rot und orange sind Screenshots, die unter dem Grenzwert liegen, und graue "
"Screenshots liegen unter der empfohlenen Auflösung."
#: app/templates/todo/user.html:127
msgid "See All"
@ -3494,18 +3502,23 @@ msgid ""
"This will blacklist an email address, preventing ContentDB from ever "
"sending emails to it - including password resets."
msgstr ""
"Damit wird eine E-Mail-Adresse auf eine schwarze Liste gesetzt, so dass "
"ContentDB keine E-Mails mehr an diese Adresse senden kann - auch nicht zum "
"Zurücksetzen von Passwörtern."
#: app/templates/users/unsubscribe.html:20
msgid "Please enter the email address you wish to blacklist."
msgstr ""
"Bitte geben Sie die E-Mail-Adresse ein, die Sie auf die schwarze Liste "
"setzen möchten."
#: app/templates/users/unsubscribe.html:21
msgid "You will then need to confirm the email"
msgstr ""
msgstr "Sie müssen dann die E-Mail bestätigen"
#: app/templates/users/unsubscribe.html:33
msgid "You may now unsubscribe."
msgstr ""
msgstr "Sie können sich jetzt abmelden."
#: app/templates/users/unsubscribe.html:40
#, python-format
@ -3513,6 +3526,8 @@ msgid ""
"Unsubscribing may prevent you from being able to sign into the account "
"'%(display_name)s'"
msgstr ""
"Wenn Sie sich abmelden, können Sie sich möglicherweise nicht mehr bei dem "
"Konto \"%(display_name)s\" anmelden"
#: app/templates/users/unsubscribe.html:44
msgid ""
@ -3520,14 +3535,18 @@ msgid ""
"essential system emails.\n"
"\t\t\t\t\tConsider editing your email notification preferences instead."
msgstr ""
"ContentDB wird nicht mehr in der Lage sein, \"Passwort vergessen\" und "
"andere wichtige System-E-Mails zu versenden.\n"
"\t\t\t\t\tSie sollten stattdessen Ihre E-Mail-Benachrichtigungseinstellungen "
"bearbeiten."
#: app/templates/users/unsubscribe.html:50
msgid "You won't be able to use this email with ContentDB anymore."
msgstr ""
msgstr "Sie werden diese E-Mail nicht mehr mit ContentDB verwenden können."
#: app/templates/users/unsubscribe.html:57
msgid "Edit Notification Preferences"
msgstr ""
msgstr "Einstellungen für Benachrichtigungen bearbeiten"
#: app/utils/user.py:50
msgid "You have a lot of notifications, you should either read or clear them"
@ -3613,4 +3632,3 @@ msgstr ""
#~ "Erwägen Sie, die Aktualisierungseinstellungen "
#~ "so zu ändern, dass stattdessen "
#~ "automatisch Releases erstellt werden."

View File

@ -8,20 +8,21 @@ msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2022-01-26 03:09+0000\n"
"PO-Revision-Date: 2022-01-23 17:15+0000\n"
"Last-Translator: pampogo kiraly <pampogo.kiraly@gmail.com>\n"
"PO-Revision-Date: 2022-01-29 19:51+0000\n"
"Last-Translator: Balázs Kovács <kovacs.balazs.ktk@gmail.com>\n"
"Language-Team: Hungarian <https://hosted.weblate.org/projects/minetest/"
"contentdb/hu/>\n"
"Language: hu\n"
"Language-Team: Hungarian "
"<https://hosted.weblate.org/projects/minetest/contentdb/hu/>\n"
"Plural-Forms: nplurals=2; plural=n != 1\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.11-dev\n"
"Generated-By: Babel 2.9.1\n"
#: app/__init__.py:102
msgid "You have been banned."
msgstr ""
msgstr "Kitiltottak."
#: app/template_filters.py:52
#, python-format
@ -36,7 +37,7 @@ msgstr "Név"
#: app/blueprints/api/tokens.py:34
msgid "Limit to package"
msgstr ""
msgstr "Csomagkorlát"
#: app/blueprints/api/tokens.py:36 app/blueprints/packages/packages.py:255
#: app/blueprints/packages/packages.py:460
@ -58,7 +59,7 @@ msgstr "Belépés sikertelen [err=gh-oauth-login-failed]"
#: app/blueprints/github/__init__.py:62
msgid "Linked GitHub to account"
msgstr ""
msgstr "A GitHub-ot hozzákapcsoltuk a fiókhoz"
#: app/blueprints/github/__init__.py:65
#, fuzzy
@ -170,11 +171,11 @@ msgstr "Tartalomra vonatkozó figyelmeztetések"
#: app/blueprints/packages/packages.py:244 app/templates/packages/view.html:431
msgid "License"
msgstr ""
msgstr "Licenc"
#: app/blueprints/packages/packages.py:245
msgid "Media License"
msgstr ""
msgstr "Média licenc"
#: app/blueprints/packages/packages.py:247
msgid "Long Description (Markdown)"
@ -182,7 +183,7 @@ msgstr "Hosszú Leírás (Markdown)"
#: app/blueprints/packages/packages.py:249
msgid "VCS Repository URL"
msgstr ""
msgstr "VCS tároló URL"
#: app/blueprints/packages/packages.py:250 app/blueprints/users/settings.py:51
msgid "Website URL"
@ -199,7 +200,7 @@ msgstr "Fórum Téma ID"
#: app/blueprints/packages/packages.py:253
msgid "Video URL"
msgstr ""
msgstr "Videó URL"
#: app/blueprints/packages/packages.py:271
msgid "Unable to find that user"
@ -222,6 +223,8 @@ msgstr "Nincs erre engedélye"
#: app/blueprints/packages/packages.py:402
msgid "Please comment what changes are needed in the approval thread"
msgstr ""
"Kérjük, hogy a jóváhagyási témában írja meg, milyen változtatásokra van "
"szükség"
#: app/blueprints/packages/packages.py:423
#: app/blueprints/packages/packages.py:439
@ -278,7 +281,7 @@ msgstr "Fájl Feltöltés"
#: app/blueprints/packages/releases.py:57
msgid "Git reference (ie: commit hash, branch, or tag)"
msgstr ""
msgstr "Git hivatkozás (azaz: commit hash, branch vagy tag)"
#: app/blueprints/packages/releases.py:59
#: app/blueprints/packages/releases.py:70
@ -299,7 +302,7 @@ msgstr "URL"
#: app/blueprints/packages/releases.py:68
msgid "Task ID"
msgstr ""
msgstr "Feladat azonosítója"
#: app/blueprints/packages/releases.py:69
#: app/blueprints/packages/screenshots.py:40
@ -316,11 +319,11 @@ msgstr ".zip fájl feltöltése"
#: app/blueprints/packages/releases.py:188
msgid "Set Min"
msgstr ""
msgstr "Minimum beállítása"
#: app/blueprints/packages/releases.py:191
msgid "Set Max"
msgstr ""
msgstr "Maximum beállítása"
#: app/blueprints/packages/releases.py:194
#, fuzzy
@ -334,19 +337,19 @@ msgstr "Frissítés"
#: app/blueprints/packages/releases.py:244
#: app/templates/packages/update_config.html:25
msgid "Trigger"
msgstr ""
msgstr "Trigger"
#: app/blueprints/packages/releases.py:245
msgid "New Commit"
msgstr ""
msgstr "Új Commit"
#: app/blueprints/packages/releases.py:246 app/templates/admin/tags/list.html:8
msgid "New Tag"
msgstr ""
msgstr "Új címke"
#: app/blueprints/packages/releases.py:248
msgid "Branch name"
msgstr ""
msgstr "Branch neve"
#: app/blueprints/packages/releases.py:249
#: app/templates/packages/update_config.html:38
@ -374,6 +377,8 @@ msgstr "Automatizálás Kikapcsolása"
#: app/blueprints/packages/releases.py:292
msgid "Please add a Git repository URL in order to set up automatic releases"
msgstr ""
"Kérjük, adja meg egy Git tároló URL címét az automatikus kiadások "
"beállításához"
#: app/blueprints/packages/releases.py:308
#, fuzzy
@ -430,14 +435,14 @@ msgstr "Borítókép"
#: app/blueprints/report/__init__.py:34
msgid "Message"
msgstr ""
msgstr "Üzenet"
#: app/blueprints/report/__init__.py:35 app/templates/base.html:238
#: app/templates/macros/threads.html:53 app/templates/packages/view.html:510
#: app/templates/report/index.html:4 app/templates/report/index.html:10
#: app/templates/users/profile.html:28
msgid "Report"
msgstr ""
msgstr "Jelentés"
#: app/blueprints/threads/__init__.py:64
msgid "Already subscribed!"
@ -612,7 +617,7 @@ msgstr ""
#: app/blueprints/users/account.py:150 app/blueprints/users/account.py:253
#: app/blueprints/users/settings.py:142
msgid "That email address has been unsubscribed/blacklisted, and cannot be used"
msgstr ""
msgstr "Ez az e-mail cím leiratkozott/fekete listára került és nem használható"
#: app/blueprints/users/account.py:190
#, fuzzy
@ -621,7 +626,7 @@ msgstr "Jelszó Visszaállítása"
#: app/blueprints/users/account.py:215
msgid "Unable to find account"
msgstr ""
msgstr "Nem találja a fiókot"
#: app/blueprints/users/account.py:225 app/blueprints/users/account.py:232
msgid "New password"
@ -658,7 +663,7 @@ msgstr "Ismeretlen ellenőrző jelszó!"
#: app/blueprints/users/account.py:328
msgid "Token has expired"
msgstr ""
msgstr "A token lejárt"
#: app/blueprints/users/account.py:342
msgid "Another user is already using that email"
@ -693,6 +698,8 @@ msgid ""
"That email is now blacklisted. Please contact an admin if you wish to "
"undo this."
msgstr ""
"Ez az e-mail mostantól feketelistán van. Kérjük, lépjen kapcsolatba egy "
"adminisztrátorral, ha le szeretné venni onnan."
#: app/blueprints/users/claim.py:40 app/blueprints/users/claim.py:65
#, fuzzy
@ -705,11 +712,11 @@ msgstr ""
#: app/blueprints/users/claim.py:45
msgid "User has already been claimed"
msgstr ""
msgstr "A felhasználónév már használatban van"
#: app/blueprints/users/claim.py:49
msgid "Unable to get GitHub username for user"
msgstr ""
msgstr "Nem sikerült lekérni a felhasználó GitHub-felhasználónevét"
#: app/blueprints/users/claim.py:72
msgid "That user has already been claimed!"
@ -3447,4 +3454,3 @@ msgstr ""
#~ "Consider changing the update settings to"
#~ " create releases automatically instead."
#~ msgstr ""

View File

@ -8,16 +8,17 @@ msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2022-01-26 03:09+0000\n"
"PO-Revision-Date: 2022-01-23 17:15+0000\n"
"PO-Revision-Date: 2022-01-31 14:54+0000\n"
"Last-Translator: Yaya - Nurul Azeera Hidayah @ Muhammad Nur Hidayat "
"Yasuyoshi <translation@mnh48.moe>\n"
"Language-Team: Malay <https://hosted.weblate.org/projects/minetest/contentdb/"
"ms/>\n"
"Language: ms\n"
"Language-Team: Malay "
"<https://hosted.weblate.org/projects/minetest/contentdb/ms/>\n"
"Plural-Forms: nplurals=1; plural=0\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 4.11-dev\n"
"Generated-By: Babel 2.9.1\n"
#: app/__init__.py:102
@ -195,7 +196,7 @@ msgstr "ID Topik Forum"
#: app/blueprints/packages/packages.py:253
msgid "Video URL"
msgstr ""
msgstr "URL Video"
#: app/blueprints/packages/packages.py:271
msgid "Unable to find that user"
@ -926,6 +927,8 @@ msgid ""
"Screenshot is too small, it should be at least %(width)s by %(height)s "
"pixels"
msgstr ""
"Tangkap layar terlalu kecil, ia mestilah sekurang-kurangnya "
"%(width)sx%(height)s piksel"
#: app/logic/uploads.py:52
#, python-format
@ -1424,8 +1427,8 @@ msgid ""
"If you weren't expecting to receive this email, then you can safely "
"ignore it."
msgstr ""
"Jika anda tidak menjangka untuk menerima e-mel ini, anda boleh abaikannya"
" sahaja."
"Jika anda tidak menyangka untuk menerima e-mel ini, anda boleh abaikannya "
"sahaja."
#: app/templates/emails/verify.html:4
#: app/templates/emails/verify_unsubscribe.html:5
@ -1949,7 +1952,7 @@ msgstr "Petua: tampalkan URL topik forum"
#: app/templates/packages/create_edit.html:120
msgid "YouTube videos will be shown in an embed."
msgstr ""
msgstr "Video YouTube akan dipaparkan dalam benaman."
#: app/templates/packages/edit_maintainers.html:4
msgid "Edit Maintainers"
@ -2361,6 +2364,8 @@ msgid ""
"The recommended resolution is 1920x1080, and screenshots must be at least"
" %(width)dx%(height)d."
msgstr ""
"Resolusi yang digalakkan ialah 1920x1080, dan tangkap layar mestilah "
"sekurang-kurangnya %(width)dx%(height)d."
#: app/templates/packages/screenshots.html:11
msgid "Add Image"
@ -2372,15 +2377,15 @@ msgstr "Tangkap layar paling atas akan digunakan sebagai imej kenit pakej."
#: app/templates/packages/screenshots.html:39 app/templates/todo/user.html:77
msgid "Way too small"
msgstr ""
msgstr "Terlalu kecil sangat"
#: app/templates/packages/screenshots.html:43 app/templates/todo/user.html:80
msgid "Too small"
msgstr ""
msgstr "Kecil sangat"
#: app/templates/packages/screenshots.html:47 app/templates/todo/user.html:83
msgid "Not HD"
msgstr ""
msgstr "Bukan HD"
#: app/templates/packages/screenshots.html:53
msgid "Awaiting approval"
@ -2400,11 +2405,11 @@ msgstr "Perubahan tertib memerlukan JavaScript."
#: app/templates/packages/screenshots.html:100
msgid "Videos"
msgstr ""
msgstr "Video"
#: app/templates/packages/screenshots.html:102
msgid "You can set a video on the Edit Details page"
msgstr ""
msgstr "Anda boleh tetapkan video di halaman Edit Maklumat"
#: app/templates/packages/share.html:10
msgid "Links"
@ -2485,21 +2490,21 @@ msgstr "Muat Turun"
#: app/templates/packages/view.html:43
#, python-format
msgid "Minetest %(min)s - %(max)s"
msgstr ""
msgstr "Minetest %(min)s - %(max)s"
#: app/templates/packages/view.html:45
#, python-format
msgid "For Minetest %(min)s and above"
msgstr ""
msgstr "Untuk Minetest %(min)s dan ke atas"
#: app/templates/packages/view.html:47
#, python-format
msgid "Minetest %(max)s and below"
msgstr ""
msgstr "Untuk Minetest %(max)s dan ke bawah"
#: app/templates/packages/view.html:67
msgid "How do I install this?"
msgstr ""
msgstr "Bagaimana untuk pasangkan ini?"
#: app/templates/packages/view.html:73
msgid "No downloads available"
@ -2911,19 +2916,23 @@ msgstr ""
#: app/templates/todo/user.html:68
msgid "Small Screenshots"
msgstr ""
msgstr "Tangkap Layar Kecil"
#: app/templates/todo/user.html:71
msgid ""
"These packages have screenshots that are too small, and should be "
"replaced."
msgstr ""
"Pakej-pakej ini mempunyai tangkap layar yang terlalu kecil, dan patut "
"digantikan."
#: app/templates/todo/user.html:72
msgid ""
"Red and orange are screenshots below the limit, and grey screenshots are "
"below the recommended resolution."
msgstr ""
"Yang warna merah dan jingga itu tangkap layar di bawah had minimum, manakala "
"yang kelabu itu tangkap layar di bawah resolusi yang digalakkan."
#: app/templates/todo/user.html:127
msgid "See All"
@ -3451,18 +3460,20 @@ msgid ""
"This will blacklist an email address, preventing ContentDB from ever "
"sending emails to it - including password resets."
msgstr ""
"Ini akan menyenaraihitamkan alamat e-mel, mencegah ContentDB daripada "
"menghantar sebarang e-mel kepadanya - termasuk penetapan semula kata laluan."
#: app/templates/users/unsubscribe.html:20
msgid "Please enter the email address you wish to blacklist."
msgstr ""
msgstr "Sila masukkan alamat e-mel yang anda ingin senaraihitamkan."
#: app/templates/users/unsubscribe.html:21
msgid "You will then need to confirm the email"
msgstr ""
msgstr "Kemudian anda perlu mengesahkan e-mel tersebut"
#: app/templates/users/unsubscribe.html:33
msgid "You may now unsubscribe."
msgstr ""
msgstr "Kini anda boleh buang langganan."
#: app/templates/users/unsubscribe.html:40
#, python-format
@ -3470,6 +3481,8 @@ msgid ""
"Unsubscribing may prevent you from being able to sign into the account "
"'%(display_name)s'"
msgstr ""
"Membuang langganan boleh menghalang anda daripada log masuk ke akaun "
"'%(display_name)s'"
#: app/templates/users/unsubscribe.html:44
msgid ""
@ -3477,14 +3490,18 @@ msgid ""
"essential system emails.\n"
"\t\t\t\t\tConsider editing your email notification preferences instead."
msgstr ""
"ContentDB tidak akan mampu menghantar e-mel \"terlupa kata laluan\" dan e-"
"mel sistem asas yang lain.\n"
"\t\t\t\t\tSila pertimbangkan untuk edit keutamaan pemberitahuan e-mel "
"menggantikan ini."
#: app/templates/users/unsubscribe.html:50
msgid "You won't be able to use this email with ContentDB anymore."
msgstr ""
msgstr "Anda tidak akan mampu menggunakan e-mel ini dengan ContentDB lagi."
#: app/templates/users/unsubscribe.html:57
msgid "Edit Notification Preferences"
msgstr ""
msgstr "Edit Keutamaan Pemberitahuan"
#: app/utils/user.py:50
msgid "You have a lot of notifications, you should either read or clear them"
@ -3602,4 +3619,3 @@ msgstr ""
#~ "Pertimbangkan untuk tukar tetapan kemas "
#~ "kini untuk cipta terbitan secara "
#~ "automatik sahaja."

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -8,20 +8,21 @@ msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2022-01-26 03:09+0000\n"
"PO-Revision-Date: 2022-01-23 17:15+0000\n"
"PO-Revision-Date: 2022-02-01 14:55+0000\n"
"Last-Translator: Y.W. <y5nw@outlook.com>\n"
"Language-Team: Chinese (Simplified) <https://hosted.weblate.org/projects/"
"minetest/contentdb/zh_Hans/>\n"
"Language: zh_Hans\n"
"Language-Team: Chinese (Simplified) "
"<https://hosted.weblate.org/projects/minetest/contentdb/zh_Hans/>\n"
"Plural-Forms: nplurals=1; plural=0\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 4.11-dev\n"
"Generated-By: Babel 2.9.1\n"
#: app/__init__.py:102
msgid "You have been banned."
msgstr "被封号了。"
msgstr "被封号了。"
#: app/template_filters.py:52
#, python-format
@ -58,15 +59,15 @@ msgstr "验证失败 [err=gh-oauth-login-failed]"
#: app/blueprints/github/__init__.py:62
msgid "Linked GitHub to account"
msgstr "绑定猫站账号"
msgstr "已绑定GitHub账号"
#: app/blueprints/github/__init__.py:65
msgid "GitHub account is already associated with another user"
msgstr "猫站账号已与其他用户绑定"
msgstr "GitHub账号已与其他用户绑定"
#: app/blueprints/github/__init__.py:71
msgid "Unable to find an account for that GitHub user"
msgstr "无法找到猫站用户的账号"
msgstr "无法找到该GitHub用户的账号"
#: app/blueprints/github/__init__.py:76
msgid "Authorization failed [err=gh-login-failed]"
@ -192,7 +193,7 @@ msgstr "论坛贴子ID"
#: app/blueprints/packages/packages.py:253
msgid "Video URL"
msgstr ""
msgstr "视频URL"
#: app/blueprints/packages/packages.py:271
msgid "Unable to find that user"
@ -415,7 +416,6 @@ msgid "Cover Image"
msgstr "封面图像"
#: app/blueprints/report/__init__.py:34
#, fuzzy
msgid "Message"
msgstr "消息"
@ -424,7 +424,7 @@ msgstr "消息"
#: app/templates/report/index.html:4 app/templates/report/index.html:10
#: app/templates/users/profile.html:28
msgid "Report"
msgstr ""
msgstr "举报"
#: app/blueprints/threads/__init__.py:64
msgid "Already subscribed!"
@ -469,23 +469,23 @@ msgstr "评论需要在3到2000个字符之间。"
#: app/blueprints/threads/__init__.py:275
#: app/templates/macros/package_approval.html:107
msgid "Open Thread"
msgstr ""
msgstr "打开贴子"
#: app/blueprints/threads/__init__.py:287
msgid "Unable to find that package!"
msgstr ""
msgstr "找不到软件包!"
#: app/blueprints/threads/__init__.py:301
msgid "Unable to create thread!"
msgstr ""
msgstr "无法创建贴子!"
#: app/blueprints/threads/__init__.py:306
msgid "An approval thread already exists!"
msgstr ""
msgstr "审核贴已存在!"
#: app/blueprints/threads/__init__.py:310
msgid "Please wait before opening another thread"
msgstr ""
msgstr "开启另外一个贴子前请等待"
#: app/blueprints/users/account.py:37 app/templates/users/login.html:15
msgid "Username or email"
@ -513,16 +513,15 @@ msgstr "电子邮件或密码不正确"
#: app/blueprints/users/account.py:54
#, python-format
msgid "User %(username)s does not exist"
msgstr ""
msgstr "用户%(username)s不存在"
#: app/blueprints/users/account.py:57
#, fuzzy
msgid "Incorrect password. Did you set one?"
msgstr "密码不正确。您设置了一个吗?"
msgstr "密码不正确。您设置了吗?"
#: app/blueprints/users/account.py:60
msgid "You need to confirm the registration email"
msgstr "你需要确认注册电子邮件"
msgstr "你需要确认注册电子邮件"
#: app/blueprints/users/account.py:68
msgid "Login failed"
@ -530,7 +529,7 @@ msgstr "登录失败"
#: app/blueprints/users/account.py:103 app/blueprints/users/settings.py:50
msgid "Display Name"
msgstr ""
msgstr "显示名称"
#: app/blueprints/users/account.py:104 app/blueprints/users/settings.py:263
#: app/templates/users/list.html:18
@ -541,7 +540,7 @@ msgstr "用户名"
#: app/templates/users/claim_forums.html:68
#: app/templates/users/register.html:16
msgid "Only a-zA-Z0-9._ allowed"
msgstr ""
msgstr "只允许使用这些字符a-zA-Z0-9._"
#: app/blueprints/users/account.py:106 app/blueprints/users/account.py:189
#: app/blueprints/users/account.py:224 app/blueprints/users/account.py:376
@ -551,7 +550,7 @@ msgstr "电子邮件"
#: app/blueprints/users/account.py:108
msgid "What is the result of the above calculation?"
msgstr ""
msgstr "上述算式的计算结果是什么?"
#: app/blueprints/users/account.py:109
msgid "I agree"
@ -564,7 +563,7 @@ msgstr "注册"
#: app/blueprints/users/account.py:115
msgid "Incorrect captcha answer"
msgstr ""
msgstr "验证码错误"
#: app/blueprints/users/account.py:119
msgid "Username is invalid"
@ -572,27 +571,27 @@ msgstr "用户名无效"
#: app/blueprints/users/account.py:130
msgid "An account already exists for that username but hasn't been claimed yet."
msgstr ""
msgstr "该用户名已存在账户,但尚未被认领。"
#: app/blueprints/users/account.py:133 app/blueprints/users/account.py:140
msgid "That username/display name is already in use, please choose another."
msgstr ""
msgstr "该用户名/显示名称已被使用,请选择另一个。"
#: app/blueprints/users/account.py:145 app/blueprints/users/account.py:258
msgid "Email already in use"
msgstr ""
msgstr "电子邮件已使用"
#: app/blueprints/users/account.py:146 app/blueprints/users/account.py:259
#, python-format
msgid ""
"We were unable to create the account as the email is already in use by "
"%(display_name)s. Try a different email address."
msgstr ""
msgstr "我们无法创建该账户,因为该电子邮件已经被%(display_name)s使用。请尝试使用另一个电子邮件地址。"
#: app/blueprints/users/account.py:150 app/blueprints/users/account.py:253
#: app/blueprints/users/settings.py:142
msgid "That email address has been unsubscribed/blacklisted, and cannot be used"
msgstr ""
msgstr "该电子邮件地址已被取消关注/列入黑名单,不能再使用"
#: app/blueprints/users/account.py:190
msgid "Reset Password"
@ -607,7 +606,6 @@ msgid "New password"
msgstr "新密码"
#: app/blueprints/users/account.py:226 app/blueprints/users/account.py:233
#, fuzzy
msgid "Verify password"
msgstr "确认密码"
@ -633,11 +631,11 @@ msgstr "旧密码不正确"
#: app/blueprints/users/account.py:322
msgid "Unknown verification token!"
msgstr ""
msgstr "未知验证口令!"
#: app/blueprints/users/account.py:328
msgid "Token has expired"
msgstr ""
msgstr "口令已过期"
#: app/blueprints/users/account.py:342
msgid "Another user is already using that email"
@ -645,21 +643,19 @@ msgstr "另一个用户已经在使用该电子邮件"
#: app/blueprints/users/account.py:345
msgid "Confirmed email change"
msgstr ""
msgstr "已确认的电子邮件变更"
#: app/blueprints/users/account.py:350
msgid "Email address changed"
msgstr ""
msgstr "电子邮箱已变更"
#: app/blueprints/users/account.py:351
#, fuzzy
msgid ""
"Your email address has changed. If you didn't request this, please "
"contact an administrator."
msgstr "您的电子邮件地址已被更改。如果您没有请求更改电子邮件地址,请与管理员联系。"
#: app/blueprints/users/account.py:369
#, fuzzy
msgid "You may now log in"
msgstr "您现在可以登录了"
@ -668,7 +664,6 @@ msgid "Send"
msgstr "发送"
#: app/blueprints/users/account.py:408
#, fuzzy
msgid ""
"That email is now blacklisted. Please contact an admin if you wish to "
"undo this."
@ -678,11 +673,12 @@ msgstr "该电子邮件现在已被列入黑名单。如果您希望撤消此操
msgid ""
"Invalid username - must only contain A-Za-z0-9._. Consider contacting an "
"admin"
msgstr ""
msgstr "用户名只能包含 A-Za-z0-9._。请考虑联系管理员"
#: app/blueprints/users/claim.py:45
#, fuzzy
msgid "User has already been claimed"
msgstr ""
msgstr "用户已被认领"
#: app/blueprints/users/claim.py:49
msgid "Unable to get GitHub username for user"
@ -690,16 +686,16 @@ msgstr "无法获取用户的 GitHub 用户名"
#: app/blueprints/users/claim.py:72
msgid "That user has already been claimed!"
msgstr ""
msgstr "该用户已被认领!"
#: app/blueprints/users/claim.py:86
#, fuzzy, python-format
#, python-format
msgid "Error whilst attempting to access forums: %(message)s"
msgstr "尝试访问论坛时出错:%(message)s"
#: app/blueprints/users/claim.py:90
msgid "Unable to get forum signature - does the user exist?"
msgstr ""
msgstr "无法获得论坛签名 - 用户是否存在?"
#: app/blueprints/users/claim.py:105
msgid "Unable to login as user"
@ -707,15 +703,15 @@ msgstr "无法以用户身份登录"
#: app/blueprints/users/claim.py:111
msgid "Could not find the key in your signature!"
msgstr ""
msgstr "在您的签名中找不到密钥!"
#: app/blueprints/users/claim.py:114
msgid "Unknown claim type"
msgstr ""
msgstr "未知的认领类型"
#: app/blueprints/users/profile.py:112
msgid "Top reviewer"
msgstr ""
msgstr "顶级评论者"
#: app/blueprints/users/profile.py:113
#, python-format
@ -724,60 +720,59 @@ msgstr "%(display_name)s在ContentDB上写了最有帮助的评论。"
#: app/blueprints/users/profile.py:118
msgid "2nd most helpful reviewer"
msgstr ""
msgstr "第二位最有帮助的评论者"
#: app/blueprints/users/profile.py:120
msgid "3rd most helpful reviewer"
msgstr ""
msgstr "第三位最有帮助的评论者"
#: app/blueprints/users/profile.py:121
#, python-format
msgid "This puts %(display_name)s in the top %(perc)s%%"
msgstr ""
msgstr "这使%(display_name)s位于顶部%(perc)s%%"
#: app/blueprints/users/profile.py:125
#, python-format
msgid "Top %(perc)s%% reviewer"
msgstr ""
msgstr "最高 %(perc)s%% 评论者"
#: app/blueprints/users/profile.py:126
#, python-format
msgid "Only %(place)d users have written more helpful reviews."
msgstr ""
msgstr "只有%(place)d个用户写了更多有帮助的评论。"
#: app/blueprints/users/profile.py:131
#, fuzzy
msgid "Consider writing more helpful reviews to get a medal."
msgstr "请考虑写更多有帮助的评论以获得奖章。"
#: app/blueprints/users/profile.py:133
#, python-format
msgid "You are in place %(place)s."
msgstr ""
msgstr "你在第%(place)s位。"
#: app/blueprints/users/profile.py:161
#, python-format
msgid "Top %(type)s"
msgstr ""
msgstr "最高%(type)s"
#: app/blueprints/users/profile.py:163
#, python-format
msgid "Top %(group)d %(type)s"
msgstr ""
msgstr "最高 %(group)d %(type)s"
#: app/blueprints/users/profile.py:172
#, python-format
msgid "%(display_name)s has a %(type)s placed at #%(place)d."
msgstr ""
msgstr "%(display_name)s 有一个 %(type)s 放置在 #%(place)d 处。"
#: app/blueprints/users/profile.py:187
#, python-format
msgid "Your packages have %(downloads)d downloads in total."
msgstr ""
msgstr "您的软件包总共有 %(downloads)d 次下载。"
#: app/blueprints/users/profile.py:188
msgid "First medal is at 50k."
msgstr ""
msgstr "第一张奖章为50k。"
#: app/blueprints/users/profile.py:193
msgid ">300k downloads"
@ -856,19 +851,20 @@ msgid "Can't promote a user to a rank higher than yourself!"
msgstr ""
#: app/logic/packages.py:95
#, fuzzy
msgid ""
"Name can only contain lower case letters (a-z), digits (0-9), and "
"underscores (_)"
msgstr "名称只能包含小写字母a-z、数字0-9和下划线_"
msgstr "名称只能包含小写字母a-z、数字0-9和下划线_"
#: app/logic/packages.py:109
#, fuzzy
msgid "You do not have permission to edit this package"
msgstr ""
msgstr "您没有权限编辑这个软件包"
#: app/logic/packages.py:113
#, fuzzy
msgid "You do not have permission to change the package name"
msgstr ""
msgstr "您没有权限修改这个软件包的名字"
#: app/logic/packages.py:165
#, python-format
@ -876,8 +872,9 @@ msgid "Unable to add protected tag %(title)s to package"
msgstr ""
#: app/logic/releases.py:32
#, fuzzy
msgid "You do not have permission to make releases"
msgstr ""
msgstr "您没有权限发布新版本"
#: app/logic/releases.py:37
msgid ""
@ -905,7 +902,7 @@ msgid ""
msgstr ""
#: app/logic/uploads.py:52
#, fuzzy, python-format
#, python-format
msgid "Please upload %(file_desc)s"
msgstr "请上传%(file_desc)s"
@ -914,33 +911,36 @@ msgid "Uploaded image isn't actually an image"
msgstr ""
#: app/models/packages.py:65
#, fuzzy
msgid "Mod"
msgstr "Mod"
#: app/models/packages.py:67
#, fuzzy
msgid "Game"
msgstr ""
msgstr "子游戏"
#: app/models/packages.py:69
#, fuzzy
msgid "Texture Pack"
msgstr ""
msgstr "材质包"
#: app/models/packages.py:74 app/templates/base.html:27
msgid "Mods"
msgstr ""
#: app/models/packages.py:76 app/templates/base.html:30
#, fuzzy
msgid "Games"
msgstr ""
msgstr "子游戏"
#: app/models/packages.py:78 app/templates/base.html:33
#, fuzzy
msgid "Texture Packs"
msgstr ""
msgstr "材质包"
#: app/models/packages.py:167
msgid "Submit for Approval"
msgstr ""
msgstr "提交并申请批准"
#: app/models/packages.py:169
msgid "Approve"
@ -956,10 +956,11 @@ msgid "Delete"
msgstr "删除"
#: app/tasks/emails.py:102
#, fuzzy
msgid ""
"You are receiving this email because you are a registered user of "
"ContentDB."
msgstr ""
msgstr "您是ContentDB的一名用户所以收到了这封邮件。"
#: app/tasks/emails.py:109 app/templates/emails/verify.html:30
msgid ""
@ -968,7 +969,7 @@ msgid ""
msgstr ""
#: app/tasks/emails.py:143
#, fuzzy, python-format
#, python-format
msgid "%(num)d new notifications"
msgstr "%(num)d个新通知"
@ -990,7 +991,7 @@ msgstr "管理电子邮件设置"
#: app/templates/threads/view.html:31 app/templates/users/unsubscribe.html:4
#: app/templates/users/unsubscribe.html:61
msgid "Unsubscribe"
msgstr "取消订阅"
msgstr "取消关注"
#: app/templates/404.html:4
msgid "Page not found"
@ -1044,12 +1045,14 @@ msgid "Notifications"
msgstr "通知"
#: app/templates/base.html:105
#, fuzzy
msgid "Add Package"
msgstr ""
msgstr "添加软件包"
#: app/templates/base.html:122
#, fuzzy
msgid "Profile"
msgstr ""
msgstr "个人资料"
#: app/templates/base.html:140
msgid "Admin"
@ -1076,8 +1079,9 @@ msgid "Settings"
msgstr "设置"
#: app/templates/base.html:161
#, fuzzy
msgid "Sign out"
msgstr ""
msgstr "退出"
#: app/templates/base.html:190
msgid "Help translate ContentDB"
@ -1125,7 +1129,6 @@ msgstr ""
#: app/templates/index.html:69 app/templates/packages/reviews_list.html:4
#: app/templates/packages/view.html:177 app/templates/packages/view.html:293
#: app/templates/users/profile.html:206
#, fuzzy
msgid "Reviews"
msgstr "评价"
@ -1145,16 +1148,18 @@ msgstr ""
#: app/templates/index.html:115 app/templates/index.html:122
#: app/templates/index.html:129 app/templates/index.html:151
#: app/templates/index.html:158
#, fuzzy
msgid "See more"
msgstr ""
msgstr "查看更多"
#: app/templates/index.html:103
msgid "Recently Added"
msgstr "最近添加的"
#: app/templates/index.html:110
#, fuzzy
msgid "Recently Updated"
msgstr ""
msgstr "最近更新的"
#: app/templates/index.html:117
msgid "Top Games"
@ -1173,8 +1178,9 @@ msgid "Search by Tags"
msgstr ""
#: app/templates/index.html:153
#, fuzzy
msgid "Highest Reviewed"
msgstr ""
msgstr "评价最高的"
#: app/templates/index.html:160
msgid "Recent Positive Reviews"
@ -1203,13 +1209,14 @@ msgid "Send bulk email"
msgstr "批量发送电子邮件"
#: app/templates/admin/send_bulk_notification.html:4
#, fuzzy
msgid "Send bulk notification"
msgstr ""
msgstr "批量发送消息"
#: app/templates/admin/send_email.html:4
#, python-format
#, fuzzy, python-format
msgid "Send email to %(username)s"
msgstr ""
msgstr "给%(username)s发送电子邮件"
#: app/templates/admin/licenses/list.html:8
msgid "New License"
@ -1239,17 +1246,20 @@ msgstr ""
#: app/templates/admin/versions/list.html:4
#: app/templates/admin/versions/list.html:10
#, fuzzy
msgid "Minetest Versions"
msgstr ""
msgstr "Minetest版本"
#: app/templates/admin/versions/list.html:8
#, fuzzy
msgid "New Version"
msgstr ""
msgstr "新版本"
#: app/templates/admin/warnings/list.html:4
#: app/templates/admin/warnings/list.html:10
#, fuzzy
msgid "Warnings"
msgstr ""
msgstr "警告"
#: app/templates/admin/warnings/list.html:8
msgid "New Warning"
@ -1285,8 +1295,9 @@ msgid ""
msgstr ""
#: app/templates/api/create_edit_token.html:40
#, fuzzy
msgid "Reset"
msgstr ""
msgstr "重置"
#: app/templates/api/create_edit_token.html:49
msgid "Human-readable name to tell tokens apart."
@ -1305,12 +1316,14 @@ msgstr ""
#: app/templates/macros/topics.html:65
#: app/templates/packages/alias_list.html:13
#: app/templates/packages/releases_list.html:29
#, fuzzy
msgid "Create"
msgstr ""
msgstr "创建"
#: app/templates/api/list_tokens.html:9
#, fuzzy
msgid "API Documentation"
msgstr ""
msgstr "API文档"
#: app/templates/api/list_tokens.html:19
msgid "No tokens created"
@ -1327,8 +1340,9 @@ msgid "From %(username)s."
msgstr ""
#: app/templates/emails/notification.html:19
#, fuzzy
msgid "View Notification"
msgstr ""
msgstr "查看消息"
#: app/templates/emails/notification.html:26
#: app/templates/emails/notification_digest.html:43
@ -1354,12 +1368,14 @@ msgid "from %(username)s."
msgstr ""
#: app/templates/emails/notification_digest.html:22
#, fuzzy
msgid "Other Notifications"
msgstr ""
msgstr "其它消息"
#: app/templates/emails/notification_digest.html:36
#, fuzzy
msgid "View Notifications"
msgstr ""
msgstr "查看消息"
#: app/templates/emails/unable_to_find_account.html:2
msgid ""
@ -1407,8 +1423,9 @@ msgid "If this was you, then please click this link to confirm the address:"
msgstr ""
#: app/templates/emails/verify.html:19
#, fuzzy
msgid "Confirm Email Address"
msgstr ""
msgstr "确认电子邮箱地址"
#: app/templates/emails/verify.html:23
#: app/templates/emails/verify_unsubscribe.html:17
@ -1445,12 +1462,14 @@ msgid "Start typing to see suggestions"
msgstr ""
#: app/templates/macros/package_approval.html:5 app/templates/todo/user.html:35
#, fuzzy
msgid "State"
msgstr ""
msgstr "状态"
#: app/templates/macros/package_approval.html:22
#, fuzzy
msgid "Create first release"
msgstr ""
msgstr "发布第一个版本"
#: app/templates/macros/package_approval.html:26
msgid "Set up releases"
@ -1660,12 +1679,14 @@ msgid "No outdated packages."
msgstr ""
#: app/templates/macros/topics.html:6 app/templates/packages/view.html:160
#, fuzzy
msgid "Author"
msgstr ""
msgstr "作者"
#: app/templates/macros/topics.html:8
#, fuzzy
msgid "Date"
msgstr ""
msgstr "日期"
#: app/templates/macros/topics.html:9
msgid "Actions"
@ -1679,8 +1700,9 @@ msgid "WIP"
msgstr ""
#: app/templates/macros/topics.html:35
#, fuzzy
msgid "Show"
msgstr ""
msgstr "显示"
#: app/templates/macros/topics.html:37
msgid "Discard"
@ -2680,9 +2702,8 @@ msgid "Missing tags only"
msgstr ""
#: app/templates/todo/tags.html:31
#, fuzzy
msgid "Edit Tags"
msgstr "编辑详细信息"
msgstr "编辑标签"
#: app/templates/todo/todo_base.html:11 app/templates/todo/user.html:4
#, python-format
@ -3370,4 +3391,3 @@ msgstr ""
#~ "Consider changing the update settings to"
#~ " create releases automatically instead."
#~ msgstr ""