Compare commits

..

1 Commits

Author SHA1 Message Date
rubenwardy b652cfd857 Update to Bootstrap v5 2022-01-27 20:00:33 +00:00
106 changed files with 1061 additions and 5309 deletions

View File

@ -4,9 +4,7 @@
Content database for Minetest mods, games, and more.\
Developed by rubenwardy, license AGPLv3.0+.
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.
See [Getting Started](docs/getting_started.md).
## How-tos

View File

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

View File

@ -16,18 +16,15 @@
import os
import sys
from typing import List
import requests
from celery import group
from flask import redirect, url_for, flash, current_app, jsonify
from flask import redirect, url_for, flash, current_app
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, PackageGameSupport
from app.tasks.emails import send_pending_digests
NotificationType, PackageUpdateConfig, License, UserRank, PackageType
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
@ -324,15 +321,3 @@ 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,14 +13,13 @@
#
# 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 joinedload
from sqlalchemy.orm import subqueryload, joinedload
from sqlalchemy.sql.expression import func
from app import csrf
@ -31,8 +30,7 @@ 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, api_set_cover_image
from .support import error, api_create_vcs_release, api_create_zip_release, api_create_screenshot, api_order_screenshots, api_edit_package
from functools import wraps
@ -304,7 +302,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, isYes(data.get("is_cover_image")))
return api_create_screenshot(token, package, data["title"], file)
@bp.route("/api/packages/<author>/<name>/screenshots/<int:id>/")
@ -357,7 +355,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 change screenshots")
error(403, "You do not have the permission to delete screenshots")
if not token.canOperateOnPackage(package):
error(403, "API token does not have access to the package")
@ -369,28 +367,6 @@ 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
@ -501,26 +477,6 @@ 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, do_set_cover_image
from app.logic.screenshots import do_create_screenshot, do_order_screenshots
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, is_cover_image: bool, reason="API"):
def api_create_screenshot(token: APIToken, package: Package, title: str, file, 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, is_cover_image, reason)
ss : PackageScreenshot = guard(do_create_screenshot)(token.owner, package, title, file, reason)
return jsonify({
"success": True,
@ -94,17 +94,6 @@ 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,11 +53,12 @@ def view(name):
.filter(Dependency.optional==True, Package.state==PackageState.APPROVED) \
.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()
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()
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, game_hub
from . import packages, screenshots, releases, reviews

View File

@ -1,54 +0,0 @@
# 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,6 +115,9 @@ 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))
@ -205,6 +208,9 @@ 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:
@ -587,6 +593,9 @@ 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")
@ -594,6 +603,9 @@ 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,6 +33,9 @@ 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")
@ -108,6 +111,9 @@ 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, AuditSeverity
from app.utils import is_package_page, addNotification, get_int_or_abort, isYes, is_safe_url, rank_required, addAuditLog
Permission
from app.utils import is_package_page, addNotification, get_int_or_abort, isYes, is_safe_url, rank_required
from app.tasks.webhooktasks import post_discord_webhook
@ -54,6 +54,9 @@ 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)
@ -126,19 +129,14 @@ def review(package):
form=form, package=package, review=review)
@bp.route("/packages/<author>/<name>/reviews/<reviewer>/delete/", methods=["POST"])
@bp.route("/packages/<author>/<name>/review/delete/", methods=["POST"])
@login_required
@is_package_page
def delete_review(package, reviewer):
review = PackageReview.query \
.filter(PackageReview.package == package, PackageReview.author.has(username=reviewer)) \
.first()
def delete_review(package):
review = PackageReview.query.filter_by(package=package, author=current_user).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()
@ -149,17 +147,10 @@ def delete_review(package, reviewer):
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())
@ -237,4 +228,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, False)
do_create_screenshot(current_user, package, form.title.data, form.fileUpload.data)
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, Package.tags==None) \
.order_by(db.asc(Package.title)).all()
.filter(Package.state != PackageState.DELETED) \
.filter_by(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,7 +90,6 @@ 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.
@ -226,7 +225,6 @@ 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)
@ -234,16 +232,12 @@ 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.
@ -255,11 +249,6 @@ 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/ \
@ -269,11 +258,6 @@ 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 }"
```
@ -346,11 +330,9 @@ Supported query parameters:
### Tags
* GET `/api/tags/` ([View](/api/tags/)): List of:
* `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.
* `name`: technical name
* `title`: human-readable title
* `description`: tag description or null
### Content Warnings
@ -394,5 +376,3 @@ 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 and deprecated packages
* `desktop_default`: currently same as `deprecated`. Hides deprecated packages
* `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
## Content Warnings

View File

@ -1,188 +0,0 @@
# 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:
continue
break
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, is_cover_image: bool, reason: str = None):
def do_create_screenshot(user: User, package: Package, title: str, file, 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,10 +47,6 @@ def do_create_screenshot(user: User, package: Package, title: str, file, is_cove
db.session.commit()
if is_cover_image:
package.cover_image = ss
db.session.commit()
return ss
@ -70,18 +66,3 @@ 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,25 +344,6 @@ 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
@ -415,12 +396,6 @@ 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")
@ -475,14 +450,6 @@ 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)
@ -504,11 +471,6 @@ 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,
@ -580,15 +542,7 @@ class Package(db.Model):
"release": release and release.id,
"score": round(self.score * 10) / 10,
"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()
]
"downloads": self.downloads
}
def getThumbnailOrPlaceholder(self, level=2):
@ -645,7 +599,7 @@ class Package(db.Model):
def checkPerm(self, user, perm):
if not user.is_authenticated:
return False
return perm == Permission.SEE_PACKAGE and self.state == PackageState.APPROVED
if type(perm) == str:
perm = Permission[perm]
@ -656,7 +610,10 @@ 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.CREATE_THREAD:
if perm == Permission.SEE_PACKAGE:
return self.state == PackageState.APPROVED or isMaintainer or isApprover
elif perm == Permission.CREATE_THREAD:
return user.rank.atLeast(UserRank.MEMBER)
# Members can edit their own packages, and editors can edit any packages
@ -730,8 +687,7 @@ class Package(db.Model):
needsScreenshot = \
(self.type == self.type.GAME or self.type == self.type.TXP) and \
self.screenshots.count() == 0
return self.releases.filter(PackageRelease.task_id.is_(None)).count() > 0 and not needsScreenshot
return self.releases.count() > 0 and not needsScreenshot
elif state == PackageState.CHANGES_NEEDED:
return self.checkPerm(user, Permission.APPROVE_NEW)
@ -862,13 +818,7 @@ class Tag(db.Model):
def getAsDictionary(self):
description = self.description if self.description != "" else None
return {
"name": self.name,
"title": self.title,
"description": description,
"is_protected": self.is_protected,
"views": self.views,
}
return { "name": self.name, "title": self.title, "description": description }
class MinetestRelease(db.Model):
@ -1092,11 +1042,8 @@ 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,8 +200,7 @@ class PackageReview(db.Model):
def getDeleteURL(self):
return url_for("packages.delete_review",
author=self.package.author.username,
name=self.package.name,
reviewer=self.author.username)
name=self.package.name)
def getVoteUrl(self, next_url=None):
return url_for("packages.review_vote",
@ -214,20 +213,6 @@ 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,6 +59,7 @@ class UserRank(enum.Enum):
class Permission(enum.Enum):
SEE_PACKAGE = "SEE_PACKAGE"
EDIT_PACKAGE = "EDIT_PACKAGE"
DELETE_PACKAGE = "DELETE_PACKAGE"
CHANGE_AUTHOR = "CHANGE_AUTHOR"
@ -86,7 +87,6 @@ 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"
@ -145,7 +145,7 @@ class User(db.Model, UserMixin):
github_access_token = db.Column(db.String(50), nullable=True, server_default=None)
# User email information
email = db.Column(db.String(255), nullable=True, unique=True)
email = db.Column(db.String(320), nullable=True, unique=True)
email_confirmed_at = db.Column(db.DateTime(), nullable=True, server_default=None)
locale = db.Column(db.String(10), nullable=True, default=None)
@ -291,7 +291,7 @@ class User(db.Model, UserMixin):
class UserEmailVerification(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
email = db.Column(db.String(100), nullable=False)
email = db.Column(db.String(320), nullable=False)
token = db.Column(db.String(32), nullable=True)
user = db.relationship("User", foreign_keys=[user_id], back_populates="email_verifications")
is_password_reset = db.Column(db.Boolean, nullable=False, default=False)
@ -300,7 +300,7 @@ class UserEmailVerification(db.Model):
class EmailSubscription(db.Model):
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(100), nullable=False, unique=True)
email = db.Column(db.String(320), nullable=False, unique=True)
blacklisted = db.Column(db.Boolean, nullable=False, default=False)
token = db.Column(db.String(32), nullable=True, default=None)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,3 @@
// @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,6 +1,3 @@
// @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,6 +1,3 @@
// @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,6 +1,3 @@
// @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");
@ -10,7 +7,7 @@ document.querySelectorAll(".video-embed").forEach(ele => {
ele.addEventListener("click", () => {
ele.parentNode.classList.add("d-block");
ele.classList.add("embed-responsive");
ele.classList.add("embed-responsive-16by9");
ele.classList.add("embed-responsive-16x9");
ele.innerHTML = `
<iframe title="YouTube video player" frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"

View File

@ -75,10 +75,6 @@ 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
@ -136,9 +132,6 @@ 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

@ -272,3 +272,7 @@ blockquote {
margin-bottom: 0 !important;
}
}
.form-group {
margin-bottom: 1rem !important;
}

View File

@ -78,3 +78,15 @@
padding: 0;
margin: 0 0.75em;
}
.jumbotron {
padding: 4rem 2rem;
background-size: cover;
background-repeat: no-repeat;
background-position: center;
padding: 2rem 1rem;
margin-bottom: 2rem;
background-color: #303030;
border-radius: .3rem;
}

View File

@ -13,7 +13,6 @@
#
# 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
@ -23,11 +22,10 @@ from kombu import uuid
from app.models import *
from app.tasks import celery, TaskError
from app.utils import randomString, post_bot_message, addSystemNotification, addSystemAuditLog
from app.utils import randomString, post_bot_message, addSystemNotification, addSystemAuditLog, get_system_user
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
@ -115,11 +113,6 @@ 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

@ -6,7 +6,7 @@
{% block content %}
{% if entry.url %}
<a class="float-right btn btn-primary" href="{{ entry.url }}">View</a>
<a class="float-end btn btn-primary" href="{{ entry.url }}">View</a>
{% endif %}
<h1>{{ entry.title }}</h1>

View File

@ -9,7 +9,7 @@
{% endblock %}
{% block content %}
<a class="btn btn-primary float-right" href="{{ url_for('admin.create_edit_license') }}">New License</a>
<a class="btn btn-primary float-end" href="{{ url_for('admin.create_edit_license') }}">New License</a>
<a class="btn btn-secondary mb-4" href="{{ url_for('admin.license_list') }}">Back to list</a>
{% from "macros/forms.html" import render_field, render_checkbox_field, render_submit_field %}

View File

@ -5,7 +5,7 @@ Licenses
{% endblock %}
{% block content %}
<a class="btn btn-primary float-right" href="{{ url_for('admin.create_edit_license') }}">{{ _("New License") }}</a>
<a class="btn btn-primary float-end" href="{{ url_for('admin.create_edit_license') }}">{{ _("New License") }}</a>
<h1>{{ _("Licenses") }}</h1>
@ -13,7 +13,7 @@ Licenses
{% for l in licenses %}
<a class="list-group-item list-group-item-action"
href="{{ url_for('admin.create_edit_license', name=l.name) }}">
<span class="float-right badge {% if l.is_foss %}badge-primary{% else %}badge-warning{% endif %} badge-pill">
<span class="float-end badge {% if l.is_foss %}bg-primary{% else %}bg-warning{% endif %} rounded-pill">
{{ l.is_foss and "Free" or "Non-free"}}
</span>
{{ l.name }}

View File

