Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: add support for regions (add, edit, filter) #165

Merged
merged 1 commit into from
Mar 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 17 additions & 0 deletions webclient/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
_api_url = None
_frontend_url = None
_tus_url = None # None means equal to _api_url
_regions = None


def content_type(key, singular, plural):
Expand All @@ -31,6 +32,21 @@ def content_type(key, singular, plural):
)


def get_regions():
# Delayed import to avoid circular imports
from .api import api_get
TrueBrain marked this conversation as resolved.
Show resolved Hide resolved

global _regions
if not _regions:
_regions = {}

regions = api_get(("config", "regions"))
for region in regions:
_regions[region["code"]] = region

return _regions


@click_helper.extend
@click.option(
"--api-url",
Expand Down Expand Up @@ -69,6 +85,7 @@ def template(*args, **kwargs):
kwargs["globals"] = {
"copyright_year": datetime.datetime.utcnow().year,
"content_types": _content_types,
"regions": get_regions(),
}

response = flask.make_response(flask.render_template(*args, **kwargs))
Expand Down
7 changes: 5 additions & 2 deletions webclient/pages/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from . import static # noqa
from . import login # noqa
from . import package_list, package_info, version_info # noqa
from . import package_info # noqa
from . import package_list # noqa
from . import regions # noqa
from . import static # noqa
from . import version_info # noqa
6 changes: 6 additions & 0 deletions webclient/pages/package_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ def manager_package_edit(session, content_type, unique_id):
record_change(changes, package, "name", form.get("name").strip())
record_change(changes, package, "url", form.get("url").strip())

regions = form.get("regions").strip().splitlines()
regions = set(t.strip() for t in regions)
regions.discard("")
regions = sorted(regions)
record_change(changes, package, "regions", regions)

desc = "\n".join(t.rstrip() for t in form.get("description").strip().splitlines())
record_change(changes, package, "description", desc)

Expand Down
27 changes: 27 additions & 0 deletions webclient/pages/regions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import flask

from ..app import app
from ..helpers import get_regions


@app.route("/regions")
def region_search():
query = flask.request.args.get("search")

if not query:
return flask.jsonify({"result": []})

regions = get_regions()

query = query.strip()
if len(query) < 2:
return flask.jsonify({"result": []})

matches = []
for region in regions.values():
if query.lower() in region["name"].lower():
matches.append(region)
if query.lower() in region["code"].lower():
matches.append(region)

return flask.jsonify({"result": matches})
32 changes: 28 additions & 4 deletions webclient/pages/version_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
api_put,
)
from ..helpers import (
get_regions,
redirect,
template,
tus_host,
Expand Down Expand Up @@ -105,7 +106,26 @@ def record_change_dependencies(changes, data, form, messages):
return valid_data


def record_change_descripton(changes, data, desc):
def record_change_regions(changes, data, regions, messages):
valid_data = True

regions = regions.strip().splitlines()
regions = set(r.strip() for r in regions)
regions.discard("")
regions = sorted(regions)

known_regions = get_regions()
for region in regions:
if region not in known_regions:
valid_data = False
messages.append("Invalid region: {}".format(region))

record_change(changes, data, "regions", regions, True)

return valid_data


def record_change_description(changes, data, desc):
desc = "\n".join(t.rstrip() for t in desc.strip().splitlines())
record_change(changes, data, "description", desc, True)

Expand Down Expand Up @@ -185,7 +205,9 @@ def manager_version_edit(session, content_type, unique_id, upload_date):
record_change_compatibility(changes, version, form)
if not record_change_dependencies(changes, version, form, messages):
valid_data = False
record_change_descripton(changes, version, form.get("description"))
if not record_change_regions(changes, version, form.get("regions"), messages):
valid_data = False
record_change_description(changes, version, form.get("description"))

version.update(changes)
if not valid_csrf:
Expand Down Expand Up @@ -255,7 +277,9 @@ def manager_new_package_upload(session, token):
record_change_compatibility(changes, version, form)
if not record_change_dependencies(changes, version, form, messages):
valid_data = False
record_change_descripton(changes, version, form.get("description"))
if not record_change_regions(changes, version, form.get("regions"), messages):
valid_data = False
record_change_description(changes, version, form.get("description"))

version.update(changes)
if not valid_csrf:
Expand Down Expand Up @@ -331,5 +355,5 @@ def manager_new_package_upload(session, token):

# Allow connecting to the tus host from this page. It is always on a
# different domain than we are.
response.headers["Content-Security-Policy"] = "default-src 'self'; connect-src " + tus_host()
response.headers["Content-Security-Policy"] = f"default-src 'self'; connect-src 'self' {tus_host()}"
return response
16 changes: 16 additions & 0 deletions webclient/static/css/bananas.css
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,19 @@
margin-bottom: 6px;
width: 150px;
}

