Add dependencies

This commit is contained in:
rubenwardy 2018-05-27 21:31:11 +01:00
parent 82159d488d
commit 63af1535b9
No known key found for this signature in database
GPG Key ID: A1E29D52FF81513C
8 changed files with 178 additions and 20 deletions

@ -233,7 +233,6 @@ class PackagePropertyKey(enum.Enum):
else: else:
return str(value) return str(value)
provides = db.Table("provides", provides = db.Table("provides",
db.Column("package_id", db.Integer, db.ForeignKey("package.id"), primary_key=True), 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) 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) 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): class Package(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
@ -270,6 +337,8 @@ class Package(db.Model):
provides = db.relationship("MetaPackage", secondary=provides, lazy="subquery", provides = db.relationship("MetaPackage", secondary=provides, lazy="subquery",
backref=db.backref("packages", lazy=True)) 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", tags = db.relationship("Tag", secondary=tags, lazy="subquery",
backref=db.backref("packages", lazy=True)) backref=db.backref("packages", lazy=True))
@ -403,6 +472,7 @@ class Package(db.Model):
class MetaPackage(db.Model): class MetaPackage(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), unique=True, nullable=False) name = db.Column(db.String(100), unique=True, nullable=False)
dependencies = db.relationship("Dependency", backref="meta_package", lazy="dynamic")
def __init__(self, name=None): def __init__(self, name=None):
self.name = name self.name = name

@ -86,6 +86,18 @@
input = $('input[type=text]', this); input = $('input[type=text]', this);
var selected = []; 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(); }) selector.click(function() { input.focus(); })
.delegate('.tag a', 'click', function() { .delegate('.tag a', 'click', function() {
@ -122,8 +134,8 @@
function recreate() { function recreate() {
selector.find("span").remove(); selector.find("span").remove();
for (var i = 0; i < selected.length; i++) { for (var i = 0; i < selected.length; i++) {
var value = source[selected[i]] || selected[i]; var value = lookup[selected[i]] || { value: selected[i] };
addTag(selected[i], value); addTag(selected[i], value.value);
} }
result.val(selected.join(",")) result.val(selected.join(","))
} }
@ -134,7 +146,9 @@
e.preventDefault(); e.preventDefault();
else if (e.keyCode === $.ui.keyCode.COMMA) { else if (e.keyCode === $.ui.keyCode.COMMA) {
var item = input.val(); 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); selectItem(item);
recreate(); recreate();
input.val(""); input.val("");
@ -148,7 +162,8 @@
var item = selected[selected.length - 1]; var item = selected[selected.length - 1];
selected.splice(selected.length - 1, 1); selected.splice(selected.length - 1, 1);
recreate(); recreate();
input.val(item); if (!(item.indexOf("/") > 0))
input.val(item);
e.preventDefault(); e.preventDefault();
return true; return true;
} }
@ -207,6 +222,12 @@
var input = $(this).parent().children("input[type='text']"); var input = $(this).parent().children("input[type='text']");
input.hide(); input.hide();
$(this).csvSelector(meta_packages, input.attr("name"), input); $(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); })(jQuery);

@ -58,6 +58,25 @@
</div> </div>
{% endmacro %} {% 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="Start typing to see suggestions">
<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) -%} {% macro render_checkbox_field(field, label=None) -%}
{% if not label %}{% set label=field.label.text %}{% endif %} {% if not label %}{% set label=field.label.text %}{% endif %}
<div class="checkbox"> <div class="checkbox">

