Compare commits

..

41 Commits

Author SHA1 Message Date
rubenwardy
aad4fd2a70 Add list of similar packages in details page 2018-06-03 19:27:56 +01:00
rubenwardy
d2bda0fded Update inclusion policy 2018-06-03 15:19:17 +01:00
rubenwardy
b84727b187 Fix username being case-sensitive 2018-06-03 01:50:58 +01:00
rubenwardy
6fd36dbfff Add WIP things note to policy 2018-06-02 21:40:48 +01:00
rubenwardy
8e134a7c85 Fix todo topics sort order 2018-06-02 19:44:57 +01:00
rubenwardy
389258a10c Fix button CSS issue 2018-06-02 19:42:46 +01:00
rubenwardy
3657316fa2 Clean up todo topics related HTML 2018-06-02 19:41:13 +01:00
rubenwardy
a6f4249afb Increase link string length limit 2018-06-02 18:32:07 +01:00
rubenwardy
70afb94d3b Add topics todo list based on forum parser 2018-06-02 18:26:17 +01:00
rubenwardy
8984adaa72 Update policy document 2018-06-02 17:17:32 +01:00
rubenwardy
c523624696 Fix button in alert borders 2018-05-30 04:00:27 +01:00
rubenwardy
072f189006 Add alternatives section to package page 2018-05-30 02:59:11 +01:00
rubenwardy
9967101d9f Add package inclusion policy and guidance 2018-05-30 01:20:47 +01:00
rubenwardy
1ed09b646b Fix double single quote 2018-05-29 23:21:24 +01:00
rubenwardy
f554bfc92b Fix max package grid cell size 2018-05-29 23:18:13 +01:00
rubenwardy
c80ea2c1b1 Sort meta list, and packages on profile 2018-05-29 23:15:41 +01:00
rubenwardy
edd51b86d0 Add package grid to profile page 2018-05-29 22:58:46 +01:00
rubenwardy
944b8a4eb0 Add placeholder to release title 2018-05-29 22:43:42 +01:00
rubenwardy
a627893355 Add trusted member color 2018-05-29 21:40:10 +01:00
rubenwardy
1600687449 Add non-free warning 2018-05-29 21:25:47 +01:00
rubenwardy
fa2f17526f Disable edit requests 2018-05-29 20:51:42 +01:00
rubenwardy
002e6828b6 Fix user claim verification token not being remembered due to multiple nodes 2018-05-29 20:32:15 +01:00
rubenwardy
a947472c67 Fix crash on JSON packages due to lack of None check 2018-05-29 20:18:36 +01:00
rubenwardy
e7acd7faa3 Add separate media license
Fixes #91
2018-05-29 20:17:18 +01:00
rubenwardy
f755c7d429 Fix flash being hidden behind elements
Fixes #84
2018-05-29 18:50:45 +01:00
rubenwardy
b6652547fa Improve sign in form 2018-05-29 18:31:48 +01:00
rubenwardy
be20146f25 Add migration 2018-05-29 18:29:14 +01:00
rubenwardy
df291db69b Add email/password sign up 2018-05-29 18:27:39 +01:00
rubenwardy
63a3b5e872 Add claim call to action on unclaimed accounts 2018-05-29 18:16:05 +01:00
rubenwardy
6353ac29e9 Add set password form 2018-05-29 18:07:23 +01:00
rubenwardy
a4b583bac5 Add github-less claim method 2018-05-29 17:42:27 +01:00
rubenwardy
52fdc8c212 Add clear all button to notifications page 2018-05-29 17:20:11 +01:00
rubenwardy
7e80adad56 Fix soft deleted and unapproved packages appearing where they shouldn't 2018-05-29 17:15:53 +01:00
rubenwardy
bf5080aa18 Increase thumbnail resolution 2018-05-29 16:56:35 +01:00
rubenwardy
89f95a22dc Add pagination 2018-05-29 16:52:53 +01:00
rubenwardy
f1b21b73b2 Add max package tile size 2018-05-29 16:23:29 +01:00
rubenwardy
6a13dca2d5 Add thumbnail support 2018-05-29 16:19:17 +01:00
rubenwardy
048b604a75 Fix changing email not working due to validation issue 2018-05-28 15:25:45 +01:00
rubenwardy
f7bb29c839 Fix empty release titles being allowed
Fixes #86
2018-05-28 14:55:31 +01:00
Ezhh
ba506cb16d Update ranks and permissions help page 2018-05-28 01:40:02 +01:00
Pavel Puchkin
179d0be933 Make search case-insensitive by using ilike 2018-05-28 00:06:57 +01:00
48 changed files with 1089 additions and 207 deletions

1
.gitignore vendored
View File

@@ -6,6 +6,7 @@ tmp
log.txt
*.rdb
uploads
thumbnails
# Created by https://www.gitignore.io/api/linux,macos,python,windows

View File

