Compare commits

..

28 Commits

Author SHA1 Message Date
rubenwardy
048b604a75 Fix changing email not working due to validation issue 2018-05-28 15:25:45 +01:00
rubenwardy
f7bb29c839 Fix empty release titles being allowed
Fixes #86
2018-05-28 14:55:31 +01:00
Ezhh
ba506cb16d Update ranks and permissions help page 2018-05-28 01:40:02 +01:00
Pavel Puchkin
179d0be933 Make search case-insensitive by using ilike 2018-05-28 00:06:57 +01:00
rubenwardy
d6790903a6 Add trusted member rank 2018-05-27 23:56:13 +01:00
rubenwardy
48573fe922 Use proper datetime formatting 2018-05-27 23:45:12 +01:00
rubenwardy
dff967d3df Clarify name/title 2018-05-27 23:36:35 +01:00
rubenwardy
a2b873bf38 Add 'set provides from name' admin action 2018-05-27 23:13:13 +01:00
rubenwardy
d0969263ba Fix crash due to remaining raise() in getDepends() 2018-05-27 23:02:11 +01:00
rubenwardy
d046de8057 Merge pull request #78 from minetest/dev
Add meta packages, remove current dependencies
2018-05-27 22:55:46 +01:00
rubenwardy
05e536b121 Add helpful text to field labels 2018-05-27 22:55:11 +01:00
rubenwardy
2d6b55e67b Reorder package fields 2018-05-27 22:51:50 +01:00
rubenwardy
44c9f7e58f Hide unneeded fields depending on package type 2018-05-27 22:48:53 +01:00
rubenwardy
92daa87db0 Add migration 2018-05-27 22:39:16 +01:00
rubenwardy
746cf7f4b5 Add bulk dependency importer from Github 2018-05-27 22:34:24 +01:00
rubenwardy
fb5cba4cc8 Add dependency detection to importer 2018-05-27 22:04:03 +01:00
rubenwardy
fb8aa25b71 Remove required by for now 2018-05-27 21:42:31 +01:00
rubenwardy
5d944d79d3 Improve placeholder text 2018-05-27 21:36:58 +01:00
rubenwardy
ca7708437b Fix potentiall XSS vulnerability 2018-05-27 21:33:50 +01:00
rubenwardy
63af1535b9 Add dependencies 2018-05-27 21:31:11 +01:00
rubenwardy
82159d488d Add meta package selector 2018-05-27 20:22:01 +01:00
rubenwardy
5e4613a6ef Add ability to edit provides 2018-05-27 18:52:23 +01:00
rubenwardy
e85298d890 Allow new members to edit their packages if it hasn't been approved yet 2018-05-27 18:06:46 +01:00
rubenwardy
f4c9348b7f Add metapackages pages 2018-05-27 18:01:27 +01:00
rubenwardy
7b6ad051c4 Remove dependencies, add meta packages 2018-05-27 18:01:27 +01:00
rubenwardy
65fdba5882 Require screenshots for games and texture packs 2018-05-27 18:00:38 +01:00
Ezhh
54b7e7c3f7 Update package tags help page 2018-05-27 17:12:31 +01:00
rubenwardy
19848a154d Add tag editor 2018-05-27 17:12:44 +01:00
31 changed files with 989 additions and 141 deletions

View File

