Skip to content

Commit

Permalink
Feature: add support for regions (add, edit, filter)
Browse files Browse the repository at this point in the history
  • Loading branch information
TrueBrain committed Mar 5, 2023
1 parent 5cefae1 commit 0815f2b
Show file tree
Hide file tree
Showing 15 changed files with 330 additions and 20 deletions.
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

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 %}

0 comments on commit 0815f2b

Please sign in to comment.