Compare commits

..

13 Commits

Author SHA1 Message Date
rubenwardy
39a09c5d92 Add ability to search by tag 2020-04-07 18:23:06 +01:00
rubenwardy
663a9ba07b Fix exposing abs_url_for to templates 2020-03-28 19:01:39 +00:00
rubenwardy
144ae69f5c Fix case-insensitive comparison bug 2020-03-28 18:15:15 +00:00
rubenwardy
3e07bed51b Add ability to search packages by author 2020-03-28 18:13:03 +00:00
rubenwardy
9de219fd80 Increase package name and title length limits in form validation 2020-03-27 15:30:08 +00:00
rubenwardy
4a25435f7a Fix release validation for repos with submodules 2020-03-27 15:23:18 +00:00
rubenwardy
b0f32affcb Fix scores not degrading due to missing session.commit() 2020-03-22 19:47:52 +00:00
rubenwardy
99548ea65f Fix licenses being prefilled in package editor 2020-02-23 20:40:14 +00:00
rubenwardy
325ee02b49 Fix lack of download counter checks on non-release package download 2020-02-23 20:14:56 +00:00
rubenwardy
a60786d32c Fix non-admin users not being able to set profile URLs 2020-02-23 20:12:32 +00:00
rubenwardy
2976afd5d1 Update git-archive-all 2020-02-15 15:23:43 +00:00
rubenwardy
744c52ba18 Add links to GitHub oauth connection settings 2020-01-30 21:39:51 +00:00
rubenwardy
c31c1fd92a Change API Token warning to be friendlier 2020-01-30 21:01:50 +00:00
18 changed files with 119 additions and 34 deletions

View File

@@ -60,7 +60,6 @@ if not app.debug and app.config["MAIL_UTILS_ERROR_SEND_TO"]:
from .markdown import init_app from .markdown import init_app
init_app(app) init_app(app)
# @babel.localeselector # @babel.localeselector
# def get_locale(): # def get_locale():
# return request.accept_languages.best_match(app.config['LANGUAGES'].keys()) # return request.accept_languages.best_match(app.config['LANGUAGES'].keys())

View File

@@ -18,7 +18,7 @@ from flask import Blueprint
bp = Blueprint("github", __name__) bp = Blueprint("github", __name__)
from flask import redirect, url_for, request, flash, abort, render_template, jsonify from flask import redirect, url_for, request, flash, abort, render_template, jsonify, current_app
from flask_user import current_user, login_required from flask_user import current_user, login_required
from sqlalchemy import func from sqlalchemy import func
from flask_github import GitHub from flask_github import GitHub
@@ -35,6 +35,12 @@ from wtforms import SelectField, SubmitField
def start(): def start():
return github.authorize("", redirect_uri=abs_url_for("github.callback")) return github.authorize("", redirect_uri=abs_url_for("github.callback"))
@bp.route("/github/view/")
def view_permissions():
url = "https://github.com/settings/connections/applications/" + \
current_app.config["GITHUB_CLIENT_ID"]
return redirect(url)
@bp.route("/github/callback/") @bp.route("/github/callback/")
@github.authorized_handler @github.authorized_handler
def callback(oauth_token): def callback(oauth_token):

View File

