aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md8
-rw-r--r--app/models.py186
-rw-r--r--app/public/static/package_create.js12
-rw-r--r--app/public/static/package_edit.js11
-rw-r--r--app/public/static/tagselector.js145
-rw-r--r--app/scss/components.scss16
-rw-r--r--app/tasks/importtasks.py119
-rw-r--r--app/templates/admin/list.html3
-rw-r--r--app/templates/macros/forms.html40
-rw-r--r--app/templates/meta/list.html15
-rw-r--r--app/templates/meta/view.html12
-rw-r--r--app/templates/packages/create_edit.html55
-rw-r--r--app/templates/packages/editrequest_create_edit.html2
-rw-r--r--app/templates/packages/view.html39
-rw-r--r--app/views/__init__.py6
-rw-r--r--app/views/admin.py5
-rw-r--r--app/views/meta.py34
-rw-r--r--app/views/packages/__init__.py44
-rw-r--r--migrations/versions/4e482c47e519_.py39
-rw-r--r--migrations/versions/900758871713_.py57
-rw-r--r--setup.py19
21 files changed, 761 insertions, 106 deletions
diff --git a/README.md b/README.md
index c5203e1..8dc588b 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/app/models.py b/app/models.py
index 0652136..e79b00f 100644
--- a/app/models.py
+++ b/app/models.py
@@ -219,8 +219,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 +228,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 +334,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"))
@@ -418,6 +472,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)
@@ -555,42 +657,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)
diff --git a/app/public/static/package_create.js b/app/public/static/package_create.js
index 2b992f5..771d0fd 100644
--- a/app/public/static/package_create.js
+++ b/app/public/static/package_create.js
@@ -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)
}
diff --git a/app/public/static/package_edit.js b/app/public/static/package_edit.js
new file mode 100644
index 0000000..40fbe44
--- /dev/null
+++ b/app/public/static/package_edit.js
@@ -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()
+})
diff --git a/app/public/static/tagselector.js b/app/public/static/tagselector.js
index d5895bf..2c69e6d 100644
--- a/app/public/static/tagselector.js
+++ b/app/public/static/tagselector.js
@@ -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);
diff --git a/app/scss/components.scss b/app/scss/components.scss
index 2cf8af4..a8ec31a 100644
--- a/app/scss/components.scss
+++ b/app/scss/components.scss
@@ -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;
}
diff --git a/app/tasks/importtasks.py b/app/tasks/importtasks.py
index db992b3..7ccd36c 100644
--- a/app/tasks/importtasks.py
+++ b/app/tasks/importtasks.py
@@ -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,94 @@ 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:
+ raise TaskError("Unsupported repo")
+
+ result = {}
+ if urlmaker.isValid():
+ #
+ # 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
+
+ else:
+ print(TaskError("non-github depends detector not implemented yet!"))
+ return {}
+
+
+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()
diff --git a/app/templates/admin/list.html b/app/templates/admin/list.html
index 3c15fa9..284919d 100644
--- a/app/templates/admin/list.html
+++ b/app/templates/admin/list.html
@@ -17,8 +17,9 @@
<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="importusers">Create users from mod list</option>
<option value="importscreenshots">Import screenshots from VCS</option>
+ <option value="importdepends" selected>Import dependencies from downloads</option>
</select>
<input type="submit" value="Perform" />
</form>
diff --git a/app/templates/macros/forms.html b/app/templates/macros/forms.html
index b23711a..7700fe2 100644
--- a/app/templates/macros/forms.html
+++ b/app/templates/macros/forms.html
@@ -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">
diff --git a/app/templates/meta/list.html b/app/templates/meta/list.html
new file mode 100644
index 0000000..5fec732
--- /dev/null
+++ b/app/templates/meta/list.html
@@ -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 %}
diff --git a/app/templates/meta/view.html b/app/templates/meta/view.html
new file mode 100644
index 0000000..c5473b9
--- /dev/null
+++ b/app/templates/meta/view.html
@@ -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 %}
diff --git a/app/templates/packages/create_edit.html b/app/templates/packages/create_edit.html
index fc3715a..24a23b6 100644
--- a/app/templates/packages/create_edit.html
+++ b/app/templates/packages/create_edit.html
@@ -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 %}
diff --git a/app/templates/packages/editrequest_create_edit.html b/app/templates/packages/editrequest_create_edit.html
index 987a292..d245206 100644
--- a/app/templates/packages/editrequest_create_edit.html
+++ b/app/templates/packages/editrequest_create_edit.html
@@ -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) }}
diff --git a/app/templates/packages/view.html b/app/templates/packages/view.html
index d6b74a6..2844636 100644
--- a/app/templates/packages/view.html
+++ b/app/templates/packages/view.html
@@ -68,6 +68,15 @@
<td>{{ package.name }}</td>
</tr>
<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>
+ <tr>
<td>Author</td>
<td class="{{ package.author.rank }}">
<a href="{{ url_for('user_profile_page', username=package.author.username) }}">
@@ -153,23 +162,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>
@@ -186,7 +201,7 @@
</ul>
</td>
</tr>
- </table>
+ </table> -->
{% if current_user.is_authenticated or requests %}
<h3>Edit Requests</h3>
diff --git a/app/views/__init__.py b/app/views/__init__.py
index c584bb8..2559969 100644
--- a/app/views/__init__.py
+++ b/app/views/__init__.py
@@ -28,6 +28,10 @@ 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
@@ -43,7 +47,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
+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>/')
diff --git a/app/views/admin.py b/app/views/admin.py
index 2320cc6..b1cfed6 100644
--- a/app/views/admin.py
+++ b/app/views/admin.py
@@ -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,9 @@ 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")))
else:
flash("Unknown action: " + action, "error")
diff --git a/app/views/meta.py b/app/views/meta.py
new file mode 100644
index 0000000..fe1a05a
--- /dev/null
+++ b/app/views/meta.py
@@ -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)
diff --git a/app/views/packages/__init__.py b/app/views/packages/__init__.py
index c60d68d..bd090c7 100644
--- a/app/views/packages/__init__.py
+++ b/app/views/packages/__init__.py
@@ -102,13 +102,14 @@ 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)])
+ 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
diff --git a/migrations/versions/4e482c47e519_.py b/migrations/versions/4e482c47e519_.py
new file mode 100644
index 0000000..9a23f00
--- /dev/null
+++ b/migrations/versions/4e482c47e519_.py
@@ -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 ###
diff --git a/migrations/versions/900758871713_.py b/migrations/versions/900758871713_.py
new file mode 100644
index 0000000..ed1ce98
--- /dev/null
+++ b/migrations/versions/900758871713_.py
@@ -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 ###
diff --git a/setup.py b/setup.py
index d910db7..5873067 100644
--- a/setup.py
+++ b/setup.py
@@ -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"):