ser2netweb 3.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ser2netweb/__init__.py +20 -0
- ser2netweb/__main__.py +6 -0
- ser2netweb/accounts.py +503 -0
- ser2netweb/cli.py +489 -0
- ser2netweb/clock.py +128 -0
- ser2netweb/config.py +4491 -0
- ser2netweb/console.py +181 -0
- ser2netweb/datalock.py +181 -0
- ser2netweb/engine/__init__.py +0 -0
- ser2netweb/engine/access.py +173 -0
- ser2netweb/engine/bindhint.py +115 -0
- ser2netweb/engine/bridge.py +1928 -0
- ser2netweb/engine/cluster.py +1140 -0
- ser2netweb/engine/modbus.py +530 -0
- ser2netweb/engine/modbus_gateway.py +1465 -0
- ser2netweb/engine/mqtt_pub.py +632 -0
- ser2netweb/engine/netguard.py +805 -0
- ser2netweb/engine/netinfo.py +199 -0
- ser2netweb/engine/portdiag.py +414 -0
- ser2netweb/engine/portlist.py +471 -0
- ser2netweb/engine/protocols/__init__.py +28 -0
- ser2netweb/engine/protocols/base.py +117 -0
- ser2netweb/engine/protocols/raw.py +12 -0
- ser2netweb/engine/protocols/rfc2217.py +586 -0
- ser2netweb/engine/protocols/telnet.py +172 -0
- ser2netweb/engine/serial_compat.py +598 -0
- ser2netweb/engine/serial_io.py +210 -0
- ser2netweb/engine/serial_link.py +414 -0
- ser2netweb/engine/sockopts.py +103 -0
- ser2netweb/engine/sparkplug.py +538 -0
- ser2netweb/engine/sparkplug_node.py +550 -0
- ser2netweb/engine/supervisor.py +533 -0
- ser2netweb/engine/tls.py +232 -0
- ser2netweb/envvars.py +57 -0
- ser2netweb/explain.py +117 -0
- ser2netweb/healthcheck.py +78 -0
- ser2netweb/i18n.py +110 -0
- ser2netweb/importer.py +550 -0
- ser2netweb/layout.py +118 -0
- ser2netweb/licences.py +86 -0
- ser2netweb/locale/tr.json +1569 -0
- ser2netweb/logsetup.py +253 -0
- ser2netweb/logtext.py +23 -0
- ser2netweb/notify.py +761 -0
- ser2netweb/optional.py +76 -0
- ser2netweb/reload.py +295 -0
- ser2netweb/runtime.py +762 -0
- ser2netweb/sdnotify.py +85 -0
- ser2netweb/ser2net_import.py +2001 -0
- ser2netweb/signins.py +98 -0
- ser2netweb/standalone.py +154 -0
- ser2netweb/state.py +1457 -0
- ser2netweb/storage.py +130 -0
- ser2netweb/textfold.py +51 -0
- ser2netweb/tlsreload.py +37 -0
- ser2netweb/watchdog.py +201 -0
- ser2netweb/web/__init__.py +0 -0
- ser2netweb/web/api.py +793 -0
- ser2netweb/web/auth.py +699 -0
- ser2netweb/web/certinfo.py +144 -0
- ser2netweb/web/httpguard.py +413 -0
- ser2netweb/web/ldap_auth.py +556 -0
- ser2netweb/web/oidc_auth.py +330 -0
- ser2netweb/web/proxy.py +398 -0
- ser2netweb/web/routes/__init__.py +78 -0
- ser2netweb/web/routes/cluster.py +669 -0
- ser2netweb/web/routes/common.py +359 -0
- ser2netweb/web/routes/console.py +451 -0
- ser2netweb/web/routes/forms.py +853 -0
- ser2netweb/web/routes/login.py +573 -0
- ser2netweb/web/routes/mappings.py +470 -0
- ser2netweb/web/routes/ops.py +374 -0
- ser2netweb/web/routes/settings.py +2135 -0
- ser2netweb/web/routes/system.py +308 -0
- ser2netweb/web/routes/totp.py +282 -0
- ser2netweb/web/selfsigned.py +129 -0
- ser2netweb/web/server.py +483 -0
- ser2netweb/web/static/app.css +327 -0
- ser2netweb/web/static/app.js +1102 -0
- ser2netweb/web/static/htmx.min.js +1 -0
- ser2netweb/web/static/points.js +218 -0
- ser2netweb/web/static/theme.js +7 -0
- ser2netweb/web/static/xterm-addon-fit.js +2 -0
- ser2netweb/web/static/xterm.css +218 -0
- ser2netweb/web/static/xterm.js +2 -0
- ser2netweb/web/templates/_cluster_body.html +109 -0
- ser2netweb/web/templates/_console.html +21 -0
- ser2netweb/web/templates/_footer.html +13 -0
- ser2netweb/web/templates/_login_notice.html +8 -0
- ser2netweb/web/templates/_macros.html +21 -0
- ser2netweb/web/templates/_mapping_form.html +477 -0
- ser2netweb/web/templates/_mapping_log.html +28 -0
- ser2netweb/web/templates/_mapping_traces.html +44 -0
- ser2netweb/web/templates/_mappings_body.html +149 -0
- ser2netweb/web/templates/_modbus_read.html +18 -0
- ser2netweb/web/templates/_ports_table.html +37 -0
- ser2netweb/web/templates/_serial_port_select.html +48 -0
- ser2netweb/web/templates/_shell.html +41 -0
- ser2netweb/web/templates/audit.html +55 -0
- ser2netweb/web/templates/base.html +77 -0
- ser2netweb/web/templates/dashboard.html +72 -0
- ser2netweb/web/templates/licenses.html +18 -0
- ser2netweb/web/templates/login.html +32 -0
- ser2netweb/web/templates/login_totp.html +23 -0
- ser2netweb/web/templates/settings.html +830 -0
- ser2netweb/web/templates/setup.html +43 -0
- ser2netweb/web/templates/totp.html +41 -0
- ser2netweb/web/totp.py +94 -0
- ser2netweb/winservice.py +1793 -0
- ser2netweb-3.0.0.dist-info/METADATA +120 -0
- ser2netweb-3.0.0.dist-info/RECORD +121 -0
- ser2netweb-3.0.0.dist-info/WHEEL +5 -0
- ser2netweb-3.0.0.dist-info/entry_points.txt +2 -0
- ser2netweb-3.0.0.dist-info/licenses/COMMERCIAL_LICENSE.md +94 -0
- ser2netweb-3.0.0.dist-info/licenses/LICENSE +662 -0
- ser2netweb-3.0.0.dist-info/licenses/NOTICE +20 -0
- ser2netweb-3.0.0.dist-info/licenses/THIRD-PARTY-NOTICES.md +105 -0
- ser2netweb-3.0.0.dist-info/licenses/tools/licenses/npm/htmx.org.txt +13 -0
- ser2netweb-3.0.0.dist-info/licenses/tools/licenses/npm/xterm-addon-fit.txt +19 -0
- ser2netweb-3.0.0.dist-info/licenses/tools/licenses/npm/xterm.txt +21 -0
- ser2netweb-3.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
{% from "_macros.html" import state_label %}
|
|
2
|
+
{% if peers_hidden %}
|
|
3
|
+
<div class="banner warn">{{ _('Open access: only this node is shown. <a href="%(url)s">Sign in</a> to see and manage the other cluster nodes.', url=base ~ '/login') }}</div>
|
|
4
|
+
{% endif %}
|
|
5
|
+
{% if discovery_error %}
|
|
6
|
+
<div class="banner warn" title="{{ discovery_error }}">{{ _("UDP auto-discovery is off: %(error)s. Manually-added peers still work (Settings → LAN cluster).", error=(discovery_error|explain or discovery_error)|trim('.')) }}</div>
|
|
7
|
+
{% endif %}
|
|
8
|
+
{% if notes %}
|
|
9
|
+
{% for c in notes.copies %}
|
|
10
|
+
<div class="banner warn">{{ _("Another node uses this node’s identity: %(name)s at %(addr)s — probably a copy of this installation (its data folder, disk or VM image). Give one of the two a new identity under Settings → LAN cluster.", name=c.name, addr=c.addr) }}</div>
|
|
11
|
+
{% endfor %}
|
|
12
|
+
{% for d in notes.shared %}
|
|
13
|
+
<div class="banner warn">{{ _("Several nodes use the identity of %(name)s (%(addrs)s) — probably copies of one installation. Give each copy a new identity under Settings → LAN cluster on that node.", name=d.name, addrs=d.addrs|join(", ")) }}</div>
|
|
14
|
+
{% endfor %}
|
|
15
|
+
{% if notes.other_key %}
|
|
16
|
+
<div class="banner warn">{{ _("Nodes on this network broadcast with a different cluster key, so they are not shown or contacted: %(addrs)s. Set the same key on every node (Settings → LAN cluster).", addrs=notes.other_key|join(", ")) }}</div>
|
|
17
|
+
{% endif %}
|
|
18
|
+
{% if notes.versions %}
|
|
19
|
+
<div class="banner warn">{{ _("The cluster nodes run different versions (%(versions)s). Upgrade every node to the same version: a node can’t edit the settings another version doesn’t have (README: Upgrading a cluster).", versions=notes.versions|map("default", "?", true)|join(", ")) }}</div>
|
|
20
|
+
{% endif %}
|
|
21
|
+
{% if notes.legacy %}
|
|
22
|
+
<div class="banner warn">{{ _("Nodes running 2.8.0 or older get the cluster key unsigned in every request: %(addrs)s. Upgrade them, then turn off legacy authentication under Settings → LAN cluster.", addrs=notes.legacy|join(", ")) }}</div>
|
|
23
|
+
{% endif %}
|
|
24
|
+
{% endif %}
|
|
25
|
+
<div class="table-scroll">
|
|
26
|
+
<table class="grid list cluster stackable">
|
|
27
|
+
<thead>
|
|
28
|
+
<tr>
|
|
29
|
+
<th>{{ _("Mapping") }}</th><th>{{ _("Serial") }}</th><th>{{ _("Network") }}</th>
|
|
30
|
+
<th>{{ _("State") }}</th><th>{{ _("Clients") }}</th><th title="{{ _('In: bytes read from the serial device · Out: bytes written to it (serial↔serial: A → B / B → A)') }}">{{ _("In / Out") }}</th><th class="col-actions"></th>
|
|
31
|
+
</tr>
|
|
32
|
+
</thead>
|
|
33
|
+
<tbody>
|
|
34
|
+
{% for n in nodes %}
|
|
35
|
+
<tr class="node-row{% if not n.online %} node-offline{% endif %}">
|
|
36
|
+
<td colspan="7">
|
|
37
|
+
<strong>{{ n.name }}</strong>
|
|
38
|
+
{% if n.self %}<span class="tag">{{ _("this node") }}</span>{% endif %}
|
|
39
|
+
{% if n.source == 'manual' %}<span class="tag">{{ _("manual") }}</span>{% endif %}
|
|
40
|
+
<span class="peer mono">{{ n.addr }}</span>
|
|
41
|
+
{% if n.reported %}<span class="muted">({{ _("its own address: %(addr)s", addr=n.reported) }})</span>{% endif %}
|
|
42
|
+
{% if n.online %}
|
|
43
|
+
<span class="muted"> · {{ _("up %(uptime)s", uptime=n.uptime) }}{% if n.version %} · v{{ n.version }}{% endif %} · {{ _("%(running)s/%(total)s running", running=n.running_count, total=n.mapping_count) }}</span>
|
|
44
|
+
{% if n.age == 'older' %}<span class="tag tag-warn">{{ _("older version") }}</span>{% elif n.age == 'newer' %}<span class="tag tag-warn">{{ _("newer version") }}</span>{% endif %}
|
|
45
|
+
{% if not n.self and 'control' not in n.caps %}<span class="tag" title="{{ _('This node runs a version before 2.6.0, which can’t be managed from another node. Upgrade it.') }}">{{ _("view only") }}</span>
|
|
46
|
+
{% elif not n.self and 'edit' not in n.caps %}<span class="tag" title="{{ _('This node runs a version that can’t be edited from another node (2.6.0). Upgrade it.') }}">{{ _("no remote edit") }}</span>{% endif %}
|
|
47
|
+
{% if n.auth == 'legacy' %}<span class="tag tag-warn" title="{{ _('A node running 2.8.0 or older: it gets the cluster key unsigned in every request. Upgrade it.') }}">{{ _("unsigned key") }}</span>{% endif %}
|
|
48
|
+
{% if n.previous_key %}<span class="tag tag-warn" title="{{ _('It accepts only the previous cluster key: set the new key on it.') }}">{{ _("previous key") }}</span>{% endif %}
|
|
49
|
+
{% elif n.problem == 'key' %}
|
|
50
|
+
<span class="state state-error">{{ _("cluster key rejected — check the key on that node") }}</span>
|
|
51
|
+
{% elif n.problem == 'cluster-off' %}
|
|
52
|
+
<span class="state state-error">{{ _("its cluster is switched off") }}</span>
|
|
53
|
+
{% elif n.problem == 'legacy-off' %}
|
|
54
|
+
<span class="state state-error">{{ _("2.8.0 or older — legacy authentication is off on this node") }}</span>
|
|
55
|
+
{% elif n.problem == 'manual-only' %}
|
|
56
|
+
<span class="state state-error">{{ _("2.8.0 or older and doesn’t advertise its LAN address — add it as a manual peer (Settings → LAN cluster) to manage it") }}</span>
|
|
57
|
+
{% elif n.problem == 'unsigned' %}
|
|
58
|
+
<span class="state state-error">{{ _("its answer is not signed with the cluster key") }}</span>
|
|
59
|
+
{% elif n.problem == 'clock' %}
|
|
60
|
+
<span class="state state-error">{{ _("its clock could not be matched") }}</span>
|
|
61
|
+
{% else %}
|
|
62
|
+
<span class="state state-error">{{ _("offline / unreachable") }}</span>
|
|
63
|
+
{% endif %}
|
|
64
|
+
{% if not n.self and n.online and n.url %}<a class="btn sm" href="{{ n.url }}" target="_blank" rel="noopener">{{ _("Open") }}</a>{% endif %}
|
|
65
|
+
</td>
|
|
66
|
+
</tr>
|
|
67
|
+
{% for m in n.mappings %}
|
|
68
|
+
{# ids as in _mappings_body.html, by the node's address: this node's own mappings are
|
|
69
|
+
in this table too #}
|
|
70
|
+
{% set rid = 'c-' ~ ((n.addr ~ '-' ~ m.id)|domid) %}
|
|
71
|
+
<tr>
|
|
72
|
+
<td class="indent col-name"><span id="{{ rid }}-name">{{ m.name|softbreak }}</span>{% if not m.enabled %} <span class="tag">{{ _("disabled") }}</span>{% endif %}</td>
|
|
73
|
+
<td class="mono" data-label="{{ _('Serial') }}">{{ m.serial|softbreak }}</td>
|
|
74
|
+
<td data-label="{{ _('Network') }}">
|
|
75
|
+
<div class="mono">{{ m.endpoint|softbreak }}</div>
|
|
76
|
+
<div class="sub mono">{{ m.kind }}</div>
|
|
77
|
+
</td>
|
|
78
|
+
<td data-label="{{ _('State') }}"><span class="state state-{{ m.state }}">{{ state_label(m.state) }}</span>{% if m.link == 'down' %} <span class="tag tag-warn">{{ _("link down") }}</span>{% endif %}</td>
|
|
79
|
+
<td data-label="{{ _('Clients') }}">{{ m.client_count }}</td>
|
|
80
|
+
<td class="mono nowrap" data-label="{{ _('In / Out') }}"><span id="{{ rid }}-bytes">{{ m.bytes_in|default('')|bytesize }} / {{ m.bytes_out|default('')|bytesize }}</span></td>
|
|
81
|
+
<td class="col-actions">
|
|
82
|
+
{% if can_edit and not n.self and 'control' in n.caps %}
|
|
83
|
+
<div class="row-actions"><div class="btn-group">
|
|
84
|
+
{# the peer by host + port only: the server looks up its address and scheme #}
|
|
85
|
+
{% set peer = {"host": n.ip, "port": n.port, "mid": m.id} %}
|
|
86
|
+
{% if m.enabled %}
|
|
87
|
+
{#- asked only while clients are connected, as on the node itself #}
|
|
88
|
+
{#- (not `n`: that is the node, whose caps the Edit button below reads) #}
|
|
89
|
+
{% set clients = m.client_count if m.client_count is number else 0 %}
|
|
90
|
+
<button class="btn sm" id="{{ rid }}-run" aria-describedby="{{ rid }}-name" hx-post="{{ base }}/api/cluster/control-peer" hx-vals='{{ dict(peer, action="stop")|tojson }}'{% if clients %} hx-confirm="{{ ngettext('Stop “%(name)s”? %(num)s connected client will be disconnected, and it stays stopped until it is started again.', 'Stop “%(name)s”? %(num)s connected clients will be disconnected, and it stays stopped until it is started again.', clients, name=m.name) }}"{% endif %} hx-swap="none" hx-disabled-elt="this">{{ _("Stop") }}</button>
|
|
91
|
+
<button class="btn sm" id="{{ rid }}-restart" aria-describedby="{{ rid }}-name" hx-post="{{ base }}/api/cluster/control-peer" hx-vals='{{ dict(peer, action="restart")|tojson }}'{% if clients %} hx-confirm="{{ ngettext('Restart “%(name)s”? %(num)s connected client will be disconnected.', 'Restart “%(name)s”? %(num)s connected clients will be disconnected.', clients, name=m.name) }}"{% endif %} hx-swap="none" hx-disabled-elt="this">{{ _("Restart") }}</button>
|
|
92
|
+
{% else %}
|
|
93
|
+
<button class="btn sm" id="{{ rid }}-run" aria-describedby="{{ rid }}-name" hx-post="{{ base }}/api/cluster/control-peer" hx-vals='{{ dict(peer, action="start")|tojson }}' hx-swap="none" hx-disabled-elt="this">{{ _("Start") }}</button>
|
|
94
|
+
{% endif %}
|
|
95
|
+
{% if 'edit' in n.caps %}<button class="btn sm" id="{{ rid }}-edit" aria-describedby="{{ rid }}-name" hx-get="{{ base }}/api/cluster/peer-form?{{ peer|urlencode }}" hx-target="#form-panel" hx-swap="innerHTML show:top">{{ _("Edit") }}</button>{% endif %}
|
|
96
|
+
</div></div>
|
|
97
|
+
{% endif %}
|
|
98
|
+
</td>
|
|
99
|
+
</tr>
|
|
100
|
+
{% endfor %}
|
|
101
|
+
{% if n.online and not n.mappings %}
|
|
102
|
+
<tr><td class="indent muted" colspan="7">{{ _("no mappings on this node") }}</td></tr>
|
|
103
|
+
{% endif %}
|
|
104
|
+
{% else %}
|
|
105
|
+
<tr><td colspan="7" class="muted">{{ _("No cluster nodes yet. Auto-discovery beacons run every few seconds; add manual peers under Settings → LAN cluster for routed networks.") }}</td></tr>
|
|
106
|
+
{% endfor %}
|
|
107
|
+
</tbody>
|
|
108
|
+
</table>
|
|
109
|
+
</div>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<div class="form-head">
|
|
2
|
+
<h3 id="panel-title" tabindex="-1">{{ _("Console — %(name)s", name=mapping_name|softbreak) }}</h3>
|
|
3
|
+
<div class="actions">
|
|
4
|
+
{% if interactive %}<span class="muted" data-text-only>{{ _("type to send to the device") }}</span>
|
|
5
|
+
<span class="muted">{{ _("Shift+Tab leaves the console") }}</span>
|
|
6
|
+
{#- a serial BREAK on the device's port (audited): the answer says it in the feedback
|
|
7
|
+
area, as a refusal (read-only, port not open) does #}
|
|
8
|
+
<button type="button" class="btn sm" id="console-break" hx-post="{{ base }}/api/mappings/{{ mid }}/break" hx-swap="none" hx-disabled-elt="this">{{ _("Send BREAK") }}</button>
|
|
9
|
+
{% else %}<span class="muted">{{ _("read-only") }}</span>{% endif %}
|
|
10
|
+
<button type="button" class="btn sm" data-term-mode>{{ _("Hex view") }}</button>
|
|
11
|
+
<button type="button" class="btn sm" id="console-close" data-cancel>{{ _("Close") }}</button>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
<p class="muted mb-sm" data-mode-help>{% if interactive %}{{ _("Text view: what the device sends; what you type goes to the device.") }}{% else %}{{ _("Text view: what the device sends.") }}{% endif %} {% if bridge %}{{ _("Hex view: both directions with timestamps (A → B / B → A).") }}{% elif to_device %}{{ _("Hex view: both directions with timestamps (from / to the device).") }}{% else %}{{ _("Hex view: what the device sends, with timestamps.") }}{% endif %}{% if encoding and encoding != 'utf-8' %} {{ _("Text encoding: %(encoding)s.", encoding=encoding|upper) }}{% endif %}</p>
|
|
15
|
+
<div id="term" class="term"
|
|
16
|
+
data-ws="{{ base }}/api/mappings/{{ mid }}/console"
|
|
17
|
+
data-ws-hex="{{ base }}/api/mappings/{{ mid }}/traffic"
|
|
18
|
+
data-bridge="{{ '1' if bridge else '0' }}"
|
|
19
|
+
data-interactive="{{ '1' if interactive else '0' }}"
|
|
20
|
+
data-mode="{{ 'hex' if hex_first else 'text' }}"
|
|
21
|
+
data-charset="{{ charset or '' }}"></div>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{#- The footer of every page: the licence and where the source code is (AGPL-3.0 section 13;
|
|
2
|
+
ser2netweb/licences.py). `signed_in`: the version and the licences page too (they name what
|
|
3
|
+
this copy runs, which a page before the sign-in doesn't show). Import it with context. #}
|
|
4
|
+
{% macro site_footer(signed_in) %}
|
|
5
|
+
<footer class="site-footer">
|
|
6
|
+
<span>ser2netweb{% if signed_in %} {{ app_version }}{% endif %}</span>
|
|
7
|
+
<span>{{ _("AGPL-3.0 or a commercial licence") }}</span>
|
|
8
|
+
<a href="{{ source_url }}" rel="noopener noreferrer">{{ _("Source code") }}</a>
|
|
9
|
+
{%- if signed_in %}
|
|
10
|
+
<a href="{{ base }}/licenses">{{ _("Licences") }}</a>
|
|
11
|
+
{%- endif %}
|
|
12
|
+
</footer>
|
|
13
|
+
{% endmacro %}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{# the notice an admin set for the sign-in and setup pages (Settings -> Web UI access): plain
|
|
2
|
+
text, escaped, line breaks kept (.notice-text); its link opens on its own #}
|
|
3
|
+
{% if login_notice or login_notice_url %}
|
|
4
|
+
<div class="banner notice" id="notice">
|
|
5
|
+
{%- if login_notice %}<p class="notice-text">{{ login_notice }}</p>{% endif -%}
|
|
6
|
+
{%- if login_notice_url %}<p><a href="{{ login_notice_url }}" target="_blank" rel="noopener noreferrer">{{ _("Read the full notice") }}</a></p>{% endif -%}
|
|
7
|
+
</div>
|
|
8
|
+
{% endif %}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{# shared bits of markup; import with {% from "_macros.html" import ... %} #}
|
|
2
|
+
{% macro state_label(s) -%}
|
|
3
|
+
{%- if s == 'running' -%}{{ _("running") }}
|
|
4
|
+
{%- elif s == 'stopped' -%}{{ _("stopped") }}
|
|
5
|
+
{%- elif s == 'reconnecting' -%}{{ _("reconnecting") }}
|
|
6
|
+
{%- elif s == 'device-missing' -%}{{ _("device missing") }}
|
|
7
|
+
{%- elif s == 'error' -%}{{ _("error") }}
|
|
8
|
+
{%- else -%}{{ s }}{%- endif -%}
|
|
9
|
+
{%- endmacro %}
|
|
10
|
+
|
|
11
|
+
{% macro lang_switch(request, lang, here=none) -%}
|
|
12
|
+
{#- back to this page: its path (without the base path) and query (the sign-in page's
|
|
13
|
+
?next=, the audit log's filters) - or `here`, the address that a page sent as the
|
|
14
|
+
answer to a form names (the form's own URL may answer only POST) #}
|
|
15
|
+
{%- set back = here or (route_path(request) ~ ('?' ~ request.url.query if request.url.query else '')) %}
|
|
16
|
+
<span class="lang-switch" role="group" aria-label="{{ _('Language') }}">
|
|
17
|
+
{%- for code, label in languages.items() -%}
|
|
18
|
+
<a href="{{ base_path(request) }}/lang/{{ code }}?next={{ back|urlencode }}" hreflang="{{ code }}" lang="{{ code }}" title="{{ label }}"{% if code == lang %} aria-current="true" class="on"{% endif %}>{{ code|upper }}</a>
|
|
19
|
+
{%- endfor -%}
|
|
20
|
+
</span>
|
|
21
|
+
{%- endmacro %}
|