@@ -30,7 +30,7 @@ from flask_wtf import FlaskForm
from wtforms import * from wtforms import *
from wtforms.validators import * from wtforms.validators import *
from wtforms.ext.sqlalchemy.fields import QuerySelectField, QuerySelectMultipleField from wtforms.ext.sqlalchemy.fields import QuerySelectField, QuerySelectMultipleField
from sqlalchemy import or_ from sqlalchemy import or_, func
@menu.register_menu(bp, ".mods", "Mods", order=11, endpoint_arguments_constructor=lambda: { 'type': 'mod' }) @menu.register_menu(bp, ".mods", "Mods", order=11, endpoint_arguments_constructor=lambda: { 'type': 'mod' })
@@ -64,6 +64,14 @@ def list_all():
prev_url = url_for("packages.list_all", type=type_name, q=search, page=query.prev_num) \ prev_url = url_for("packages.list_all", type=type_name, q=search, page=query.prev_num) \
if query.has_prev else None if query.has_prev else None
authors = []
if search:
authors = User.query \
.filter(or_(*[func.lower(User.username) == name.lower().strip() for name in search.split(" ")])) \
.all()
authors = [(author.username, search.lower().replace(author.username.lower(), "")) for author in authors]
topics = None topics = None
if qb.search and not query.has_next: if qb.search and not query.has_next:
qb.show_discarded = True qb.show_discarded = True
@@ -73,6 +81,7 @@ def list_all():
return render_template("packages/list.html", \ return render_template("packages/list.html", \
title=title, packages=query.items, topics=topics, \ title=title, packages=query.items, topics=topics, \
query=search, tags=tags, type=type_name, \ query=search, tags=tags, type=type_name, \
authors = authors, \
next_url=next_url, prev_url=prev_url, page=page, page_max=query.pages, packages_count=query.total) next_url=next_url, prev_url=prev_url, page=page, page_max=query.pages, packages_count=query.total)
@@ -164,22 +173,17 @@ def download(package):
flash("No download available.", "danger") flash("No download available.", "danger")
return redirect(package.getDetailsURL()) return redirect(package.getDetailsURL())
else: else:
PackageRelease.query.filter_by(id=release.id).update({ return redirect(release.getDownloadURL(), code=302)
"downloads": PackageRelease.downloads + 1
})
db.session.commit()
return redirect(release.url, code=302)
class PackageForm(FlaskForm): class PackageForm(FlaskForm):
name = StringField("Name (Technical)", [InputRequired(), Length(1, 20), Regexp("^[a-z0-9_]+$", 0, "Lower case letters (a-z), digits (0-9), and underscores (_) only")]) name = StringField("Name (Technical)", [InputRequired(), Length(1, 100), Regexp("^[a-z0-9_]+$", 0, "Lower case letters (a-z), digits (0-9), and underscores (_) only")])
title = StringField("Title (Human-readable)", [InputRequired(), Length(3, 50)]) title = StringField("Title (Human-readable)", [InputRequired(), Length(3, 100)])
short_desc = StringField("Short Description (Plaintext)", [InputRequired(), Length(1,200)]) short_desc = StringField("Short Description (Plaintext)", [InputRequired(), Length(1,200)])
desc = TextAreaField("Long Description (Markdown)", [Optional(), Length(0,10000)]) desc = TextAreaField("Long Description (Markdown)", [Optional(), Length(0,10000)])
type = SelectField("Type", [InputRequired()], choices=PackageType.choices(), coerce=PackageType.coerce, default=PackageType.MOD) type = SelectField("Type", [InputRequired()], choices=PackageType.choices(), coerce=PackageType.coerce, default=PackageType.MOD)
license = QuerySelectField("License", [InputRequired()], query_factory=lambda: License.query.order_by(db.asc(License.name)), get_pk=lambda a: a.id, get_label=lambda a: a.name) license = QuerySelectField("License", [DataRequired()], allow_blank=True, query_factory=lambda: License.query.order_by(db.asc(License.name)), get_pk=lambda a: a.id, get_label=lambda a: a.name)
media_license = QuerySelectField("Media License", [InputRequired()], query_factory=lambda: License.query.order_by(db.asc(License.name)), get_pk=lambda a: a.id, get_label=lambda a: a.name) media_license = QuerySelectField("Media License", [DataRequired()], allow_blank=True, query_factory=lambda: License.query.order_by(db.asc(License.name)), get_pk=lambda a: a.id, get_label=lambda a: a.name)
provides_str = StringField("Provides (mods included in package)", [Optional()]) provides_str = StringField("Provides (mods included in package)", [Optional()])
tags = QuerySelectMultipleField('Tags', query_factory=lambda: Tag.query.order_by(db.asc(Tag.name)), get_pk=lambda a: a.id, get_label=lambda a: a.title) tags = QuerySelectMultipleField('Tags', query_factory=lambda: Tag.query.order_by(db.asc(Tag.name)), get_pk=lambda a: a.id, get_label=lambda a: a.title)
harddep_str = StringField("Hard Dependencies", [Optional()]) harddep_str = StringField("Hard Dependencies", [Optional()])
@@ -227,6 +231,8 @@ def create_edit(author=None, name=None):
form.title.data = request.args.get("title") form.title.data = request.args.get("title")
form.repo.data = request.args.get("repo") form.repo.data = request.args.get("repo")
form.forums.data = request.args.get("forums") form.forums.data = request.args.get("forums")
form.license.data = None
form.media_license.data = None
else: else:
form.harddep_str.data = ",".join([str(x) for x in package.getSortedHardDependencies() ]) form.harddep_str.data = ",".join([str(x) for x in package.getSortedHardDependencies() ])
form.softdep_str.data = ",".join([str(x) for x in package.getSortedOptionalDependencies() ]) form.softdep_str.data = ",".join([str(x) for x in package.getSortedOptionalDependencies() ])

