Compare commits

..

32 Commits

Author SHA1 Message Date
rubenwardy
ba08becd3a Fix migration error 2018-06-11 23:42:20 +01:00
rubenwardy
68b7a5e922 Add thread watchers 2018-06-11 23:39:41 +01:00
rubenwardy
e8cc685f89 Prevent new threads being created on approved packages 2018-06-11 23:22:48 +01:00
rubenwardy
86dd137f75 Add dates to comments 2018-06-11 23:20:18 +01:00
rubenwardy
b48f684c0a Add note about review thread being private 2018-06-11 23:14:14 +01:00
rubenwardy
e0e6f3392d Improve comment CSS 2018-06-11 23:11:15 +01:00
rubenwardy
b1c349cc35 Add comment system 2018-06-11 22:52:37 +01:00
rubenwardy
40aac38d43 Fix worker stopping due to gitpython asking for credentials 2018-06-07 23:25:00 +01:00
rubenwardy
051df7ab87 Increase timeout in polltask.js 2018-06-07 23:25:00 +01:00
rubenwardy
bb1f6702f6 Add name to create link 2018-06-05 23:51:40 +01:00
rubenwardy
c9542427b4 Add create links to topic table 2018-06-05 23:45:15 +01:00
rubenwardy
8601c5e075 Add support for importing generic git releases 2018-06-05 23:13:39 +01:00
rubenwardy
3d97eca387 Add git screenshot importing 2018-06-05 22:39:08 +01:00
rubenwardy
99b21f996c Fix screenshot import being broken 2018-06-05 19:59:07 +01:00
rubenwardy
700cd7ce1f Add game detection 2018-06-05 19:51:01 +01:00
rubenwardy
8d9da5a750 Make git error public, delete dir after clone 2018-06-05 19:47:02 +01:00
rubenwardy
9a36bb7d72 Add git support for importing meta 2018-06-05 00:10:47 +01:00
rubenwardy
e424dc57e7 Set remember me to true in loginUser 2018-06-04 19:34:29 +01:00
rubenwardy
7d60e2f671 Fix crash on any type search 2018-06-04 19:02:02 +01:00
rubenwardy
8b2018852e Add redirection to set password after login if not set 2018-06-04 18:49:42 +01:00
rubenwardy
0aeefa2387 Add email usage note 2018-06-04 18:36:26 +01:00
rubenwardy
4420f489ac Require email in set password 2018-06-04 18:34:04 +01:00
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
42 changed files with 1231 additions and 268 deletions

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

@@ -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

@@ -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.

View File

@@ -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

View File

@@ -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)

View File

@@ -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
View 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;
}

View File

@@ -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;

View File

@@ -3,3 +3,4 @@
@import "nav.scss";
@import "packages.scss";
@import "packagegrid.scss";
@import "comments.scss";

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

@@ -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

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,9 +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="{{ 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>

View File

@@ -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>

View 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 %}

View 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 %}

View File

@@ -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") }}

View File

@@ -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) }}

View File

@@ -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>&#x1f512; {{ 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 %}

View 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 %}

View 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 %}

View File

@@ -0,0 +1,25 @@
{% extends "base.html" %}
{% block title %}
Threads
{% endblock %}
{% block content %}
<h1>{% if thread.private %}&#x1f512; {% 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 %}

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,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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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):

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, 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>/')

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

@@ -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"))

View File

@@ -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()))

View File

@@ -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"

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)

154
app/views/threads.py Normal file
View 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)

View File

@@ -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"])

View 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 ###

View 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 ###

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,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 ###

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

@@ -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