@@ -33,7 +33,7 @@ the current session:
If you need to, reset the db like so:
python3 setup.py -d
python3 setup.py -t
Then run the server:
@@ -43,6 +43,12 @@ Then view in your web browser: http://localhost:5000/
## How-tos
### Start celery worker
```sh
FLASK_CONFIG=../config.cfg celery -A app.tasks.celery worker
```
### Create migration
```sh

View File

@@ -1,4 +1,4 @@
title: Help
* [Ranks and Permissions](ranks_permissions)
* [Package Tags](package_tags)
* [Ranks and Permissions](ranks_permissions)

View File

@@ -2,26 +2,32 @@ title: Package Tags
## Overview
Tags should be added to packages to enable easy identification of different types of mods and games.
Tags should be added to packages to enable easy identification of different types of mods, games and texture packs.
They are only beneficial when applied correctly, so please use the following guidelines.
## Tag Usage
* **Inventory** - For mods that add new inventory systems or new inventory pages.
* **Mapgen** - For mods that add new biomes, new mapgen decorations, or any other mapgen elements.
* **Building** - For mods that focus on adding new materials or nodes to build with.
* **Mobs and NPCs** - For mods that add mobs or NPCs, or provide tools that assist with mob and NPC creation or manipulation.
* **Tools** - For mods that add new tools or new features for existing tools.
* **Player effects** - For mods that change player effects, for example speed, jump height or gravity.
* **Education** - For mods or games created for educational purposes.
* **Environment** - For mods that add environmental effects, including ambient sound and weather effects.
* **Transport** - For mods that add transportation methods. This includes teleportation, vehicles and ridable mobs.
* **Inventory** - For mods that add new inventory systems or new inventory pages.
* **Machines and Electronics** - For mods that include placeable machinery or electronic components which interact to complete tasks.
* **Maintenance** - For mods that assist with world or player maintenance. This includes large-scale map manipulation, area protection and other administrative tools.
* **Plants and farming** - For mods that add new plants or other farmable resources.
* **Mapgen** - For mods that add new biomes, new mapgen decorations, or any other mapgen elements.
* **Mobs and NPCs** - For mods that add mobs or NPCs, or provide tools that assist with mob and NPC creation or manipulation.
* **Plants and Farming** - For mods that add new plants or other farmable resources.
* **Player effects/Food** - For mods that change player effects, for example speed, jump height or gravity, and food.
* **Tools** - For mods that add new tools or new features for existing tools.
* **Transport** - For mods that add transportation methods. This includes teleportation, vehicles and ridable mobs.
* **Survival** - For mods written specifically for survival games. For example, these mods might focus on game-balance or increase the difficulty level. This tag should also be used for games with a heavy survival focus.
* **Creative** - For mods written specifically (and often exclusively) for use in creative mode. For example, these mods may add a large amount of decorative content, or content without crafting recipes. This tag should also be used for games with a heavy creative focus.
* **Multiplayer-focused** - For games that can be played with other players.
* **Singleplayer-focused** - For games that can be played alone.
* **PvP** - For games designed to be played competitively against other players.
* **PvE** - For games designed for one or multiple players which focus on combat against mobs or NPCs.
* **Puzzle** - For games with a focus on puzzle solving instead of combat.
* **Multiplayer** - For games that can be played with other players.
* **Singleplayer** - For games that can be played alone.
* **Puzzle** - For mods and games with a focus on puzzle solving instead of combat.
* **16px** - For 16px texture packs.
* **32px** - For 32px texture packs.
* **64px** - For 64px texture packs.
* **128px+** - For 128px or higher texture packs.

View File

@@ -4,6 +4,7 @@ title: Ranks and Permissions
* **New Members** - mostly untrusted, cannot change package meta data or publish releases without approval.
* **Members** - Trusted to change the meta data of their own packages', but cannot publish releases.
* **Trusted Members** - Same as above, but can approve their own releases and packages.
* **Editors** - Trusted to change the meta data of any package, and also make and publish releases.
* **Moderators** - Same as above, but can manage users.
* **Admins** - Full access.
@@ -16,6 +17,7 @@ title: Ranks and Permissions
<th>Rank</th>
<th colspan=2>New Member</th>
<th colspan=2>Member</th>
<th colspan=2>Trusted Member</th>
<th colspan=2>Editor</th>
<th colspan=2>Moderator</th>
<th colspan=2>Admin</th>
@@ -41,6 +43,8 @@ title: Ranks and Permissions
<th></th>
<th></th> <!-- member -->
<th></th>
<th></th> <!-- trusted member -->
<th></th>
<th></th> <!-- editor -->
<th></th>
<th></th> <!-- moderator -->
@@ -54,6 +58,8 @@ title: Ranks and Permissions
<th></th>
<th></th> <!-- member -->
<th></th>
<th></th> <!-- trusted member -->
<th></th>
<th></th> <!-- editor -->
<th></th>
<th></th> <!-- moderator -->
@@ -67,6 +73,8 @@ title: Ranks and Permissions
<th></th>
<th></th> <!-- member -->
<th></th>
<th></th> <!-- trusted member -->
<th></th>
<th></th> <!-- editor -->
<th></th>
<th></th> <!-- moderator -->
@@ -80,6 +88,8 @@ title: Ranks and Permissions
<th></th>
<th></th> <!-- member -->
<th></th>
<th></th> <!-- trusted member -->
<th></th>
<th></th> <!-- editor -->
<th></th>
<th></th> <!-- moderator -->
@@ -93,6 +103,8 @@ title: Ranks and Permissions
<th></th>
<th></th> <!-- member -->
<th></th>
<th></th> <!-- trusted member -->
<th></th>
<th></th> <!-- editor -->
<th></th>
<th></th> <!-- moderator -->
@@ -106,6 +118,8 @@ title: Ranks and Permissions
<th></th>
<th></th> <!-- member -->
<th></th>
<th></th> <!-- trusted member -->
<th></th>
<th></th> <!-- editor -->
<th></th>
<th></th> <!-- moderator -->
@@ -119,6 +133,8 @@ title: Ranks and Permissions
<th></th>
<th></th> <!-- member -->
<th></th>
<th></th> <!-- trusted member -->
<th></th>
<th></th> <!-- editor -->
<th></th>
<th></th> <!-- moderator -->
@@ -132,6 +148,8 @@ title: Ranks and Permissions
<th></th>
<th></th> <!-- member -->
<th></th>
<th></th> <!-- trusted member -->
<th></th>
<th></th> <!-- editor -->
<th></th>
<th></th> <!-- moderator -->
@@ -145,6 +163,8 @@ title: Ranks and Permissions
<th></th>
<th></th> <!-- member -->
<th></th>
<th></th> <!-- trusted member -->
<th></th>
<th></th> <!-- editor -->
<th></th>
<th></th> <!-- moderator -->
@@ -158,6 +178,8 @@ title: Ranks and Permissions
<th></th>
<th></th> <!-- member -->
<th></th>
<th></th> <!-- trusted member -->
<th></th>
<th></th> <!-- editor -->
<th></th>
<th></th> <!-- moderator -->
@@ -171,6 +193,8 @@ title: Ranks and Permissions
<th></th>
<th></th> <!-- member -->
<th></th>
<th></th> <!-- trusted member -->
<th></th>
<th></th> <!-- editor -->
<th></th>
<th></th> <!-- moderator -->
@@ -184,6 +208,8 @@ title: Ranks and Permissions
<th></th>
<th></th> <!-- member -->
<th></th>
<th></th> <!-- trusted member -->
<th></th>
<th></th> <!-- editor -->
<th></th>
<th><sup>3</sup></th> <!-- moderator -->

View File

@@ -31,13 +31,14 @@ migrate = Migrate(app, db)
class UserRank(enum.Enum):
BANNED = 0
NOT_JOINED = 1
NEW_MEMBER = 2
MEMBER = 3
EDITOR = 4
MODERATOR = 5
ADMIN = 6
BANNED = 0
NOT_JOINED = 1
NEW_MEMBER = 2
MEMBER = 3
TRUSTED_MEMBER = 4
EDITOR = 5
MODERATOR = 6
ADMIN = 7
def atLeast(self, min):
return self.value >= min.value
@@ -219,8 +220,7 @@ class PackagePropertyKey(enum.Enum):
type = "Type"
license = "License"
tags = "Tags"
harddeps = "Hard Dependencies"
softdeps = "Soft Dependencies"
provides = "Provides"
repo = "Repository"
website = "Website"
issueTracker = "Issue Tracker"
@@ -229,26 +229,88 @@ class PackagePropertyKey(enum.Enum):
def convert(self, value):
if self == PackagePropertyKey.tags:
return ",".join([t.title for t in value])
elif self == PackagePropertyKey.harddeps or self == PackagePropertyKey.softdeps:
return ",".join([t.author.username + "/" + t.name for t in value])
elif self == PackagePropertyKey.provides:
return ",".join([t.name for t in value])
else:
return str(value)
provides = db.Table("provides",
db.Column("package_id", db.Integer, db.ForeignKey("package.id"), primary_key=True),
db.Column("metapackage_id", db.Integer, db.ForeignKey("meta_package.id"), primary_key=True)
)
tags = db.Table("tags",
db.Column("tag_id", db.Integer, db.ForeignKey("tag.id"), primary_key=True),
db.Column("package_id", db.Integer, db.ForeignKey("package.id"), primary_key=True)
)
harddeps = db.Table("harddeps",
db.Column("package_id", db.Integer, db.ForeignKey("package.id"), primary_key=True),
db.Column("dependency_id", db.Integer, db.ForeignKey("package.id"), primary_key=True)
)
class Dependency(db.Model):
id = db.Column(db.Integer, primary_key=True)
depender_id = db.Column(db.Integer, db.ForeignKey("package.id"), nullable=True)
package_id = db.Column(db.Integer, db.ForeignKey("package.id"), nullable=True)
package = db.relationship("Package", foreign_keys=[package_id])
meta_package_id = db.Column(db.Integer, db.ForeignKey("meta_package.id"), nullable=True)
optional = db.Column(db.Boolean, nullable=False, default=False)
__table_args__ = (db.UniqueConstraint('depender_id', 'package_id', 'meta_package_id', name='_dependency_uc'), )
def __init__(self, depender=None, package=None, meta=None):
if depender is None:
return
self.depender = depender
packageProvided = package is not None
metaProvided = meta is not None
if packageProvided and not metaProvided:
self.package = package
elif metaProvided and not packageProvided:
self.meta_package = meta
else:
raise Exception("Either meta or package must be given, but not both!")
def __str__(self):
if self.package is not None:
return self.package.author.username + "/" + self.package.name
elif self.meta_package is not None:
return self.meta_package.name
else:
raise Exception("Meta and package are both none!")
@staticmethod
def SpecToList(depender, spec, cache={}):
retval = []
arr = spec.split(",")
import re
pattern1 = re.compile("^([a-z0-9_]+)$")
pattern2 = re.compile("^([A-Za-z0-9_]+)/([a-z0-9_]+)$")
for x in arr:
x = x.strip()
if x == "":
continue
if pattern1.match(x):
meta = MetaPackage.GetOrCreate(x, cache)
retval.append(Dependency(depender, meta=meta))
else:
m = pattern2.match(x)
username = m.group(1)
name = m.group(2)
user = User.query.filter_by(username=username).first()
if user is None:
raise Exception("Unable to find user " + username)
package = Package.query.filter_by(author=user, name=name).first()
if package is None:
raise Exception("Unable to find package " + name + " by " + username)
retval.append(Dependency(depender, package=package))
return retval
softdeps = db.Table("softdeps",
db.Column("package_id", db.Integer, db.ForeignKey("package.id"), primary_key=True),
db.Column("dependency_id", db.Integer, db.ForeignKey("package.id"), primary_key=True)
)
class Package(db.Model):
id = db.Column(db.Integer, primary_key=True)
@@ -273,20 +335,13 @@ class Package(db.Model):
issueTracker = db.Column(db.String(200), nullable=True)
forums = db.Column(db.Integer, nullable=True)
tags = db.relationship("Tag", secondary=tags, lazy="subquery",
provides = db.relationship("MetaPackage", secondary=provides, lazy="subquery",
backref=db.backref("packages", lazy=True))
harddeps = db.relationship("Package",
secondary=harddeps,
primaryjoin=id==harddeps.c.package_id,
secondaryjoin=id==harddeps.c.dependency_id,
backref="dependents")
dependencies = db.relationship("Dependency", backref="depender", lazy="dynamic", foreign_keys=[Dependency.depender_id])
softdeps = db.relationship("Package",
secondary=softdeps,
primaryjoin=id==softdeps.c.package_id,
secondaryjoin=id==softdeps.c.dependency_id,
backref="softdependents")
tags = db.relationship("Tag", secondary=tags, lazy="subquery",
backref=db.backref("packages", lazy=True))
releases = db.relationship("PackageRelease", backref="package",
lazy="dynamic", order_by=db.desc("package_release_releaseDate"))
@@ -401,13 +456,19 @@ class Package(db.Model):
return isOwner or user.rank.atLeast(UserRank.EDITOR)
if perm == Permission.EDIT_PACKAGE or perm == Permission.APPROVE_CHANGES:
return user.rank.atLeast(UserRank.MEMBER if isOwner else UserRank.EDITOR)
if isOwner:
return user.rank.atLeast(UserRank.MEMBER if self.approved else UserRank.NEW_MEMBER)
else:
return user.rank.atLeast(UserRank.EDITOR)
# Editors can change authors, approve new packages, and approve releases
elif perm == Permission.CHANGE_AUTHOR or perm == Permission.APPROVE_NEW \
or perm == Permission.APPROVE_RELEASE or perm == Permission.APPROVE_SCREENSHOT:
# Editors can change authors
elif perm == Permission.CHANGE_AUTHOR:
return user.rank.atLeast(UserRank.EDITOR)
elif perm == Permission.APPROVE_NEW or perm == Permission.APPROVE_RELEASE \
or perm == Permission.APPROVE_SCREENSHOT:
return user.rank.atLeast(UserRank.TRUSTED_MEMBER if isOwner else UserRank.EDITOR)
# Moderators can delete packages
elif perm == Permission.DELETE_PACKAGE or perm == Permission.CHANGE_RELEASE_URL:
return user.rank.atLeast(UserRank.MODERATOR)
@@ -415,6 +476,54 @@ class Package(db.Model):
else:
raise Exception("Permission {} is not related to packages".format(perm.name))
class MetaPackage(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), unique=True, nullable=False)
dependencies = db.relationship("Dependency", backref="meta_package", lazy="dynamic")
def __init__(self, name=None):
self.name = name
def __str__(self):
return self.name
@staticmethod
def ListToSpec(list):
return ",".join([str(x) for x in list])
@staticmethod
def GetOrCreate(name, cache={}):
mp = cache.get(name)
if mp is None:
mp = MetaPackage.query.filter_by(name=name).first()
if mp is None:
mp = MetaPackage(name)
db.session.add(mp)
cache[name] = mp
return mp
@staticmethod
def SpecToList(spec, cache={}):
retval = []
arr = spec.split(",")
import re
pattern = re.compile("^([a-z0-9_]+)$")
for x in arr:
x = x.strip()
if x == "":
continue
if not pattern.match(x):
continue
retval.append(MetaPackage.GetOrCreate(x, cache))
return retval
class Tag(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), unique=True, nullable=False)
@@ -552,42 +661,6 @@ class EditRequestChange(db.Model):
tag = Tag.query.filter_by(title=tagTitle.strip()).first()
package.tags.append(tag)
elif self.key == PackagePropertyKey.harddeps:
package.harddeps.clear()
for pair in self.newValue.split(","):
key, value = pair.split("/")
if key is None or value is None:
continue
user = User.query.filter_by(username=key).first()
if user is None:
continue
dep = Package.query.filter_by(author=user, name=value, soft_deleted=False).first()
if dep is None:
continue
package.harddeps.append(dep)
elif self.key == PackagePropertyKey.softdeps:
package.softdeps.clear()
for pair in self.newValue.split(","):
key, value = pair.split("/")
if key is None or value is None:
continue
user = User.query.filter_by(username=key).first()
if user is None:
raise Exception("No such user!")
continue
dep = Package.query.filter_by(author=user, name=value).first()
if dep is None:
raise Exception("No such package!")
continue
package.softdeps.append(dep)
else:
setattr(package, self.key.name, self.newValue)

View File

@@ -26,13 +26,25 @@ $(function() {
$(".pkg_wiz_2").show()
$(".pkg_repo").hide()
function setSpecial(id, value) {
if (value != "") {
var ele = $(id);
ele.val(value);
ele.trigger("change")
}
}
performTask("/tasks/getmeta/new/?url=" + encodeURI(repoURL)).then(function(result) {
$("#name").val(result.name || "")
setSpecial("#provides_str", result.name || "")
$("#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 || "")
if (result.forumId) {
$("#forums").val(result.forumId)
}

View File

@@ -0,0 +1,11 @@
// @author rubenwardy
// @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later
$(function() {
$("#type").change(function() {
$(".not_mod, .not_game, .not_txp").show()
$(".not_" + this.value.toLowerCase()).hide()
})
$(".not_mod, .not_game, .not_txp").show()
$(".not_" + $("#type").val().toLowerCase()).hide()
})

View File

@@ -5,7 +5,7 @@
* https://petprojects.googlecode.com/svn/trunk/GPL-LICENSE.txt
*/
(function($) {
$.fn.tagSelector = function(source, name, select) {
$.fn.selectSelector = function(source, name, select) {
return this.each(function() {
var selector = $(this),
input = $('input[type=text]', this);
@@ -80,15 +80,136 @@
});
}
$.fn.csvSelector = function(source, name, result, allowSlash) {
return this.each(function() {
var selector = $(this),
input = $('input[type=text]', this);
var selected = [];
var lookup = {};
for (var i = 0; i < source.length; i++) {
lookup[source[i].id] = source[i];
}
selector.click(function() { input.focus(); })
.delegate('.tag a', 'click', function() {
var id = $(this).parent().data("id");
for (var i = 0; i < selected.length; i++) {
if (selected[i] == id) {
selected.splice(i, 1);
}
}
recreate();
});
function selectItem(id) {
for (var i = 0; i < selected.length; i++) {
if (selected[i] == id) {
return false;
}
}
selected.push(id);
return true;
}
function addTag(id, value) {
var tag = $('<span class="tag"/>')
.text(value)
.data("id", id)
.append(' <a>x</a>')
.insertBefore(input);
input.attr("placeholder", null);
}
function recreate() {
selector.find("span").remove();
for (var i = 0; i < selected.length; i++) {
var value = lookup[selected[i]] || { value: selected[i] };
addTag(selected[i], value.value);
}
result.val(selected.join(","))
}
function readFromResult() {
selected = [];
var selected_raw = result.val().split(",");
for (var i = 0; i < selected_raw.length; i++) {
var raw = selected_raw[i].trim();
if (lookup[raw] || raw.match(/^([a-z0-9_]+)$/)) {
selected.push(raw);
}
}
recreate();
}
readFromResult();
result.change(readFromResult);
input.keydown(function(e) {
if (e.keyCode === $.ui.keyCode.TAB && $(this).data('ui-autocomplete').menu.active)
e.preventDefault();
else if (e.keyCode === $.ui.keyCode.COMMA) {
var item = input.val();
if (item.length == 0) {
input.data("ui-autocomplete").search("");
} else if (item.match(/^([a-z0-9_]+)$/)) {
selectItem(item);
recreate();
input.val("");
} else {
alert("Only lowercase alphanumeric and number names allowed.");
}
e.preventDefault();
return true;
} else if (e.keyCode === $.ui.keyCode.BACKSPACE) {
if (input.val() == "") {
var item = selected[selected.length - 1];
selected.splice(selected.length - 1, 1);
recreate();
if (!(item.indexOf("/") > 0))
input.val(item);
e.preventDefault();
return true;
}
}
})
.autocomplete({
minLength: 0,
source: source,
select: function(event, ui) {
selectItem(ui.item.id);
recreate();
input.val("");
return false;
}
});
input.data('ui-autocomplete')._renderItem = function(ul, item) {
return $('<li/>')
.data('item.autocomplete', item)
.append($('<a/>').text(item.toString()))
.appendTo(ul);
};
input.data('ui-autocomplete')._resizeMenu = function(ul, item) {
var ul = this.menu.element;
ul.outerWidth(Math.max(
ul.width('').outerWidth(),
selector.outerWidth()
));
};
});
}
$(function() {
$(".multichoice_selector").each(function() {
var ele = $(this);
var sel = ele.parent().find("select");
console.log(sel.attr("name"));
sel.css("display", "none");
sel.hide();
var options = [];
sel.find("option").each(function() {
var text = $(this).text();
options.push({
@@ -100,7 +221,19 @@
});
console.log(options);
ele.tagSelector(options, sel.attr("name"), sel);
})
ele.selectSelector(options, sel.attr("name"), sel);
});
$(".metapackage_selector").each(function() {
var input = $(this).parent().children("input[type='text']");
input.hide();
$(this).csvSelector(meta_packages, input.attr("name"), input);
});
$(".deps_selector").each(function() {
var input = $(this).parent().children("input[type='text']");
input.hide();
$(this).csvSelector(all_packages, input.attr("name"), input);
});
});
})(jQuery);

View File

@@ -87,7 +87,7 @@ a:hover {
}
.button, .buttonset li a, input[type=submit], input[type=text],
input[type=password], textarea, select, .multichoice_selector {
input[type=password], textarea, select, .bulletselector {
text-align: center;
display: inline-block;
padding: 0.4em 1em;
@@ -99,7 +99,7 @@ a:hover {
font-size: 100%;
}
input[type=text], input[type=password], textarea, select, .multichoice_selector {
input[type=text], input[type=password], textarea, select, .bulletselector {
text-align: left;
}
@@ -147,13 +147,13 @@ select:not([multiple]) {
padding: 0 8px 8px 0;
}
.form-group input, .form-group textarea, .form-group .multichoice_selector {
.form-group input, .form-group textarea, .form-group .bulletselector {
display: block;
min-width: 100%;
max-width: 100%;
}
.box .form-group input, .box .form-group textarea, .form-group .multichoice_selector {
.box .form-group input, .box .form-group textarea, .form-group .bulletselector {
min-width: 95%;
max-width: 95%;
}
@@ -197,7 +197,7 @@ select:not([multiple]) {
}
.multichoice_selector input {
.bulletselector input {
border: none;
border-radius: 0;
-moz-border-radius: 0;
@@ -211,7 +211,7 @@ select:not([multiple]) {
white-space: nowrap;
background: transparent;
}
.multichoice_selector .tag {
.bulletselector .tag {
background: #375D81;
border-radius: 3px;
-moz-border-radius: 3px;
@@ -223,11 +223,11 @@ select:not([multiple]) {
margin-bottom: 0.3em;
vertical-align: baseline;
}
.multichoice_selector .tag a {
.bulletselector .tag a {
color: #FFF;
cursor: pointer;
}
.multichoice_selector .tag a:hover {
.bulletselector .tag a:hover {
color: #0099CC;
text-decoration: none;
}

View File

@@ -55,6 +55,9 @@ class GithubURLMaker:
def getDescURL(self):
return self.baseUrl + "/description.txt"
def getDependsURL(self):
return self.baseUrl + "/depends.txt"
def getScreenshotURL(self):
return self.baseUrl + "/screenshot.png"
@@ -161,7 +164,7 @@ def getMeta(urlstr, author):
try:
contents = urllib.request.urlopen(urlmaker.getModConfURL()).read().decode("utf-8")
conf = parseConf(contents)
for key in ["name", "description", "title"]:
for key in ["name", "description", "title", "depends", "optional_depends"]:
try:
result[key] = conf[key]
except KeyError:
@@ -179,12 +182,35 @@ def getMeta(urlstr, author):
except HTTPError:
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)
result["depends"] = ",".join(hard)
result["optional_depends"] = ",".join(soft)
except HTTPError:
print("depends.txt does not exist!")
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]
info = findModInfo(author, result.get("name"), result["repo"])
if info is not None:
result["forumId"] = info.get("topicId")
@@ -264,3 +290,92 @@ def importRepoScreenshot(id):
print("screenshot.png does not exist")
return None
def getDepends(package):
url = urlparse(package.repo)
urlmaker = None
if url.netloc == "github.com":
urlmaker = GithubURLMaker(url)
else:
return {}
result = {}
if not urlmaker.isValid():
return {}
#
# Try getting depends on mod.conf
#
try:
contents = urllib.request.urlopen(urlmaker.getModConfURL()).read().decode("utf-8")
conf = parseConf(contents)
for key in ["depends", "optional_depends"]:
try:
result[key] = conf[key]
except KeyError:
pass
except HTTPError:
print("mod.conf does not exist")
if "depends" in result or "optional_depends" in result:
return result
#
# Try depends.txt
#
import re
pattern = re.compile("^([a-z0-9_]+)\??$")
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)
result["depends"] = ",".join(hard)
result["optional_depends"] = ",".join(soft)
except HTTPError:
print("depends.txt does not exist")
return result
def importDependencies(package, mpackage_cache):
if Dependency.query.filter_by(depender=package).count() != 0:
return
result = getDepends(package)
if "depends" in result:
deps = Dependency.SpecToList(package, result["depends"], mpackage_cache)
print("{} hard: {}".format(len(deps), result["depends"]))
for dep in deps:
dep.optional = False
db.session.add(dep)
if "optional_depends" in result:
deps = Dependency.SpecToList(package, result["optional_depends"], mpackage_cache)
print("{} soft: {}".format(len(deps), result["optional_depends"]))
for dep in deps:
dep.optional = True
db.session.add(dep)
@celery.task()
def importAllDependencies():
Dependency.query.delete()
mpackage_cache = {}
packages = Package.query.filter_by(type=PackageType.MOD).all()
for i, p in enumerate(packages):
print("============= {} ({}/{}) =============".format(p.name, i, len(packages)))
importDependencies(p, mpackage_cache)
db.session.commit()

View File

@@ -7,6 +7,7 @@
{% block content %}
<ul>
<li><a href="{{ url_for('user_list_page') }}">User list</a></li>
<li><a href="{{ url_for('tag_list_page') }}">Tag Editor</a></li>
<li><a href="{{ url_for('switch_user_page') }}">Sign in as another user</a></li>
</ul>
@@ -16,8 +17,10 @@
<form method="post" action="" class="box-body">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<select name="action">
<option value="importusers" selected>Create users from mod list</option>
<option value="importscreenshots">Import screenshots from VCS</option>
<option value="importusers">Create users from 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>
</select>
<input type="submit" value="Perform" />
</form>

View File

@@ -63,6 +63,9 @@
{% if current_user.rank == current_user.rank.ADMIN %}
<li><a href="{{ url_for('admin_page') }}">Admin</a></li>
{% endif %}
{% if current_user.rank == current_user.rank.MODERATOR %}
<li><a href="{{ url_for('tag_list_page') }}">Tag Editor</a></li>
{% endif %}
<li><a href="{{ url_for('user.logout') }}">Sign out</a></li>
</ul>
</li>

View File

@@ -26,7 +26,7 @@
{% if not label %}{% set label=field.label.text %}{% endif %}
<label for="{{ field.id }}" class="control-label">{{ label|safe }}</label>
{% endif %}
<div class="multichoice_selector">
<div class="multichoice_selector bulletselector">
<input type="text" placeholder="Start typing to see suggestions">
<div class="clearboth"></div>
</div>
@@ -39,6 +39,44 @@
</div>
{% endmacro %}
{% macro render_mpackage_field(field, label=None, label_visible=true, right_url=None, right_label=None) -%}
<div class="form-group {% if field.errors %}has-error{% endif %} {{ kwargs.pop('class_', '') }}">
{% if field.type != 'HiddenField' and label_visible %}
{% if not label %}{% set label=field.label.text %}{% endif %}
<label for="{{ field.id }}" class="control-label">{{ label|safe }}</label>
{% endif %}
<div class="metapackage_selector bulletselector">
<input type="text" placeholder="Comma-seperated values">
<div class="clearboth"></div>
</div>
{{ field(class_='form-control', **kwargs) }}
{% if field.errors %}
{% for e in field.errors %}
<p class="help-block">{{ e }}</p>
{% endfor %}
{% endif %}
</div>
{% endmacro %}
{% macro render_deps_field(field, label=None, label_visible=true, right_url=None, right_label=None) -%}
<div class="form-group {% if field.errors %}has-error{% endif %} {{ kwargs.pop('class_', '') }}">
{% if field.type != 'HiddenField' and label_visible %}
{% if not label %}{% set label=field.label.text %}{% endif %}
<label for="{{ field.id }}" class="control-label">{{ label|safe }}</label>
{% endif %}
<div class="deps_selector bulletselector">
<input type="text" placeholder="Comma-seperated values">
<div class="clearboth"></div>
</div>
{{ field(class_='form-control', **kwargs) }}
{% if field.errors %}
{% for e in field.errors %}
<p class="help-block">{{ e }}</p>
{% endfor %}
{% endif %}
</div>
{% endmacro %}
{% macro render_checkbox_field(field, label=None) -%}
{% if not label %}{% set label=field.label.text %}{% endif %}
<div class="checkbox">

View File

@@ -0,0 +1,15 @@
{% extends "base.html" %}
{% block title %}
Meta Packages
{% endblock %}
{% block content %}
<ul>
{% for meta in mpackages %}
<li><a href="{{ url_for('meta_package_page', name=meta.name) }}">{{ meta.name }}</a> ({{ meta.packages | count }} packages)</li>
{% else %}
<li><i>No meta packages found.</i></li>
{% endfor %}
</ul>
{% endblock %}

View File

@@ -0,0 +1,12 @@
{% extends "base.html" %}
{% block title %}
Packages providing '{{ mpackage.name }}''
{% endblock %}
{% block content %}
<h1>Packages providing '{{ mpackage.name }}''</h1>
{% from "macros/packagegridtile.html" import render_pkggrid %}
{{ render_pkggrid(mpackage.packages) }}
{% endblock %}

View File

@@ -8,23 +8,65 @@
{% endblock %}
{% block content %}
<h2>Create Package</h2>
<h1>Create Package</h1>
{% from "macros/forms.html" import render_field, render_submit_field, form_includes, render_multiselect_field %}
<script>
meta_packages = [
{% for m in mpackages %}
{# This is safe as name can only contain `[a-z0-9_]` #}
{
id: "{{ m.name }}",
value: "{{ m.name }}",
toString: function() { return "{{ m.name }}"; },
},
{% endfor %}
]
function escape(unsafe) {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
all_packages = meta_packages.slice();
{% for p in packages %}
all_packages.push({
id: "{{ p.author.username }}/{{ p.name }}",
value: escape({{ p.title | tojson }} + " by " + {{ p.author.display_name | tojson }}),
toString: function() { return escape({{ p.title | tojson }} + " by " + {{ p.author.display_name | tojson }} + " only"); },
});
{% endfor %}
</script>
{% from "macros/forms.html" import render_field, render_submit_field, form_includes, render_multiselect_field, render_mpackage_field, render_deps_field %}
{{ form_includes() }}
<form method="POST" action="" class="tableform">
{{ form.hidden_tag() }}
<h2 class="pkg_meta">Package</h2>
{{ render_field(form.type, class_="pkg_meta") }}
{{ render_field(form.name, class_="pkg_meta") }}
{{ render_field(form.title, class_="pkg_meta") }}
{{ render_field(form.shortDesc, class_="pkg_meta") }}
{{ render_field(form.desc, class_="pkg_meta") }}
{{ render_field(form.type, class_="pkg_meta") }}
{{ render_field(form.license, class_="pkg_meta") }}
{{ render_multiselect_field(form.tags, class_="pkg_meta") }}
{{ render_multiselect_field(form.harddeps, class_="pkg_meta") }}
{{ render_multiselect_field(form.softdeps, class_="pkg_meta") }}
{{ render_field(form.license, class_="pkg_meta") }}
<div class="pkg_meta">
<h2 class="not_txp">Dependency Info</h2>
{{ render_mpackage_field(form.provides_str, class_="not_txp", placeholder="Comma separated list") }}
{{ render_deps_field(form.harddep_str, class_="not_txp not_game", placeholder="Comma separated list") }}
{{ render_deps_field(form.softdep_str, class_="not_txp not_game", placeholder="Comma separated list") }}
</div>
<h2 class="pkg_meta">Repository and Links</h2>
<div class="pkg_wiz_1">
<p>Enter the repo URL for the package.
@@ -60,4 +102,5 @@
</div>
</noscript>
{% endif %}
<script src="/static/package_edit.js"></script>
{% endblock %}

View File

@@ -18,8 +18,6 @@
{{ render_field(form.type) }}
{{ render_field(form.license) }}
{{ render_multiselect_field(form.tags) }}
{{ render_multiselect_field(form.harddeps) }}
{{ render_multiselect_field(form.softdeps) }}
{{ render_field(form.repo) }}
{{ render_field(form.website) }}
{{ render_field(form.issueTracker) }}

View File

@@ -20,6 +20,10 @@
{% else %}
A release is required before this package can be approved.
{% endif %}
{% elif (package.type == package.type.GAME or package.type == package.type.TXP) and package.screenshots.count() == 0 %}
You need to add at least one screenshot.
{% else %}
{% if package.screenshots.count() == 0 %}
<b>You should add at least one screenshot, but this isn't required.</b><br />
@@ -63,6 +67,17 @@
<td>Name</td>
<td>{{ package.name }}</td>
</tr>
{% if package.provides %}
<tr>
<td>Provides</td>
<td>{% for meta in package.provides %}
<a href="{{ url_for('meta_package_page', name=meta.name) }}">{{ meta.name }}</a>
{%- if not loop.last %}
,
{% endif %}
{% endfor %}</td>
</tr>
{% endif %}
<tr>
<td>Author</td>
<td class="{{ package.author.rank }}">
@@ -79,6 +94,10 @@
<td>License</td>
<td>{{ package.license.name }}</td>
</tr>
<tr>
<td>Added</td>
<td>{{ package.created_at | datetime }}</td>
</tr>
</table>
<ul class="buttonset linedbuttonset">
@@ -117,7 +136,7 @@
{% if not rel.approved %}<i>{% endif %}
<a href="{{ rel.url }}">{{ rel.title }}</a>,
created {{ rel.releaseDate }}.
created {{ rel.releaseDate | datetime }}.
{% if rel.task_id %}
<a href="{{ url_for('check_task', id=rel.task_id, r=package.getDetailsURL()) }}">Importing...</a>
{% elif not rel.approved %}
@@ -149,23 +168,29 @@
{% endfor %}
</ul>
<table class="table-topalign">
<!-- <table class="table-topalign">
<tr>
<td>
<td> -->
<h3>Dependencies</h3>
<ul>
{% for p in package.harddeps %}
<li><a href="{{ p.getDetailsURL() }}">{{ p.title }}</a> by {{ p.author.display_name }}</li>
{% for dep in package.dependencies %}
<li>
{%- if dep.package %}
<a href="{{ dep.package.getDetailsURL() }}">{{ dep.package.title }}</a> by {{ dep.package.author.display_name }}
{% elif dep.meta_package %}
<a href="{{ url_for('meta_package_page', name=dep.meta_package.name) }}">{{ dep.meta_package.name }}</a>
{% else %}
{{ "Excepted package or meta_package in dep!" | throw }}
{% endif %}
{% if dep.optional %}
[optional]
{% endif %}
</li>
{% else %}
{% if not package.softdeps %}
<li>No dependencies.</li>
{% endif %}
{% endfor %}
{% for p in package.softdeps %}
<li><a href="{{ p.getDetailsURL() }}">{{ p.title }}</a> by {{ p.author.display_name }} [optional]</li>
<li><i>No dependencies</i></li>
{% endfor %}
</ul>
</td>
<!-- </td>
<td>
<h3>Required by</h3>
<ul>
@@ -182,7 +207,7 @@
</ul>
</td>
</tr>
</table>
</table> -->
{% if current_user.is_authenticated or requests %}
<h3>Edit Requests</h3>

View File

@@ -0,0 +1,27 @@
{% extends "base.html" %}
{% block title %}
{% if tag %}
Edit {{ tag.title }}
{% else %}
New tag
{% endif %}
{% endblock %}
{% block content %}
<p>
<a href="{{ url_for('tag_list_page') }}">Back to list</a> |
<a href="{{ url_for('createedit_tag_page') }}">New Tag</a>
</p>
{% 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) }}
{% if tag %}
{{ render_field(form.name) }}
{% endif %}
{{ render_submit_field(form.submit) }}
</form>
{% endblock %}

View File

@@ -0,0 +1,16 @@
{% extends "base.html" %}
{% block title %}
Tags
{% endblock %}
{% block content %}
<p>
<a href="{{ url_for('createedit_tag_page') }}">New Tag</a>
</p>
<ul>
{% for t in tags %}
<li><a href="{{ url_for('createedit_tag_page', name=t.name) }}">{{ t.title }}</a> [{{ t.packages | count }} packages]</li>
{% endfor %}
</ul>
{% endblock %}

View File

@@ -27,10 +27,18 @@ from werkzeug.contrib.cache import SimpleCache
from urllib.parse import urlparse
cache = SimpleCache()
@app.template_filter()
def throw(err):
raise Exception(err)
@app.template_filter()
def domain(url):
return urlparse(url).netloc
@app.template_filter()
def datetime(value):
return value.strftime("%Y-%m-%d %H:%M") + " UTC"
@app.route("/uploads/<path:path>")
def send_upload(path):
return send_from_directory("public/uploads", path)
@@ -43,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
from . import users, githublogin, packages, sass, tasks, admin, notifications, tagseditor, meta
@menu.register_menu(app, ".help", "Help", order=19, endpoint_arguments_constructor=lambda: { 'path': 'help' })
@app.route('/<path:path>/')

View File

@@ -20,7 +20,7 @@ from flask_user import *
from flask.ext import menu
from app import app
from app.models import *
from app.tasks.importtasks import importRepoScreenshot
from app.tasks.importtasks import importRepoScreenshot, importAllDependencies
from app.tasks.forumtasks import importUsersFromModList
from flask_wtf import FlaskForm
from wtforms import *
@@ -52,6 +52,19 @@ def admin_page():
package.soft_deleted = False
db.session.commit()
return redirect(url_for("admin_page"))
elif action == "importdepends":
task = importAllDependencies.delay()
return redirect(url_for("check_task", id=task.id, r=url_for("admin_page")))
elif action == "modprovides":
packages = Package.query.filter_by(type=PackageType.MOD).all()
mpackage_cache = {}
for p in packages:
if len(p.provides) == 0:
p.provides.append(MetaPackage.GetOrCreate(p.name, mpackage_cache))
db.session.commit()
return redirect(url_for("admin_page"))
else:
flash("Unknown action: " + action, "error")

34
app/views/meta.py Normal file
View File

@@ -0,0 +1,34 @@
# 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 *
@app.route("/metapackages/")
def meta_package_list_page():
mpackages = MetaPackage.query.order_by(db.desc(MetaPackage.name)).all()
return render_template("meta/list.html", mpackages=mpackages)
@app.route("/metapackages/<name>/")
def meta_package_page(name):
mpackage = MetaPackage.query.filter_by(name=name).first()
if mpackage is None:
abort(404)
return render_template("meta/view.html", mpackage=mpackage)

View File

@@ -51,7 +51,7 @@ def packages_page():
search = request.args.get("q")
if search is not None:
query = query.filter(Package.title.contains(search))
query = query.filter(Package.title.ilike('%' + search + '%'))
if shouldReturnJson():
pkgs = [package.getAsDictionary(app.config["BASE_URL"]) \
@@ -100,15 +100,16 @@ def package_download_page(package):
class PackageForm(FlaskForm):
name = StringField("Name", [InputRequired(), Length(1, 20), Regexp("^[a-z0-9_]", 0, "Lower case letters (a-z), digits (0-9), and underscores (_) only")])
title = StringField("Title", [InputRequired(), Length(3, 50)])
shortDesc = StringField("Short Description", [InputRequired(), Length(1,200)])
desc = TextAreaField("Long Description", [Optional(), Length(0,10000)])
name = StringField("Name (Technical)", [InputRequired(), Length(1, 20), Regexp("^[a-z0-9_]", 0, "Lower case letters (a-z), digits (0-9), and underscores (_) only")])
title = StringField("Title (Human-readable)", [InputRequired(), Length(3, 50)])
shortDesc = StringField("Short Description (Plaintext)", [InputRequired(), Length(1,200)])
desc = TextAreaField("Long Description (Markdown)", [Optional(), Length(0,10000)])
type = SelectField("Type", [InputRequired()], choices=PackageType.choices(), coerce=PackageType.coerce, default=PackageType.MOD)
license = QuerySelectField("License", [InputRequired()], query_factory=lambda: License.query, get_pk=lambda a: a.id, get_label=lambda a: a.name)
provides_str = StringField("Provides (mods included in package)", [Optional(), Length(0,1000)])
tags = QuerySelectMultipleField('Tags', query_factory=lambda: Tag.query.order_by(db.asc(Tag.name)), get_pk=lambda a: a.id, get_label=lambda a: a.title)
harddeps = QuerySelectMultipleField('Dependencies', query_factory=lambda: Package.query.filter_by(soft_deleted=False,approved=True).join(User).order_by(db.asc(Package.title), db.asc(User.display_name)), get_pk=lambda a: a.id, get_label=lambda a: a.title + " by " + a.author.display_name)
softdeps = QuerySelectMultipleField('Soft Dependencies', query_factory=lambda: Package.query.filter_by(soft_deleted=False,approved=True).join(User).order_by(db.asc(Package.title), db.asc(User.display_name)), get_pk=lambda a: a.id, get_label=lambda a: a.title + " by " + a.author.display_name)
harddep_str = StringField("Hard Dependencies", [Optional(), Length(0,1000)])
softdep_str = StringField("Soft Dependencies", [Optional(), Length(0,1000)])
repo = StringField("Repo URL", [Optional(), URL()])
website = StringField("Website URL", [Optional(), URL()])
issueTracker = StringField("Issue Tracker URL", [Optional(), URL()])
@@ -146,6 +147,12 @@ 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 == "POST" and form.validate():
wasNew = False
if not package:
@@ -166,6 +173,27 @@ def create_edit_package_page(author=None, name=None):
form.populate_obj(package) # copy to row
mpackage_cache = {}
package.provides.clear()
mpackages = MetaPackage.SpecToList(form.provides_str.data, mpackage_cache)
for m in mpackages:
package.provides.append(m)
Dependency.query.filter_by(depender=package).delete()
deps = Dependency.SpecToList(package, form.harddep_str.data, mpackage_cache)
for dep in deps:
dep.optional = False
db.session.add(dep)
deps = Dependency.SpecToList(package, form.softdep_str.data, mpackage_cache)
for dep in deps:
dep.optional = True
db.session.add(dep)
if wasNew and package.type == PackageType.MOD and not package.name in mpackage_cache:
m = MetaPackage.GetOrCreate(package.name, mpackage_cache)
package.provides.append(m)
package.tags.clear()
for tag in form.tags.raw_data:
package.tags.append(Tag.query.get(tag))
@@ -178,9 +206,15 @@ def create_edit_package_page(author=None, name=None):
return redirect(package.getDetailsURL())
package_query = Package.query.filter_by(approved=True, soft_deleted=False)
if package is not None:
package_query = package_query.filter(Package.id != package.id)
enableWizard = name is None and request.method != "POST"
return render_template("packages/create_edit.html", package=package, \
form=form, author=author, enable_wizard=enableWizard)
form=form, author=author, enable_wizard=enableWizard, \
packages=package_query.all(), \
mpackages=MetaPackage.query.order_by(db.asc(MetaPackage.name)).all())
@app.route("/packages/<author>/<name>/approve/", methods=["POST"])
@login_required

View File

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

57
app/views/tagseditor.py Normal file
View File

@@ -0,0 +1,57 @@
# 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 flask_wtf import FlaskForm
from wtforms import *
from wtforms.validators import *
from app.utils import rank_required
@app.route("/tags/")
@rank_required(UserRank.MODERATOR)
def tag_list_page():
return render_template("tags/list.html", tags=Tag.query.all())
class TagForm(FlaskForm):
title = StringField("Title", [InputRequired(), Length(3,100)])
name = StringField("Name", [Optional(), Length(1, 20), Regexp("^[a-z0-9_]", 0, "Lower case letters (a-z), digits (0-9), and underscores (_) only")])
submit = SubmitField("Save")
@app.route("/tags/new/", methods=["GET", "POST"])
@app.route("/tags/<name>/edit/", methods=["GET", "POST"])
@rank_required(UserRank.MODERATOR)
def createedit_tag_page(name=None):
tag = None
if name is not None:
tag = Tag.query.filter_by(name=name).first()
if tag is None:
abort(404)
form = TagForm(formdata=request.form, obj=tag)
if request.method == "POST" and form.validate():
if tag is None:
tag = Tag(form.title.data)
db.session.add(tag)
else:
form.populate_obj(tag)
db.session.commit()
return redirect(url_for("createedit_tag_page", name=tag.name))
return render_template("tags/edit.html", tag=tag, form=form)

View File

@@ -31,9 +31,9 @@ from app.tasks.emails import sendVerifyEmail
# Define the User profile form
class UserProfileForm(FlaskForm):
display_name = StringField("Display name", [InputRequired(), Length(2, 20)])
email = StringField("Email")
rank = SelectField("Rank", [InputRequired()], choices=UserRank.choices(), coerce=UserRank.coerce, default=UserRank.NEW_MEMBER)
display_name = StringField("Display name", [Optional(), Length(2, 20)])
email = StringField("Email", [Optional(), Email()])
rank = SelectField("Rank", [Optional()], choices=UserRank.choices(), coerce=UserRank.coerce, default=UserRank.NEW_MEMBER)
submit = SubmitField("Save")
@app.route("/users/", methods=["GET"])

View File

@@ -0,0 +1,39 @@
"""empty message
Revision ID: 4e482c47e519
Revises: 900758871713
Create Date: 2018-05-27 22:38:16.507155
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '4e482c47e519'
down_revision = '900758871713'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('dependency',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('depender_id', sa.Integer(), nullable=True),
sa.Column('package_id', sa.Integer(), nullable=True),
sa.Column('meta_package_id', sa.Integer(), nullable=True),
sa.Column('optional', sa.Boolean(), nullable=False),
sa.ForeignKeyConstraint(['depender_id'], ['package.id'], ),
sa.ForeignKeyConstraint(['meta_package_id'], ['meta_package.id'], ),
sa.ForeignKeyConstraint(['package_id'], ['package.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('depender_id', 'package_id', 'meta_package_id', name='_dependency_uc')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('dependency')
# ### end Alembic commands ###

View File

@@ -0,0 +1,57 @@
"""empty message
Revision ID: 900758871713
Revises: ea5a023711e0
Create Date: 2018-05-27 16:36:44.258935
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '900758871713'
down_revision = 'ea5a023711e0'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('meta_package',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=100), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
op.create_table('provides',
sa.Column('package_id', sa.Integer(), nullable=False),
sa.Column('metapackage_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['metapackage_id'], ['meta_package.id'], ),
sa.ForeignKeyConstraint(['package_id'], ['package.id'], ),
sa.PrimaryKeyConstraint('package_id', 'metapackage_id')
)
op.drop_table('harddeps')
op.drop_table('softdeps')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('softdeps',
sa.Column('package_id', sa.INTEGER(), nullable=False),
sa.Column('dependency_id', sa.INTEGER(), nullable=False),
sa.ForeignKeyConstraint(['dependency_id'], ['package.id'], ),
sa.ForeignKeyConstraint(['package_id'], ['package.id'], ),
sa.PrimaryKeyConstraint('package_id', 'dependency_id')
)
op.create_table('harddeps',
sa.Column('package_id', sa.INTEGER(), nullable=False),
sa.Column('dependency_id', sa.INTEGER(), nullable=False),
sa.ForeignKeyConstraint(['dependency_id'], ['package.id'], ),
sa.ForeignKeyConstraint(['package_id'], ['package.id'], ),
sa.PrimaryKeyConstraint('package_id', 'dependency_id')
)
op.drop_table('provides')
op.drop_table('meta_package')
# ### end Alembic commands ###

View File

@@ -0,0 +1,29 @@
"""empty message
Revision ID: b254f55eadd2
Revises: 4e482c47e519
Create Date: 2018-05-27 23:51:11.008936
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'b254f55eadd2'
down_revision = '4e482c47e519'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
conn = op.get_bind()
conn.execute("ALTER TYPE userrank ADD VALUE 'TRUSTED_MEMBER'")
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@@ -255,7 +255,6 @@ No warranty is provided, express or implied, for any part of the project.
mod.title = "Sweet Foods"
mod.license = licenses["CC0"]
mod.type = PackageType.MOD
mod.harddeps.append(food)
mod.author = ruben
mod.tags.append(tags["player_effects"])
mod.repo = "https://github.com/rubenwardy/food_sweet/"
@@ -263,6 +262,7 @@ No warranty is provided, express or implied, for any part of the project.
mod.forums = 9039
mod.shortDesc = "Adds sweet food"
mod.desc = "This is the long desc"
food_sweet = mod
db.session.add(mod)
game1 = Package()
@@ -314,6 +314,23 @@ Uses the CTF PvP Engine.
rel.approved = True
db.session.add(rel)
db.session.commit()
metas = {}
for package in Package.query.filter_by(type=PackageType.MOD).all():
meta = None
try:
meta = metas[package.name]
except KeyError:
meta = MetaPackage(package.name)
db.session.add(meta)
metas[package.name] = meta
package.provides.append(meta)
dep = Dependency(food_sweet, meta=metas["food"])
db.session.add(dep)
delete_db = len(sys.argv) >= 2 and sys.argv[1].strip() == "-d"
if delete_db and os.path.isfile("db.sqlite"):