View File

@@ -63,6 +63,8 @@ def profile(username):
# Copy form fields to user_profile fields # Copy form fields to user_profile fields
if user.checkPerm(current_user, Permission.CHANGE_DNAME): if user.checkPerm(current_user, Permission.CHANGE_DNAME):
user.display_name = form["display_name"].data user.display_name = form["display_name"].data
if user.checkPerm(current_user, Permission.CHANGE_PROFILE_URLS):
user.website_url = form["website_url"].data user.website_url = form["website_url"].data
user.donate_url = form["donate_url"].data user.donate_url = form["donate_url"].data

View File

@@ -23,19 +23,20 @@ The process is as follows:
## Setting up ## Setting up
### Github (automatic) ### GitHub (automatic)
1. Go to your package page. 1. Go to your package's page.
2. Make sure that the repository URL is set to a Github repository. 2. Make sure that the repository URL is set to a Github repository.
Only github.com is supported. Only github.com is supported.
3. Go to "Create a release", and click "Setup webhook" at the top of the page. 3. Go to "Releases" > "+", and click "Setup webhook" at the top of the create release
page.
If you do not see this, either the repository isn't using Github or you do If you do not see this, either the repository isn't using Github or you do
not have permission to use webhook releases (ie: you're not a Trusted Member). not have permission to use webhook releases (ie: you're not a Trusted Member).
4. Grant ContentDB the ability to manage Webhooks. 4. Grant ContentDB the ability to manage Webhooks.
5. Set the event to either "New tag" or "Push". New tag is highlight recommended. 5. Set the event to either "New tag or Github Release" (highly recommended) or "Push".
N.B.: GitHub uses tags to power GitHub Releases, meaning that creating a webhook N.B.: GitHub uses tags to power GitHub Releases, meaning that creating a webhook
on "new tag" will sync GitHub and ContentDB releases. on "New tag" will sync GitHub and ContentDB releases.
### GitHub (manual) ### GitHub (manual)
@@ -48,7 +49,7 @@ The process is as follows:
7. Set the events 7. Set the events
* If you want a rolling release, choose "just the push event". * If you want a rolling release, choose "just the push event".
* Or if you want a stable release cycle based on tags, * Or if you want a stable release cycle based on tags,
choose "Let me select" > Branch or tag creation. choose "Let me select" > Branch or tag creation.
### GitLab (manual) ### GitLab (manual)

View File