.region-search {
margin-left: 10px;
vertical-align: top;
}

.region-search > ul {
display: inline-block;
height: 170px;
margin: 0;
overflow: auto;
}
.region-search > ul > li {
cursor: pointer;
text-decoration: underline;
}
46 changes: 41 additions & 5 deletions webclient/static/filter.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,35 @@ function filterList() {
let match = true;
let filters = document.getElementsByClassName("filter-select");
for (let filter of filters) {
let value = row.dataset[filter.name];
/* Filter that is not set is always a match. */
if (filter.value == "") continue;

if (filter.value == "(none)") {
/* If the filter is set to "(none)", then the entry should not have
* this key at all. */
for (let key in row.dataset) {
if (key == filter.name || key.startsWith(filter.name + "--")) {
match = false;
break;
}
}
continue;
}

/* Find all the dataset entries that match this filter. Some can
* end with --<number>, to have unique entries in the dataset.
* But that postfix should be ignored for matching. */
let matches = 0;
for (let key in row.dataset) {
if (key == filter.name || key.startsWith(filter.name + "--")) {
if (row.dataset[key] == filter.value) {
matches++;
}
}
}

if (filter.value != "" && value != filter.value) {
if (matches == 0) {
/* If there are no matches, this entry is not a match. */
match = false;
break;
}
Expand Down Expand Up @@ -55,14 +81,24 @@ document.addEventListener("DOMContentLoaded", function(event) {
let list = document.getElementById("bananas-table");
for (var i = 0; i < list.rows.length; i++) {
let row = list.rows[i];
for (let key in row.dataset) {
let value = row.dataset[key];
for (let rawkey in row.dataset) {
let value = row.dataset[rawkey];

/* If a key ends with --<index>-<number>, remove this postfix. We do this,
* as some entries, like regions, are in fact a list. "dataset"
* doesn't support this, so we postfix it to make the key unique. */
let key = rawkey.replace(/--\d+-\d+$/, "");

if (classifications.has(key)) {
classifications.get(key).add(value);
} else {
classifications.set(key, new Set([value]));
}

/* For multi-selects, add a "None" option. */
if (key != rawkey) {
classifications.get(key).add("(none)")
}
}
}

Expand All @@ -87,7 +123,7 @@ document.addEventListener("DOMContentLoaded", function(event) {

let option = document.createElement("option");
option.value = "";
option.text = "All";
option.text = "(All)";
select.appendChild(option);

for (let value of Array.from(classification[1]).sort()) {
Expand Down
64 changes: 64 additions & 0 deletions webclient/static/regions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"use strict";

let xhr = new XMLHttpRequest();

function regionClick(event) {
let region = event.target.dataset.region;
let regions = document.getElementById("regions");
regions.value += region + "\n";
}

function searchResult(regions) {
let list = document.getElementById("region-list");
list.innerHTML = "";

if (regions["result"].length == 0) {
let span = document.createElement("span");
span.textContent = "No regions found.";
list.appendChild(span);
return;
}

for (let region of regions["result"]) {
let li = document.createElement("li");

li.dataset.region = region["code"];
li.textContent = region["code"] + ": " + region["name"];

li.addEventListener("click", regionClick);
list.appendChild(li);
}
}

function searchRequest() {
let search = document.getElementById("region-search");
let value = search.value;

xhr.abort();

xhr.open("GET", "/regions?search=" + encodeURIComponent(value));
xhr.send();

xhr.onload = function() {
if (xhr.status != 200) {
alert("Error: " + xhr.status + " " + xhr.statusText);
return;
}

let regions = JSON.parse(xhr.responseText);
searchResult(regions);
};
}

document.addEventListener("DOMContentLoaded", function(event) {
let search = document.getElementById("region-search");
let search_timer;

/* Search for regions when the user stops typing for 300ms. */
search.addEventListener("input", function(event) {
if (search_timer) {
clearTimeout(search_timer);
}
search_timer = setTimeout(searchRequest, 300);
});
});
22 changes: 19 additions & 3 deletions webclient/templates/manager_new_package.html
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ <h3>Step 3: Complete the description</h3>
</tr>
{% endfor %}
</table>
<p id="input-desc">Enter version requirements like "&gt;= 1.2.0" or "&lt; 1.10.0"</p>
<p id="input-desc">Enter version requirements like "&gt;= 1.2.0" or "&lt; 13.0".</p>
</td></tr>
<tr><th>Dependencies</th><td>
{% if deps_editable %}
Expand All @@ -120,11 +120,26 @@ <h3>Step 3: Complete the description</h3>
</ul>
{% endif %}
</td></tr>
{% if not package["name"] and version["content-type"] and (version["content-type"] == "newgrf" or version["content-type"] == "scenario" or version["content-type"] == "heightmap") %}
<tr><th>Regions<br/><small>Only available with NewGRFs, Heightmaps, and Scenarios</small></th><td>
<textarea name="regions" id="regions" cols="20" rows="10">
{%- for r in version["regions"] -%}
{{ r }}
{% endfor -%}
</textarea>
<span class="region-search">Search: <input type="text" id="region-search" placeholder="Start typing ..." /></span>
<span class="region-search"><ul id="region-list"></ul></span>
<p id="input-desc">Enter one region per row. Maximum 10 regions.{% if package %} Leave empty to use the regions from the package.{% endif %}</p>
</td></tr>
{% else %}
<input type="hidden" name="regions" />
{% endif %}
<tr><th>Description</th><td>
{% if package %}Leave empty to use the description from the package.<br/>{% endif %}
<textarea name="description" cols="50" rows="20" placeholder="{{ package["description"] }}">
{{- version["description"] -}}
</textarea></td></tr>
</textarea>
{% if package %}<p id="input-desc">Leave empty to use the description from the package.</p>{% endif %}
</td></tr>
</table>

<h3>Step 4: Validate and publish</h3>
Expand Down Expand Up @@ -152,6 +167,7 @@ <h3>Step 4: Validate and publish</h3>

</form>

<script src="/static/regions.js"></script>
<script src="/static/tus.min.js"></script>
<script src="/static/uploader.js"></script>

Expand Down
16 changes: 16 additions & 0 deletions webclient/templates/manager_package_edit.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,20 @@ <h1>{% block title %}{{ package["name"] }}{% endblock %}</h1>
<li>{{ a["display-name"] }}</li>
{% endfor %}
</ul></td></tr>
{% if package["content-type"] and (package["content-type"] == "newgrf" or package["content-type"] == "scenario" or package["content-type"] == "heightmap") %}
<tr><th>Regions<br/><small>Only available with NewGRFs, Heightmaps, and Scenarios</small></th><td>
<textarea name="regions" id="regions" cols="20" rows="10">
{%- for r in package["regions"] -%}
{{ r }}
{% endfor -%}
</textarea>
<span class="region-search">Search: <input type="text" id="region-search" placeholder="Start typing ..." /></span>
<span class="region-search"><ul id="region-list"></ul></span>
<p id="input-desc">Enter one region per row. Maximum 10 regions.</p>
</td></tr>
{% else %}
<input type="hidden" name="regions" />
{% endif %}
<tr><th>Description</th><td>
<textarea name="description" cols="50" rows="20">
{{- package["description"] -}}
Expand All @@ -28,4 +42,6 @@ <h1>{% block title %}{{ package["name"] }}{% endblock %}</h1>
<input type="submit" value="Save"/>
</form>

<script src="/static/regions.js"></script>

{% endblock %}