@ -21,9 +21,20 @@
}, },
{% endfor %} {% 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 %}
</script> </script>
{% 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() }} {{ form_includes() }}
<form method="POST" action="" class="tableform"> <form method="POST" action="" class="tableform">
@ -36,6 +47,8 @@
{{ render_field(form.type, class_="pkg_meta") }} {{ render_field(form.type, class_="pkg_meta") }}
{{ render_field(form.license, class_="pkg_meta") }} {{ render_field(form.license, class_="pkg_meta") }}
{{ render_mpackage_field(form.provides_str, class_="pkg_meta", placeholder="Comma separated list") }} {{ 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") }} {{ render_multiselect_field(form.tags, class_="pkg_meta") }}
<div class="pkg_wiz_1"> <div class="pkg_wiz_1">

@ -162,27 +162,33 @@
{% endfor %} {% endfor %}
</ul> </ul>
<!-- <table class="table-topalign"> <table class="table-topalign">
<tr> <tr>
<td> <td>
<h3>Dependencies</h3> <h3>Dependencies</h3>
<ul> <ul>
{% for p in package.harddeps %} {% for dep in package.dependencies %}
<li><a href="{{ p.getDetailsURL() }}">{{ p.title }}</a> by {{ p.author.display_name }}</li> <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 %} {% else %}
{% if not package.softdeps %} <li><i>No dependencies</i></li>
<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>
{% endfor %} {% endfor %}
</ul> </ul>
</td> </td>
<td> <td>
<h3>Required by</h3> <h3>Required by</h3>
<ul> <ul>
{% for p in package.dependents %} <!-- {% for p in package.dependents %}
<li><a href="{{ p.getDetailsURL() }}">{{ p.title }}</a> by {{ p.author.display_name }}</li> <li><a href="{{ p.getDetailsURL() }}">{{ p.title }}</a> by {{ p.author.display_name }}</li>
{% else %} {% else %}
{% if not package.softdependents %} {% if not package.softdependents %}
@ -191,11 +197,11 @@
{% endfor %} {% endfor %}
{% for p in package.softdependents %} {% for p in package.softdependents %}
<li><a href="{{ p.getDetailsURL() }}">{{ p.title }}</a> by {{ p.author.display_name }} [optional]</li> <li><a href="{{ p.getDetailsURL() }}">{{ p.title }}</a> by {{ p.author.display_name }} [optional]</li>
{% endfor %} {% endfor %} -->
</ul> </ul>
</td> </td>
</tr> </tr>
</table> --> </table>
{% if current_user.is_authenticated or requests %} {% if current_user.is_authenticated or requests %}
<h3>Edit Requests</h3> <h3>Edit Requests</h3>

@ -27,6 +27,10 @@ from werkzeug.contrib.cache import SimpleCache
from urllib.parse import urlparse from urllib.parse import urlparse
cache = SimpleCache() cache = SimpleCache()
@app.template_filter()
def throw(err):
raise Exception(err)
@app.template_filter() @app.template_filter()
def domain(url): def domain(url):
return urlparse(url).netloc return urlparse(url).netloc

@ -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) 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)]) 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) tags = QuerySelectMultipleField('Tags', query_factory=lambda: Tag.query.order_by(db.asc(Tag.name)), get_pk=lambda a: a.id, get_label=lambda a: a.title)
harddep_str = StringField("Hard Dependencies", [Optional(), Length(0,1000)])
softdep_str = StringField("Soft Dependencies", [Optional(), Length(0,1000)])
repo = StringField("Repo URL", [Optional(), URL()]) repo = StringField("Repo URL", [Optional(), URL()])
website = StringField("Website URL", [Optional(), URL()]) website = StringField("Website URL", [Optional(), URL()])
issueTracker = StringField("Issue Tracker 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 # Initial form class from post data and default data
if request.method == "GET" and package is not None: 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) form.provides_str.data = MetaPackage.ListToSpec(package.provides)
if request.method == "POST" and form.validate(): if request.method == "POST" and form.validate():
@ -174,11 +179,21 @@ def create_edit_package_page(author=None, name=None):
for m in mpackages: for m in mpackages:
package.provides.append(m) 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: if wasNew and package.type == PackageType.MOD and not package.name in mpackage_cache:
m = MetaPackage.GetOrCreate(package.name, mpackage_cache) m = MetaPackage.GetOrCreate(package.name, mpackage_cache)
package.provides.append(m) package.provides.append(m)
package.tags.clear() package.tags.clear()
for tag in form.tags.raw_data: for tag in form.tags.raw_data:
package.tags.append(Tag.query.get(tag)) package.tags.append(Tag.query.get(tag))
@ -191,9 +206,14 @@ def create_edit_package_page(author=None, name=None):
return redirect(package.getDetailsURL()) 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" enableWizard = name is None and request.method != "POST"
return render_template("packages/create_edit.html", package=package, \ 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()) mpackages=MetaPackage.query.order_by(db.asc(MetaPackage.name)).all())
@app.route("/packages/<author>/<name>/approve/", methods=["POST"]) @app.route("/packages/<author>/<name>/approve/", methods=["POST"])

@ -262,6 +262,7 @@ No warranty is provided, express or implied, for any part of the project.
mod.forums = 9039 mod.forums = 9039
mod.shortDesc = "Adds sweet food" mod.shortDesc = "Adds sweet food"
mod.desc = "This is the long desc" mod.desc = "This is the long desc"
food_sweet = mod
db.session.add(mod) db.session.add(mod)
game1 = Package() game1 = Package()
@ -326,6 +327,10 @@ Uses the CTF PvP Engine.
metas[package.name] = meta metas[package.name] = meta
package.provides.append(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" delete_db = len(sys.argv) >= 2 and sys.argv[1].strip() == "-d"
if delete_db and os.path.isfile("db.sqlite"): if delete_db and os.path.isfile("db.sqlite"):