@@ -93,6 +93,7 @@ class Permission(enum.Enum):
UNAPPROVE_PACKAGE = "UNAPPROVE_PACKAGE" UNAPPROVE_PACKAGE = "UNAPPROVE_PACKAGE"
TOPIC_DISCARD = "TOPIC_DISCARD" TOPIC_DISCARD = "TOPIC_DISCARD"
CREATE_TOKEN = "CREATE_TOKEN" CREATE_TOKEN = "CREATE_TOKEN"
CHANGE_PROFILE_URLS = "CHANGE_PROFILE_URLS"
# Only return true if the permission is valid for *all* contexts # Only return true if the permission is valid for *all* contexts
# See Package.checkPerm for package-specific contexts # See Package.checkPerm for package-specific contexts
@@ -192,7 +193,7 @@ class User(db.Model, UserMixin):
return user.rank.atLeast(UserRank.EDITOR) return user.rank.atLeast(UserRank.EDITOR)
elif perm == Permission.CHANGE_RANK or perm == Permission.CHANGE_DNAME: elif perm == Permission.CHANGE_RANK or perm == Permission.CHANGE_DNAME:
return user.rank.atLeast(UserRank.MODERATOR) return user.rank.atLeast(UserRank.MODERATOR)
elif perm == Permission.CHANGE_EMAIL: elif perm == Permission.CHANGE_EMAIL or perm == Permission.CHANGE_PROFILE_URLS:
return user == self or (user.rank.atLeast(UserRank.MODERATOR) and user.rank.atLeast(self.rank)) return user == self or (user.rank.atLeast(UserRank.MODERATOR) and user.rank.atLeast(self.rank))
elif perm == Permission.CREATE_TOKEN: elif perm == Permission.CREATE_TOKEN:
if user == self: if user == self:

View File

@@ -1,4 +1,5 @@
from .models import db, PackageType, Package, ForumTopic, License, MinetestRelease, PackageRelease from .models import db, PackageType, Package, ForumTopic, License, MinetestRelease, PackageRelease, User, Tag
from .models import tags as Tags
from .utils import isNo, isYes from .utils import isNo, isYes
from sqlalchemy.sql.expression import func from sqlalchemy.sql.expression import func
from flask import abort from flask import abort
@@ -19,18 +20,30 @@ class QueryBuilder:
if len(types) > 0: if len(types) > 0:
title = ", ".join([type.value + "s" for type in types]) title = ", ".join([type.value + "s" for type in types])
# Get tags types
tags = args.getlist("tag")
tags = [Tag.query.filter_by(name=tname).first() for tname in tags]
tags = [tag for tag in tags if tag is not None]
# Hid
hide_flags = args.getlist("hide") hide_flags = args.getlist("hide")
self.title = title self.title = title
self.types = types self.types = types
self.search = args.get("q") self.tags = tags
self.random = "random" in args self.random = "random" in args
self.lucky = "lucky" in args self.lucky = "lucky" in args
self.hide_nonfree = "nonfree" in hide_flags self.hide_nonfree = "nonfree" in hide_flags
self.limit = 1 if self.lucky else None self.limit = 1 if self.lucky else None
self.order_by = args.get("sort") self.order_by = args.get("sort")
self.order_dir = args.get("order") or "desc" self.order_dir = args.get("order") or "desc"
# Filters
self.search = args.get("q")
self.protocol_version = args.get("protocol_version") self.protocol_version = args.get("protocol_version")
self.author = args.get("author")
self.show_discarded = isYes(args.get("show_discarded")) self.show_discarded = isYes(args.get("show_discarded"))
self.show_added = args.get("show_added") self.show_added = args.get("show_added")
@@ -84,6 +97,16 @@ class QueryBuilder:
query = query.order_by(to_order) query = query.order_by(to_order)
if self.author:
author = User.query.filter_by(username=self.author).first()
if not author:
abort(404)
query = query.filter_by(author=author)
for tag in self.tags:
query = query.filter(Package.tags.any(Tag.id == tag.id))
if self.hide_nonfree: if self.hide_nonfree:
query = query.filter(Package.license.has(License.is_foss == True)) query = query.filter(Package.license.has(License.is_foss == True))
query = query.filter(Package.media_license.has(License.is_foss == True)) query = query.filter(Package.media_license.has(License.is_foss == True))