@ -26,14 +26,14 @@
<form method="post" action="" class="card-body">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<div class="row px-3">
<select name="action" class="custom-select col">
<select name="action" class="form-select col">
{% for id, action in actions.items() %}
<option value="{{ id }}" {% if loop.first %}selected{% endif %}>
{{ action["title"] }}
</option>
{% endfor %}
</select>
<input type="submit" value="Perform" class="col-sm-auto btn btn-primary ml-2" />
<input type="submit" value="Perform" class="col-sm-auto btn btn-primary ms-2" />
</div>
</form>
</div>
@ -45,12 +45,12 @@
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="hidden" name="action" value="restore" />
<div class="row px-3">
<select name="package" class="custom-select col">
<select name="package" class="form-select col">
{% for p in deleted_packages %}
<option value={{ p.id }}>{{ p.id}}) {{ p.title }} by {{ p.author.display_name }}</option>
{% endfor %}
</select>
<input type="submit" value="Restore" class="col-sm-auto btn btn-primary ml-2" />
<input type="submit" value="Restore" class="col-sm-auto btn btn-primary ms-2" />
</div>
</form>

View File

@ -11,7 +11,7 @@
<form method="post" action="" class="card-body">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<div class="">
<select name="package" class="custom-select px-3" required>
<select name="package" class="form-select px-3" required>
<option disabled selected>-- please select --</option>
{% for p in deleted_packages %}
<option value="{{ p.id }}">
@ -21,8 +21,8 @@
</select>
<div class="mt-3">
<input type="submit" name="submit" value="To Draft" class="col-sm-auto btn btn-warning" />
<input type="submit" name="submit" value="To Changes Needed" class="col-sm-auto btn btn-danger ml-2" />
<input type="submit" name="submit" value="To Ready for Review" class="col-sm-auto btn btn-success ml-2" />
<input type="submit" name="submit" value="To Changes Needed" class="col-sm-auto btn btn-danger ms-2" />
<input type="submit" name="submit" value="To Ready for Review" class="col-sm-auto btn btn-success ms-2" />
</div>
</div>
</form>

View File

@ -9,7 +9,7 @@
{% endblock %}
{% block content %}
<a class="btn btn-primary float-right" href="{{ url_for('admin.create_edit_tag') }}">New Tag</a>
<a class="btn btn-primary float-end" href="{{ url_for('admin.create_edit_tag') }}">New Tag</a>
<a class="btn btn-secondary mb-4" href="{{ url_for('admin.tag_list') }}">Back to list</a>
{% from "macros/forms.html" import render_field, render_submit_field, render_checkbox_field %}

View File

@ -5,11 +5,11 @@
{% endblock %}
{% block content %}
<a class="btn btn-primary float-right" href="{{ url_for('admin.create_edit_tag') }}">{{ _("New Tag") }}</a>
<a class="btn btn-primary float-end" href="{{ url_for('admin.create_edit_tag') }}">{{ _("New Tag") }}</a>
<h1>{{ _("Tags") }}</h1>
<p class="float-right">
<p class="float-end">
Sort by:
<a href="{{ url_set_query(sort='name') }}">Name</a> |
<a href="{{ url_set_query(sort='views') }}">Views</a>

View File

@ -9,7 +9,7 @@
{% endblock %}
{% block content %}
<a class="btn btn-primary float-right" href="{{ url_for('admin.create_edit_version') }}">New Version</a>
<a class="btn btn-primary float-end" href="{{ url_for('admin.create_edit_version') }}">New Version</a>
<a class="btn btn-secondary mb-4" href="{{ url_for('admin.version_list') }}">Back to list</a>
{% from "macros/forms.html" import render_field, render_submit_field %}

View File

@ -5,7 +5,7 @@
{% endblock %}
{% block content %}
<a class="btn btn-primary float-right" href="{{ url_for('admin.create_edit_version') }}">{{ _("New Version") }}</a>
<a class="btn btn-primary float-end" href="{{ url_for('admin.create_edit_version') }}">{{ _("New Version") }}</a>
<h1>{{ _("Minetest Versions") }}</h1>

View File

@ -9,7 +9,7 @@
{% endblock %}
{% block content %}
<a class="btn btn-primary float-right" href="{{ url_for('admin.create_edit_warning') }}">New Warning</a>
<a class="btn btn-primary float-end" href="{{ url_for('admin.create_edit_warning') }}">New Warning</a>
<a class="btn btn-secondary mb-4" href="{{ url_for('admin.warning_list') }}">Back to list</a>
{% from "macros/forms.html" import render_field, render_submit_field %}

View File

@ -5,7 +5,7 @@
{% endblock %}
{% block content %}
<a class="btn btn-primary float-right" href="{{ url_for('admin.create_edit_warning') }}">{{ _("New Warning") }}</a>
<a class="btn btn-primary float-end" href="{{ url_for('admin.create_edit_warning') }}">{{ _("New Warning") }}</a>
<h1>{{ _("Warnings") }}</h1>

View File

@ -12,7 +12,7 @@
{% block content %}
{% if token %}
<form class="float-right" method="POST" action="{{ url_for('api.delete_token', username=token.owner.username, id=token.id) }}">
<form class="float-end" method="POST" action="{{ url_for('api.delete_token', username=token.owner.username, id=token.id) }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input class="btn btn-danger" type="submit" value="{{ _('Delete') }}">
</form>

View File

@ -5,8 +5,8 @@
{% endblock %}
{% block pane %}
<a class="btn btn-primary float-right" href="{{ url_for('api.create_edit_token', username=user.username) }}">{{ _("Create") }}</a>
<a class="btn btn-secondary mr-2 float-right" href="/help/api/">{{ _("API Documentation") }}</a>
<a class="btn btn-primary float-end" href="{{ url_for('api.create_edit_token', username=user.username) }}">{{ _("Create") }}</a>
<a class="btn btn-secondary me-2 float-end" href="/help/api/">{{ _("API Documentation") }}</a>
<h2 class="mt-0">{{ _("API Tokens") }}</h2>
<div class="list-group">

View File

@ -5,8 +5,8 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}title{% endblock %} - {{ config.USER_APP_NAME }}</title>
<link rel="stylesheet" type="text/css" href="/static/libs/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="/static/custom.css?v=34">
<link rel="stylesheet" type="text/css" href="/static/libs/bootstrap.min.css?v=2">
<link rel="stylesheet" type="text/css" href="/static/custom.css?v=35">
<link rel="search" type="application/opensearchdescription+xml" href="/static/opensearch.xml" title="ContentDB" />
<link rel="shortcut icon" href="/favicon-16.png" sizes="16x16">
<link rel="icon" href="/favicon-128.png" sizes="128x128">
@ -22,7 +22,7 @@
</button>
<div class="collapse navbar-collapse" id="navbarColor01">
<ul class="navbar-nav mr-auto">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" href="{{ url_for('packages.list_all', type='mod') }}">{{ _("Mods") }}</a>
</li>
@ -42,18 +42,14 @@
<a class="nav-link" href="{{ url_for('threads.list_all') }}">{{ _("Threads") }}</a>
</li>
</ul>
<form class="form-inline my-2 my-lg-0" method="GET" action="/packages/">
<form class="d-flex my-2 my-lg-0" method="GET" action="/packages/">
{% if type %}<input type="hidden" name="type" value="{{ type }}" />{% endif %}
<input class="form-control" name="q" type="text"
<input class="form-control me-1" name="q" type="text"
placeholder="{% if query_hint %}{{ _('Search %(type)s', type=query_hint | lower) }}{% else %}{{ _('Search all packages') }}{% endif %}"
value="{{ query or ''}}">
<input class="btn btn-secondary my-2 my-sm-0 mr-sm-2" type="submit" value="{{ _('Search') }}" />
<!-- <input class="btn btn-secondary my-2 my-sm-0"
data-toggle="tooltip" data-placement="bottom"
title="Go to the first found result for this query."
type="submit" name="lucky" value="First" /> -->
<input class="btn btn-secondary" type="submit" value="{{ _('Search') }}" />
</form>
<ul class="navbar-nav ml-auto">
<ul class="navbar-nav ms-auto">
{% if current_user.is_authenticated %}
{% if todo_list_count is not none %}
<li class="nav-item">
@ -62,7 +58,7 @@
title="{{ _('Work Queue') }}">
{% if todo_list_count > 0 %}
<i class="fas fa-inbox"></i>
<span class="badge badge-pill badge-notify">{{ todo_list_count }}</span>
<span class="badge rounded-pill badge-notify">{{ todo_list_count }}</span>
{% else %}
<i class="fas fa-inbox" ></i>
{% endif %}
@ -86,11 +82,11 @@
<i class="fas fa-bell"></i>
{% set num_notifs = current_user.notifications | length %}
{% if num_notifs > 60 %}
<span class="badge badge-pill badge-notify badge-emoji">
<span class="badge rounded-pill badge-notify badge-emoji">
😢
</span>
{% else %}
<span class="badge badge-pill badge-notify">
<span class="badge rounded-pill badge-notify">
{{ num_notifs }}
</span>
{% endif %}

View File

@ -46,7 +46,7 @@
<body>
<div style="font-family: 'Arial', 'sans-serif'; max-width: 700px; margin: auto; padding: 0;">
<div style="background: #2C3E50; padding: 1.2rem 1.2rem 1.2rem 2em; color: white;">
<h1 style="margin: 0; font-size: 120%; font-weight: normal;">ContentDB</h1>
<h1 style="margin: 0; font-size: 120%;" class="m-0 fw-normal">ContentDB</h1>
</div>
<div style="padding: 2em; background: white;">
{% block content %}

View File

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

View File

