diff --git a/README.md b/README.md index c5203e11..8dc588b7 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 06521365..e79b00fc 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 2b992f53..771d0fdb 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 00000000..40fbe440 --- /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 d5895bfe..2c69e6d5 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 = $('') + .text(value) + .data("id", id) + .append(' x') + .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 $('
') + .data('item.autocomplete', item) + .append($('').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 2cf8af4e..a8ec31ad 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 db992b3c..7ccd36c5 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 3c15fa99..284919dc 100644 --- a/app/templates/admin/list.html +++ b/app/templates/admin/list.html @@ -17,8 +17,9 @@ diff --git a/app/templates/macros/forms.html b/app/templates/macros/forms.html index b23711a7..7700fe2e 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 %} {% endif %} -{{ e }}
+ {% endfor %} + {% endif %} +{{ e }}
+ {% endfor %} + {% endif %} +