Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba08becd3a | ||
|
|
68b7a5e922 | ||
|
|
e8cc685f89 | ||
|
|
86dd137f75 | ||
|
|
b48f684c0a | ||
|
|
e0e6f3392d | ||
|
|
b1c349cc35 | ||
|
|
40aac38d43 | ||
|
|
051df7ab87 | ||
|
|
bb1f6702f6 | ||
|
|
c9542427b4 | ||
|
|
8601c5e075 | ||
|
|
3d97eca387 | ||
|
|
99b21f996c | ||
|
|
700cd7ce1f | ||
|
|
8d9da5a750 | ||
|
|
9a36bb7d72 | ||
|
|
e424dc57e7 | ||
|
|
7d60e2f671 | ||
|
|
8b2018852e | ||
|
|
0aeefa2387 | ||
|
|
4420f489ac | ||
|
|
aad4fd2a70 | ||
|
|
d2bda0fded | ||
|
|
b84727b187 | ||
|
|
6fd36dbfff | ||
|
|
8e134a7c85 | ||
|
|
389258a10c | ||
|
|
3657316fa2 | ||
|
|
a6f4249afb | ||
|
|
70afb94d3b | ||
|
|
8984adaa72 |
@@ -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
|
||||
```
|
||||
|
||||
@@ -2,3 +2,4 @@ title: Help
|
||||
|
||||
* [Package Tags](package_tags)
|
||||
* [Ranks and Permissions](ranks_permissions)
|
||||
* [Reporting Content](reporting)
|
||||
|
||||
8
app/flatpages/help/reporting.md
Normal file
8
app/flatpages/help/reporting.md
Normal 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>
|
||||
@@ -10,20 +10,45 @@ 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.
|
||||
|
||||
## 2. Technical Names
|
||||
Also see the [help page on tags](/help/package_tags/).
|
||||
|
||||
### 2.1 Right to a name
|
||||
|
||||
## 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.
|
||||
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.
|
||||
|
||||
### 2.2 Mod Forks and Reimplementations
|
||||
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
|
||||
@@ -33,15 +58,17 @@ We reserve the right to decide whether a mod counts as a fork or
|
||||
reimplementation of the mod that owns the name.
|
||||
|
||||
|
||||
## 3. License
|
||||
## 4. Licenses
|
||||
|
||||
### 3.1 Allowed 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 is not permitted.
|
||||
This includes CC-ND (No-Derivatives) and lots of closed source licenses.
|
||||
**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.
|
||||
@@ -49,7 +76,7 @@ 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).
|
||||
|
||||
### 3.2 Recommended Licenses
|
||||
### 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
|
||||
@@ -61,6 +88,18 @@ 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.
|
||||
|
||||
|
||||
## 4. Other
|
||||
## 5. Promotions and Advertisements (inc. asking for donations)
|
||||
|
||||
See the [help page on tags](/help/package_tags/).
|
||||
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.
|
||||
|
||||
114
app/models.py
114
app/models.py
@@ -76,6 +76,7 @@ class Permission(enum.Enum):
|
||||
CHANGE_RANK = "CHANGE_RANK"
|
||||
CHANGE_EMAIL = "CHANGE_EMAIL"
|
||||
EDIT_EDITREQUEST = "EDIT_EDITREQUEST"
|
||||
SEE_THREAD = "SEE_THREAD"
|
||||
|
||||
# Only return true if the permission is valid for *all* contexts
|
||||
# See Package.checkPerm for package-specific contexts
|
||||
@@ -91,20 +92,19 @@ class Permission(enum.Enum):
|
||||
else:
|
||||
raise Exception("Non-global permission checked globally. Use Package.checkPerm or User.checkPerm instead.")
|
||||
|
||||
|
||||
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)
|
||||
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)
|
||||
@@ -120,6 +120,8 @@ class User(db.Model, UserMixin):
|
||||
# causednotifs = db.relationship("Notification", backref="causer", lazy="dynamic")
|
||||
packages = db.relationship("Package", backref="author", lazy="dynamic")
|
||||
requests = db.relationship("EditRequest", backref="author", lazy="dynamic")
|
||||
threads = db.relationship("Thread", backref="author", lazy="dynamic")
|
||||
replies = db.relationship("ThreadReply", backref="author", lazy="dynamic")
|
||||
|
||||
def __init__(self, username, active=False, email=None, password=None):
|
||||
import datetime
|
||||
@@ -337,6 +339,9 @@ class Package(db.Model):
|
||||
approved = db.Column(db.Boolean, nullable=False, default=False)
|
||||
soft_deleted = db.Column(db.Boolean, nullable=False, default=False)
|
||||
|
||||
review_thread_id = db.Column(db.Integer, db.ForeignKey("thread.id"), nullable=True, default=None)
|
||||
review_thread = db.relationship("Thread", foreign_keys=[review_thread_id])
|
||||
|
||||
# Downloads
|
||||
repo = db.Column(db.String(200), nullable=True)
|
||||
website = db.Column(db.String(200), nullable=True)
|
||||
@@ -434,26 +439,6 @@ class Package(db.Model):
|
||||
|
||||
return None
|
||||
|
||||
def canImportScreenshot(self):
|
||||
if self.repo is None:
|
||||
return False
|
||||
|
||||
url = urlparse(self.repo)
|
||||
if url.netloc == "github.com":
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def canMakeReleaseFromVCS(self):
|
||||
if self.repo is None:
|
||||
return False
|
||||
|
||||
url = urlparse(self.repo)
|
||||
if url.netloc == "github.com":
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def checkPerm(self, user, perm):
|
||||
if not user.is_authenticated:
|
||||
return False
|
||||
@@ -678,6 +663,87 @@ class EditRequestChange(db.Model):
|
||||
else:
|
||||
setattr(package, self.key.name, self.newValue)
|
||||
|
||||
|
||||
watchers = db.Table("watchers",
|
||||
db.Column("user_id", db.Integer, db.ForeignKey("user.id"), primary_key=True),
|
||||
db.Column("thread_id", db.Integer, db.ForeignKey("thread.id"), primary_key=True)
|
||||
)
|
||||
|
||||
class Thread(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
package_id = db.Column(db.Integer, db.ForeignKey("package.id"), nullable=True)
|
||||
package = db.relationship("Package", foreign_keys=[package_id])
|
||||
|
||||
author_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
|
||||
title = db.Column(db.String(100), nullable=False)
|
||||
private = db.Column(db.Boolean, server_default="0")
|
||||
|
||||
created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
|
||||
|
||||
replies = db.relationship("ThreadReply", backref="thread", lazy="dynamic")
|
||||
|
||||
watchers = db.relationship("User", secondary=watchers, lazy="subquery", \
|
||||
backref=db.backref("watching", lazy=True))
|
||||
|
||||
def checkPerm(self, user, perm):
|
||||
if not user.is_authenticated:
|
||||
return not self.private
|
||||
|
||||
if type(perm) == str:
|
||||
perm = Permission[perm]
|
||||
elif type(perm) != Permission:
|
||||
raise Exception("Unknown permission given to Thread.checkPerm()")
|
||||
|
||||
isOwner = user == self.author
|
||||
|
||||
if perm == Permission.SEE_THREAD:
|
||||
return not self.private or isOwner or user.rank.atLeast(UserRank.EDITOR)
|
||||
|
||||
else:
|
||||
raise Exception("Permission {} is not related to threads".format(perm.name))
|
||||
|
||||
class ThreadReply(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
thread_id = db.Column(db.Integer, db.ForeignKey("thread.id"), nullable=False)
|
||||
comment = db.Column(db.String(500), nullable=False)
|
||||
author_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
|
||||
created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
REPO_BLACKLIST = [".zip", "mediafire.com", "dropbox.com", "weebly.com", \
|
||||
"minetest.net", "dropboxusercontent.com", "4shared.com", \
|
||||
"digitalaudioconcepts.com", "hg.intevation.org", "www.wtfpl.net", \
|
||||
"imageshack.com", "imgur.com"]
|
||||
|
||||
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
|
||||
|
||||
def getRepoURL(self):
|
||||
for item in REPO_BLACKLIST:
|
||||
if item in self.link:
|
||||
return None
|
||||
|
||||
return self.link.replace("repo.or.cz/w/", "repo.or.cz/")
|
||||
|
||||
|
||||
# Setup Flask-User
|
||||
db_adapter = SQLAlchemyAdapter(db, User) # Register the User model
|
||||
user_manager = UserManager(db_adapter, app) # Initialize Flask-User
|
||||
|
||||
@@ -9,19 +9,11 @@ $(function() {
|
||||
$(".pkg_meta").show()
|
||||
}
|
||||
|
||||
function repoIsSupported(url) {
|
||||
try {
|
||||
return URI(url).hostname() == "github.com"
|
||||
} catch(e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
$(".pkg_meta").hide()
|
||||
$(".pkg_wiz_1").show()
|
||||
$("#pkg_wiz_1_next").click(function() {
|
||||
const repoURL = $("#repo").val();
|
||||
if (repoIsSupported(repoURL)) {
|
||||
if (repoURL.trim() != "") {
|
||||
$(".pkg_wiz_1").hide()
|
||||
$(".pkg_wiz_2").show()
|
||||
$(".pkg_repo").hide()
|
||||
@@ -35,19 +27,24 @@ $(function() {
|
||||
}
|
||||
|
||||
performTask("/tasks/getmeta/new/?url=" + encodeURI(repoURL)).then(function(result) {
|
||||
$("#name").val(result.name || "")
|
||||
setSpecial("#provides_str", result.name || "")
|
||||
$("#title").val(result.title || "")
|
||||
$("#name").val(result.name)
|
||||
setSpecial("#provides_str", result.provides)
|
||||
$("#title").val(result.title)
|
||||
$("#repo").val(result.repo || repoURL)
|
||||
$("#issueTracker").val(result.issueTracker || "")
|
||||
$("#desc").val(result.description || "")
|
||||
$("#shortDesc").val(result.short_description || "")
|
||||
setSpecial("#harddep_str", result.depends || "")
|
||||
setSpecial("#softdep_str", result.optional_depends || "")
|
||||
$("#shortDesc").val(result.short_description || "")
|
||||
$("#issueTracker").val(result.issueTracker)
|
||||
$("#desc").val(result.description)
|
||||
$("#shortDesc").val(result.short_description)
|
||||
setSpecial("#harddep_str", result.depends)
|
||||
setSpecial("#softdep_str", result.optional_depends)
|
||||
$("#shortDesc").val(result.short_description)
|
||||
if (result.forumId) {
|
||||
$("#forums").val(result.forumId)
|
||||
}
|
||||
|
||||
if (result.type && result.type.length > 2) {
|
||||
$("#type").val(result.type)
|
||||
}
|
||||
|
||||
finish()
|
||||
}).catch(function(e) {
|
||||
alert(e)
|
||||
|
||||
@@ -22,7 +22,7 @@ function pollTask(poll_url, disableTimeout) {
|
||||
var tries = 0;
|
||||
function retry() {
|
||||
tries++;
|
||||
if (!disableTimeout && tries > 10) {
|
||||
if (!disableTimeout && tries > 30) {
|
||||
reject("timeout")
|
||||
} else {
|
||||
const interval = Math.min(tries*100, 1000)
|
||||
|
||||
49
app/scss/comments.scss
Normal file
49
app/scss/comments.scss
Normal file
@@ -0,0 +1,49 @@
|
||||
.comments, .comments li {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 1px solid #444;
|
||||
}
|
||||
|
||||
.comments {
|
||||
border-radius: 5px;
|
||||
margin: 15px 0;
|
||||
background: #333;
|
||||
|
||||
.info_strip, .msg {
|
||||
display: block;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.info_strip {
|
||||
padding: 0.2em 1em;
|
||||
border-bottom: 1px solid #444;
|
||||
}
|
||||
|
||||
.msg {
|
||||
padding: 1em;
|
||||
background: #222;
|
||||
}
|
||||
|
||||
.author {
|
||||
font-weight: bold;
|
||||
float: left;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.info_strip span {
|
||||
float: right;
|
||||
display: inline-block;
|
||||
color: #bbb;
|
||||
}
|
||||
}
|
||||
|
||||
.comment_form {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.comment_form textarea {
|
||||
min-width: 60%;
|
||||
max-width: 100%;
|
||||
margin: 0 0 1em 0;
|
||||
}
|
||||
@@ -235,10 +235,6 @@ select:not([multiple]) {
|
||||
|
||||
/* Alerts */
|
||||
|
||||
.alert {
|
||||
padding: 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.alert .alert_right, .alert > form {
|
||||
display: inline-block;
|
||||
@@ -249,36 +245,41 @@ select:not([multiple]) {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.alert .alert_right:not(.button) {
|
||||
padding: 0;
|
||||
}
|
||||
.alert {
|
||||
padding: 10px;
|
||||
position: relative;
|
||||
|
||||
.alert .alert_right form {
|
||||
height: 100%;
|
||||
}
|
||||
.alert_right:not(.button) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.alert form {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.alert_right form {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.alert input {
|
||||
height: 100%;
|
||||
}
|
||||
form {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.alert 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 {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.alert input:hover, .button:hover {
|
||||
border: 0;
|
||||
border-left: 1px solid rgba(255,255,255,0.2);
|
||||
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 {
|
||||
@@ -314,6 +315,11 @@ select:not([multiple]) {
|
||||
border: 1px solid #c96;
|
||||
}
|
||||
|
||||
.alert-primary {
|
||||
background: #339;
|
||||
border: 1px solid #66a;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: #161;
|
||||
border: 1px solid #393;
|
||||
|
||||
@@ -3,3 +3,4 @@
|
||||
@import "nav.scss";
|
||||
@import "packages.scss";
|
||||
@import "packagegrid.scss";
|
||||
@import "comments.scss";
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -15,16 +15,18 @@
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import flask, json, os
|
||||
import flask, json, os, git, tempfile, shutil
|
||||
from git import GitCommandError
|
||||
from flask.ext.sqlalchemy import SQLAlchemy
|
||||
from urllib.error import HTTPError
|
||||
import urllib.request
|
||||
from urllib.parse import urlparse, quote_plus
|
||||
from urllib.parse import urlparse, quote_plus, urlsplit
|
||||
from app import app
|
||||
from app.models import *
|
||||
from app.tasks import celery, TaskError
|
||||
from app.utils import randomString
|
||||
|
||||
|
||||
class GithubURLMaker:
|
||||
def __init__(self, url):
|
||||
# Rewrite path
|
||||
@@ -46,18 +48,6 @@ class GithubURLMaker:
|
||||
def getRepoURL(self):
|
||||
return "https://github.com/{}/{}".format(self.user, self.repo)
|
||||
|
||||
def getIssueTrackerURL(self):
|
||||
return "https://github.com/{}/{}/issues/".format(self.user, self.repo)
|
||||
|
||||
def getModConfURL(self):
|
||||
return self.baseUrl + "/mod.conf"
|
||||
|
||||
def getDescURL(self):
|
||||
return self.baseUrl + "/description.txt"
|
||||
|
||||
def getDependsURL(self):
|
||||
return self.baseUrl + "/depends.txt"
|
||||
|
||||
def getScreenshotURL(self):
|
||||
return self.baseUrl + "/screenshot.png"
|
||||
|
||||
@@ -69,7 +59,6 @@ class GithubURLMaker:
|
||||
return "https://github.com/{}/{}/archive/{}.zip" \
|
||||
.format(self.user, self.repo, commit)
|
||||
|
||||
|
||||
krock_list_cache = None
|
||||
krock_list_cache_by_name = None
|
||||
def getKrockList():
|
||||
@@ -97,9 +86,9 @@ def getKrockList():
|
||||
return {
|
||||
"title": x["title"],
|
||||
"author": x["author"],
|
||||
"name": x["name"],
|
||||
"name": x["name"],
|
||||
"topicId": x["topicId"],
|
||||
"link": x["link"],
|
||||
"link": x["link"],
|
||||
}
|
||||
|
||||
krock_list_cache = [g(x) for x in list if h(x)]
|
||||
@@ -143,99 +132,208 @@ def parseConf(string):
|
||||
return retval
|
||||
|
||||
|
||||
@celery.task()
|
||||
def getMeta(urlstr, author):
|
||||
url = urlparse(urlstr)
|
||||
class PackageTreeNode:
|
||||
def __init__(self, baseDir, author=None, repo=None, name=None):
|
||||
print("Scanning " + baseDir)
|
||||
self.baseDir = baseDir
|
||||
self.author = author
|
||||
self.name = name
|
||||
self.repo = repo
|
||||
self.meta = None
|
||||
self.children = []
|
||||
|
||||
urlmaker = None
|
||||
if url.netloc == "github.com":
|
||||
urlmaker = GithubURLMaker(url)
|
||||
else:
|
||||
raise TaskError("Unsupported repo")
|
||||
# Detect type
|
||||
type = None
|
||||
is_modpack = False
|
||||
if os.path.isfile(baseDir + "/game.conf"):
|
||||
type = PackageType.GAME
|
||||
elif os.path.isfile(baseDir + "/init.lua"):
|
||||
type = PackageType.MOD
|
||||
elif os.path.isfile(baseDir + "/modpack.txt"):
|
||||
type = PackageType.MOD
|
||||
is_modpack = True
|
||||
elif os.path.isdir(baseDir + "/mods"):
|
||||
type = PackageType.GAME
|
||||
elif os.listdir(baseDir) == []:
|
||||
# probably a submodule
|
||||
return
|
||||
else:
|
||||
raise TaskError("Unable to detect package type!")
|
||||
|
||||
if not urlmaker.isValid():
|
||||
raise TaskError("Error! Url maker not valid")
|
||||
self.type = type
|
||||
self.readMetaFiles()
|
||||
|
||||
result = {}
|
||||
if self.type == PackageType.GAME:
|
||||
self.addChildrenFromModDir(baseDir + "/mods")
|
||||
elif is_modpack:
|
||||
self.addChildrenFromModDir(baseDir)
|
||||
|
||||
result["repo"] = urlmaker.getRepoURL()
|
||||
result["issueTracker"] = urlmaker.getIssueTrackerURL()
|
||||
|
||||
try:
|
||||
contents = urllib.request.urlopen(urlmaker.getModConfURL()).read().decode("utf-8")
|
||||
conf = parseConf(contents)
|
||||
for key in ["name", "description", "title", "depends", "optional_depends"]:
|
||||
try:
|
||||
result[key] = conf[key]
|
||||
except KeyError:
|
||||
pass
|
||||
except HTTPError:
|
||||
print("mod.conf does not exist")
|
||||
def readMetaFiles(self):
|
||||
result = {}
|
||||
|
||||
if "name" in result:
|
||||
result["title"] = result["name"].replace("_", " ").title()
|
||||
|
||||
if not "description" in result:
|
||||
# .conf file
|
||||
try:
|
||||
contents = urllib.request.urlopen(urlmaker.getDescURL()).read().decode("utf-8")
|
||||
result["description"] = contents.strip()
|
||||
except HTTPError:
|
||||
with open(self.baseDir + "/mod.conf", "r") as myfile:
|
||||
conf = parseConf(myfile.read())
|
||||
for key in ["name", "description", "title", "depends", "optional_depends"]:
|
||||
try:
|
||||
result[key] = conf[key]
|
||||
except KeyError:
|
||||
pass
|
||||
except IOError:
|
||||
print("description.txt does not exist!")
|
||||
|
||||
import re
|
||||
pattern = re.compile("^([a-z0-9_]+)\??$")
|
||||
if not "depends" in result and not "optional_depends" in result:
|
||||
try:
|
||||
contents = urllib.request.urlopen(urlmaker.getDependsURL()).read().decode("utf-8")
|
||||
soft = []
|
||||
hard = []
|
||||
for line in contents.split("\n"):
|
||||
line = line.strip()
|
||||
if pattern.match(line):
|
||||
if line[len(line) - 1] == "?":
|
||||
soft.append( line[:-1])
|
||||
else:
|
||||
hard.append(line)
|
||||
# description.txt
|
||||
if not "description" in result:
|
||||
try:
|
||||
with open(self.baseDir + "/description.txt", "r") as myfile:
|
||||
result["description"] = myfile.read()
|
||||
except IOError:
|
||||
print("description.txt does not exist!")
|
||||
|
||||
result["depends"] = ",".join(hard)
|
||||
result["optional_depends"] = ",".join(soft)
|
||||
# depends.txt
|
||||
import re
|
||||
pattern = re.compile("^([a-z0-9_]+)\??$")
|
||||
if not "depends" in result and not "optional_depends" in result:
|
||||
try:
|
||||
with open(self.baseDir + "/depends.txt", "r") as myfile:
|
||||
contents = myfile.read()
|
||||
soft = []
|
||||
hard = []
|
||||
for line in contents.split("\n"):
|
||||
line = line.strip()
|
||||
if pattern.match(line):
|
||||
if line[len(line) - 1] == "?":
|
||||
soft.append( line[:-1])
|
||||
else:
|
||||
hard.append(line)
|
||||
|
||||
result["depends"] = hard
|
||||
result["optional_depends"] = soft
|
||||
|
||||
except IOError:
|
||||
print("depends.txt does not exist!")
|
||||
|
||||
else:
|
||||
if "depends" in result:
|
||||
result["depends"] = [x.strip() for x in result["depends"].split(",")]
|
||||
if "optional_depends" in result:
|
||||
result["optional_depends"] = [x.strip() for x in result["optional_depends"].split(",")]
|
||||
|
||||
|
||||
except HTTPError:
|
||||
print("depends.txt does not exist!")
|
||||
# Calculate Title
|
||||
if "name" in result and not "title" in result:
|
||||
result["title"] = result["name"].replace("_", " ").title()
|
||||
|
||||
if "description" in result:
|
||||
desc = result["description"]
|
||||
idx = desc.find(".") + 1
|
||||
cutIdx = min(len(desc), 200 if idx < 5 else idx)
|
||||
result["short_description"] = desc[:cutIdx]
|
||||
# Calculate short description
|
||||
if "description" in result:
|
||||
desc = result["description"]
|
||||
idx = desc.find(".") + 1
|
||||
cutIdx = min(len(desc), 200 if idx < 5 else idx)
|
||||
result["short_description"] = desc[:cutIdx]
|
||||
|
||||
# Get forum ID
|
||||
info = findModInfo(self.author, result.get("name"), self.repo)
|
||||
if info is not None:
|
||||
result["forumId"] = info.get("topicId")
|
||||
|
||||
if "name" in result:
|
||||
self.name = result["name"]
|
||||
del result["name"]
|
||||
|
||||
self.meta = result
|
||||
|
||||
def addChildrenFromModDir(self, dir):
|
||||
for entry in next(os.walk(dir))[1]:
|
||||
path = dir + "/" + entry
|
||||
if not entry.startswith('.') and os.path.isdir(path):
|
||||
self.children.append(PackageTreeNode(path, name=entry))
|
||||
|
||||
|
||||
info = findModInfo(author, result.get("name"), result["repo"])
|
||||
if info is not None:
|
||||
result["forumId"] = info.get("topicId")
|
||||
def fold(self, attr, key=None, acc=None):
|
||||
if acc is None:
|
||||
acc = set()
|
||||
|
||||
if self.meta is None:
|
||||
return acc
|
||||
|
||||
at = getattr(self, attr)
|
||||
value = at if key is None else at.get(key)
|
||||
|
||||
if isinstance(value, list):
|
||||
acc |= set(value)
|
||||
elif value is not None:
|
||||
acc.add(value)
|
||||
|
||||
for child in self.children:
|
||||
child.fold(attr, key, acc)
|
||||
|
||||
return acc
|
||||
|
||||
def get(self, key):
|
||||
return self.meta.get(key)
|
||||
|
||||
def generateGitURL(urlstr):
|
||||
scheme, netloc, path, query, frag = urlsplit(urlstr)
|
||||
|
||||
return "http://:@" + netloc + path + query
|
||||
|
||||
# Clones a repo from an unvalidated URL.
|
||||
# Returns a tuple of path and repo on sucess.
|
||||
# Throws `TaskError` on failure.
|
||||
# Caller is responsible for deleting returned directory.
|
||||
def cloneRepo(urlstr, ref=None, recursive=False):
|
||||
gitDir = tempfile.gettempdir() + "/" + randomString(10)
|
||||
|
||||
err = None
|
||||
try:
|
||||
gitUrl = generateGitURL(urlstr)
|
||||
print("Cloning from " + gitUrl)
|
||||
repo = git.Repo.clone_from(gitUrl, gitDir, \
|
||||
progress=None, env=None, depth=1, recursive=recursive, kill_after_timeout=15)
|
||||
|
||||
if ref is not None:
|
||||
repo.create_head("myhead", ref).checkout()
|
||||
return gitDir, repo
|
||||
except GitCommandError as e:
|
||||
# This is needed to stop the backtrace being weird
|
||||
err = e.stderr
|
||||
|
||||
raise TaskError(err.replace("stderr: ", "") \
|
||||
.replace("Cloning into '" + gitDir + "'...", "") \
|
||||
.strip())
|
||||
|
||||
@celery.task()
|
||||
def getMeta(urlstr, author):
|
||||
gitDir, _ = cloneRepo(urlstr, recursive=True)
|
||||
tree = PackageTreeNode(gitDir, author=author, repo=urlstr)
|
||||
shutil.rmtree(gitDir)
|
||||
|
||||
result = {}
|
||||
result["name"] = tree.name
|
||||
result["provides"] = tree.fold("name")
|
||||
result["type"] = tree.type.name
|
||||
|
||||
for key in ["depends", "optional_depends"]:
|
||||
result[key] = tree.fold("meta", key)
|
||||
|
||||
for key in ["title", "repo", "issueTracker", "forumId", "description", "short_description"]:
|
||||
result[key] = tree.get(key)
|
||||
|
||||
for mod in result["provides"]:
|
||||
result["depends"].discard(mod)
|
||||
result["optional_depends"].discard(mod)
|
||||
|
||||
for key, value in result.items():
|
||||
if isinstance(value, set):
|
||||
result[key] = list(value)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@celery.task()
|
||||
def makeVCSRelease(id, branch):
|
||||
release = PackageRelease.query.get(id)
|
||||
|
||||
if release is None:
|
||||
raise TaskError("No such release!")
|
||||
|
||||
if release.package is None:
|
||||
raise TaskError("No package attached to release")
|
||||
|
||||
url = urlparse(release.package.repo)
|
||||
|
||||
urlmaker = None
|
||||
if url.netloc == "github.com":
|
||||
urlmaker = GithubURLMaker(url)
|
||||
else:
|
||||
raise TaskError("Unsupported repo")
|
||||
|
||||
def makeVCSReleaseFromGithub(id, branch, release, url):
|
||||
urlmaker = GithubURLMaker(url)
|
||||
if not urlmaker.isValid():
|
||||
raise TaskError("Invalid github repo URL")
|
||||
|
||||
@@ -254,6 +352,37 @@ def makeVCSRelease(id, branch):
|
||||
return release.url
|
||||
|
||||
|
||||
|
||||
@celery.task()
|
||||
def makeVCSRelease(id, branch):
|
||||
release = PackageRelease.query.get(id)
|
||||
if release is None:
|
||||
raise TaskError("No such release!")
|
||||
elif release.package is None:
|
||||
raise TaskError("No package attached to release")
|
||||
|
||||
urlmaker = None
|
||||
url = urlparse(release.package.repo)
|
||||
if url.netloc == "github.com":
|
||||
return makeVCSReleaseFromGithub(id, branch, release, url)
|
||||
else:
|
||||
gitDir, repo = cloneRepo(release.package.repo, ref=branch, recursive=True)
|
||||
|
||||
try:
|
||||
filename = randomString(10) + ".zip"
|
||||
destPath = os.path.join("app/public/uploads", filename)
|
||||
with open(destPath, "wb") as fp:
|
||||
repo.archive(fp)
|
||||
|
||||
release.url = "/uploads/" + filename
|
||||
print(release.url)
|
||||
release.task_id = None
|
||||
db.session.commit()
|
||||
|
||||
return release.url
|
||||
finally:
|
||||
shutil.rmtree(gitDir)
|
||||
|
||||
@celery.task()
|
||||
def importRepoScreenshot(id):
|
||||
package = Package.query.get(id)
|
||||
@@ -261,34 +390,35 @@ def importRepoScreenshot(id):
|
||||
raise Exception("Unexpected none package")
|
||||
|
||||
# Get URL Maker
|
||||
url = urlparse(package.repo)
|
||||
urlmaker = None
|
||||
if url.netloc == "github.com":
|
||||
urlmaker = GithubURLMaker(url)
|
||||
else:
|
||||
raise TaskError("Unsupported repo")
|
||||
|
||||
if not urlmaker.isValid():
|
||||
raise TaskError("Error! Url maker not valid")
|
||||
|
||||
try:
|
||||
filename = randomString(10) + ".png"
|
||||
imagePath = os.path.join("app/public/uploads", filename)
|
||||
print(imagePath)
|
||||
urllib.request.urlretrieve(urlmaker.getScreenshotURL(), imagePath)
|
||||
gitDir, _ = cloneRepo(package.repo)
|
||||
except TaskError as e:
|
||||
# ignore download errors
|
||||
print(e)
|
||||
return None
|
||||
|
||||
ss = PackageScreenshot()
|
||||
ss.approved = True
|
||||
ss.package = package
|
||||
ss.title = "screenshot.png"
|
||||
ss.url = "/uploads/" + filename
|
||||
db.session.add(ss)
|
||||
db.session.commit()
|
||||
# Find and import screenshot
|
||||
try:
|
||||
for ext in ["png", "jpg", "jpeg"]:
|
||||
sourcePath = gitDir + "/screenshot." + ext
|
||||
if os.path.isfile(sourcePath):
|
||||
filename = randomString(10) + "." + ext
|
||||
destPath = os.path.join("app/public/uploads", filename)
|
||||
shutil.copyfile(sourcePath, destPath)
|
||||
|
||||
return "/uploads/" + filename
|
||||
except HTTPError:
|
||||
print("screenshot.png does not exist")
|
||||
ss = PackageScreenshot()
|
||||
ss.approved = True
|
||||
ss.package = package
|
||||
ss.title = "screenshot.png"
|
||||
ss.url = "/uploads/" + filename
|
||||
db.session.add(ss)
|
||||
db.session.commit()
|
||||
|
||||
return "/uploads/" + filename
|
||||
finally:
|
||||
shutil.rmtree(gitDir)
|
||||
|
||||
print("screenshot.png does not exist")
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -103,9 +103,9 @@
|
||||
{% endblock %}
|
||||
|
||||
<footer>
|
||||
Copyright © 2018 to <a href="https://rubenwardy.com/">rubenwardy</a> |
|
||||
ContentDB © 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="{{ url_for('flatpage', path='policy_and_guidance') }}">Policy and Guidance</a> |
|
||||
<a href="https://github.com/minetest/contentdb">GitHub</a>
|
||||
<a href="{{ url_for('flatpage', path='help/reporting') }}">Report / DMCA</a>
|
||||
</footer>
|
||||
</html>
|
||||
|
||||
@@ -11,10 +11,6 @@ Sign in
|
||||
<h2>{%trans%}Sign in{%endtrans%}</h2>
|
||||
|
||||
<form action="" method="POST" class="form box-body" role="form">
|
||||
<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() }}
|
||||
|
||||
@@ -38,17 +34,13 @@ Sign in
|
||||
{# Password field #}
|
||||
{% set field = form.password %}
|
||||
<div class="form-group {% if field.errors %}has-error{% endif %}">
|
||||
{# Label on left, "Forgot your Password?" on right #}
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<label for="{{ field.id }}" class="control-label">{{ field.label.text }}</label>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
{% if user_manager.enable_forgot_password %}
|
||||
<label for="{{ field.id }}" class="control-label">{{ field.label.text }}
|
||||
{% if user_manager.enable_forgot_password %}
|
||||
<a href="{{ url_for('user.forgot_password') }}" tabindex='195'>
|
||||
{%trans%}Forgot your Password?{%endtrans%}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
[{%trans%}Forgot My Password{%endtrans%}]</a>
|
||||
{% endif %}
|
||||
</label>
|
||||
</div>
|
||||
{{ field(class_='form-control', tabindex=120) }}
|
||||
{% if field.errors %}
|
||||
@@ -64,7 +56,12 @@ Sign in
|
||||
{% endif %}
|
||||
|
||||
{# Submit button #}
|
||||
{{ render_submit_field(form.submit, tabindex=180) }}
|
||||
<p>
|
||||
{{ render_submit_field(form.submit, tabindex=180) }}
|
||||
</p>
|
||||
|
||||
<h3>Sign in with Github</h3>
|
||||
<p><a class="button" href="{{ url_for('github_signin_page') }}">GitHub</a></p>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
34
app/templates/macros/threads.html
Normal file
34
app/templates/macros/threads.html
Normal file
@@ -0,0 +1,34 @@
|
||||
{% macro render_thread(thread, current_user) -%}
|
||||
<ul class="comments">
|
||||
{% for r in thread.replies %}
|
||||
<li>
|
||||
<div class="info_strip">
|
||||
<a class="author {{ r.author.rank.name }}"
|
||||
href="{{ url_for('user_profile_page', username=r.author.username) }}">
|
||||
{{ r.author.display_name }}</a>
|
||||
<span>{{ r.created_at | datetime }}</span>
|
||||
<div class="clearboth"></div>
|
||||
</div>
|
||||
<div class="msg">
|
||||
{{ r.comment }}
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
<form method="post" action="{{ url_for('thread_page', id=thread.id)}}" class="comment_form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<textarea required maxlength=500 name="comment"></textarea><br />
|
||||
<input type="submit" value="Comment" />
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro render_threadlist(threads) -%}
|
||||
<ul>
|
||||
{% for t in threads %}
|
||||
<li><a href="{{ url_for('thread_page', id=t.id) }}">{{ t.title }}</a> by {{ t.author.display_name }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endmacro %}
|
||||
26
app/templates/macros/topictable.html
Normal file
26
app/templates/macros/topictable.html
Normal file
@@ -0,0 +1,26 @@
|
||||
{% macro render_topictable(topics, show_author=True) -%}
|
||||
<table>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>Title</th>
|
||||
{% if show_author %}<th>Author</th>{% endif %}
|
||||
<th>Name</th>
|
||||
<th>Link</th>
|
||||
<th>Actions</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>
|
||||
{% if show_author %}
|
||||
<td><a href="{{ url_for('user_profile_page', username=topic.author.username) }}">{{ topic.author.display_name}}</a></td>
|
||||
{% endif %}
|
||||
<td>{{ topic.name or ""}}</td>
|
||||
<td><a href="{{ topic.link }}">{{ topic.link | domain }}</a></td>
|
||||
<td>
|
||||
<a href="{{ url_for('create_edit_package_page', author=topic.author.username, repo=topic.getRepoURL(), forums=topic.topic_id, title=topic.title, bname=topic.name) }}">Create</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endmacro %}
|
||||
@@ -49,7 +49,7 @@
|
||||
|
||||
<div class="pkg_wiz_1">
|
||||
<p>Enter the repo URL for the package.
|
||||
If it's hosted on Github then metadata will automatically be imported.</p>
|
||||
If the repo uses git then the metadata will be automatically imported.</p>
|
||||
|
||||
<p>Leave blank if you don't have a repo.</p>
|
||||
</div>
|
||||
@@ -61,7 +61,7 @@
|
||||
</div>
|
||||
|
||||
<div class="pkg_wiz_2">
|
||||
Importing...
|
||||
Importing... (This may take a while)
|
||||
</div>
|
||||
|
||||
{{ render_field(form.website, class_="pkg_meta") }}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
{{ render_field(form.title, placeholder="Human readable. Eg: 1.0.0 or 2018-05-28") }}
|
||||
{{ render_field(form.uploadOpt) }}
|
||||
{% if package.canMakeReleaseFromVCS() %}
|
||||
{% if package.repo %}
|
||||
{{ render_field(form.vcsLabel) }}
|
||||
{% endif %}
|
||||
{{ render_field(form.fileUpload) }}
|
||||
|
||||
@@ -43,6 +43,25 @@
|
||||
{% endif %}
|
||||
<div style="clear: both;"></div>
|
||||
</div>
|
||||
|
||||
{% if package.author == current_user or package.checkPerm(current_user, "APPROVE_NEW") %}
|
||||
{% if review_thread %}
|
||||
<h2>🔒 {{ review_thread.title }}</h2>
|
||||
<p><i>
|
||||
This thread is only visible to the package owner and users of
|
||||
Editor rank or above.
|
||||
</i></p>
|
||||
|
||||
{% from "macros/threads.html" import render_thread %}
|
||||
{{ render_thread(review_thread, current_user) }}
|
||||
{% else %}
|
||||
<div class="box box_grey alert alert-info">
|
||||
Privately ask a question or give feedback
|
||||
|
||||
<a class="alert_right button" href="{{ url_for('new_thread_page', pid=package.id, title='Package approval comments') }}">Open Thread</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<h1>{{ package.title }} by {{ package.author.display_name }}</h1>
|
||||
@@ -258,4 +277,27 @@
|
||||
{% 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>
|
||||
[{{ t.getType().value }}]
|
||||
<a href="https://forum.minetest.net/viewtopic.php?t={{ t.topic_id }}">
|
||||
{{ t.title }} by {{ t.author.display_name }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
12
app/templates/threads/list.html
Normal file
12
app/templates/threads/list.html
Normal file
@@ -0,0 +1,12 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
Threads
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Threads</h1>
|
||||
|
||||
{% from "macros/threads.html" import render_threadlist %}
|
||||
{{ render_threadlist(threads) }}
|
||||
{% endblock %}
|
||||
19
app/templates/threads/new.html
Normal file
19
app/templates/threads/new.html
Normal file
@@ -0,0 +1,19 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
New Thread
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% from "macros/forms.html" import render_field, render_submit_field %}
|
||||
<form method="POST" action="" enctype="multipart/form-data">
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
{{ render_field(form.title) }}
|
||||
{{ render_field(form.comment) }}
|
||||
{{ render_field(form.private) }}
|
||||
{{ render_submit_field(form.submit) }}
|
||||
|
||||
<p>Only the you, the package author, and users of Editor rank and above can read private threads.</p>
|
||||
</form>
|
||||
{% endblock %}
|
||||
25
app/templates/threads/view.html
Normal file
25
app/templates/threads/view.html
Normal file
@@ -0,0 +1,25 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}
|
||||
Threads
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{% if thread.private %}🔒 {% endif %}{{ thread.title }}</h1>
|
||||
|
||||
{% if thread.package %}
|
||||
<p>
|
||||
Package: <a href="{{ thread.package.getDetailsURL() }}">{{ thread.package.title }}</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% if thread.private %}
|
||||
<i>
|
||||
This thread is only visible to its creator, the package owner, and users of
|
||||
Editor rank or above.
|
||||
</i>
|
||||
{% endif %}
|
||||
|
||||
{% from "macros/threads.html" import render_thread %}
|
||||
{{ render_thread(thread, current_user) }}
|
||||
{% endblock %}
|
||||
@@ -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 %}
|
||||
17
app/templates/todo/topics.html
Normal file
17
app/templates/todo/topics.html
Normal file
@@ -0,0 +1,17 @@
|
||||
{% 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>
|
||||
|
||||
{% from "macros/topictable.html" import render_topictable %}
|
||||
{{ render_topictable(topics) }}
|
||||
{% endblock %}
|
||||
@@ -5,24 +5,36 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% if optional %}
|
||||
<div class="box box_grey alert alert-primary">
|
||||
It is recommended that you set a password for your account.
|
||||
|
||||
<a class="alert_right button" href="{{ url_for('home_page') }}">Skip</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<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() }}
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
{% if not current_user.email %}
|
||||
{{ render_field(form.email, tabindex=230) }}
|
||||
{% endif %}
|
||||
{% if not current_user.email %}
|
||||
{{ render_field(form.email, tabindex=230) }}
|
||||
|
||||
{{ render_field(form.password, tabindex=230) }}
|
||||
{{ render_field(form.password2, tabindex=240) }}
|
||||
<p>
|
||||
Your email is needed to recover your account if you forget your
|
||||
password, and to optionally send notifications.
|
||||
Your email will never be shared to a third-party.
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{{ render_submit_field(form.submit, tabindex=280) }}
|
||||
</div>
|
||||
</div>
|
||||
{{ render_field(form.password, tabindex=230) }}
|
||||
{{ render_field(form.password2, tabindex=240) }}
|
||||
|
||||
{{ render_submit_field(form.submit, tabindex=280) }}
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -98,4 +98,22 @@
|
||||
{% 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>
|
||||
|
||||
|
||||
|
||||
{% from "macros/topictable.html" import render_topictable %}
|
||||
{{ render_topictable(topics_to_add, show_author=False) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -99,7 +99,7 @@ def loginUser(user):
|
||||
if user_manager.enable_username:
|
||||
user_mixin = user_manager.find_user_by_username(user.username)
|
||||
|
||||
return _do_login_user(user_mixin, False)
|
||||
return _do_login_user(user_mixin, True)
|
||||
|
||||
def rank_required(rank):
|
||||
def decorator(f):
|
||||
|
||||
@@ -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, thumbnails
|
||||
from . import users, githublogin, packages, sass, tasks, admin, notifications, tagseditor, meta, thumbnails, threads
|
||||
|
||||
@menu.register_menu(app, ".help", "Help", order=19, endpoint_arguments_constructor=lambda: { 'path': 'help' })
|
||||
@app.route('/<path:path>/')
|
||||
|
||||
@@ -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) \
|
||||
|
||||
@@ -64,7 +64,10 @@ def github_authorized(oauth_token):
|
||||
flash("Unable to find an account for that Github user", "error")
|
||||
return redirect(url_for("user_claim_page"))
|
||||
elif loginUser(userByGithub):
|
||||
return redirect(next_url or url_for("home_page"))
|
||||
if current_user.password is None:
|
||||
return redirect(next_url or url_for("set_password_page", optional=True))
|
||||
else:
|
||||
return redirect(next_url or url_for("home_page"))
|
||||
else:
|
||||
flash("Authorization failed [err=gh-login-failed]", "danger")
|
||||
return redirect(url_for("user.login"))
|
||||
|
||||
@@ -38,9 +38,10 @@ from wtforms.ext.sqlalchemy.fields import QuerySelectField, QuerySelectMultipleF
|
||||
@menu.register_menu(app, ".txp", "Texture Packs", order=13, endpoint_arguments_constructor=lambda: { 'type': 'txp' })
|
||||
@app.route("/packages/")
|
||||
def packages_page():
|
||||
type = request.args.get("type")
|
||||
if type is not None:
|
||||
type = PackageType[type.upper()]
|
||||
type_name = request.args.get("type")
|
||||
type = None
|
||||
if type_name is not None:
|
||||
type = PackageType[type_name.upper()]
|
||||
|
||||
title = "Packages"
|
||||
query = Package.query.filter_by(soft_deleted=False)
|
||||
@@ -50,7 +51,7 @@ def packages_page():
|
||||
query = query.filter_by(type=type, approved=True)
|
||||
|
||||
search = request.args.get("q")
|
||||
if search is not None:
|
||||
if search is not None and search.strip() != "":
|
||||
query = query.filter(Package.title.ilike('%' + search + '%'))
|
||||
|
||||
if shouldReturnJson():
|
||||
@@ -62,14 +63,14 @@ def packages_page():
|
||||
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) \
|
||||
next_url = url_for("packages_page", type=type_name, 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) \
|
||||
prev_url = url_for("packages_page", type=type_name, 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.items, \
|
||||
query=search, tags=tags, type=None if type is None else type.toName(), \
|
||||
query=search, tags=tags, type=type_name, \
|
||||
next_url=next_url, prev_url=prev_url, page=page, page_max=query.pages, packages_count=query.total)
|
||||
|
||||
|
||||
@@ -96,11 +97,28 @@ def package_page(package):
|
||||
.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]
|
||||
|
||||
review_thread = Thread.query.filter_by(package_id=package.id, private=True).first()
|
||||
if review_thread is not None and not review_thread.checkPerm(current_user, Permission.SEE_THREAD):
|
||||
review_thread = None
|
||||
|
||||
return render_template("packages/view.html", \
|
||||
package=package, releases=releases, requests=requests, \
|
||||
alternatives=alternatives)
|
||||
alternatives=alternatives, similar_topics=similar_topics, \
|
||||
review_thread=review_thread)
|
||||
|
||||
|
||||
@app.route("/packages/<author>/<name>/download/")
|
||||
@@ -131,7 +149,7 @@ class PackageForm(FlaskForm):
|
||||
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)])
|
||||
softdep_str = StringField("Soft Dependencies", [Optional(), Length(0,1000)])
|
||||
repo = StringField("Repo URL", [Optional(), URL()])
|
||||
repo = StringField("VCS Repository URL", [Optional(), URL()])
|
||||
website = StringField("Website URL", [Optional(), URL()])
|
||||
issueTracker = StringField("Issue Tracker URL", [Optional(), URL()])
|
||||
forums = IntegerField("Forum Topic ID", [Optional(), NumberRange(0,999999)])
|
||||
@@ -168,11 +186,17 @@ def create_edit_package_page(author=None, name=None):
|
||||
form = PackageForm(formdata=request.form, obj=package)
|
||||
|
||||
# Initial form class from post data and default data
|
||||
if request.method == "GET" and package is not None:
|
||||
deps = 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(package.provides)
|
||||
if request.method == "GET":
|
||||
if package is None:
|
||||
form.name.data = request.args.get("bname")
|
||||
form.title.data = request.args.get("title")
|
||||
form.repo.data = request.args.get("repo")
|
||||
form.forums.data = request.args.get("forums")
|
||||
else:
|
||||
deps = 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(package.provides)
|
||||
|
||||
if request.method == "POST" and form.validate():
|
||||
wasNew = False
|
||||
@@ -221,7 +245,7 @@ def create_edit_package_page(author=None, name=None):
|
||||
|
||||
db.session.commit() # save
|
||||
|
||||
if wasNew and package.canImportScreenshot():
|
||||
if wasNew and package.repo is not None:
|
||||
task = importRepoScreenshot.delay(package.id)
|
||||
return redirect(url_for("check_task", id=task.id, r=package.getDetailsURL()))
|
||||
|
||||
|
||||
@@ -52,8 +52,8 @@ def create_release_page(package):
|
||||
|
||||
# Initial form class from post data and default data
|
||||
form = CreatePackageReleaseForm()
|
||||
if package.canMakeReleaseFromVCS():
|
||||
form["uploadOpt"].choices = [("vcs", "From VCS Commit or Branch"), ("upload", "File Upload")]
|
||||
if package.repo is not None:
|
||||
form["uploadOpt"].choices = [("vcs", "From Git Commit or Branch"), ("upload", "File Upload")]
|
||||
if request.method != "POST":
|
||||
form["uploadOpt"].data = "vcs"
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
154
app/views/threads.py
Normal file
154
app/views/threads.py
Normal file
@@ -0,0 +1,154 @@
|
||||
# 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 flask_user import *
|
||||
from app import app
|
||||
from app.models import *
|
||||
from app.utils import triggerNotif, clearNotifications
|
||||
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import *
|
||||
from wtforms.validators import *
|
||||
|
||||
@app.route("/threads/")
|
||||
def threads_page():
|
||||
threads = Thread.query.filter_by(private=False).all()
|
||||
return render_template("threads/list.html", threads=threads)
|
||||
|
||||
@app.route("/threads/<int:id>/", methods=["GET", "POST"])
|
||||
def thread_page(id):
|
||||
clearNotifications(url_for("thread_page", id=id))
|
||||
|
||||
thread = Thread.query.get(id)
|
||||
if thread is None or not thread.checkPerm(current_user, Permission.SEE_THREAD):
|
||||
abort(404)
|
||||
|
||||
if current_user.is_authenticated and request.method == "POST":
|
||||
comment = request.form["comment"]
|
||||
|
||||
if len(comment) <= 500 and len(comment) > 3:
|
||||
reply = ThreadReply()
|
||||
reply.author = current_user
|
||||
reply.comment = comment
|
||||
db.session.add(reply)
|
||||
|
||||
thread.replies.append(reply)
|
||||
if not current_user in thread.watchers:
|
||||
thread.watchers.append(current_user)
|
||||
|
||||
msg = None
|
||||
if thread.package is None:
|
||||
msg = "New comment on '{}'".format(thread.title)
|
||||
else:
|
||||
msg = "New comment on '{}' on package {}".format(thread.title, thread.package.title)
|
||||
|
||||
|
||||
for user in thread.watchers:
|
||||
if user != current_user:
|
||||
triggerNotif(user, current_user, msg, url_for("thread_page", id=thread.id))
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return redirect(url_for("thread_page", id=id))
|
||||
|
||||
else:
|
||||
flash("Comment needs to be between 3 and 500 characters.")
|
||||
|
||||
return render_template("threads/view.html", thread=thread)
|
||||
|
||||
|
||||
class ThreadForm(FlaskForm):
|
||||
title = StringField("Title", [InputRequired(), Length(3,100)])
|
||||
comment = TextAreaField("Comment", [InputRequired(), Length(10, 500)])
|
||||
private = BooleanField("Private")
|
||||
submit = SubmitField("Open Thread")
|
||||
|
||||
@app.route("/threads/new/", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def new_thread_page():
|
||||
form = ThreadForm(formdata=request.form)
|
||||
|
||||
package = None
|
||||
if "pid" in request.args:
|
||||
package = Package.query.get(int(request.args.get("pid")))
|
||||
if package is None:
|
||||
flash("Unable to find that package!", "error")
|
||||
|
||||
# Don't allow making threads on approved packages for now
|
||||
if package is None or package.approved:
|
||||
abort(403)
|
||||
|
||||
def_is_private = request.args.get("private") or False
|
||||
if not package.approved:
|
||||
def_is_private = True
|
||||
allow_change = package.approved
|
||||
is_review_thread = package is not None and not package.approved
|
||||
|
||||
# Check that user can make the thread
|
||||
if is_review_thread and not (package.author == current_user or \
|
||||
package.checkPerm(current_user, Permission.APPROVE_NEW)):
|
||||
flash("Unable to create thread!", "error")
|
||||
return redirect(url_for("home_page"))
|
||||
|
||||
# Only allow creating one thread when not approved
|
||||
elif is_review_thread and package.review_thread is not None:
|
||||
flash("A review thread already exists!", "error")
|
||||
if request.method == "GET":
|
||||
return redirect(url_for("thread_page", id=package.review_thread.id))
|
||||
|
||||
# Set default values
|
||||
elif request.method == "GET":
|
||||
form.private.data = def_is_private
|
||||
form.title.data = request.args.get("title") or ""
|
||||
|
||||
# Validate and submit
|
||||
elif request.method == "POST" and form.validate():
|
||||
thread = Thread()
|
||||
thread.author = current_user
|
||||
thread.title = form.title.data
|
||||
thread.private = form.private.data if allow_change else def_is_private
|
||||
thread.package = package
|
||||
db.session.add(thread)
|
||||
|
||||
thread.watchers.append(current_user)
|
||||
if package is not None and package.author != current_user:
|
||||
thread.watchers.append(package.author)
|
||||
|
||||
reply = ThreadReply()
|
||||
reply.thread = thread
|
||||
reply.author = current_user
|
||||
reply.comment = form.comment.data
|
||||
db.session.add(reply)
|
||||
|
||||
thread.replies.append(reply)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
if is_review_thread:
|
||||
package.review_thread = thread
|
||||
|
||||
if package is not None:
|
||||
triggerNotif(package.author, current_user,
|
||||
"New thread '{}' on package {}".format(thread.title, package.title), url_for("thread_page", id=thread.id))
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return redirect(url_for("thread_page", id=thread.id))
|
||||
|
||||
|
||||
return render_template("threads/new.html", form=form, allow_private_change=allow_change)
|
||||
@@ -50,12 +50,6 @@ def user_profile_page(username):
|
||||
if not user:
|
||||
abort(404)
|
||||
|
||||
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))
|
||||
|
||||
form = None
|
||||
if user.checkPerm(current_user, Permission.CHANGE_DNAME) or \
|
||||
user.checkPerm(current_user, Permission.CHANGE_EMAIL) or \
|
||||
@@ -97,12 +91,25 @@ 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, packages=packages)
|
||||
user=user, form=form, packages=packages, topics_to_add=topics_to_add)
|
||||
|
||||
class SetPasswordForm(FlaskForm):
|
||||
email = StringField("Email (Optional)", [Optional(), Email()])
|
||||
email = StringField("Email", [Optional(), Email()])
|
||||
password = PasswordField("New password", [InputRequired(), Length(2, 20)])
|
||||
password2 = PasswordField("Verify password", [InputRequired(), Length(2, 20)])
|
||||
submit = SubmitField("Save")
|
||||
@@ -114,6 +121,9 @@ def set_password_page():
|
||||
return redirect(url_for("user.change_password"))
|
||||
|
||||
form = SetPasswordForm(request.form)
|
||||
if current_user.email == None:
|
||||
form.email.validators = [InputRequired(), Email()]
|
||||
|
||||
if request.method == "POST" and form.validate():
|
||||
one = form.password.data
|
||||
two = form.password2.data
|
||||
@@ -152,7 +162,7 @@ def set_password_page():
|
||||
else:
|
||||
flash("Passwords do not match", "error")
|
||||
|
||||
return render_template("users/set_password.html", form=form)
|
||||
return render_template("users/set_password.html", form=form, optional=request.args.get("optional"))
|
||||
|
||||
|
||||
@app.route("/user/claim/", methods=["GET", "POST"])
|
||||
|
||||
29
migrations/versions/28a427cbd4cf_.py
Normal file
29
migrations/versions/28a427cbd4cf_.py
Normal file
@@ -0,0 +1,29 @@
|
||||
"""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! ###
|
||||
pass
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
pass
|
||||
# ### end Alembic commands ###
|
||||
55
migrations/versions/605b3d74ada1_.py
Normal file
55
migrations/versions/605b3d74ada1_.py
Normal file
@@ -0,0 +1,55 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 605b3d74ada1
|
||||
Revises: 28a427cbd4cf
|
||||
Create Date: 2018-06-11 22:50:36.828818
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '605b3d74ada1'
|
||||
down_revision = '28a427cbd4cf'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('thread',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('package_id', sa.Integer(), nullable=True),
|
||||
sa.Column('author_id', sa.Integer(), nullable=False),
|
||||
sa.Column('title', sa.String(length=100), nullable=False),
|
||||
sa.Column('private', sa.Boolean(), server_default='0', nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['author_id'], ['user.id'], ),
|
||||
sa.ForeignKeyConstraint(['package_id'], ['package.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('thread_reply',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('thread_id', sa.Integer(), nullable=False),
|
||||
sa.Column('comment', sa.String(length=500), nullable=False),
|
||||
sa.Column('author_id', sa.Integer(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['author_id'], ['user.id'], ),
|
||||
sa.ForeignKeyConstraint(['thread_id'], ['thread.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
|
||||
op.add_column('package', sa.Column('review_thread_id', sa.Integer(), nullable=True))
|
||||
op.create_foreign_key(None, 'package', 'thread', ['review_thread_id'], ['id'])
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_constraint(None, 'package', type_='foreignkey')
|
||||
op.drop_constraint(None, 'package', type_='foreignkey')
|
||||
op.drop_column('package', 'review_thread_id')
|
||||
op.drop_table('thread_reply')
|
||||
op.drop_table('thread')
|
||||
# ### end Alembic commands ###
|
||||
37
migrations/versions/adad68a5e370_.py
Normal file
37
migrations/versions/adad68a5e370_.py
Normal 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 ###
|
||||
34
migrations/versions/de004661c5e1_.py
Normal file
34
migrations/versions/de004661c5e1_.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: de004661c5e1
|
||||
Revises: 605b3d74ada1
|
||||
Create Date: 2018-06-11 23:38:38.611039
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'de004661c5e1'
|
||||
down_revision = '605b3d74ada1'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('watchers',
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
sa.Column('thread_id', sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['thread_id'], ['thread.id'], ),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
||||
sa.PrimaryKeyConstraint('user_id', 'thread_id')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('watchers')
|
||||
# ### end Alembic commands ###
|
||||
34
migrations/versions/e9f534df23a8_.py
Normal file
34
migrations/versions/e9f534df23a8_.py
Normal 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 ###
|
||||
@@ -13,3 +13,4 @@ lxml==4.2.1
|
||||
Flask-FlatPages==0.6
|
||||
Flask-Migrate==2.1.1
|
||||
pillow==5.1.0
|
||||
GitPython==2.1.10
|
||||
|
||||
Reference in New Issue
Block a user