View File

@@ -162,6 +162,9 @@ def cloneRepo(urlstr, ref=None, recursive=False):
origin.fetch() origin.fetch()
origin.pull(ref) origin.pull(ref)
for submodule in repo.submodules:
submodule.update(init=True)
return gitDir, repo return gitDir, repo
except GitCommandError as e: except GitCommandError as e:

View File

@@ -15,9 +15,10 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
from app.models import Package from app.models import Package, db
from app.tasks import celery from app.tasks import celery
@celery.task() @celery.task()
def updatePackageScores(): def updatePackageScores():
Package.query.update({ "score": Package.score * 0.8 }) Package.query.update({ "score": Package.score * 0.95 })
db.session.commit()

View File

@@ -1,10 +1,16 @@
from . import app from . import app
from .utils import abs_url_for
from urllib.parse import urlparse from urllib.parse import urlparse
@app.context_processor @app.context_processor
def inject_debug(): def inject_debug():
return dict(debug=app.debug) return dict(debug=app.debug)
@app.context_processor
def inject_functions():
return dict(abs_url_for=abs_url_for)
@app.template_filter() @app.template_filter()
def throw(err): def throw(err):
raise Exception(err) raise Exception(err)

View File

@@ -21,7 +21,8 @@
<h1 class="mt-0">{{ self.title() }}</h1> <h1 class="mt-0">{{ self.title() }}</h1>
<div class="alert alert-warning"> <div class="alert alert-warning">
{{ _("Use carefully, as you may be held responsible for any damage caused by rogue scripts") }} {{ _("API Tokens allow scripts to act on your behalf.") }}
{{ _("Be careful with what/whom you share tokens with, as you are responsible for your account's actions.") }}
</div> </div>
{% if token %} {% if token %}

View File

@@ -129,7 +129,7 @@
{% endblock %} {% endblock %}
<footer class="container footer-copyright my-5 page-footer font-small text-center"> <footer class="container footer-copyright my-5 page-footer font-small text-center">
ContentDB &copy; 2018-9 to <a href="https://rubenwardy.com/">rubenwardy</a> | ContentDB &copy; 2018-20 to <a href="https://rubenwardy.com/">rubenwardy</a> |
<a href="https://github.com/minetest/contentdb">GitHub</a> | <a href="https://github.com/minetest/contentdb">GitHub</a> |
<a href="{{ url_for('flatpage', path='help') }}">{{ _("Help") }}</a> | <a href="{{ url_for('flatpage', path='help') }}">{{ _("Help") }}</a> |
<a href="{{ url_for('flatpage', path='policy_and_guidance') }}">{{ _("Policy and Guidance") }}</a> | <a href="{{ url_for('flatpage', path='policy_and_guidance') }}">{{ _("Policy and Guidance") }}</a> |

View File

@@ -20,4 +20,10 @@
{{ render_submit_field(form.submit) }} {{ render_submit_field(form.submit) }}
</form> </form>
<p class="mt-4">
You will need admin access to the repository.
When setting up hooks on an organisation,
<a href="{{ url_for('github.view_permissions') }}">make sure that you have granted access</a>.
</p>
{% endblock %} {% endblock %}

View File

@@ -5,6 +5,21 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% if authors %}
<p class="alert alert-primary">
Did you mean to search for packages by
{% for author in authors %}
<a href="{{ url_for('packages.list_all', type=type, author=author[0], q=author[1]) }}">{{ author[0] }}</a>
{% if not loop.last %}
,
{% endif %}
{% endfor %}
?
</p>
{% endif %}
{% from "macros/packagegridtile.html" import render_pkggrid %} {% from "macros/packagegridtile.html" import render_pkggrid %}
{{ render_pkggrid(packages) }} {{ render_pkggrid(packages) }}