@ -33,7 +33,7 @@
{% set tags = package.tags | sort(attribute="views", reverse=True) %}
<div class="carousel-item {% if loop.index == 1 %}active{% endif %}">
<a href="{{ package.getURL("packages.view") }}">
<div class="embed-responsive embed-responsive-16by9">
<div class="embed-responsive embed-responsive-16x9">
<img class="embed-responsive-item" src="{{ cover_image }}"
alt="{{ _('%(title)s by %(author)s', title=package.title, author=package.author.display_name) }}">
</div>
@ -50,18 +50,18 @@
</p>
{% if package.author %}
<div class="d-none d-md-block">
<span class="mr-2">
<span class="me-2">
{{ package.type.text }}
</span>
{% for warning in package.content_warnings %}
<span class="badge badge-warning" title="{{ warning.description }}">
<span class="badge bg-warning" title="{{ warning.description }}">
<i class="fas fa-exclamation-circle" style="margin-right: 0.3em;"></i>
{{ warning.title }}
</span>
{% endfor %}
{% for t in tags[:3] %}
{% if t.name != "featured" %}
<span class="badge badge-primary" title="{{ t.description or '' }}">
<span class="badge bg-primary" title="{{ t.description or '' }}">
{{ t.title }}
</span>
{% endif %}
@ -90,42 +90,42 @@
<span class="sr-only">{{ _("Next") }}</span>
</a>
</div>
<div class="text-right mb-5 text-muted" style="opacity: 0.4;">
<div class="text-end mb-5 text-muted" style="opacity: 0.4;">
<a href="/help/featured/" class="btn">
<i class="fas fa-question-circle mr-1"></i>
<i class="fas fa-question-circle me-1"></i>
{{ _("Featured") }}
</a>
</div>
<a href="{{ url_for('packages.list_all', sort='approved_at', order='desc') }}" class="btn btn-secondary float-right">
<a href="{{ url_for('packages.list_all', sort='approved_at', order='desc') }}" class="btn btn-secondary float-end">
{{ _("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') }}" class="btn btn-secondary float-right">
<a href="{{ url_for('packages.list_all', sort='last_release', order='desc') }}" class="btn btn-secondary float-end">
{{ _("See more") }}
</a>
<h2 class="my-3">{{ _("Recently Updated") }}</h2>
{{ render_pkggrid(updated) }}
<a href="{{ url_for('packages.list_all', type='game', sort='score', order='desc') }}" class="btn btn-secondary float-right">
<a href="{{ url_for('packages.list_all', type='game', sort='score', order='desc') }}" class="btn btn-secondary float-end">
{{ _("See more") }}
</a>
<h2 class="my-3">{{ _("Top Games") }}</h2>
{{ render_pkggrid(pop_gam) }}
<a href="{{ url_for('packages.list_all', type='mod', sort='score', order='desc') }}" class="btn btn-secondary float-right">
<a href="{{ url_for('packages.list_all', type='mod', sort='score', order='desc') }}" class="btn btn-secondary float-end">
{{ _("See more") }}
</a>
<h2 class="my-3">{{ _("Top Mods") }}</h2>
{{ render_pkggrid(pop_mod) }}
<a href="{{ url_for('packages.list_all', type='txp', sort='score', order='desc') }}" class="btn btn-secondary float-right">
<a href="{{ url_for('packages.list_all', type='txp', sort='score', order='desc') }}" class="btn btn-secondary float-end">
{{ _("See more") }}
</a>
<h2 class="my-3">{{ _("Top Texture Packs") }}</h2>
@ -141,20 +141,20 @@
title="{{ tag.description or '' }}"
href="{{ url_for('packages.list_all', tag=tag.name) }}">
{{ tag.title }}
<span class="badge badge-pill badge-light ml-1">{{ count }}</span>
<span class="badge rounded-pill bg-light ms-1">{{ count }}</span>
</a>
{% endfor %}
<div class="clearfix mb-4"></div>
<a href="{{ url_for('packages.list_all', sort='reviews', order='desc') }}" class="btn btn-secondary float-right">
<a href="{{ url_for('packages.list_all', sort='reviews', order='desc') }}" class="btn btn-secondary float-end">
{{ _("See more") }}
</a>
<h2 class="my-3">{{ _("Highest Reviewed") }}</h2>
{{ render_pkggrid(high_reviewed) }}
<a href="{{ url_for('packages.list_reviews') }}" class="btn btn-secondary float-right">
<a href="{{ url_for('packages.list_reviews') }}" class="btn btn-secondary float-end">
{{ _("See more") }}
</a>
<h2 class="my-3">{{ _("Recent Positive Reviews") }}</h2>

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 %}
@ -27,7 +27,7 @@
style="max-height: 22px;"
src="{{ entry.causer.getProfilePicURL() }}" />
<span class="pl-2">{{ entry.causer.username }}</span>
<span class="ps-2">{{ entry.causer.username }}</span>
{% else %}
<i>{{ _("Deleted User") }}</i>
{% endif %}
@ -37,13 +37,13 @@
{{ entry.title}}
{% if entry.description %}
<i class="fas fa-paperclip ml-3"></i>
<i class="fas fa-paperclip ms-3"></i>
{% endif %}
</div>
{% if entry.package %}
<div class="col-sm-auto text-muted">
<span class="pr-2">
<span class="pe-2">
{{ entry.package.title }}
</span>

View File

@ -8,7 +8,7 @@
<div class="form-group {% if field.errors %}has-danger{% endif %} {{ kwargs.pop('class_', '') }}">
{% if field.type != 'HiddenField' %}
{% if not label and label != "" %}{% set label=field.label.text %}{% endif %}
{% if label %}<label for="{{ field.id }}" {% if not label_visible %}class="sr-only"{% endif %}>{{ label|safe }}</label>{% endif %}
{% if label %}<label for="{{ field.id }}" {% if not label_visible %}class="sr-only"{% else %}class="form-label"{% endif %}>{{ label|safe }}</label>{% endif %}
{% endif %}
{{ field(class_=fieldclass or 'form-control', **kwargs) }}
{% if hint %}
@ -22,13 +22,11 @@
<div class="form-group {% if field.errors %}has-danger{% endif %} {{ kwargs.pop('class_', '') }}">
{% if field.type != 'HiddenField' and label_visible %}
{% if not label and label != "" %}{% set label=field.label.text %}{% endif %}
{% if label %}<label for="{{ field.id }}">{{ label|safe }}</label>{% endif %}
{% if label %}<label for="{{ field.id }}" class="form-label">{{ label|safe }}</label>{% endif %}
{% endif %}
<div class="input-group mb-3">
<div class="input-group-prepend">
<span class="input-group-text" id="basic-addon1">{{ prefix }}</span>
</div>
<span class="input-group-text" id="{{ field.name }}">{{ prefix }}</span>
{{ field(class_=fieldclass or 'form-control', **kwargs) }}
</div>
@ -40,13 +38,11 @@
<div class="form-group {% if field.errors %}has-danger{% endif %} {{ kwargs.pop('class_', '') }}">
{% if field.type != 'HiddenField' and label_visible %}
{% if not label and label != "" %}{% set label=field.label.text %}{% endif %}
{% if label %}<label for="{{ field.id }}">{{ label|safe }}</label>{% endif %}
{% if label %}<label for="{{ field.id }}" class="form-label">{{ label|safe }}</label>{% endif %}
{% endif %}
<div class="input-group mb-3">
<div class="input-group-prepend">
<span class="input-group-text" id="basic-addon1">{{ prefix }}</span>
</div>
<span class="input-group-text" id="basic-addon1">{{ prefix }}</span>
{{ field(class_=fieldclass or 'form-control', **kwargs) }}
<a class="btn btn-secondary" id="{{ field.name }}-button">
{{ _("View") }}
@ -101,7 +97,7 @@
<div class="form-group {% if field.errors %}has-danger{% endif %} {{ kwargs.pop('class_', '') }}">
{% if field.type != 'HiddenField' and label_visible %}
{% if not label %}{% set label=field.label.text %}{% endif %}
<label for="{{ field.id }}">{{ label|safe }}</label>
<label for="{{ field.id }}" class="form-label">{{ label|safe }}</label>
{% endif %}
<div class="multichoice_selector bulletselector form-control">
<input type="text" placeholder="{{ _('Start typing to see suggestions') }}">
@ -115,9 +111,10 @@
{% macro render_checkbox_field(field, label=None) -%}
{% if not label %}{% set label=field.label.text %}{% endif %}
<div class="checkbox {{ kwargs.pop('class_', '') }}">
<label>
{{ field(type='checkbox', **kwargs) }} {{ label }}
<div class="form-sheck {{ kwargs.pop('class_', '') }}">
{{ field(type='checkbox', class_="form-check-input", **kwargs) }}
<label class="form-check-label" for="{{ field.id }}">
{{ label }}
</label>
</div>
{%- endmacro %}
@ -149,7 +146,7 @@
<label class="btn btn-primary{% if checked %} active{% endif %}">
{% set icon = icons[value] %}
{% if icon %}
<i class="fas {{ icon }} mr-2"></i>
<i class="fas {{ icon }} me-2"></i>
{% endif %}
<input type="radio" name="{{ field.id }}" id="{{ field.id }}" value="{{ value }}" autocomplete="off" {% if checked %} checked{% endif %}>
{{ label }}

View File

@ -14,24 +14,19 @@
</div>
{% set level = "warning" %}
{% if package.releases.filter_by(task_id=None).count() == 0 %}
{% if package.releases.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 release") }}
<a class="btn btn-sm btn-warning float-end" href="{{ package.getURL("packages.create_release") }}">
{{ _("Create first 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-end" href="{{ package.getURL("packages.setup_releases") }}">
{{ _("Set up releases") }}
</a>
{% endif %}
{% 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 %}
{{ _("You need to create a release before this package can be approved.") }}
{% else %}
{{ _("A release is required before this package can be approved.") }}
{% endif %}
@ -95,7 +90,7 @@
{% if conflicting_modnames %}
<div class="alert alert-warning">
<a class="float-right btn btn-sm btn-warning" href="{{ package.getURL('packages.similar') }}">
<a class="float-end btn btn-sm btn-warning" href="{{ package.getURL('packages.similar') }}">
More info
</a>
{% if conflicting_modnames | length > 4 %}
@ -108,7 +103,7 @@
{% if not package.review_thread and (package.author == current_user or package.checkPerm(current_user, "APPROVE_NEW")) %}
<div class="alert alert-secondary">
<a class="float-right btn btn-sm btn-secondary" href="{{ url_for('threads.new', pid=package.id, title='Package approval comments') }}">
<a class="float-end btn btn-sm btn-secondary" href="{{ url_for('threads.new', pid=package.id, title='Package approval comments') }}">
{{ _("Open Thread") }}
</a>

View File

@ -10,7 +10,7 @@
<small>{{ package.author.display_name }}</small>
{% endif %}
{% if not package.approved %}
<span class="badge ml-1 {% if package.state == package.state.CHANGES_NEEDED %}bg-danger{% else %}bg-warning{% endif %}">
<span class="badge ms-1 {% if package.state == package.state.CHANGES_NEEDED %}bg-danger{% else %}bg-warning{% endif %}">
{{ package.state.value }}
</span>
{% endif %}

View File

@ -2,7 +2,7 @@
{% for rel in releases %}
<a class="list-group-item list-group-item-action" href="{{ rel.getEditURL() }}">
{{ rel.title }}
<span class="text-muted ml-1">
<span class="text-muted ms-1">
{% if rel.min_rel and rel.max_rel %}
[MT {{ rel.min_rel.name }}-{{ rel.max_rel.name }}]
{% elif rel.min_rel %}
@ -29,7 +29,7 @@
{% if rel.approved or package.checkPerm(current_user, "MAKE_RELEASE") or rel.checkPerm(current_user, "APPROVE_RELEASE") %}
<a class="list-group-item list-group-item-action" href="{{ rel.getDownloadURL() }}">
{{ rel.title }}
<span class="text-muted ml-1">
<span class="text-muted ms-1">
{% if rel.min_rel and rel.max_rel %}
[MT {{ rel.min_rel.name }}-{{ rel.max_rel.name }}]
{% elif rel.min_rel %}
@ -55,7 +55,7 @@
{% macro render_releases(releases, package, current_user) -%}
{% for rel in releases %}
<div class="list-group-item">
<a class="btn btn-sm btn-primary float-right" href="{{ rel.getEditURL() }}">
<a class="btn btn-sm btn-primary float-end" href="{{ rel.getEditURL() }}">
{% if not rel.task_id and not rel.approved and rel.checkPerm(current_user, "APPROVE_RELEASE") %}
{{ _("Edit / Approve") }}
{% else %}
@ -69,7 +69,7 @@
{{ rel.title }}
</a>
<span class="text-muted ml-1">
<span class="text-muted ms-1">
{% if rel.min_rel and rel.max_rel %}
[MT {{ rel.min_rel.name }}-{{ rel.max_rel.name }}]
{% elif rel.min_rel %}

View File

@ -6,13 +6,13 @@
<button class="btn {% if is_positive == true %}btn-primary{% else %}btn-secondary{% endif %}" name="is_positive" value="yes">
{{ _("Helpful") }}
{% if positive > 0 %}
<span class="badge badge-light ml-1">{{ positive }}</span>
<span class="badge bg-light ms-1">{{ positive }}</span>
{% endif %}
</button>
<button class="btn {% if is_positive == false %}btn-primary{% else %}btn-secondary{% endif %}" name="is_positive" value="no">
{{ _("Unhelpful") }}
{% if negative > 0 %}
<span class="badge badge-light ml-1">{{ negative }}</span>
<span class="badge bg-light ms-1">{{ negative }}</span>
{% endif %}
</button>
</div>
@ -30,7 +30,7 @@
<img class="img-fluid user-photo img-thumbnail img-thumbnail-1" src="{{ review.author.getProfilePicURL() }}">
</a>
</div>
<div class="col-md-auto pl-1 pr-3 pt-2 text-center" style=" font-size: 200%;">
<div class="col-md-auto ps-1 pe-3 pt-2 text-center" style=" font-size: 200%;">
{% if review.recommends %}
<i class="fas fa-thumbs-up" style="color:#6f6;"></i>
{% else %}
@ -39,7 +39,7 @@
</div>
{% if review.thread %}
{% set reply = review.thread.replies[0] %}
<div class="col pr-0">
<div class="col pe-0">
<div class="card">
<div class="card-header">
<a class="author {{ review.author.rank.name }}"
@ -47,7 +47,7 @@
{{ review.author.display_name }}
</a>
<a name="reply-{{ reply.id }}" class="text-muted float-right"
<a name="reply-{{ reply.id }}" class="text-muted float-end"
href="{{ url_for('threads.view', id=review.thread.id) }}#reply-{{ reply.id }}">
{{ review.created_at | datetime }}
</a>
@ -55,7 +55,7 @@
<div class="card-body markdown">
{% if current_user == review.author %}
<a class="btn btn-primary btn-sm ml-1 float-right"
<a class="btn btn-primary btn-sm ms-1 float-end"
href="{{ review.package.getURL("packages.review") }}">
<i class="fas fa-pen"></i>
</a>
@ -69,16 +69,16 @@
<div class="btn-toolbar mt-2 mb-0">
{% if show_package_link %}
<a class="btn btn-primary mr-1" href="{{ review.package.getURL("packages.view") }}">
<a class="btn btn-primary me-1" href="{{ review.package.getURL("packages.view") }}">
{{ _("%(title)s by %(author)s",
title="<b>" | safe + review.package.title + "</b>" | safe,
author=review.package.author.display_name) }}
</a>
{% endif %}
<a class="btn {% if review.thread.replies.count() > 1 %} btn-primary {% else %} btn-secondary {% endif %} mr-1"
<a class="btn {% if review.thread.replies.count() > 1 %} btn-primary {% else %} btn-secondary {% endif %} me-1"
href="{{ url_for('threads.view', id=review.thread.id) }}">
<i class="fas fa-comments mr-2"></i>
<i class="fas fa-comments me-2"></i>
{{ _("%(num)d comments", num=review.thread.replies.count() - 1) }}
</a>
@ -111,11 +111,11 @@
<div class="btn-group btn-group-toggle" data-toggle="buttons">
<label class="btn btn-primary">
<i class="fas fa-thumbs-up mr-2"></i>
<i class="fas fa-thumbs-up me-2"></i>
<input type="radio" name="recommends" value="yes" autocomplete="off"> {{ _("Yes") }}
</label>
<label class="btn btn-primary">
<i class="fas fa-thumbs-down mr-2"></i>
<i class="fas fa-thumbs-down me-2"></i>
<input type="radio" name="recommends" value="no" autocomplete="off"> {{ _("No") }}
</label>
</div>
@ -150,11 +150,11 @@
<div class="btn-group">
<button class="btn btn-primary" name="recommends" value="yes">
<i class="fas fa-thumbs-up mr-2"></i>
<i class="fas fa-thumbs-up me-2"></i>
{{ _("Yes") }}
</button>
<button class="btn btn-primary" name="recommends" value="no">
<i class="fas fa-thumbs-down mr-2"></i>
<i class="fas fa-thumbs-down me-2"></i>
{{ _("No") }}
</button>
</div>

View File

@ -10,32 +10,32 @@
<img class="img-fluid user-photo img-thumbnail img-thumbnail-1" src="{{ r.author.getProfilePicURL() }}">
</a>
</div>
<div class="col pr-0">
<div class="col pe-0">
<div class="card">
<div class="card-header">
<a class="author {{ r.author.rank.name }} mr-3"
<a class="author {{ r.author.rank.name }} me-3"
href="{{ url_for('users.profile', username=r.author.username) }}">
{{ r.author.display_name }}
</a>
{% if r.author.username != r.author.display_name %}
<span class="text-muted small mr-2">
<span class="text-muted small me-2">
({{ r.author.username }})
</span>
{% endif %}
{% if r.author in thread.package.maintainers %}
<span class="badge badge-dark">
<span class="badge bg-dark">
{{ _("Maintainer") }}
</span>
{% endif %}
{% if r.author.rank == r.author.rank.BOT %}
<span class="badge badge-dark">
<span class="badge bg-dark">
{{ r.author.rank.getTitle() }}
</span>
{% endif %}
<a name="reply-{{ r.id }}" class="text-muted float-right"
<a name="reply-{{ r.id }}" class="text-muted float-end"
href="{{ r.get_url() }}">
{{ r.created_at | datetime }}
</a>
@ -43,26 +43,26 @@
<div class="card-body markdown">
{% if r.checkPerm(current_user, "DELETE_REPLY") %}
<a class="float-right btn btn-secondary btn-sm ml-2"
<a class="float-end btn btn-secondary btn-sm ms-2"
href="{{ url_for('threads.delete_reply', id=thread.id, reply=r.id) }}">
<i class="fas fa-trash"></i>
</a>
{% endif %}
{% if current_user != r.author %}
<a class="float-right btn-secondary btn-sm ml-2"
<a class="float-end btn-secondary btn-sm ms-2"
title="{{ _('Report') }}"
href="{{ url_for('report.report', url=r.get_url()) }}">
<i class="fas fa-flag mr-1"></i>
<i class="fas fa-flag me-1"></i>
</a>
{% endif %}
{% if current_user == thread.author and thread.review and thread.replies[0] == r %}
<a class="float-right btn btn-primary btn-sm ml-2"
<a class="float-end btn btn-primary btn-sm ms-2"
href="{{ thread.review.package.getURL('packages.review') }}">
<i class="fas fa-pen"></i>
</a>
{% elif r.checkPerm(current_user, "EDIT_REPLY") %}
<a class="float-right btn btn-primary btn-sm ml-2"
<a class="float-end btn btn-primary btn-sm ms-2"
href="{{ url_for('threads.edit_reply', id=thread.id, reply=r.id) }}">
<i class="fas fa-pen"></i>
</a>
@ -82,7 +82,7 @@
{% if thread.locked %}
<p class="my-0 py-4 text-center">
<i class="fas fa-lock mr-3"></i>
<i class="fas fa-lock me-3"></i>
{{ _("This thread has been locked by a moderator.") }}
</p>
{% endif %}
@ -93,7 +93,7 @@
<img class="img-fluid user-photo img-thumbnail img-thumbnail-1"
src="{{ current_user.getProfilePicURL() }}">
</div>
<div class="col pr-0">
<div class="col pe-0">
<div class="card">
<div class="card-header {{ current_user.rank.name }}">
{{ current_user.display_name }}
@ -171,13 +171,13 @@
{% else %}
<i class="fas fa-thumbs-down" style="color:#f66;"></i>
{% endif %}
<strong class="ml-1">
<strong class="ms-1">
{{ t.title }}
</strong><br />
<span>
{{ t.author.display_name }}
</span>
<span class="text-muted ml-3">
<span class="text-muted ms-3">
{{ t.created_at | datetime }}
</span>
</div>
@ -185,7 +185,7 @@
{% if replies > 0 %}
<span class="col-md-auto text-muted">
{{ replies }}
<i class="fas fa-comment ml-1"></i>
<i class="fas fa-comment ms-1"></i>
</span>
{% endif %}
@ -202,13 +202,13 @@
</div>
{% if t.package %}
<div class="col-md-2 text-muted text-right">
<div class="col-md-2 text-muted text-end">
<img
class="img-fluid"
style="max-height: 22px; max-width: 22px;"
src="{{ t.package.getThumbnailOrPlaceholder() }}" /><br />
<span class="pl-2">
<span class="ps-2">
{{ t.package.title }}
</span>
</div>

View File

@ -11,7 +11,7 @@
style="max-height: 22px; max-width: 22px;"
src="{{ package.getThumbnailOrPlaceholder() }}" />
<span class="pl-2">
<span class="ps-2">
{{ package.title }}
</span>
</a>
@ -32,21 +32,21 @@
<div class="col-sm-auto">
{% if not show_config %}
{% if package.checkPerm(current_player, "MAKE_RELEASE") %}
<a class="btn btn-sm btn-primary mr-2" href="{{ config.get_create_release_url() }}">
<i class="fas fa-plus mr-1"></i>
<a class="btn btn-sm btn-primary me-2" href="{{ config.get_create_release_url() }}">
<i class="fas fa-plus me-1"></i>
{{ _("Release") }}
</a>
{% endif %}
{% endif %}
<a class="btn btn-sm btn-secondary mr-2" href="{{ package.repo }}">
<i class="fas fa-code-branch mr-1"></i>
<a class="btn btn-sm btn-secondary me-2" href="{{ package.repo }}">
<i class="fas fa-code-branch me-1"></i>
{{ _("Repo") }}
</a>
{% if package.checkPerm(current_player, "MAKE_RELEASE") %}
<a class="btn btn-sm btn-secondary" href="{{ package.getURL("packages.update_config") }}">
<i class="fas fa-cog mr-1"></i>
<i class="fas fa-cog me-1"></i>
{{ _("Update settings") }}
</a>
{% endif %}

View File

@ -4,21 +4,18 @@
{{ 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>
<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()) }}
{% from "macros/packagegridtile.html" import render_pkggrid %}
{{ render_pkggrid(mpackage.packages.filter_by(state="APPROVED").all()) }}
{% if similar_topics %}
<h3>{{ _("Forum Topics") }}</h3>
<p>
{{ _("Unfortunately, this isn't on ContentDB yet! Here's some forum topic(s):") }}
</p>
<ul>
{% for t in similar_topics %}
<li>

View File

@ -6,11 +6,11 @@
{% block content %}
{% if current_user.notifications %}
<form method="post" action="{{ url_for('notifications.clear') }}" class="float-right">
<form method="post" action="{{ url_for('notifications.clear') }}" class="float-end">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="submit" class="btn btn-primary" value="Clear All" />
</form>
<a href="{{ url_for('users.email_notifications', username=current_user.username) }}" class="btn btn-secondary float-right mr-3">
<a href="{{ url_for('users.email_notifications', username=current_user.username) }}" class="btn btn-secondary float-end me-3">
{{ _("Edit email notification settings") }}
</a>
{% endif %}
@ -38,7 +38,7 @@
style="max-height: 22px; max-width: 22px;"
src="{{ n.package.getThumbnailOrPlaceholder() }}" />
<span class="pl-2">
<span class="ps-2">
{{ n.package.title }}
</span>
</div>
@ -48,8 +48,8 @@
{{ n.title}}
</div>
<div class="col-sm-auto text-muted text-right">
<span class="pr-2">{{ n.causer.display_name }}</span>
<div class="col-sm-auto text-muted text-end">
<span class="pe-2">{{ n.causer.display_name }}</span>
<img
class="img-fluid user-photo img-thumbnail img-thumbnail-1"
style="max-height: 22px;"
@ -76,7 +76,7 @@
style="max-height: 22px; max-width: 22px;"
src="{{ n.package.getThumbnailOrPlaceholder() }}" />
<span class="pl-2">
<span class="ps-2">
{{ n.package.title }}
</span>
</div>
@ -86,8 +86,8 @@
{{ n.title}}
</div>
<div class="col-sm-auto text-muted text-right">
<span class="pr-2">{{ n.causer.display_name }}</span>
<div class="col-sm-auto text-muted text-end">
<span class="pe-2">{{ n.causer.display_name }}</span>
<img
class="img-fluid user-photo img-thumbnail img-thumbnail-1"
style="max-height: 22px;"

View File

@ -9,7 +9,7 @@
{% endblock %}
{% block content %}
<a class="btn btn-primary float-right" href="{{ package.getURL("packages.alias_create_edit") }}">
<a class="btn btn-primary float-end" href="{{ package.getURL("packages.alias_create_edit") }}">
{{ _("Create") }}
</a>
<h1>{{ _("Aliases for %(title)s by %(author)s", title=self.link(), author=package.author.display_name) }}</h1>

View File

@ -5,7 +5,7 @@
{% endblock %}
{% block content %}
<a class="btn btn-secondary float-right" href="/help/update_config/">{{ _("Help") }}</a>
<a class="btn btn-secondary float-end" href="/help/update_config/">{{ _("Help") }}</a>
<h1 class="mb-5">{{ self.title() }}</h1>
<h2>{{ _("Packages with Update Settings") }}</h2>

View File

@ -38,13 +38,13 @@
{% if not package %}
<div class="alert alert-info">
<a class="float-right btn btn-sm btn-default" href="{{ url_for('flatpage', path='policy_and_guidance') }}">{{ _("View") }}</a>
<a class="float-end btn btn-sm btn-default" href="{{ url_for('flatpage', path='policy_and_guidance') }}">{{ _("View") }}</a>
{{ _("Have you read the Package Inclusion Policy and Guidance yet?") }}
</div>
{% else %}
<div class="alert alert-secondary">
<a class="float-right btn btn-sm btn-default" href="/help/package_config/#cdbjson">{{ _("Read more") }}</a>
<a class="float-end btn btn-sm btn-default" href="/help/package_config/#cdbjson">{{ _("Read more") }}</a>
{{ _("You can include a .cdb.json file in your %(type)s to update these details automatically.", type=package.type.text.lower()) }}
</div>
@ -81,11 +81,8 @@
{{ render_multiselect_field(form.content_warnings, class_="pkg_meta") }}
<div class="pkg_meta row">
{{ render_field(form.license, class_="not_txp col-sm-6") }}
{{ render_field(form.media_license, class_="col-sm-6") }}
</div>
<div class="pkg_meta row">
<div class="not_txp col-sm-6"></div>
<div class="not_txp col-sm-6">{{ _("If there is no media, set the Media License to the same as the License.") }}</div>
{{ render_field(form.media_license, class_="col-sm-6",
hint=_("If there is no media, set the Media License to the same as the License.")) }}
</div>
{{ render_field(form.desc, class_="pkg_meta", fieldclass="form-control markdown") }}
</fieldset>

View File

@ -1,57 +0,0 @@
{% 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

@ -34,14 +34,14 @@
title="{{ tag.description or '' }}"
href="{{ url_set_query(page=1, _remove={ 'tag': tag.name }) }}">
{{ tag.title }}
<span class="badge badge-pill badge-light ml-1">{{ count }}</span>
<span class="badge rounded-pill bg-light ms-1">{{ count }}</span>
</a>
{% else %}
<a class="btn btn-sm btn-secondary m-1" rel="nofollow"
title="{{ tag.description or '' }}"
href="{{ url_set_query(page=1, _add={ 'tag': tag.name }) }}">
{{ tag.title }}
<span class="badge badge-pill badge-light ml-1">{{ count }}</span>
<span class="badge rounded-pill bg-light ms-1">{{ count }}</span>
</a>
{% endif %}
{% endfor %}

View File

@ -12,7 +12,7 @@
<img class="img-fluid user-photo img-thumbnail img-thumbnail-1"
src="{{ package.getThumbnailOrPlaceholder(1) }}" alt="{{ _('Thumbnail') }}" style="max-height: 20px;">
</span>
<span class="col m-0 p-0 pl-2">
<span class="col m-0 p-0 ps-2">
{{ package.title }}
</span>
</span>

View File

@ -72,7 +72,7 @@
{% if release.checkPerm(current_user, "DELETE_RELEASE") %}
<form method="POST" action="{{ release.getDeleteURL() }}" class="alert alert-secondary mb-5">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input class="btn btn-sm btn-danger float-right" type="submit" value="{{ _('Delete') }}">
<input class="btn btn-sm btn-danger float-end" type="submit" value="{{ _('Delete') }}">
<b>{{ _("This is permanent.") }}</b>
{{ _("Any associated uploads will not be deleted immediately, but the release will no longer be listed.") }}
<div style="clear:both;"></div>

View File

@ -9,7 +9,7 @@
{% if package.update_config %}
<p class="alert alert-secondary mb-4">
<a class="float-right btn btn-sm btn-secondary" href="{{ package.getURL("packages.update_config") }}">{{ _("Settings") }}</a>
<a class="float-end btn btn-sm btn-secondary" href="{{ package.getURL("packages.update_config") }}">{{ _("Settings") }}</a>
{% if package.update_config.make_release %}
{{ _("You have automatic releases enabled.") }}
{% else %}
@ -20,13 +20,13 @@
{% else %}
<p class="alert alert-info mb-4">
{% if package.repo %}
<a class="float-right btn btn-sm btn-info" href="{{ package.getURL("packages.setup_releases") }}">{{ _("Set up") }}</a>
<i class="fas fa-info mr-2"></i>
<a class="float-end btn btn-sm btn-info" href="{{ package.getURL("packages.setup_releases") }}">{{ _("Set up") }}</a>
<i class="fas fa-info me-2"></i>
{{ _("You can create releases automatically when you push commits or tags to your repository.") }}
{% else %}
<a class="float-right btn btn-sm btn-info" href="{{ package.getURL("packages.create_edit") }}">{{ _("Add Git repo") }}</a>
<i class="fas fa-info mr-2"></i>
<a class="float-end btn btn-sm btn-info" href="{{ package.getURL("packages.create_edit") }}">{{ _("Add Git repo") }}</a>
<i class="fas fa-info me-2"></i>
{{ _("Using Git would allow you to create releases automatically when you push code or tags.") }}
{% endif %}
@ -71,7 +71,7 @@
</p>
<p>
<i class="fas fa-exclamation-circle mr-2"></i>
<i class="fas fa-exclamation-circle me-2"></i>
{{ _("The .conf of your package can <a href='/help/package_config/'>set this automatically</a>,
which will override your selection.") }}
</p>
@ -82,6 +82,7 @@
<br />
{{ _("Leave both as None if in doubt.") }}
</p>
<p class="mt-5">
{{ render_submit_field(form.submit) }}
</p>

View File

@ -5,7 +5,7 @@
{% endblock %}
{% block content %}
<a class="btn btn-secondary float-right" href="{{ package.getURL("packages.view") }}">
<a class="btn btn-secondary float-end" href="{{ package.getURL("packages.view") }}">
{{ _("Later") }}
</a>
<h1>{{ self.title() }}</h1>
@ -33,10 +33,10 @@
<a class="btn btn-primary" href="{{ package.getURL("packages.update_config", trigger="commit") }}">
{{ _("Rolling Release") }}
</a>
<a class="btn btn-primary ml-2" href="{{ package.getURL("packages.update_config", trigger="tag") }}">
<a class="btn btn-primary ms-2" href="{{ package.getURL("packages.update_config", trigger="tag") }}">
{{ _("On Git Tag") }}
</a>
{# <a class="btn btn-secondary ml-2" href="{{ package.getURL("packages.update_config") }}">#}
{# <a class="btn btn-secondary ms-2" href="{{ package.getURL("packages.update_config") }}">#}
{# {{ _("Advanced") }}#}
{# </a>#}
</p>
@ -48,7 +48,7 @@
<a class="btn btn-secondary" href="{{ package.getURL("packages.update_config", action="notification") }}">
{{ _("With reminders") }}
</a>
<a class="btn btn-secondary ml-2" href="{{ package.getURL("packages.create_release") }}">
<a class="btn btn-secondary ms-2" href="{{ package.getURL("packages.create_release") }}">
{{ _("No reminders") }}
</a>
</p>
@ -68,10 +68,10 @@
<a class="btn btn-primary" href="{{ package.getURL("packages.create_edit") }}">
{{ _("Add Git repo") }}
</a>
<a class="btn btn-secondary ml-2" href="{{ package.getURL("packages.create_release") }}">
<a class="btn btn-secondary ms-2" href="{{ package.getURL("packages.create_release") }}">
{{ _("Create releases manually") }}
</a>
<a class="btn btn-secondary ml-2" href="{{ package.getURL("packages.view") }}">
<a class="btn btn-secondary ms-2" href="{{ package.getURL("packages.view") }}">
{{ _("Later") }}
</a>
</p>

View File

@ -6,26 +6,26 @@
{% block content %}
{% if package.checkPerm(current_user, "MAKE_RELEASE") %}
<p class="float-right">
<p class="float-end">
{% if package.update_config %}
<a class="btn btn-secondary" href="{{ package.getURL("packages.update_config") }}">
<i class="fas fa-cog mr-1"></i>
<i class="fas fa-cog me-1"></i>
{{ _("Update settings") }}
</a>
{% elif package.repo %}
<a class="btn btn-secondary" href="{{ package.getURL("packages.setup_releases") }}">
<i class="fas fa-hat-wizard mr-1"></i>
<i class="fas fa-hat-wizard me-1"></i>
{{ _("Set up automatic releases") }}
</a>
{% endif %}
<a class="btn btn-secondary ml-1" href="{{ package.getURL("packages.bulk_change_release") }}">
<i class="fas fa-wrench mr-1"></i>
<a class="btn btn-secondary ms-1" href="{{ package.getURL("packages.bulk_change_release") }}">
<i class="fas fa-wrench me-1"></i>
{{ _("Bulk update") }}
</a>
<a class="btn btn-primary ml-1" href="{{ package.getURL("packages.create_release") }}">
<i class="fas fa-plus mr-1"></i>
<a class="btn btn-primary ms-1" href="{{ package.getURL("packages.create_release") }}">
<i class="fas fa-plus me-1"></i>
{{ _("Create") }}
</a>
</p>

View File

@ -24,16 +24,16 @@ Remove {{ package.title }}
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<div class="form-group">
<label for="reason">{{ _("Reason") }}</label>
<label for="reason" class="form-label">{{ _("Reason") }}</label>
<input id="reason" class="form-control" type="text" name="reason" required minlength="5">
<small class="form-text text-muted">
{{ _("Reason for unapproval / deletion, this is shown in the audit log") }}
</small>
</div>
<a class="btn btn-secondary float-right" href="{{ package.getURL("packages.view") }}">{{ _("Cancel") }}</a>
<a class="btn btn-secondary float-end" href="{{ package.getURL("packages.view") }}">{{ _("Cancel") }}</a>
<input type="submit" name="delete" value="Remove" class="btn btn-danger mr-2" />
<input type="submit" name="delete" value="Remove" class="btn btn-danger me-2" />
{% if package.approved %}
<input type="submit" name="unapprove" value="Unapprove" class="btn btn-warning" />

View File

@ -52,7 +52,7 @@
{% if review %}
<form method="POST" action="{{ review.getDeleteURL() }}" class="alert alert-secondary my-5">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input class="btn btn-sm btn-danger float-right" type="submit" value="{{ _('Delete') }}">
<input class="btn btn-sm btn-danger float-end" type="submit" value="{{ _('Delete') }}">
<b>{{ _("Delete review.") }}</b>
{{ _("This will convert the review into a thread, keeping the comments but removing its effect on the package's rating.") }}
<div style="clear:both;"></div>

View File

@ -55,9 +55,9 @@
<tr>
<th colspan="2">
{% if review.recommends %}
<i class="fas fa-thumbs-up text-success mr-2"></i>
<i class="fas fa-thumbs-up text-success me-2"></i>
{% else %}
<i class="fas fa-thumbs-down text-danger mr-2"></i>
<i class="fas fa-thumbs-down text-danger me-2"></i>
{% endif %}
<a href="{{ review.thread.getViewURL() }}">
{{ review.thread.title }}
@ -68,7 +68,7 @@
<td>
{% for vote in review.votes %}
{% if vote.is_positive %}
<a href="{{ url_for('users.profile', username=vote.user.username) }}" class="badge badge-secondary">
<a href="{{ url_for('users.profile', username=vote.user.username) }}" class="badge bg-secondary">
{{ vote.user.username }}
</a>
{% endif %}
@ -77,7 +77,7 @@
<td>
{% for vote in review.votes %}
{% if not vote.is_positive %}
<a href="{{ url_for('users.profile', username=vote.user.username) }}" class="badge badge-secondary">
<a href="{{ url_for('users.profile', username=vote.user.username) }}" class="badge bg-secondary">
{{ vote.user.username }}
</a>
{% endif %}
@ -86,4 +86,4 @@
</tr>
{% endfor %}
</table>
{% endblock %}
{% endblock %}

View File

@ -23,7 +23,7 @@
{{ render_submit_field(form.submit) }}
</form>
<a href="{{ screenshot.url }}" class="col-md-4 text-right">
<a href="{{ screenshot.url }}" class="col-md-4 text-end">
<img src="{{ screenshot.getThumbnailURL() }}" alt="{{ screenshot.title }}" />
</a>
</div>

View File

@ -6,8 +6,8 @@
{% block content %}
{% if package.checkPerm(current_user, "ADD_SCREENSHOTS") %}
<a href="{{ package.getURL('packages.create_screenshot') }}" class="btn btn-primary float-right">
<i class="fas fa-plus mr-1"></i>
<a href="{{ package.getURL('packages.create_screenshot') }}" class="btn btn-primary float-end">
<i class="fas fa-plus me-1"></i>
{{ _("Add Image") }}
</a>
{% endif %}
@ -22,7 +22,7 @@
{% if ss.approved or package.checkPerm(current_user, "ADD_SCREENSHOTS") %}
<li class="list-group-item" data-id="{{ ss.id }}">
<div class="row">
<div class="col-auto text-muted pr-2">
<div class="col-auto text-muted pe-2">
<i class="fas fa-bars"></i>
</div>
<div class="col-auto">
@ -35,32 +35,32 @@
{{ ss.width }} x {{ ss.height }}
{% if ss.is_low_res() %}
{% if ss.is_very_small() %}
<span class="badge badge-danger ml-3">
<span class="badge bg-danger ms-3">
{{ _("Way too small") }}
</span>
{% elif ss.is_too_small() %}
<span class="badge badge-warning ml-3">
<span class="badge bg-warning ms-3">
{{ _("Too small") }}
</span>
{% else %}
<span class="badge badge-secondary ml-3">
<span class="badge bg-secondary ms-3">
{{ _("Not HD") }}
</span>
{% endif %}
{% endif %}
{% if not ss.approved %}
<span class="ml-3">
<span class="ms-3">
{{ _("Awaiting approval") }}
</span>
{% endif %}
</div>
</div>
<form action="{{ ss.getDeleteURL() }}" method="POST" class="col-auto text-right" role="form">
<form action="{{ ss.getDeleteURL() }}" method="POST" class="col-auto text-end" role="form">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<a class="btn btn-sm btn-primary" href="{{ ss.getEditURL() }}">
<i class="fas fa-pen"></i>
</a>
<button type="submit" class="btn btn-sm btn-danger ml-2">
<button type="submit" class="btn btn-sm btn-danger ms-2">
<i class="fas fa-trash"></i>
</button>
</form>

View File

@ -5,7 +5,7 @@
{% endblock %}
{% block content %}
<a class="btn btn-secondary float-right" href="/help/update_config/">{{ _("Help") }}</a>
<a class="btn btn-secondary float-end" href="/help/update_config/">{{ _("Help") }}</a>
<h1>{{ _("Configure Git Update Detection") }}</h1>
<p>
@ -46,7 +46,7 @@
<p class="mt-5 pt-4">
{{ render_submit_field(form.submit) }}
{{ render_submit_field(form.disable, class_="btn btn-secondary ml-2") }}
{{ render_submit_field(form.disable, class_="btn btn-secondary ms-2") }}
</p>
</form>
{% endblock %}

View File

@ -31,7 +31,7 @@
{% block download_btn %}
{% if release %}
<a class="btn btn-block btn-download" rel="nofollow" download="{{ release.getDownloadFileName() }}"
<a class="btn w-100 btn-download btn-lg" rel="nofollow" download="{{ release.getDownloadFileName() }}"
href="{{ package.getURL('packages.download') }}">
<div>
{{ _("Download") }}
@ -63,7 +63,7 @@
<p class="text-center mt-1 mb-4">
<a href="{{ installing_url }}">
<small>
<i class="fas fa-question-circle mr-1"></i>
<i class="fas fa-question-circle me-1"></i>
{{ _("How do I install this?") }}
</small>
</a>
@ -91,22 +91,22 @@
background-repeat: no-repeat;
background-position: center;">
<div class="container">
<div class="btn-group float-right mb-4">
<div class="btn-group float-end mb-4">
{% if package.checkPerm(current_user, "EDIT_PACKAGE") %}
<a class="btn btn-primary" href="{{ package.getURL('packages.create_edit') }}">
<i class="fas fa-pen mr-1"></i>
<i class="fas fa-pen me-1"></i>
{{ _("Edit") }}
</a>
{% endif %}
{% if package.checkPerm(current_user, "MAKE_RELEASE") %}
<a class="btn btn-primary" href="{{ package.getURL('packages.create_release') }}">
<i class="fas fa-plus mr-1"></i>
<i class="fas fa-plus me-1"></i>
{{ _("Release") }}
</a>
{% endif %}
{% if package.checkPerm(current_user, "DELETE_PACKAGE") or package.checkPerm(current_user, "UNAPPROVE_PACKAGE") %}
<a class="btn btn-danger" href="{{ package.getURL('packages.remove') }}">
<i class="fas fa-trash mr-1"></i>
<i class="fas fa-trash me-1"></i>
{{ _("Remove") }}
</a>
{% endif %}
@ -122,32 +122,32 @@
<p>
{% if package.dev_state.name == "LOOKING_FOR_MAINTAINER" or package.dev_state.name == "DEPRECATED" %}
<span class="badge badge-warning" title="{{ package.dev_state.get_desc() }}">
<span class="badge bg-warning" title="{{ package.dev_state.get_desc() }}">
<i class="fas fa-exclamation-circle" style="margin-right: 0.3em;"></i>
{{ package.dev_state.value }}
</span>
{% endif %}
{% if package_warning %}
<a class="badge badge-danger" href="/help/non_free/">
<a class="badge bg-danger" href="/help/non_free/">
<i class="fas fa-exclamation-circle" style="margin-right: 0.3em;"></i>
{{ package_warning }}
</a>
{% endif %}
{% for warning in package.content_warnings %}
<a class="badge badge-warning" rel="nofollow" href="/help/content_flags/"
<a class="badge bg-warning" rel="nofollow" href="/help/content_flags/"
title="{{ warning.description }}">
<i class="fas fa-exclamation-circle" style="margin-right: 0.3em;"></i>
{{ warning.title }}
</a>
{% endfor %}
{% if package.dev_state.name == "WIP" %}
<span class="badge badge-info" title="{{ package.dev_state.get_desc() }}">
<span class="badge bg-info" title="{{ package.dev_state.get_desc() }}">
<i class="fas fa-tools" style="margin-right: 0.3em;"></i>
{{ _("Work in Progress") }}
</span>
{% endif %}
{% for t in package.tags %}
<a class="badge badge-primary" rel="nofollow"
<a class="badge bg-primary" rel="nofollow"
title="{{ t.description or '' }}"
href="{{ url_for('packages.list_all', tag=t.name) }}">
{{ t.title }}
@ -242,8 +242,8 @@
{% set screenshots = package.screenshots.all() %}
{% if package.checkPerm(current_user, "ADD_SCREENSHOTS") %}
<a href="{{ package.getURL('packages.screenshots') }}" class="btn btn-primary float-right">
<i class="fas fa-images mr-1"></i>
<a href="{{ package.getURL('packages.screenshots') }}" class="btn btn-primary float-end">
<i class="fas fa-images me-1"></i>
{{ _("Edit") }}
</a>
{% endif %}
@ -268,7 +268,7 @@
<a href="{{ ss.url }}" class="gallery-image">
<img src="{{ ss.getThumbnailURL() }}" alt="{{ ss.title }}" />
{% if not ss.approved %}
<span class="badge bg-dark badge-tr">{{ _("Awaiting review") }}</span>
<span class="badge bg-dark badge-topright">{{ _("Awaiting review") }}</span>
{% endif %}
</a>
</li>
@ -322,13 +322,6 @@
{% 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">
@ -346,16 +339,16 @@
{{ config.get_message() }}
</p>
<p class="mt-0 my-1" style="font-size: 80%; opacity: 85%;">
<i class="fas fa-lock mr-1"></i>
<i class="fas fa-lock me-1"></i>
{{ _("Only visible to the author and Editors.") }}
</p>
<div class="btn-group btn-group-sm my-0">
<a class="btn btn-warning" href="{{ config.get_create_release_url() }}">
<i class="fas fa-plus mr-1"></i>
<i class="fas fa-plus me-1"></i>
{{ _("Release") }}
</a>
<a class="btn btn-warning" href="{{ package.getURL("packages.update_config") }}">
<i class="fas fa-cog mr-1"></i>
<i class="fas fa-cog me-1"></i>
{{ _("Update settings") }}
</a>
</div>
@ -363,7 +356,7 @@
{% endif %}
{% if package_warning %}
<p class="alert alert-danger">
<a href="/help/non_free/" class="float-right">Info</a>
<a href="/help/non_free/" class="float-end">Info</a>
<b>{{ _("Warning") }}:</b> {{ package_warning }}
</p>
{% endif %}
@ -372,18 +365,12 @@
<div class="alert alert-secondary mb-4">
<p>{{ _("Like this package? Help support its development by making a donation", display_name=package.author.display_name) }}</p>
<a class="btn btn-block btn-primary" href="{{ package.author.donate_url }}" rel="nofollow">
<i class="fas fa-heart mr-2"></i>
<i class="fas fa-heart me-2"></i>
{{ _("Donate now") }}
</a>
</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>
@ -391,13 +378,13 @@
<dd>
{% for dep in package.getSortedHardDependencies() %}
{%- if dep.package %}
<a class="badge badge-primary"
<a class="badge bg-primary"
href="{{ dep.package.getURL("packages.view") }}">
{{ _("%(title)s by %(display_name)s",
title=dep.package.title, display_name=dep.package.author.display_name) }}
</a>
{% elif dep.meta_package %}
<a class="badge badge-primary"
<a class="badge bg-primary"
href="{{ url_for('metapackages.view', name=dep.meta_package.name) }}">
{{ dep.meta_package.name }}
</a>
@ -415,12 +402,12 @@
<dd>
{% for dep in optional_deps %}
{%- if dep.package %}
<a class="badge badge-secondary"
<a class="badge bg-secondary"
href="{{ dep.package.getURL("packages.view") }}">
{{ _("%(title)s by %(display_name)s",
title=dep.package.title, display_name=dep.package.author.display_name) }}
{% elif dep.meta_package %}
<a class="badge badge-secondary"
<a class="badge bg-secondary"
href="{{ url_for('metapackages.view', name=dep.meta_package.name) }}">
{{ dep.meta_package.name }}
{% else %}
@ -432,23 +419,6 @@
</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>
@ -480,7 +450,7 @@
<dt>{{ _("Maintainers") }}</dt>
<dd>
{% for user in package.maintainers %}
<a class="badge badge-secondary"
<a class="badge bg-secondary"
href="{{ url_for('users.profile', username=user.username) }}">
{{ user.display_name }}
</a>
@ -496,7 +466,7 @@
{% if package.provides %}
<dt>{{ _("Provides") }}</dt>
<dd>{% for meta in package.provides %}
<a class="badge badge-secondary"
<a class="badge bg-secondary"
href="{{ url_for('metapackages.view', name=meta.name) }}">{{ meta.name }}</a>
{% endfor %}</dd>
{% endif %}
@ -504,7 +474,7 @@
<h3>
{% if package.checkPerm(current_user, "MAKE_RELEASE") %}
<a class="btn btn-primary btn-sm float-right" href="{{ package.getURL("packages.create_release") }}"><i class="fas fa-plus"></i></a>
<a class="btn btn-primary btn-sm float-end" href="{{ package.getURL("packages.create_release") }}"><i class="fas fa-plus"></i></a>
{% endif %}
{{ _("Releases") }}
</h3>
@ -522,7 +492,7 @@
<h3>
{% if package.approved and package.checkPerm(current_user, "CREATE_THREAD") %}
<div class="btn-group float-right">
<div class="btn-group float-end">
<a class="btn btn-primary btn-sm mx-1" href="{{ url_for('threads.new', pid=package.id) }}"><i class="fas fa-plus"></i></a>
</div>
{% endif %}
@ -536,7 +506,7 @@
<p class="mt-3">
{% if package.approved and current_user != package.author %}
<a href="{{ url_for('report.report', url=url_current()) }}">
<i class="fas fa-flag mr-1"></i>
<i class="fas fa-flag me-1"></i>
{{ _("Report") }}
</a>
{% endif %}

View File

@ -15,7 +15,7 @@
<div class="card-body">
<p>{{ _("Deleting is permanent") }}</p>
<a class="btn btn-secondary mr-3" href="{{ thread.getViewURL() }}">{{ _("Cancel") }}</a>
<a class="btn btn-secondary me-3" href="{{ thread.getViewURL() }}">{{ _("Cancel") }}</a>
<input type="submit" value="{{ _('Delete') }}" class="btn btn-danger" />
</div>
</form>

View File

@ -15,7 +15,7 @@
<div class="card-body">
<p>{{ _("Deleting is permanent") }}</p>
<a class="btn btn-secondary mr-3" href="{{ thread.getViewURL() }}">{{ _("Cancel") }}</a>
<a class="btn btn-secondary me-3" href="{{ thread.getViewURL() }}">{{ _("Cancel") }}</a>
<input type="submit" value="{{ _('Delete') }}" class="btn btn-danger" />
</div>
</form>

View File

@ -21,33 +21,33 @@
<img class="img-fluid user-photo img-thumbnail img-thumbnail-1" src="{{ r.author.getProfilePicURL() }}">
</a>
</div>
<div class="col pr-0">
<div class="col pe-0">
<div class="card">
<div class="card-header">
<a class="author {{ r.author.rank.name }} mr-3"
<a class="author {{ r.author.rank.name }} me-3"
href="{{ url_for('users.profile', username=r.author.username) }}">
{{ r.author.display_name }}
</a>
{% if r.author.username != r.author.display_name %}
<span class="text-muted small mr-2">
<span class="text-muted small me-2">
({{ r.author.username }})
</span>
{% endif %}
{% if r == r.thread.replies[0] %}
<a class="badge badge-primary" href="{{ r.thread.getViewURL() }}">
<a class="badge bg-primary" href="{{ r.thread.getViewURL() }}">
{{ r.thread.title }}
</a>
{% else %}
<i class="fas fa-reply mr-2"></i>
<a class="badge badge-dark" href="{{ r.thread.getViewURL() }}">
<i class="fas fa-reply me-2"></i>
<a class="badge bg-dark" href="{{ r.thread.getViewURL() }}">
{{ _("Reply to <b>%(title)s</b>", title=r.thread.title) }}
</a>
{% endif %}
<a name="reply-{{ r.id }}" class="text-muted float-right"
<a name="reply-{{ r.id }}" class="text-muted float-end"
href="{{ url_for('threads.view', id=r.thread.id) }}#reply-{{ r.id }}">
{{ r.created_at | datetime }}
</a>

View File

@ -26,33 +26,27 @@
{% block content %}
{% if current_user.is_authenticated %}
{% if current_user in thread.watchers %}
<form method="post" action="{{ thread.getUnsubscribeURL() }}" class="float-right">
<form method="post" action="{{ thread.getUnsubscribeURL() }}" class="float-end">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="submit" class="btn btn-primary" value="{{ _('Unsubscribe') }}" />
</form>
{% else %}
<form method="post" action="{{ thread.getSubscribeURL() }}" class="float-right">
<form method="post" action="{{ thread.getSubscribeURL() }}" class="float-end">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="submit" class="btn btn-primary" value="{{ _('Subscribe') }}" />
</form>
{% endif %}
{% 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>
{% if thread and thread.checkPerm(current_user, "DELETE_THREAD") %}
<a href="{{ url_for('threads.delete_thread', id=thread.id) }}" class="float-end me-2 btn btn-danger">{{ _('Delete') }}</a>
{% endif %}
{% 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 and 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">
<form method="post" action="{{ url_for('threads.set_lock', id=thread.id, lock=0) }}" class="float-end me-2">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="submit" class="btn btn-secondary" value="{{ _('Unlock') }}" />
</form>
{% else %}
<form method="post" action="{{ url_for('threads.set_lock', id=thread.id, lock=1) }}" class="float-right mr-2">
<form method="post" action="{{ url_for('threads.set_lock', id=thread.id, lock=1) }}" class="float-end me-2">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="submit" class="btn btn-secondary" value="{{ _('Lock') }}" />
</form>
@ -61,7 +55,7 @@
{% endif %}
{% if current_user == thread.author and thread.review %}
<a class="btn btn-primary ml-1 float-right mr-2"
<a class="btn btn-primary ms-1 float-end me-2"
href="{{ thread.review.package.getURL("packages.review") }}">
<i class="fas fa-pen"></i>
{{ _("Edit Review") }}
@ -71,9 +65,9 @@
<h1>
{% if thread.review %}
{% if thread.review.recommends %}
<i class="fas fa-thumbs-up mr-2" style="color:#6f6;"></i>
<i class="fas fa-thumbs-up me-2" style="color:#6f6;"></i>
{% else %}
<i class="fas fa-thumbs-down mr-2" style="color:#f66;"></i>
<i class="fas fa-thumbs-down me-2" style="color:#f66;"></i>
{% endif %}
{% endif %}
{% if thread.private %}&#x1f512; {% endif %}{{ thread.title }}

View File

@ -9,7 +9,7 @@
{% if canApproveScn and screenshots %}
<div class="card my-4">
<h3 class="card-header">{{ _("Screenshots") }}
<form class="float-right" method="post" action="{{ url_for('todo.view_editor') }}">
<form class="float-end" method="post" action="{{ url_for('todo.view_editor') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="hidden" name="action" value="screenshots_approve_all" />
<input class="btn btn-sm btn-primary" type="submit" value="{{ _('Approve All') }}" />
@ -47,16 +47,16 @@
<div class="list-group list-group-flush">
{% for p in packages %}
<a href="{{ p.getURL("packages.view") }}" class="list-group-item list-group-item-action">
<span class="float-right" title="Created {{ p.created_at | datetime }}">
<span class="float-end" title="Created {{ p.created_at | datetime }}">
<small>
{{ p.created_at | timedelta }} ago
</small>
</span>
{% if "Other" in p.license.name or "Other" in p.media_license.name %}
<span class="mr-2 badge badge-info">License</span>
<span class="me-2 badge bg-info">License</span>
{% else %}
<span class="mr-2 badge badge-success">Ready</span>
<span class="me-2 badge bg-success">Ready</span>
{% endif %}
{{ p.title }} by {{ p.author.display_name }}
@ -77,7 +77,7 @@
{% for r in releases %}
<li class="list-group-item">
{% if r.task_id %}
<span class="mr-2 badge badge-warning">{{ _("Importing") }}</span>
<span class="me-2 badge bg-warning">{{ _("Importing") }}</span>
{% endif %}
<a href="{{ r.getEditURL() }}">{{ r.title }}</a>
on
@ -108,13 +108,13 @@
<div class="list-group list-group-flush">
{% for p in license_needed %}
<a href="{{ p.getURL("packages.view") }}" class="list-group-item list-group-item-action">
<span class="float-right" title="Created {{ p.created_at | datetime }}">
<span class="float-end" title="Created {{ p.created_at | datetime }}">
<small>
{{ p.created_at | timedelta }} ago
</small>
</span>
<span class="mr-2 badge badge-{{ p.state.color }}">{{ p.state.value }}</span>
<span class="me-2 badge bg-{{ p.state.color }}">{{ p.state.value }}</span>
{{ p.title }} by {{ p.author.display_name }}
</a>
@ -142,7 +142,7 @@
{% if unfulfilled_meta_packages %}
<h2 class="mt-5">
<span class="fas fa-exclamation-triangle pr-2" style="color: orange;"></span>
<span class="fas fa-exclamation-triangle pe-2" style="color: orange;"></span>
{{ unfulfilled_meta_packages }}
{{ _("Unfulfilled Dependencies") }}
</h2>
@ -165,16 +165,16 @@
<div class="list-group list-group-flush" style="max-height: 300px; overflow: hidden auto;">
{% for p in wip_packages %}
<a href="{{ p.getURL("packages.view") }}" class="list-group-item list-group-item-action">
<span class="float-right" title="Created {{ p.created_at | datetime }}">
<span class="float-end" title="Created {{ p.created_at | datetime }}">
<small>
{{ p.created_at | timedelta }} ago
</small>
</span>
{% if p.state == p.state.WIP %}
<span class="mr-2 badge badge-warning">{{ _("WIP") }}</span>
<span class="me-2 badge bg-warning">{{ _("WIP") }}</span>
{% else %}
<span class="mr-2 badge badge-danger">{{ p.state.value }}</span>
<span class="me-2 badge bg-danger">{{ p.state.value }}</span>
{% endif %}
{{ p.title }} by {{ p.author.display_name }}

View File

@ -5,8 +5,8 @@
{% endblock %}
{% block content %}
<div class="btn-toolbar float-right">
<div class="btn-group btn-group-sm mr-2">
<div class="btn-toolbar float-end">
<div class="btn-group btn-group-sm me-2">
{% if is_mtm_only %}
<a class="btn btn-sm btn-primary active" href="{{ url_set_query(mtm=0) }}">
{{ _("Minetest-Mods org only") }}

View File

@ -11,13 +11,11 @@
<label for="q" class="sr-only">{{ _('Search all packages') }}</label>
<div class="input-group">
<input class="form-control" id="q" name="q" type="text" placeholder="{{ _('Search all packages') }}">
<div class="input-group-append">
<input class="btn btn-primary" type="submit" value="{{ _('Search') }}">
</div>
<input class="btn btn-primary" type="submit" value="{{ _('Search') }}">
</div>
</form>
</div>
<div class="col-md-6 text-right">
<div class="col-md-6 text-end">
{% if only_no_tags %}
<a class="btn btn-primary" href="{{ url_set_query(no_tags=0) }}">
{{ _("Missing tags only") }}
@ -28,7 +26,7 @@
</a>
{% endif %}
{% if check_global_perm(current_user, "EDIT_TAGS") %}
<a class="btn btn-secondary ml-2" href="{{ url_for('admin.tag_list') }}">{{ _("Edit Tags") }}</a>
<a class="btn btn-secondary ms-2" href="{{ url_for('admin.tag_list') }}">{{ _("Edit Tags") }}</a>
{% endif %}
</div>
</div>
@ -57,12 +55,12 @@
</td>
<td class="tags">
{% for tag in package.tags %}
<a class="badge badge-primary mr-1"
<a class="badge bg-primary me-1"
href="{{ url_set_query(_add={ 'tag': tag.name }) }}">
{{ tag.title }}
</a>
{% endfor %}
<!-- <a class="badge badge-secondary add-btn px-2" href="#">
<!-- <a class="badge bg-secondary add-btn px-2" href="#">
<i class="fas fa-plus"></i>
</a> -->
</td>

View File

@ -5,8 +5,8 @@ Topics to be Added
{% endblock %}
{% block content %}
<div class="float-right">
<div class="btn-group btn-group-sm mr-2">
<div class="float-end">
<div class="btn-group btn-group-sm me-2">
<a class="btn btn-secondary {% if sort_by=='date' %}active{% endif %}"
href="{{ url_for('todo.topics', q=query, show_discarded=show_discarded, n=n, sort='date') }}">
{{ _("Sort by date") }}
@ -69,7 +69,7 @@ Topics to be Added
<input type="hidden" name="n" value={{ n }} />
<input type="hidden" name="sort" value={{ sort_by or "date" }} />
<input name="q" type="text" placeholder="Search topics" value="{{ query or ''}}">
<input class="btn btn-secondary my-2 my-sm-0 mr-sm-2" type="submit" value="Search" />
<input class="btn btn-secondary my-2 my-sm-0 me-sm-2" type="submit" value="Search" />
</form>
{% from "macros/topics.html" import render_topics_table %}

View File

@ -26,7 +26,7 @@
style="max-height: 22px; max-width: 22px;"
src="{{ package.getThumbnailOrPlaceholder() }}" />
<span class="pl-2">
<span class="ps-2">
{{ package.title }}
</span>
</div>
@ -41,14 +41,14 @@
{% endfor %}
</div>
<a class="btn btn-secondary float-right" href="/help/update_config/">
<a class="btn btn-secondary float-end" href="/help/update_config/">
{{ _("Help") }}
</a>
<a class="btn btn-secondary float-right mr-2" href="{{ url_for('packages.bulk_update_config', username=user.username) }}">
<a class="btn btn-secondary float-end me-2" href="{{ url_for('packages.bulk_update_config', username=user.username) }}">
{{ _("See all Update Settings") }}
</a>
{% if outdated_packages %}
<form class="float-right mr-2" method="post" action="{{ url_for('todo.apply_all_updates', username=user.username) }}">
<form class="float-end me-2" method="post" action="{{ url_for('todo.apply_all_updates', username=user.username) }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input class="btn btn-primary" type="submit" value="{{ _("Create All Releases") }}" />
</form>
@ -73,13 +73,13 @@
{{ _("The recommended resolution is 1920x1080, and screenshots must be at least %(width)dx%(height)d.",
width=920, height=517) }}
<span class="badge badge-danger ml-3">
<span class="badge bg-danger ms-3">
{{ _("Way too small") }}
</span>
<span class="badge badge-warning">
<span class="badge bg-warning">
{{ _("Too small") }}
</span>
<span class="badge badge-secondary">
<span class="badge bg-secondary">
{{ _("Not HD") }}
</span>
</p>
@ -94,7 +94,7 @@
style="max-height: 22px; max-width: 22px;"
src="{{ package.getThumbnailOrPlaceholder() }}" />
<span class="pl-2">
<span class="ps-2">
{{ package.title }}
</span>
</div>
@ -103,13 +103,13 @@
{% for ss in package.screenshots %}
{% if ss.is_low_res() %}
{% if ss.is_very_small() %}
{% set badge_color = "badge-danger" %}
{% set badge_color = "bg-danger" %}
{% elif ss.is_too_small() %}
{% set badge_color = "badge-warning" %}
{% set badge_color = "bg-warning" %}
{% else %}
{% set badge_color = "badge-secondary" %}
{% set badge_color = "bg-secondary" %}
{% endif %}
<span class="badge {{ badge_color }} ml-2" title="{{ ss.title }}">
<span class="badge {{ badge_color }} ms-2" title="{{ ss.title }}">
{{ ss.width }} x {{ ss.height }}
</span>
{% endif %}
@ -123,7 +123,7 @@
</div>
<a class="btn btn-secondary float-right" href="{{ url_for('todo.tags', author=user.username) }}">
<a class="btn btn-secondary float-end" href="{{ url_for('todo.tags', author=user.username) }}">
{{_ ("See All") }}</a>
<h2>{{ _("Packages Without Tags") }}</h2>
<p>
@ -137,7 +137,7 @@
style="max-height: 22px; max-width: 22px;"
src="{{ package.getThumbnailOrPlaceholder() }}" />
<span class="pl-2">
<span class="ps-2">
{{ package.title }}
</span>
</a>

View File

@ -53,7 +53,7 @@
</a>
{% if user == current_user %}
<a class="btn btn-secondary ml-2" href="{{ url_for('github.view_permissions') }}">
<a class="btn btn-secondary ms-2" href="{{ url_for('github.view_permissions') }}">
{{ _("View ContentDB's GitHub Permissions") }}
</a>
{% endif %}
@ -69,7 +69,7 @@
</table>
{% if current_user.rank.atLeast(current_user.rank.MODERATOR) %}
<a class="btn btn-secondary float-right" href="{{ url_for('admin.audit', username=user.username) }}">
<a class="btn btn-secondary float-end" href="{{ url_for('admin.audit', username=user.username) }}">
{{ _("View All") }}
</a>
{% endif %}

View File

@ -18,10 +18,10 @@
</p>
<p class="mt-5">
<a class="btn btn-primary mr-3" href="{{ url_for('users.claim_forums') }}">
<a class="btn btn-primary me-3" href="{{ url_for('users.claim_forums') }}">
{{ _("<b>Yes</b>, I have a forums account") }}
</a>
<a class="btn btn-primary mr-3" href="{{ url_for('users.register') }}">
<a class="btn btn-primary me-3" href="{{ url_for('users.register') }}">
{{ _("<b>No</b>, I don't have one") }}
</a>
<a class="btn btn-secondary" href="https://forum.minetest.net/ucp.php?mode=register">

View File

@ -23,7 +23,7 @@ Create Account from Forums User
<div class="col-sm-6">
<div class="card">
<div class="card-header">
<span class="badge badge-pill badge-dark mr-2">{{ _("Option 1") }}</span>
<span class="badge rounded-pill bg-dark me-2">{{ _("Option 1") }}</span>
{{ _("Use GitHub field in forum profile") }}
</div>
@ -52,7 +52,7 @@ Create Account from Forums User
<div class="col-sm-6">
<div class="card">
<div class="card-header">
<span class="badge badge-pill badge-dark mr-2">{{ _("Option 2") }}</span>
<span class="badge rounded-pill bg-dark me-2">{{ _("Option 2") }}</span>
{{ _("Verification token") }}
</div>

View File

@ -26,7 +26,7 @@
</p>
{% endif %}
<a class="btn btn-secondary mr-3" href="{{ url_for('users.account', username=user.username) }}">
<a class="btn btn-secondary me-3" href="{{ url_for('users.account', username=user.username) }}">
{{ _("Cancel") }}
</a>
<input type="submit"
@ -37,7 +37,7 @@
{% endif %}
class="btn btn-danger" />
{% if not can_delete and current_user.rank.atLeast(current_user.rank.ADMIN) %}
<input type="submit" name="delete" value="{{ _('Delete Anyway') }}" class="btn btn-danger ml-3" />
<input type="submit" name="delete" value="{{ _('Delete Anyway') }}" class="btn btn-danger ms-3" />
{% endif %}
</div>
</form>

View File

@ -33,17 +33,17 @@
<div class="col-sm-2 {{ user.rank }}"
title="{{ _('Rank: %(rank)s.', rank=user.rank.getTitle()) }}">
{% if user.rank == user.rank.ADMIN %}
<i class="fas fa-user-cog mr-2"></i>
<i class="fas fa-user-cog me-2"></i>
{% elif user.rank == user.rank.MODERATOR %}
<i class="fas fa-user-shield mr-2"></i>
<i class="fas fa-user-shield me-2"></i>
{% elif user.rank == user.rank.EDITOR %}
<i class="fas fa-user-edit mr-2"></i>
<i class="fas fa-user-edit me-2"></i>
{% elif user.rank == user.rank.APPROVER %}
<i class="fas fa-user-check mr-2"></i>
<i class="fas fa-user-check me-2"></i>
{% elif user.rank == user.rank.BOT %}
<i class="fas fa-robot mr-2"></i>
<i class="fas fa-robot me-2"></i>
{% else %}
<i class="fas fa-user mr-2"></i>
<i class="fas fa-user me-2"></i>
{% endif %}
{{ user.rank.getTitle() }}
@ -52,7 +52,7 @@
<span class="col-sm {{ user.rank }}">
{{ user.display_name }}
{% if user.username != user.display_name %}
<span class="text-muted small ml-2">
<span class="text-muted small ms-2">
({{ user.username }})
</span>
{% endif %}

View File

@ -10,7 +10,7 @@
<form class="signin" method="POST">
{{ form.hidden_tag() }}
<h1 class="h3 mb-4 font-weight-normal">{{ self.title() }}</h1>
<h1 class="h3 mb-4 fw-normal">{{ self.title() }}</h1>
{{ render_field(form.username, tabindex=110, label_visible=False, placeholder=_("Username or email")) }}
{{ render_field(form.password, tabindex=120, label_visible=False, placeholder=_("Password")) }}
@ -25,12 +25,12 @@
<hr class="my-5" />
<p>
<a class="btn btn-secondary mr-3" href="{{ url_for('github.start') }}">
<i class="fab fa-github mr-1"></i>
<a class="btn btn-secondary me-3" href="{{ url_for('github.start') }}">
<i class="fab fa-github me-1"></i>
{{ _("GitHub") }}
</a>
<a class="btn btn-secondary" href="{{ url_for('users.claim') }}">
<i class="fas fa-user-plus mr-1"></i>
<i class="fas fa-user-plus me-1"></i>
{{ _("Register") }}
</a>
</p>

View File

@ -12,41 +12,41 @@
</div>
<div class="col">
{% if user.can_see_edit_profile(current_user) %}
<a class="btn btn-primary float-right" href="{{ url_for('users.profile_edit', username=user.username) }}">
<i class="fas fa-pen mr-1"></i>
<a class="btn btn-primary float-end" href="{{ url_for('users.profile_edit', username=user.username) }}">
<i class="fas fa-pen me-1"></i>
{{ _("Edit Profile") }}
</a>
<a class="btn btn-secondary float-right mr-3" href="{{ url_for('todo.view_user', username=user.username) }}">
<i class="fas fa-tasks mr-1"></i>
<a class="btn btn-secondary float-end me-3" href="{{ url_for('todo.view_user', username=user.username) }}">
<i class="fas fa-tasks me-1"></i>
{{ _("To Do List") }}
</a>
{% endif %}
<a class="btn btn-secondary float-right mr-3" href="{{ url_for('report.report', url=url_current()) }}">
<i class="fas fa-flag mr-1"></i>
<a class="btn btn-secondary float-end me-3" href="{{ url_for('report.report', url=url_current()) }}">
<i class="fas fa-flag me-1"></i>
{{ _("Report") }}
</a>
{% if current_user.is_authenticated and current_user.rank.atLeast(current_user.rank.MODERATOR) %}
{% if not user.rank.atLeast(current_user.rank) %}
<a class="btn btn-secondary float-right mr-3" href="{{ url_for('users.modtools', username=user.username) }}">
<i class="fas fa-user-shield mr-1"></i>
<a class="btn btn-secondary float-end me-3" href="{{ url_for('users.modtools', username=user.username) }}">
<i class="fas fa-user-shield me-1"></i>
{{ _("Moderator Tools") }}
</a>
{% endif %}
{% if user.email %}
<a class="btn btn-secondary float-right mr-3" href="{{ url_for('admin.send_single_email', username=user.username) }}">
<i class="fas fa-envelope mr-1"></i>
<a class="btn btn-secondary float-end me-3" href="{{ url_for('admin.send_single_email', username=user.username) }}">
<i class="fas fa-envelope me-1"></i>
{{ _("Send Email") }}
</a>
{% endif %}
{% endif %}
<h1 class="ml-3 my-0 {{ user.rank.name }}">
<h1 class="ms-3 my-0 {{ user.rank.name }}">
{{ user.display_name }}
{% if user.username != user.display_name %}
<span class="text-muted small ml-2">
<span class="text-muted small ms-2">
({{ user.username }})
</span>
{% endif %}
@ -129,7 +129,7 @@
{% if not current_user.is_authenticated and user.rank == user.rank.NOT_JOINED and user.forums_username %}
<div class="alert alert-secondary mb-5">
<a class="float-right btn btn-default btn-sm"
<a class="float-end btn btn-default btn-sm"
href="{{ url_for('users.claim_forums', username=user.forums_username) }}">{{ _("Claim") }}</a>
{{ _("Is this you? Claim your account now!") }}
@ -139,10 +139,10 @@
{% for medal in medals_unlocked %}
<div class="col-md-4">
<div class="card h-100">
<div class="card-body media align-items-center">
<i class="fas {{ medal.icon }} ml-2 mr-4 text-size"
<div class="card-body d-flex align-items-center">
<i class="flex-shrink-0 fas {{ medal.icon }} ms-2 me-4 text-size"
style="font-size: 45px; color: {{ medal.color }};"></i>
<div class="media-body">
<div class="flex-grow-1 ms-3">
<h5 class="mt-0">
{{ medal.title }}
</h5>
@ -176,14 +176,14 @@
{% endif %}
{% if current_user == user or user.checkPerm(current_user, "CHANGE_AUTHOR") %}
<a class="float-right btn btn-sm btn-primary"
<a class="float-end btn btn-sm btn-primary"
href="{{ url_for('packages.create_edit', author=user.username) }}">
<i class="fas fa-plus mr-1"></i>
<i class="fas fa-plus me-1"></i>
{{ _("Create package") }}
</a>
{% endif %}
{% if current_user == user or (current_user.is_authenticated and current_user.rank.atLeast(current_user.rank.EDITOR)) %}
<a class="float-right btn btn-sm btn-secondary mr-2"
<a class="float-end btn btn-sm btn-secondary me-2"
href="{{ url_for('todo.tags', author=user.username) }}">
{{ _("View list of tags") }}
</a>

View File

@ -7,7 +7,7 @@
{% block content %}
{% from "macros/forms.html" import render_field, render_checkbox_field, render_submit_field %}
<div class="card w-50 text-left" style="margin: 2em auto;">
<div class="card w-50 text-start" style="margin: 2em auto;">
<h2 class="card-header">{{ self.title() }}</h2>
<form action="" method="POST" class="form card-body" role="form">

View File

@ -9,7 +9,7 @@
<span class="col-auto m-0 p-0">
<img class="img-fluid user-photo img-thumbnail img-thumbnail-1" src="{{ user.getProfilePicURL() }}" alt="Profile picture" style="max-height: 20px;">
</span>
<span class="col m-0 p-0 pl-2">
<span class="col m-0 p-0 ps-2">
{{ user.display_name }}
</span>
</span>

View File

@ -53,7 +53,7 @@
<div class="button-group mt-4">
{% if user %}
<a class="btn btn-primary mr-3" href="{{ url_for('users.email_notifications', username=user.username) }}">
<a class="btn btn-primary me-3" href="{{ url_for('users.email_notifications', username=user.username) }}">
{{ _("Edit Notification Preferences") }}
</a>
{% endif %}

View File

@ -18,7 +18,8 @@
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):
@ -39,14 +40,15 @@ def is_package_page(f):
if not ("author" in kwargs and "name" in kwargs):
abort(400)
author = kwargs["author"]
name = kwargs["name"]
author = kwargs.pop("author")
name = kwargs.pop("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))
@ -59,8 +61,6 @@ 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 --concurrency 1
command: celery -A app.tasks.celery worker
env_file:
- config.env
environment:

View File

@ -1,105 +0,0 @@
# 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,5 +54,3 @@ 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

@ -1,34 +0,0 @@
"""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')

Some files were not shown because too many files have changed in this diff Show More