diff --git a/app/models.py b/app/models.py index bfd8a31c..32835952 100644 --- a/app/models.py +++ b/app/models.py @@ -233,7 +233,6 @@ class PackagePropertyKey(enum.Enum): 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) @@ -244,6 +243,74 @@ tags = db.Table("tags", db.Column("package_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 + + + class Package(db.Model): id = db.Column(db.Integer, primary_key=True) @@ -270,6 +337,8 @@ class Package(db.Model): provides = db.relationship("MetaPackage", secondary=provides, lazy="subquery", backref=db.backref("packages", lazy=True)) + dependencies = db.relationship("Dependency", backref="depender", lazy="dynamic", foreign_keys=[Dependency.depender_id]) + tags = db.relationship("Tag", secondary=tags, lazy="subquery", backref=db.backref("packages", lazy=True)) @@ -403,6 +472,7 @@ class Package(db.Model): 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 diff --git a/app/public/static/tagselector.js b/app/public/static/tagselector.js index 2e906574..32e48823 100644 --- a/app/public/static/tagselector.js +++ b/app/public/static/tagselector.js @@ -86,6 +86,18 @@ input = $('input[type=text]', this); var selected = []; + var lookup = {}; + for (var i = 0; i < source.length; i++) { + lookup[source[i].id] = source[i]; + } + + var selected_raw = result.val().split(","); + for (var i = 0; i < selected_raw.length; i++) { + var raw = selected_raw[i].trim(); + if (lookup[raw]) { + selected.push(raw); + } + } selector.click(function() { input.focus(); }) .delegate('.tag a', 'click', function() { @@ -122,8 +134,8 @@ function recreate() { selector.find("span").remove(); for (var i = 0; i < selected.length; i++) { - var value = source[selected[i]] || selected[i]; - addTag(selected[i], value); + var value = lookup[selected[i]] || { value: selected[i] }; + addTag(selected[i], value.value); } result.val(selected.join(",")) } @@ -134,7 +146,9 @@ e.preventDefault(); else if (e.keyCode === $.ui.keyCode.COMMA) { var item = input.val(); - if (item.match(/^([a-z0-9_]+)$/)) { + if (item.length == 0) { + input.data("ui-autocomplete").search(""); + } else if (item.match(/^([a-z0-9_]+)$/)) { selectItem(item); recreate(); input.val(""); @@ -148,7 +162,8 @@ var item = selected[selected.length - 1]; selected.splice(selected.length - 1, 1); recreate(); - input.val(item); + if (!(item.indexOf("/") > 0)) + input.val(item); e.preventDefault(); return true; } @@ -207,6 +222,12 @@ 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/templates/macros/forms.html b/app/templates/macros/forms.html index 430c4e8d..940c4a23 100644 --- a/app/templates/macros/forms.html +++ b/app/templates/macros/forms.html @@ -58,6 +58,25 @@ {% endmacro %} +{% macro render_deps_field(field, label=None, label_visible=true, right_url=None, right_label=None) -%} +
+ {% if field.type != 'HiddenField' and label_visible %} + {% if not label %}{% set label=field.label.text %}{% endif %} + + {% endif %} +
+ +
+
+ {{ field(class_='form-control', **kwargs) }} + {% if field.errors %} + {% for e in field.errors %} +

{{ e }}

+ {% endfor %} + {% endif %} +
+{% endmacro %} + {% macro render_checkbox_field(field, label=None) -%} {% if not label %}{% set label=field.label.text %}{% endif %}
diff --git a/app/templates/packages/create_edit.html b/app/templates/packages/create_edit.html index c7c2dbbf..666d4cdb 100644 --- a/app/templates/packages/create_edit.html +++ b/app/templates/packages/create_edit.html @@ -21,9 +21,20 @@ }, {% endfor %} ] + + all_packages = meta_packages.slice(); + + {% for p in packages %} + {# This is safe as name can only contain `[a-z0-9_]` #} + all_packages.push({ + id: "{{ p.author.username }}/{{ p.name }}", + value: {{ p.title | tojson }} + " by " + {{ p.author.display_name | tojson }}, + toString: function() { return {{ p.title | tojson }} + " by " + {{ p.author.display_name | tojson }} + " only"; }, + }); + {% endfor %} - {% from "macros/forms.html" import render_field, render_submit_field, form_includes, render_multiselect_field, render_mpackage_field %} + {% from "macros/forms.html" import render_field, render_submit_field, form_includes, render_multiselect_field, render_mpackage_field, render_deps_field %} {{ form_includes() }}
@@ -36,6 +47,8 @@ {{ render_field(form.type, class_="pkg_meta") }} {{ render_field(form.license, class_="pkg_meta") }} {{ render_mpackage_field(form.provides_str, class_="pkg_meta", placeholder="Comma separated list") }} + {{ render_deps_field(form.harddep_str, class_="pkg_meta", placeholder="Comma separated list") }} + {{ render_deps_field(form.softdep_str, class_="pkg_meta", placeholder="Comma separated list") }} {{ render_multiselect_field(form.tags, class_="pkg_meta") }}
diff --git a/app/templates/packages/view.html b/app/templates/packages/view.html index 1623f1cd..834ea637 100644 --- a/app/templates/packages/view.html +++ b/app/templates/packages/view.html @@ -162,27 +162,33 @@ {% endfor %} - - --> + {% if current_user.is_authenticated or requests %}

Edit Requests

diff --git a/app/views/__init__.py b/app/views/__init__.py index 8fff788d..2559969c 100644 --- a/app/views/__init__.py +++ b/app/views/__init__.py @@ -27,6 +27,10 @@ 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 diff --git a/app/views/packages/__init__.py b/app/views/packages/__init__.py index 3c3acf0e..07f62a96 100644 --- a/app/views/packages/__init__.py +++ b/app/views/packages/__init__.py @@ -108,6 +108,8 @@ class PackageForm(FlaskForm): 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", [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) + 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 +148,9 @@ def create_edit_package_page(author=None, name=None): # 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(): @@ -174,11 +179,21 @@ def create_edit_package_page(author=None, name=None): 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)) @@ -191,9 +206,14 @@ 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, \ + packages=package_query.all(), \ mpackages=MetaPackage.query.order_by(db.asc(MetaPackage.name)).all()) @app.route("/packages///approve/", methods=["POST"]) diff --git a/setup.py b/setup.py index 22b80cf0..58730677 100644 --- a/setup.py +++ b/setup.py @@ -262,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() @@ -326,6 +327,10 @@ Uses the CTF PvP Engine. 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"):