View File

@@ -38,7 +38,8 @@
</span> </span>
{% endif %} {% endif %}
{% for t in package.tags %} {% for t in package.tags %}
<span class="badge badge-primary">{{ t.title }}</span> <a class="badge badge-primary"
href="{{ url_for('packages.list_all', tag=t.name) }}">{{ t.title }}</a>
{% endfor %} {% endfor %}
</p> </p>

View File

@@ -64,6 +64,7 @@
| <a href="{{ user.website_url }}" rel="nofollow">Website</a> | <a href="{{ user.website_url }}" rel="nofollow">Website</a>
{% endif %} {% endif %}
{% if user == current_user %} {% if user == current_user %}
<br> <br>
<small class="text-muted"> <small class="text-muted">
@@ -73,6 +74,16 @@
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
{% if user == current_user and user.github_username %}
<tr>
<td>Privacy:</td>
<td>
<a href="{{ url_for('github.view_permissions') }}">View ContentDB's GitHub Permissions</a>
</td>
</tr>
{% endif %}
{% if current_user.is_authenticated and current_user.rank.atLeast(current_user.rank.MODERATOR) %} {% if current_user.is_authenticated and current_user.rank.atLeast(current_user.rank.MODERATOR) %}
<tr> <tr>
<td>Admin</td> <td>Admin</td>
@@ -115,7 +126,7 @@
</a> </a>
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
<tr> <tr>
<td>Password:</td> <td>Password:</td>
<td> <td>
@@ -153,6 +164,9 @@
{% if user.checkPerm(current_user, "CHANGE_DNAME") %} {% if user.checkPerm(current_user, "CHANGE_DNAME") %}
{{ render_field(form.display_name, tabindex=230) }} {{ render_field(form.display_name, tabindex=230) }}
{% endif %}
{% if user.checkPerm(current_user, "CHANGE_PROFILE_URLS") %}
{{ render_field(form.website_url, tabindex=232) }} {{ render_field(form.website_url, tabindex=232) }}
{{ render_field(form.donate_url, tabindex=233) }} {{ render_field(form.donate_url, tabindex=233) }}
{% endif %} {% endif %}
@@ -166,7 +180,9 @@
{{ render_field(form.rank, tabindex=250) }} {{ render_field(form.rank, tabindex=250) }}
{% endif %} {% endif %}
{{ render_submit_field(form.submit, tabindex=280) }} <p>
{{ render_submit_field(form.submit, tabindex=280) }}
</p>
</form> </form>
</div> </div>
</div> </div>

View File

@@ -18,12 +18,10 @@
from flask import request, flash, abort, redirect from flask import request, flash, abort, redirect
from flask_user import * from flask_user import *
from flask_login import login_user, logout_user from flask_login import login_user, logout_user
from app.models import * from .models import *
from app import app from . import app
import random, string, os, imghdr import random, string, os, imghdr
@app.template_filter()
def abs_url_for(path, **kwargs): def abs_url_for(path, **kwargs):
scheme = "https" if app.config["BASE_URL"][:5] == "https" else "http" scheme = "https" if app.config["BASE_URL"][:5] == "https" else "http"
return url_for(path, _external=True, _scheme=scheme, **kwargs) return url_for(path, _external=True, _scheme=scheme, **kwargs)

View File

@@ -17,7 +17,7 @@ beautifulsoup4~=4.6
celery~=4.4 celery~=4.4
kombu~=4.6 kombu~=4.6
GitPython~=3.0 GitPython~=3.0
git-archive-all~=1.20 git-archive-all~=1.21
lxml~=4.2 lxml~=4.2
pillow~=7.0 pillow~=7.0
pyScss~=1.3 pyScss~=1.3