@@ -60,5 +60,5 @@ rm db.sqlite && python setup.py -t && FLASK_CONFIG=../config.cfg FLASK_APP=app/_
FLASK_CONFIG=../config.cfg FLASK_APP=app/__init__.py flask db migrate
# Run migration
FLASK_CONFIG=../config.cfg FLASK_APP=app/__init__.py flask db migrate
FLASK_CONFIG=../config.cfg FLASK_APP=app/__init__.py flask db upgrade
```

View File

@@ -2,3 +2,4 @@ title: Help
* [Package Tags](package_tags)
* [Ranks and Permissions](ranks_permissions)
* [Reporting Content](reporting)

View File

@@ -193,7 +193,7 @@ title: Ranks and Permissions
<th></th>
<th></th> <!-- member -->
<th></th>
<th></th> <!-- trusted member -->
<th></th> <!-- trusted member -->
<th></th>
<th></th> <!-- editor -->
<th></th>

View File

@@ -0,0 +1,8 @@
title: Reporting Content
Please let us know if anything on the ContentDB violates our rules or any applicable
laws.
We take copyright violation and other offenses very seriously.
<a href="https://rubenwardy.com/contact/" class="button btn_green">Contact</a>

View File

@@ -0,0 +1,105 @@
title: Package Inclusion Policy and Guidance
<div class="box box_grey alert alert-warning">
<b>Note:</b> This is a draft
</div>
## 1. General
It is not permitted to submit abusive, obscene, vulgar, slanderous, hateful,
threatening, sexually-orientated or any material that may violate any laws be
it of your country, the country where "Content DB” is hosted or International Law.
The ContentDB admin reserves the right to remove packages for any reason,
including ones not covered by this document, and to ban users who abuse this service.
Also see the [help page on tags](/help/package_tags/).
## 2. Accepted Content and State of Completion
The submission of malware is strictly prohibited. This includes software which
does not do as it advertises, for example if it posts telemetry without stating
clearly that it does in the package meta.
ContentDB should only currently contain playable content, ie: stuff that would
be in Mod Releases and Game Releases. Please don't upload any Work In Progress (WIP)
things. This will probably change in future if/when an "early access" feature is
added.
Adding non-player facing mods, such as libraries and server tools, is perfectly fine.
ContentDB isn't just for player-facing things, and adding libraries allows them to be
installed when a mod depends on it.
## 3. Technical Names
### 3.1 Right to a name
The first package to use a name based on the creation of its forum topic or
contentdb submission has the right to the technical name. The use of a package
on a server or in private doesn't reserve its name. No other packages of the same
type may use the same name, except for the exception given by 2.2.
If it turns out that we made a mistake by approving a package and that the
name should have been given to another package, then we *may* unapprove the
package and give the name to the correct one.
If you submit a package where you don't have the right to the name you will be asked
to change the name of the package, or your package won't be accepted.
### 3.2 Mod Forks and Reimplementations
An exception to the above is that mods are allowed to have the same name as a
mod if its a fork of that mod (or a close reimplementation). In real terms, it
should be possible to use the new mod as a drop-in replacement.
We reserve the right to decide whether a mod counts as a fork or
reimplementation of the mod that owns the name.
## 4. Licenses
### 4.1 Allowed Licenses
Please ensure that you correctly credit any resources (code, assets, or otherwise)
that you have used in your package.
**The use of licenses which do not allow derivatives or redistribution is not
permitted. This includes CC-ND (No-Derivatives) and lots of closed source licenses.**
However, closed sourced licenses are allowed if they allow the above.
If the license you use is not on the list then please choose the correct "Other"
option.
Please note that the definitions of "free" and "non-free" is the same as that
of the [Free Software Foundation](https://www.gnu.org/philosophy/free-sw.en.html).
### 4.2 Recommended Licenses
It is recommended that you use a proper license for code with a warranty
disclaimer, such as the (L)GPL or MIT. You should also use a proper media license
for media, such as a Creative Commons license.
The use of WTFPL is discouraged as it doesn't contain a valid warranty disclaimer,
and also includes swearing which dissuades teachers from using your content.
Public domain is not a valid license in many countries, please use CC0 or MIT instead.
## 5. Promotions and Advertisements (inc. asking for donations)
Any information other than the long description - including screenshots - must
not contain any promotions or advertisements. This includes asking for donations,
promoting online shops, or linking to personal websites and social media.
ContentDB is for the community. We may remove any promotions if we feel that
they're inappropriate.
Paid promotions are not allowed at all, anywhere.
## 6. Reporting Violations
See the [Reporting Content](/help/reporting/) page.

View File

@@ -96,15 +96,15 @@ class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
# User authentication information
username = db.Column(db.String(50), nullable=False, unique=True)
password = db.Column(db.String(255), nullable=False, server_default="")
username = db.Column(db.String(50, collation="NOCASE"), nullable=False, unique=True, index=True)
password = db.Column(db.String(255), nullable=True)
reset_password_token = db.Column(db.String(100), nullable=False, server_default="")
rank = db.Column(db.Enum(UserRank))
# Account linking
github_username = db.Column(db.String(50), nullable=True, unique=True)
forums_username = db.Column(db.String(50), nullable=True, unique=True)
github_username = db.Column(db.String(50, collation="NOCASE"), nullable=True, unique=True)
forums_username = db.Column(db.String(50, collation="NOCASE"), nullable=True, unique=True)
# User email information
email = db.Column(db.String(255), nullable=True, unique=True)
@@ -121,12 +121,15 @@ class User(db.Model, UserMixin):
packages = db.relationship("Package", backref="author", lazy="dynamic")
requests = db.relationship("EditRequest", backref="author", lazy="dynamic")
def __init__(self, username):
def __init__(self, username, active=False, email=None, password=None):
import datetime
self.username = username
self.confirmed_at = datetime.datetime.now() - datetime.timedelta(days=6000)
self.display_name = username
self.active = active
self.email = email
self.password = password
self.rank = UserRank.NOT_JOINED
def canAccessTodoList(self):
@@ -181,12 +184,13 @@ class Notification(db.Model):
class License(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50), nullable=False, unique=True)
packages = db.relationship("Package", backref="license", lazy="dynamic")
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50), nullable=False, unique=True)
is_foss = db.Column(db.Boolean, nullable=False, default=True)
def __init__(self, v):
def __init__(self, v, is_foss=True):
self.name = v
self.is_foss = is_foss
def __str__(self):
return self.name
@@ -213,18 +217,19 @@ class PackageType(enum.Enum):
class PackagePropertyKey(enum.Enum):
name = "Name"
title = "Title"
shortDesc = "Short Description"
desc = "Description"
type = "Type"
license = "License"
tags = "Tags"
provides = "Provides"
repo = "Repository"
website = "Website"
issueTracker = "Issue Tracker"
forums = "Forum Topic ID"
name = "Name"
title = "Title"
shortDesc = "Short Description"
desc = "Description"
type = "Type"
license = "License"
media_license = "Media License"
tags = "Tags"
provides = "Provides"
repo = "Repository"
website = "Website"
issueTracker = "Issue Tracker"
forums = "Forum Topic ID"
def convert(self, value):
if self == PackagePropertyKey.tags:
@@ -324,7 +329,10 @@ class Package(db.Model):
type = db.Column(db.Enum(PackageType))
created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
license_id = db.Column(db.Integer, db.ForeignKey("license.id"))
license_id = db.Column(db.Integer, db.ForeignKey("license.id"), nullable=False, default=1)
license = db.relationship("License", foreign_keys=[license_id])
media_license_id = db.Column(db.Integer, db.ForeignKey("license.id"), nullable=False, default=1)
media_license = db.relationship("License", foreign_keys=[media_license_id])
approved = db.Column(db.Boolean, nullable=False, default=False)
soft_deleted = db.Column(db.Boolean, nullable=False, default=False)
@@ -336,7 +344,7 @@ class Package(db.Model):
forums = db.Column(db.Integer, nullable=True)
provides = db.relationship("MetaPackage", secondary=provides, lazy="subquery",
backref=db.backref("packages", lazy=True))
backref=db.backref("packages", lazy="dynamic"))
dependencies = db.relationship("Dependency", backref="depender", lazy="dynamic", foreign_keys=[Dependency.depender_id])
@@ -364,6 +372,7 @@ class Package(db.Model):
setattr(self, e.name, getattr(package, e.name))
def getAsDictionary(self, base_url):
tnurl = self.getThumbnailURL()
return {
"name": self.name,
"title": self.title,
@@ -374,9 +383,18 @@ class Package(db.Model):
"repo": self.repo,
"url": base_url + self.getDownloadURL(),
"release": self.getDownloadRelease().id if self.getDownloadRelease() is not None else None,
"screenshots": [base_url + ss.url for ss in self.screenshots]
"screenshots": [base_url + ss.url for ss in self.screenshots],
"thumbnail": (base_url + tnurl) if tnurl is not None else None
}
def getThumbnailURL(self):
screenshot = self.screenshots.filter_by(approved=True).first()
return screenshot.getThumbnailURL() if screenshot is not None else None
def getMainScreenshotURL(self):
screenshot = self.screenshots.filter_by(approved=True).first()
return screenshot.url if screenshot is not None else None
def getDetailsURL(self):
return url_for("package_page",
author=self.author.username, name=self.name)
@@ -409,10 +427,6 @@ class Package(db.Model):
return url_for("package_download_page",
author=self.author.username, name=self.name)
def getMainScreenshotURL(self):
screenshot = self.screenshots.filter_by(approved=True).first()
return screenshot.url if screenshot is not None else None
def getDownloadRelease(self):
for rel in self.releases:
if rel.approved:
@@ -575,7 +589,7 @@ class PackageScreenshot(db.Model):
id=self.id)
def getThumbnailURL(self):
return self.url # TODO
return self.url.replace("/uploads/", "/thumbnails/350x233/")
class EditRequest(db.Model):
id = db.Column(db.Integer, primary_key=True)
@@ -664,6 +678,25 @@ class EditRequestChange(db.Model):
else:
setattr(package, self.key.name, self.newValue)
class KrockForumTopic(db.Model):
topic_id = db.Column(db.Integer, primary_key=True, autoincrement=False)
author_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
author = db.relationship("User")
ttype = db.Column(db.Integer, nullable=False)
title = db.Column(db.String(200), nullable=False)
name = db.Column(db.String(30), nullable=True)
link = db.Column(db.String(200), nullable=True)
def getType(self):
if self.ttype == 1 or self.ttype == 2:
return PackageType.MOD
elif self.ttype == 6:
return PackageType.GAME
# Setup Flask-User
db_adapter = SQLAlchemyAdapter(db, User) # Register the User model
user_manager = UserManager(db_adapter, app) # Initialize Flask-User

View File

@@ -25,7 +25,7 @@ a:hover {
padding: 0;
}
.box h2, .box h3 {
.box > h2, .box > h3 {
margin: 0;
padding: 0.5em 0.5em 0.5em 15px;
border-bottom: 1px solid #444;
@@ -235,43 +235,51 @@ select:not([multiple]) {
/* Alerts */
.alert {
padding: 10px;
position: relative;
}
.alert .alert_right, .alert > form {
display: inline-block;
margin: 0;
padding: 0;
position: absolute;
top: 0;
right: 0;
bottom: 0;
}
.alert .alert_right form {
height: 100%;
}
.alert {
padding: 10px;
position: relative;
.alert form {
display: inline-block;
margin: 0;
padding: 0;
}
.alert_right:not(.button) {
padding: 0;
}
.alert input {
margin: 0;
height: 100%;
background: 0;
border: 0;
border-left: 1px solid rgba(255,255,255,0.12);
border-radius: 0;
}
.alert_right form {
height: 100%;
}
.alert input:hover {
border: 0;
border-left: 1px solid rgba(255,255,255,0.2);
form {
display: inline-block;
margin: 0;
padding: 0;
}
input {
height: 100%;
}
input, .button {
margin: 0;
background: 0;
border: 0;
border-left: 1px solid rgba(255,255,255,0.12);
border-radius: 0;
vertical-align: middle;
}
input:hover, .button:hover {
border: 0;
border-left: 1px solid rgba(255,255,255,0.2);
}
}
#alerts {
@@ -280,6 +288,7 @@ select:not([multiple]) {
bottom: 15px;
left: 0;
right: 0;
z-index: 1000;
}
#alerts .alert {
@@ -383,6 +392,10 @@ table.fancyTable tfoot td {
color: #b6f;
}
.TRUSTED_MEMBER a, a.TRUSTED_MEMBER {
color: #2c2;
}
/*
Aside
*/

View File

@@ -13,6 +13,7 @@
display: block;
min-width: 300px;
min-height: 200px;
max-width: 332px;
padding: 0;
margin: 7px;
}

View File

@@ -75,3 +75,47 @@ def importUsersFromModList():
db.session.commit()
for author in found:
checkForumAccount.delay(author, None)
BANNED_NAMES = ["mod", "game", "old", "outdated", "wip", "api"]
ALLOWED_TYPES = [1, 2, 6]
@celery.task()
def importKrocksModList():
contents = urllib.request.urlopen("http://krock-works.16mb.com/MTstuff/modList.php").read().decode("utf-8")
list = json.loads(contents)
username_to_user = {}
KrockForumTopic.query.delete()
for x in list:
type = int(x["type"])
if not type in ALLOWED_TYPES:
continue
username = x["author"]
user = username_to_user.get(username)
if user is None:
user = User.query.filter_by(forums_username=username).first()
assert(user is not None)
username_to_user[username] = user
import re
tags = re.findall("\[([a-z0-9_]+)\]", x["title"])
name = None
for tag in reversed(tags):
if len(tag) < 30 and not tag in BANNED_NAMES and \
not re.match("^([a-z][0-9]+)$", tag):
name = tag
break
topic = KrockForumTopic()
topic.topic_id = x["topicId"]
topic.author_id = user.id
topic.ttype = type
topic.title = x["title"]
topic.name = name
topic.link = x.get("link")
db.session.add(topic)
db.session.commit()

View File

@@ -18,6 +18,7 @@
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<select name="action">
<option value="importusers">Create users from mod list</option>
<option value="importmodlist">Import Krock's mod list</option>
<option value="importscreenshots" selected>Import screenshots from VCS</option>
<option value="importdepends">Import dependencies from downloads</option>
<option value="modprovides">Set provides to mod name</option>

View File

@@ -103,8 +103,9 @@
{% endblock %}
<footer>
Copyright &copy; 2018 to <a href="https://rubenwardy.com/">rubenwardy</a> |
ContentDB &copy; 2018 to <a href="https://rubenwardy.com/">rubenwardy</a> |
<a href="https://github.com/minetest/contentdb">GitHub</a> |
<a href="{{ url_for('flatpage', path='help') }}">Help</a> |
<a href="https://github.com/minetest/contentdb">GitHub</a>
<a href="{{ url_for('flatpage', path='help/reporting') }}">Report / DMCA</a>
</footer>
</html>

View File

@@ -11,9 +11,11 @@ Sign in
<h2>{%trans%}Sign in{%endtrans%}</h2>
<form action="" method="POST" class="form box-body" role="form">
<a href="{{ url_for('github_signin_page') }}">GitHub</a>
<h3>Sign in with Github</h3>
<p><a class="button" href="{{ url_for('github_signin_page') }}">GitHub</a></p>
<h3>Sign in with username/password</h3>
{{ form.hidden_tag() }}
{# Username or Email field #}
@@ -71,7 +73,7 @@ Sign in
<h2>New here?</h2>
<div class="box-body">
<p>Create an account using your forum account.</p>
<p>Create an account using your forum account or email.</p>
<a href="{{ url_for('user_claim_page') }}" class="button">{%trans%}Claim your account{%endtrans%}</a>
</div>

View File

@@ -20,6 +20,40 @@
<script src="/static/tagselector.js"></script>
{% endmacro %}
{% macro package_lists() -%}
<script>
meta_packages = [
{% for m in mpackages %}
{# This is safe as name can only contain `[a-z0-9_]` #}
{
id: "{{ m.name }}",
value: "{{ m.name }}",
toString: function() { return "{{ m.name }}"; },
},
{% endfor %}
]
function escape(unsafe) {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
all_packages = meta_packages.slice();
{% for p in packages %}
all_packages.push({
id: "{{ p.author.username }}/{{ p.name }}",
value: escape({{ p.title | tojson }} + " by " + {{ p.author.display_name | tojson }}),
toString: function() { return escape({{ p.title | tojson }} + " by " + {{ p.author.display_name | tojson }} + " only"); },
});
{% endfor %}
</script>
{% endmacro %}
{% macro render_multiselect_field(field, label=None, label_visible=true, right_url=None, right_label=None) -%}
<div class="form-group {% if field.errors %}has-error{% endif %} {{ kwargs.pop('class_', '') }}">
{% if field.type != 'HiddenField' and label_visible %}

View File

@@ -1,21 +1,42 @@
{% macro render_pkgtile(package) -%}
{% macro render_pkgtile(package, show_author) -%}
<li><a href="{{ package.getDetailsURL() }}"
style="background-image: url({{ package.getMainScreenshotURL() or '/static/placeholder.png' }});">
style="background-image: url({{ package.getThumbnailURL() or '/static/placeholder.png' }});">
<div class="packagegridscrub"></div>
<div class="packagegridinfo">
<h3>{{ package.title }} by {{ package.author.display_name }}</h3>
<h3>
{{ package.title }}
{% if show_author %}
by {{ package.author.display_name }}
{% endif %}
</h3>
<p>
{{ package.shortDesc }}
</p>
{% if not package.license.is_foss and not package.media_license.is_foss and package.type != package.type.TXP %}
<p style="color:#f33;">
<b>Warning:</b> Non-free code and media.
</p>
{% elif not package.license.is_foss and package.type != package.type.TXP %}
<p style="color:#f33;">
<b>Warning:</b> Non-free code.
</p>
{% elif not package.media_license.is_foss %}
<p style="color:#f33;">
<b>Warning:</b> Non-free media.
</p>
{% endif %}
</div>
</a></li>
{% endmacro %}
{% macro render_pkggrid(packages) -%}
{% macro render_pkggrid(packages, show_author=True) -%}
<ul class="packagegrid">
{% for p in packages %}
{{ render_pkgtile(p) }}
{{ render_pkgtile(p, show_author) }}
{% else %}
<li><i>No packages available</i></ul>
{% endfor %}

View File

@@ -7,7 +7,7 @@ Meta Packages
{% block content %}
<ul>
{% for meta in mpackages %}
<li><a href="{{ url_for('meta_package_page', name=meta.name) }}">{{ meta.name }}</a> ({{ meta.packages | count }} packages)</li>
<li><a href="{{ url_for('meta_package_page', name=meta.name) }}">{{ meta.name }}</a> ({{ meta.packages.filter_by(soft_deleted=False, approved=True).all() | count }} packages)</li>
{% else %}
<li><i>No meta packages found.</i></li>
{% endfor %}

View File

@@ -5,8 +5,8 @@ Packages providing '{{ mpackage.name }}''
{% endblock %}
{% block content %}
<h1>Packages providing '{{ mpackage.name }}''</h1>
<h1>Packages providing '{{ mpackage.name }}'</h1>
{% from "macros/packagegridtile.html" import render_pkggrid %}
{{ render_pkggrid(mpackage.packages) }}
{{ render_pkggrid(mpackage.packages.filter_by(approved=True, soft_deleted=False).all()) }}
{% endblock %}

View File

@@ -5,6 +5,12 @@ Notifications
{% endblock %}
{% block content %}
{% if current_user.notifications %}
<form method="post" action="{{ url_for('clear_notifications_page') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="submit" value="Clear All" />
</form>
{% endif %}
<ul>
{% for n in current_user.notifications %}
<li><a href="{{ n.url }}">

View File

@@ -10,40 +10,16 @@
{% block content %}
<h1>Create Package</h1>
<script>
meta_packages = [
{% for m in mpackages %}
{# This is safe as name can only contain `[a-z0-9_]` #}
{
id: "{{ m.name }}",
value: "{{ m.name }}",
toString: function() { return "{{ m.name }}"; },
},
{% endfor %}
]
<div class="box box_grey alert alert-info">
Have you read the Package Inclusion Policy and Guidance yet?
function escape(unsafe) {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
<a class="alert_right button" href="{{ url_for('flatpage', path='policy_and_guidance') }}">View</a>
</div>
all_packages = meta_packages.slice();
{% for p in packages %}
all_packages.push({
id: "{{ p.author.username }}/{{ p.name }}",
value: escape({{ p.title | tojson }} + " by " + {{ p.author.display_name | tojson }}),
toString: function() { return escape({{ p.title | tojson }} + " by " + {{ p.author.display_name | tojson }} + " only"); },
});
{% endfor %}
</script>
{% from "macros/forms.html" import render_field, render_submit_field, form_includes, render_multiselect_field, render_mpackage_field, render_deps_field %}
{% from "macros/forms.html" import render_field, render_submit_field, form_includes, render_multiselect_field, render_mpackage_field, render_deps_field, package_lists %}
{{ form_includes() }}
{{ package_lists() }}
<form method="POST" action="" class="tableform">
{{ form.hidden_tag() }}
@@ -56,7 +32,10 @@
{{ render_field(form.shortDesc, class_="pkg_meta") }}
{{ render_field(form.desc, class_="pkg_meta") }}
{{ render_multiselect_field(form.tags, class_="pkg_meta") }}
{{ render_field(form.license, class_="pkg_meta") }}
<div class="pkg_meta">
{{ render_field(form.license, class_="not_txp") }}
</div>
{{ render_field(form.media_license, class_="pkg_meta") }}
<div class="pkg_meta">
<h2 class="not_txp">Dependency Info</h2>

View File

@@ -5,19 +5,30 @@
{% endblock %}
{% block content %}
{% from "macros/forms.html" import render_field, render_submit_field, form_includes, render_multiselect_field %}
{% from "macros/forms.html" import render_field, render_submit_field, form_includes, render_multiselect_field, render_mpackage_field, render_deps_field, package_lists %}
{{ form_includes() }}
{{ package_lists() }}
<form method="POST" action="">
{{ form.hidden_tag() }}
<h2 class="pkg_meta">Package</h2>
{{ render_field(form.type) }}
{{ render_field(form.name) }}
{{ render_field(form.title) }}
{{ render_field(form.shortDesc) }}
{{ render_field(form.desc) }}
{{ render_field(form.type) }}
{{ render_field(form.license) }}
{{ render_multiselect_field(form.tags) }}
<h2 class="not_txp">Dependency Info</h2>
{{ render_mpackage_field(form.provides_str, class_="not_txp", placeholder="Comma separated list") }}
{{ render_deps_field(form.harddep_str, class_="not_txp not_game", placeholder="Comma separated list") }}
{{ render_deps_field(form.softdep_str, class_="not_txp not_game", placeholder="Comma separated list") }}
{{ render_field(form.license) }}
{{ render_field(form.media_license) }}
{{ render_field(form.repo) }}
{{ render_field(form.website) }}
{{ render_field(form.issueTracker) }}
@@ -29,4 +40,6 @@
{{ render_field(form.edit_desc) }}
{{ render_submit_field(form.submit) }}
</form>
<script src="/static/package_edit.js"></script>
{% endblock %}

View File

@@ -11,7 +11,7 @@
<input type="submit" value="Search" />
<p>
Found {{ packages | count }} packages.
Found {{ packages_count }} packages.
</p>
</form>
@@ -31,4 +31,10 @@
{% from "macros/packagegridtile.html" import render_pkggrid %}
{{ render_pkggrid(packages) }}
<ul class="buttonset linedbuttonset">
{% if prev_url %}<li><a href="{{ prev_url }}">Previous</a></li>{% endif %}
<li>{{ page }} / {{ page_max }}</li>
{% if next_url %}<li><a href="{{ next_url }}">Next</a></li> {% endif %}
</ul>
{% endblock %}

View File

@@ -9,7 +9,7 @@
<form method="POST" action="" enctype="multipart/form-data">
{{ form.hidden_tag() }}
{{ render_field(form.title) }}
{{ render_field(form.title, placeholder="Human readable. Eg: 1.0.0 or 2018-05-28") }}
{{ render_field(form.uploadOpt) }}
{% if package.canMakeReleaseFromVCS() %}
{{ render_field(form.vcsLabel) }}

View File

@@ -61,7 +61,21 @@
<aside class="asideright box box_grey">
<h3>Details</h3>
<div class="box-body">
{% if not package.license.is_foss and not package.media_license.is_foss and package.type != package.type.TXP %}
<div class="box box_grey alert alert-error" style="margin-top: 0;">
<b>Warning:</b> Non-free code and media.
</div>
{% elif not package.license.is_foss and package.type != package.type.TXP %}
<div class="box box_grey alert alert-error" style="margin-top: 0;">
<b>Warning:</b> Non-free code.
</div>
{% elif not package.media_license.is_foss %}
<div class="box box_grey alert alert-error" style="margin-top: 0;">
<b>Warning:</b> Non-free media.
</div>
{% endif %}
<table>
<tr>
<td>Name</td>
@@ -92,7 +106,16 @@
</tr>
<tr>
<td>License</td>
<td>{{ package.license.name }}</td>
<td>
{% if package.license == package.media_license %}
{{ package.license.name }}
{% elif package.type == package.type.TXP %}
{{ package.media_license.name }}
{% else %}
{{ package.license.name }} for code,<br />
{{ package.media_license.name }} for media.
{% endif %}
</td>
</tr>
<tr>
<td>Added</td>
@@ -101,7 +124,7 @@
</table>
<ul class="buttonset linedbuttonset">
{% if package.getDownloadRelease() %}<li><a href="{{ package.getDownloadURL() }}">Download</a></li>{% endif %}
{% if package.getDownloadRelease() %}<li><a href="{{ package.getDownloadURL() }}" class="btn_green">Download</a></li>{% endif %}
{% if package.repo %}<li><a href="{{ package.repo }}">View Source</a></li>{% endif %}
{% if package.forums %}<li><a href="https://forum.minetest.net/viewtopic.php?t={{ package.forums }}">Forums</a></li>{% endif %}
{% if package.issueTracker %}<li><a href="{{ package.issueTracker }}">Issue Tracker</a></li>{% endif %}
@@ -110,9 +133,9 @@
<li><a href="{{ package.getEditURL() }}">Edit</a></li>
<li><a href="{{ package.getNewScreenshotURL() }}">Add screenshot</a></li>
{% endif %}
{% if current_user.is_authenticated %}
{# {% if current_user.is_authenticated %}
<li><a href="{{ package.getCreateEditRequestURL() }}">Suggest Changes</a></li>
{% endif %}
{% endif %} #}
{% if package.checkPerm(current_user, "MAKE_RELEASE") %}
<li><a href="{{ package.getCreateReleaseURL() }}">Create Release</a></li>
{% endif %}
@@ -209,18 +232,52 @@
</tr>
</table> -->
{% if current_user.is_authenticated or requests %}
<h3>Edit Requests</h3>
{#
{% if current_user.is_authenticated or requests %}
<h3>Edit Requests</h3>
<ul>
{% for r in requests %}
<li>
<a href="{{ r.getURL() }}">{{ r.title }}</a>
by
<a href="{{ url_for('user_profile_page', username=r.author.username) }}">{{ r.author.display_name }}</a>
</li>
{% else %}
<li>No edit requests have been made.</li>
{% endfor %}
</ul>
{% endif %}
#}
{% if alternatives %}
<h3>Alternatives</h3>
<ul>
{% for r in requests %}
{% for p in alternatives %}
<li><a href="{{ p.getDetailsURL() }}">{{ p.title }} by {{ p.author.display_name }}</a></li>
{% endfor %}
</ul>
{% endif %}
{% if similar_topics %}
<h3>Similar Forum Topics</h3>
{% if not package.approved and package.type == package.type.MOD %}
<div class="box box_grey alert alert-warning">
Please make sure that this package has the right to
the name '{{ package.name }}'.
See the
<a href="/policy_and_guidance/">Inclusion Policy</a>
for more info.
</div>
{% endif %}
<ul>
{% for t in similar_topics %}
<li>
<a href="{{ r.getURL() }}">{{ r.title }}</a>
by
<a href="{{ url_for('user_profile_page', username=r.author.username) }}">{{ r.author.display_name }}</a>
[{{ t.getType().value }}]
<a href="https://forum.minetest.net/viewtopic.php?t={{ t.topic_id }}">
{{ t.title }} by {{ t.author.display_name }}
</a>
</li>
{% else %}
<li>No edit requests have been made.</li>
{% endfor %}
</ul>
{% endif %}

View File

@@ -5,8 +5,10 @@
{% endblock %}
{% block content %}
{% if canApproveNew %}
<h2>Packages Awaiting Approval</h2>
<h2>Awaiting Approval</h2>
{% if canApproveNew and packages %}
<h3>Packages</h3>
<ul>
{% for p in packages %}
<li><a href="{{ p.getDetailsURL() }}">
@@ -18,8 +20,8 @@
</ul>
{% endif %}
{% if canApproveScn %}
<h2>Screenshots Awaiting Approval</h2>
{% if canApproveScn and screenshots %}
<h3>Screenshots</h3>
<ul>
{% for s in screenshots %}
<li>
@@ -35,8 +37,8 @@
</ul>
{% endif %}
{% if canApproveRel %}
<h2>Releases Awaiting Approval</h2>
{% if canApproveRel and releases %}
<h3>Releases</h3>
<ul>
{% for r in releases %}
<li>
@@ -51,4 +53,18 @@
{% endfor %}
</ul>
{% endif %}
{% if not (packages or screenshots or releases) %}
<p>
<i>All done!</i>
</p>
{% endif %}
<h2>Unadded Topic List</h2>
<p>
There are
<a href="{{ url_for('todo_topics_page') }}">{{ topics_to_add }} packages</a>
to be added to cdb, based on forum topics picked up by Krock's mod search.
</p>
{% endblock %}

View File

@@ -0,0 +1,34 @@
{% extends "base.html" %}
{% block title %}
Topics to be Added
{% endblock %}
{% block content %}
<h1>Topics to be Added</h1>
<p>
{{ total - (topics | count) }} / {{ total }} packages have been added.
{{ topics | count }} remaining.
</p>
<table>
<tr>
<th>Id</th>
<th>Title</th>
<th>Author</th>
<th>Name</th>
<th>Link</th>
</tr>
{% for topic in topics %}
<tr>
<td>{{ topic.topic_id }}</td>
<td>[{{ topic.getType().value }}] <a href="https://forum.minetest.net/viewtopic.php?t={{ topic.topic_id}}">{{ topic.title }}</a></td>
<td><a href="{{ url_for('user_profile_page', username=topic.author.username) }}">{{ topic.author.display_name}}</a></td>
<td>{{ topic.name or ""}}</td>
<td><a href="{{ topic.link }}">{{ topic.link | domain }}</a></td>
</tr>
{% endfor %}
</table>
{% endblock %}

View File

@@ -1,44 +1,45 @@
{% extends "base.html" %}
{% block title %}
Verify forum account
Creating an Account
{% endblock %}
{% block content %}
<div class="box box_grey">
<h2>{{ self.title() }}</h2>
<p>
Create an account by linking it to your forum account and optionally
your github account.
</p>
{% if current_user.is_authenticated %}
<div class="box-body">
<p>
Please log out to continue.
</p>
<p>
<a href="{{ url_for('user.logout', next=url_for('user_claim_page')) }}" class="button">Logout</a>
</p>
{% else %}
<p>
<b>Don't have a forum account?</b>
Unfortunately, you need a forum account to register.
This is because you also need to create forum topics for any packages
you may upload.
If you have a forum account, you'll need to prove that you own it
to get an account on ContentDB.
</p>
<a href="https://forum.minetest.net/ucp.php?mode=register">
Create a Forum Account
</a>
{% endif %}
{% if current_user.is_authenticated %}
<p>
Please log out to continue.
</p>
<p>
<a href="{{ url_for('user.logout', next=url_for('user_claim_page')) }}" class="button">Logout</a>
</p>
{% else %}
<p>
<b>Don't have a forum account?</b>
You don't need one, however it's recommended to make the most
out of the Minetest community.
</p>
<a href="https://forum.minetest.net/ucp.php?mode=register">
Create a Forum Account
</a>
{% endif %}
</div>
</div>
{% if not current_user.is_authenticated %}
<div class="box box_grey">
<h2>Option 1 - Use GitHub field in forum profile</h2>
<form method="post" action="{{ url_for('user_claim_page') }}">
<form method="post" class="box-body" action="{{ url_for('user_claim_page') }}">
<input type="hidden" name="claim_type" value="github">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
@@ -59,10 +60,10 @@ Verify forum account
</form>
</div>
<!--<div class="box box_grey">
<div class="box box_grey">
<h2>Option 2 - Paste verification token into signature</h2>
<form method="post" action="{{ url_for('user_claim_page') }}">
<form method="post" class="box-body" action="{{ url_for('user_claim_page') }}">
<input type="hidden" name="claim_type" value="forum">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
@@ -93,6 +94,22 @@ Verify forum account
<input type="submit" value="Next">
</form>
</div>-->
</div>
<div class="box box_grey">
<h2>Option 3 - Email/password sign up</h2>
<div class="box-body">
<p>
<b>Only do this if you don't have a forum account!</b>
</p>
<p>
If you have a forum account, please use one of the other two
options.
</p>
<a class="button" href="{{ url_for('user.register') }}">Register</a>
</div>
</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,28 @@
{% extends "base.html" %}
{% block title %}
Set Password
{% endblock %}
{% block content %}
<h1>Set Password</h1>
{% from "macros/forms.html" import render_field, render_submit_field %}
<form action="" method="POST" class="form" role="form">
<div class="row">
<div class="col-sm-6 col-md-5 col-lg-4">
{{ form.hidden_tag() }}
{% if not current_user.email %}
{{ render_field(form.email, tabindex=230) }}
{% endif %}
{{ render_field(form.password, tabindex=230) }}
{{ render_field(form.password2, tabindex=240) }}
{{ render_submit_field(form.submit, tabindex=280) }}
</div>
</div>
</form>
{% endblock %}

View File

@@ -6,6 +6,14 @@
{% block content %}
{% if not current_user.is_authenticated and user.rank == user.rank.NOT_JOINED and user.forums_username %}
<div class="box box_grey alert alert-info">
Is this you? Claim your account now!
<a class="alert_right button" href="{{ url_for('user_claim_page', username=user.forums_username) }}">Claim</a>
</div>
{% endif %}
<div class="box box_grey">
<h2>{{ user.display_name }}</h2>
@@ -24,7 +32,7 @@
Minetest Forum
</a>
{% elif user == current_user %}
<a href="">Link Forums Account</a>
No forum account
{% endif %}
{% if (user.forums_username and user.github_username) or user == current_user %}
@@ -42,27 +50,19 @@
{% endif %}
</td>
</tr>
</table>
</div>
<div class="box box_grey">
<h2>Packages</h2>
<div class="box-body">
<ul>
{% for p in user.packages %}
<li><a href="{{ p.getDetailsURL() }}">
{{ p.title }} by {{ p.author.display_name }}
</a></li>
{% else %}
<li><i>No packages available</i></ul>
{% endfor %}
</ul>
{% if user == current_user or user.checkPerm(current_user, "CHANGE_AUTHOR") %}
<p><a class="button" href="{{ url_for('create_edit_package_page', author=user.username) }}">
Create
</a></p>
{% if user == current_user %}
<tr>
<td>Password:</td>
<td>
{% if user.password %}
Set | <a href="{{ url_for('user.change_password') }}">Change</a>
{% else %}
Not set | <a href="{{ url_for('set_password_page') }}">Set</a>
{% endif %}
</td>
</tr>
{% endif %}
</div>
</table>
</div>
{% if form %}
@@ -94,4 +94,38 @@
</form>
</div>
{% endif %}
{% from "macros/packagegridtile.html" import render_pkggrid %}
{{ render_pkggrid(packages, show_author=False) }}
{% if topics_to_add %}
<div class="box box_grey">
<h2>Unadded Packages</h2>
<div class="box-body">
<p>
List of your topics without a matching package.
Powered by Krock's Mod Search.
</p>
<table>
<tr>
<th>Id</th>
<th>Title</th>
<th>Name</th>
<th>Link</th>
</tr>
{% for topic in topics_to_add %}
<tr>
<td>{{ topic.topic_id }}</td>
<td>[{{ topic.getType().value }}] <a href="https://forum.minetest.net/viewtopic.php?t={{ topic.topic_id}}">{{ topic.title }}</a></td>
<td>{{ topic.name or ""}}</td>
<td><a href="{{ topic.link }}">{{ topic.link | domain }}</a></td>
</tr>
{% endfor %}
</table>
</div>
</div>
{% endif %}
{% endblock %}

View File

@@ -68,7 +68,7 @@ def _do_login_user(user, remember_me=False):
user.active = True
if not user.rank.atLeast(UserRank.NEW_MEMBER):
user.rank = UserRank.NEW_MEMBER
user.rank = UserRank.MEMBER
db.session.commit()

View File

@@ -51,7 +51,7 @@ def home_page():
packages = query.order_by(db.desc(Package.created_at)).limit(15).all()
return render_template("index.html", packages=packages, count=count)
from . import users, githublogin, packages, sass, tasks, admin, notifications, tagseditor, meta
from . import users, githublogin, packages, sass, tasks, admin, notifications, tagseditor, meta, thumbnails
@menu.register_menu(app, ".help", "Help", order=19, endpoint_arguments_constructor=lambda: { 'path': 'help' })
@app.route('/<path:path>/')
@@ -62,7 +62,11 @@ def flatpage(path):
@app.before_request
def do_something_whenever_a_request_comes_in():
if current_user.is_authenticated and current_user.rank == UserRank.BANNED:
flash("You have been banned.", "error")
logout_user()
return redirect(url_for('user.login'))
if current_user.is_authenticated:
if current_user.rank == UserRank.BANNED:
flash("You have been banned.", "error")
logout_user()
return redirect(url_for('user.login'))
elif current_user.rank == UserRank.NOT_JOINED:
current_user.rank = UserRank.MEMBER
db.session.commit()

View File

@@ -21,7 +21,7 @@ from flask.ext import menu
from app import app
from app.models import *
from app.tasks.importtasks import importRepoScreenshot, importAllDependencies
from app.tasks.forumtasks import importUsersFromModList
from app.tasks.forumtasks import importUsersFromModList, importKrocksModList
from flask_wtf import FlaskForm
from wtforms import *
from app.utils import loginUser, rank_required
@@ -34,6 +34,9 @@ def admin_page():
if action == "importusers":
task = importUsersFromModList.delay()
return redirect(url_for("check_task", id=task.id, r=url_for("user_list_page")))
elif action == "importmodlist":
task = importKrocksModList.delay()
return redirect(url_for("check_task", id=task.id, r=url_for("todo_topics_page")))
elif action == "importscreenshots":
packages = Package.query \
.filter_by(soft_deleted=False) \

View File

@@ -22,13 +22,13 @@ from app.models import *
@app.route("/metapackages/")
def meta_package_list_page():
mpackages = MetaPackage.query.order_by(db.desc(MetaPackage.name)).all()
return render_template("meta/list.html", mpackages=mpackages)
mpackages = MetaPackage.query.order_by(db.asc(MetaPackage.name)).all()
return render_template("meta/list.html", mpackages=mpackages)
@app.route("/metapackages/<name>/")
def meta_package_page(name):
mpackage = MetaPackage.query.filter_by(name=name).first()
if mpackage is None:
abort(404)
mpackage = MetaPackage.query.filter_by(name=name).first()
if mpackage is None:
abort(404)
return render_template("meta/view.html", mpackage=mpackage)
return render_template("meta/view.html", mpackage=mpackage)

View File

@@ -23,4 +23,11 @@ from app.models import *
@app.route("/notifications/")
@login_required
def notifications_page():
return render_template("notifications/list.html")
return render_template("notifications/list.html")
@app.route("/notifications/clear/", methods=["POST"])
@login_required
def clear_notifications_page():
current_user.notifications.clear()
db.session.commit()
return redirect(url_for("notifications_page"))

View File

@@ -51,16 +51,26 @@ def packages_page():
search = request.args.get("q")
if search is not None:
query = query.filter(Package.title.contains(search))
query = query.filter(Package.title.ilike('%' + search + '%'))
if shouldReturnJson():
pkgs = [package.getAsDictionary(app.config["BASE_URL"]) \
for package in query.all() if package.getDownloadRelease() is not None]
return jsonify(pkgs)
else:
page = int(request.args.get("page") or 1)
num = min(42, int(request.args.get("n") or 100))
query = query.paginate(page, num, True)
next_url = url_for("packages_page", type=type.toName(), q=search, page=query.next_num) \
if query.has_next else None
prev_url = url_for("packages_page", type=type.toName(), q=search, page=query.prev_num) \
if query.has_prev else None
tags = Tag.query.all()
return render_template("packages/list.html", title=title, packages=query.all(), \
query=search, tags=tags, type=None if type is None else type.toName())
return render_template("packages/list.html", title=title, packages=query.items, \
query=search, tags=tags, type=None if type is None else type.toName(), \
next_url=next_url, prev_url=prev_url, page=page, page_max=query.pages, packages_count=query.total)
def getReleases(package):
@@ -78,9 +88,30 @@ def package_page(package):
else:
clearNotifications(package.getDetailsURL())
alternatives = None
if package.type == PackageType.MOD:
alternatives = Package.query \
.filter_by(name=package.name, type=PackageType.MOD, soft_deleted=False) \
.filter(Package.id != package.id) \
.order_by(db.asc(Package.title)) \
.all()
show_similar_topics = current_user == package.author or \
package.checkPerm(current_user, Permission.APPROVE_NEW)
similar_topics = None if not show_similar_topics else \
KrockForumTopic.query \
.filter_by(name=package.name) \
.filter(KrockForumTopic.topic_id != package.forums) \
.filter(~ db.exists().where(Package.forums==KrockForumTopic.topic_id)) \
.order_by(db.asc(KrockForumTopic.name), db.asc(KrockForumTopic.title)) \
.all()
releases = getReleases(package)
requests = [r for r in package.requests if r.status == 0]
return render_template("packages/view.html", package=package, releases=releases, requests=requests)
return render_template("packages/view.html", \
package=package, releases=releases, requests=requests, \
alternatives=alternatives, similar_topics=similar_topics)
@app.route("/packages/<author>/<name>/download/")
@@ -106,6 +137,7 @@ class PackageForm(FlaskForm):
desc = TextAreaField("Long Description (Markdown)", [Optional(), Length(0,10000)])
type = SelectField("Type", [InputRequired()], choices=PackageType.choices(), coerce=PackageType.coerce, default=PackageType.MOD)
license = QuerySelectField("License", [InputRequired()], query_factory=lambda: License.query, get_pk=lambda a: a.id, get_label=lambda a: a.name)
media_license = QuerySelectField("Media License", [InputRequired()], query_factory=lambda: License.query, get_pk=lambda a: a.id, get_label=lambda a: a.name)
provides_str = StringField("Provides (mods included in package)", [Optional(), Length(0,1000)])
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(), Length(0,1000)])
@@ -261,4 +293,4 @@ def delete_package_page(package):
return redirect(url)
from . import todo, screenshots, editrequests, releases
from . import todo, screenshots, releases

View File

@@ -58,8 +58,13 @@ def create_edit_editrequest_page(package, id=None):
edited_package = Package(package)
erequest.applyAll(edited_package)
form = EditRequestForm(request.form, obj=edited_package)
if request.method == "GET":
deps = edited_package.dependencies
form.harddep_str.data = ",".join([str(x) for x in deps if not x.optional])
form.softdep_str.data = ",".join([str(x) for x in deps if x.optional])
form.provides_str.data = MetaPackage.ListToSpec(edited_package.provides)
if request.method == "POST" and form.validate():
if erequest is None:
erequest = EditRequest()

View File

@@ -30,16 +30,14 @@ from wtforms import *
from wtforms.validators import *
class CreatePackageReleaseForm(FlaskForm):
name = StringField("Name")
title = StringField("Title")
title = StringField("Title", [InputRequired(), Length(1, 30)])
uploadOpt = RadioField ("Method", choices=[("upload", "File Upload")], default="upload")
vcsLabel = StringField("VCS Commit or Branch", default="master")
fileUpload = FileField("File Upload")
submit = SubmitField("Save")
class EditPackageReleaseForm(FlaskForm):
name = StringField("Name")
title = StringField("Title")
title = StringField("Title", [InputRequired(), Length(1, 30)])
url = StringField("URL", [URL])
task_id = StringField("Task ID")
approved = BooleanField("Is Approved")

View File

@@ -40,6 +40,25 @@ def todo_page():
if canApproveScn:
screenshots = PackageScreenshot.query.filter_by(approved=False).all()
return render_template("todo.html", title="Reports and Work Queue",
topics_to_add = KrockForumTopic.query \
.filter(~ db.exists().where(Package.forums==KrockForumTopic.topic_id)) \
.count()
return render_template("todo/list.html", title="Reports and Work Queue",
packages=packages, releases=releases, screenshots=screenshots,
canApproveNew=canApproveNew, canApproveRel=canApproveRel, canApproveScn=canApproveScn)
canApproveNew=canApproveNew, canApproveRel=canApproveRel, canApproveScn=canApproveScn,
topics_to_add=topics_to_add)
@app.route("/todo/topics/")
@login_required
def todo_topics_page():
total = KrockForumTopic.query.count()
topics = KrockForumTopic.query \
.filter(~ db.exists().where(Package.forums==KrockForumTopic.topic_id)) \
.order_by(db.asc(KrockForumTopic.name), db.asc(KrockForumTopic.title)) \
.all()
return render_template("todo/topics.html", topics=topics, total=total)

View File

@@ -27,7 +27,7 @@ from app.utils import rank_required
@app.route("/tags/")
@rank_required(UserRank.MODERATOR)
def tag_list_page():
return render_template("tags/list.html", tags=Tag.query.all())
return render_template("tags/list.html", tags=Tag.query.order_by(db.asc(Tag.title)).all())
class TagForm(FlaskForm):
title = StringField("Title", [InputRequired(), Length(3,100)])

46
app/views/thumbnails.py Normal file
View File

@@ -0,0 +1,46 @@
# Content DB
# Copyright (C) 2018 rubenwardy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from flask import *
from app import app
import glob, os
from PIL import Image
ALLOWED_RESOLUTIONS=[(350,233)]
def mkdir(path):
if not os.path.isdir(path):
os.mkdir(path)
mkdir("app/public/thumbnails/")
@app.route("/thumbnails/<img>")
@app.route("/thumbnails/<int:w>x<int:h>/<img>")
def make_thumbnail(img, w=350, h=233):
if not (w, h) in ALLOWED_RESOLUTIONS:
abort(403)
mkdir("app/public/thumbnails/{}x{}/".format(w, h))
cache_filepath = "public/thumbnails/{}x{}/{}".format(w, h, img)
source_filepath = "public/uploads/" + img
im = Image.open("app/" + source_filepath)
im.thumbnail((w, h), Image.ANTIALIAS)
im.save("app/" + cache_filepath, optimize=True)
return send_file(cache_filepath)

View File

@@ -25,15 +25,16 @@ from flask_wtf import FlaskForm
from flask_user.forms import RegisterForm
from wtforms import *
from wtforms.validators import *
from app.utils import rank_required, randomString
from app.utils import rank_required, randomString, loginUser
from app.tasks.forumtasks import checkForumAccount
from app.tasks.emails import sendVerifyEmail
from app.tasks.phpbbparser import getProfile
# Define the User profile form
class UserProfileForm(FlaskForm):
display_name = StringField("Display name", [InputRequired(), Length(2, 20)])
email = StringField("Email")
rank = SelectField("Rank", [InputRequired()], choices=UserRank.choices(), coerce=UserRank.coerce, default=UserRank.NEW_MEMBER)
display_name = StringField("Display name", [Optional(), Length(2, 20)])
email = StringField("Email", [Optional(), Email()])
rank = SelectField("Rank", [Optional()], choices=UserRank.choices(), coerce=UserRank.coerce, default=UserRank.NEW_MEMBER)
submit = SubmitField("Save")
@app.route("/users/", methods=["GET"])
@@ -90,12 +91,78 @@ def user_profile_page(username):
# Redirect to home page
return redirect(url_for("user_profile_page", username=username))
packages = user.packages.filter_by(soft_deleted=False)
if not current_user.is_authenticated or (user != current_user and not current_user.canAccessTodoList()):
packages = packages.filter_by(approved=True)
packages = packages.order_by(db.asc(Package.title))
topics_to_add = None
if current_user == user or user.checkPerm(current_user, Permission.CHANGE_AUTHOR):
topics_to_add = KrockForumTopic.query \
.filter_by(author_id=user.id) \
.filter(~ db.exists().where(Package.forums==KrockForumTopic.topic_id)) \
.order_by(db.asc(KrockForumTopic.name), db.asc(KrockForumTopic.title)) \
.all()
# Process GET or invalid POST
return render_template("users/user_profile_page.html",
user=user, form=form)
user=user, form=form, packages=packages, topics_to_add=topics_to_add)
class SetPasswordForm(FlaskForm):
email = StringField("Email (Optional)", [Optional(), Email()])
password = PasswordField("New password", [InputRequired(), Length(2, 20)])
password2 = PasswordField("Verify password", [InputRequired(), Length(2, 20)])
submit = SubmitField("Save")
@app.route("/user/set-password/", methods=["GET", "POST"])
@login_required
def set_password_page():
if current_user.password is not None:
return redirect(url_for("user.change_password"))
form = SetPasswordForm(request.form)
if request.method == "POST" and form.validate():
one = form.password.data
two = form.password2.data
if one == two:
# Hash password
hashed_password = user_manager.hash_password(form.password.data)
# Change password
user_manager.update_password(current_user, hashed_password)
# Send 'password_changed' email
if user_manager.enable_email and user_manager.send_password_changed_email and current_user.email:
emails.send_password_changed_email(current_user)
# Send password_changed signal
signals.user_changed_password.send(current_app._get_current_object(), user=current_user)
# Prepare one-time system message
flash('Your password has been changed successfully.', 'success')
newEmail = form["email"].data
if newEmail != current_user.email and newEmail.strip() != "":
token = randomString(32)
ver = UserEmailVerification()
ver.user = current_user
ver.token = token
ver.email = newEmail
db.session.add(ver)
db.session.commit()
task = sendVerifyEmail.delay(newEmail, token)
return redirect(url_for("check_task", id=task.id, r=url_for("user_profile_page", username=current_user.username)))
else:
return redirect(url_for("user_profile_page", username=current_user.username))
else:
flash("Passwords do not match", "error")
return render_template("users/set_password.html", form=form)
@app.route("/users/claim/", methods=["GET", "POST"])
@app.route("/user/claim/", methods=["GET", "POST"])
def user_claim_page():
username = request.args.get("username")
if username is None:
@@ -116,8 +183,15 @@ def user_claim_page():
if user is not None and method == "github":
return redirect(url_for("github_signin_page"))
token = None
if "forum_token" in session:
token = session["forum_token"]
else:
token = randomString(32)
session["forum_token"] = token
if request.method == "POST":
ctype = request.form.get("claim_type")
ctype = request.form.get("claim_type")
username = request.form.get("username")
if username is None or len(username.strip()) < 2:
@@ -126,12 +200,41 @@ def user_claim_page():
task = checkForumAccount.delay(username)
return redirect(url_for("check_task", id=task.id, r=url_for("user_claim_page", username=username, method="github")))
elif ctype == "forum":
token = request.form.get("token")
flash("Unimplemented", "error")
user = User.query.filter_by(forums_username=username).first()
if user is not None and user.rank.atLeast(UserRank.NEW_MEMBER):
flash("That user has already been claimed!", "error")
return redirect(url_for("user_claim_page"))
# Get signature
sig = None
try:
profile = getProfile("https://forum.minetest.net", username)
sig = profile.signature
except IOError:
flash("Unable to get forum signature - does the user exist?", "error")
return redirect(url_for("user_claim_page", username=username))
# Look for key
if token in sig:
if user is None:
user = User(username)
user.forums_username = username
db.session.add(user)
db.session.commit()
if loginUser(user):
return redirect(url_for("set_password_page"))
else:
flash("Unable to login as user", "error")
return redirect(url_for("user_claim_page", username=username))
else:
flash("Could not find the key in your signature!", "error")
return redirect(url_for("user_claim_page", username=username))
else:
flash("Unknown claim type", "error")
return render_template("users/claim.html", username=username, key=randomString(32))
return render_template("users/claim.html", username=username, key=token)
@app.route("/users/verify/")
def verify_email_page():

View File

@@ -0,0 +1,35 @@
"""empty message
Revision ID: 28a427cbd4cf
Revises: e9f534df23a8
Create Date: 2018-06-03 01:47:33.006039
"""
from alembic import op
import sqlalchemy as sa
import sqlalchemy.types as ty
# revision identifiers, used by Alembic.
revision = '28a427cbd4cf'
down_revision = 'e9f534df23a8'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('user','username', type_=ty.VARCHAR(50, collation='NOCASE'))
op.alter_column('user','github_username', type_=ty.VARCHAR(50, collation='NOCASE'))
op.alter_column('user','forums_username', type_=ty.VARCHAR(50, collation='NOCASE'))
op.create_index(op.f('ix_user_username'), 'user', ['username'], unique=True)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('user','username', type_=ty.VARCHAR(50))
op.alter_column('user','github_username', type_=ty.VARCHAR(50))
op.alter_column('user','forums_username', type_=ty.VARCHAR(50))
op.drop_index(op.f('ix_user_username'), table_name='user')
# ### end Alembic commands ###

View File

@@ -0,0 +1,34 @@
"""empty message
Revision ID: aa6d21889d22
Revises: b254f55eadd2
Create Date: 2018-05-29 18:28:28.540416
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'aa6d21889d22'
down_revision = 'b254f55eadd2'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('user', 'password',
existing_type=sa.VARCHAR(length=255),
nullable=True,
existing_server_default=sa.text("''"))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('user', 'password',
existing_type=sa.VARCHAR(length=255),
nullable=False,
existing_server_default=sa.text("''"))
# ### end Alembic commands ###

View File

@@ -0,0 +1,35 @@
"""empty message
Revision ID: aa6d7b595a94
Revises: aa6d21889d22
Create Date: 2018-05-29 20:09:56.647358
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'aa6d7b595a94'
down_revision = 'aa6d21889d22'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('package', sa.Column('media_license_id', sa.Integer()))
op.execute('UPDATE package SET media_license_id=license_id')
op.alter_column('package', 'media_license_id', nullable=False)
op.alter_column('package', 'license_id', existing_type=sa.INTEGER(), nullable=False)
op.create_foreign_key(None, 'package', 'license', ['media_license_id'], ['id'])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('package', 'license_id',
existing_type=sa.INTEGER(),
nullable=True)
op.drop_column('package', 'media_license_id')
# ### end Alembic commands ###

View File

@@ -0,0 +1,37 @@
"""empty message
Revision ID: adad68a5e370
Revises: d0bec9e5698e
Create Date: 2018-06-02 18:23:18.123340
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'adad68a5e370'
down_revision = 'd0bec9e5698e'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('krock_forum_topic',
sa.Column('topic_id', sa.Integer(), autoincrement=False, nullable=False),
sa.Column('author_id', sa.Integer(), nullable=False),
sa.Column('ttype', sa.Integer(), nullable=False),
sa.Column('title', sa.String(length=200), nullable=False),
sa.Column('name', sa.String(length=30), nullable=True),
sa.Column('link', sa.String(length=50), nullable=True),
sa.ForeignKeyConstraint(['author_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('topic_id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('krock_forum_topic')
# ### end Alembic commands ###

View File

@@ -0,0 +1,28 @@
"""empty message
Revision ID: d0bec9e5698e
Revises: aa6d7b595a94
Create Date: 2018-05-29 21:23:43.847738
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'd0bec9e5698e'
down_revision = 'aa6d7b595a94'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('license', sa.Column('is_foss', sa.Boolean(), nullable=False, server_default="true"))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('license', 'is_foss')
# ### end Alembic commands ###

View File

@@ -0,0 +1,34 @@
"""empty message
Revision ID: e9f534df23a8
Revises: adad68a5e370
Create Date: 2018-06-02 18:30:54.234366
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'e9f534df23a8'
down_revision = 'adad68a5e370'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('krock_forum_topic', 'link',
existing_type=sa.VARCHAR(length=50),
type_=sa.String(length=200),
existing_nullable=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('package_release', 'link',
existing_type=sa.String(length=200),
type_=sa.VARCHAR(length=50),
existing_nullable=False)
# ### end Alembic commands ###

View File

@@ -12,3 +12,4 @@ beautifulsoup4==4.6.0
lxml==4.2.1
Flask-FlatPages==0.6
Flask-Migrate==2.1.1
pillow==5.1.0

View File

@@ -36,6 +36,7 @@ def defineDummyData(licenses, tags, ruben):
jeija = User("Jeija")
jeija.github_username = "Jeija"
jeija.forums_username = "Jeija"
db.session.add(jeija)
@@ -358,11 +359,16 @@ for tag in ["Inventory", "Mapgen", "Building", \
licenses = {}
for license in ["GPLv2.1", "GPLv3", "LGPLv2.1", "LGPLv3", "AGPLv2.1", "AGPLv3",
"Apache", "BSD 3-Clause", "BSD 2-Clause", "CC0", "CC-BY-SA",
"CC-BY", "CC-BY-NC-SA", "MIT", "ZLib"]:
"CC-BY", "MIT", "ZLib"]:
row = License(license)
licenses[row.name] = row
db.session.add(row)
for license in ["CC-BY-NC-SA"]:
row = License(license, False)
licenses[row.name] = row
db.session.add(row)
if test_data:
defineDummyData(licenses, tags, ruben)