From af6dec2f198671ea01b654b6589d41ddeb8ba9c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Kore=C4=8Dek?= Date: Sun, 18 Sep 2022 21:03:11 +0200 Subject: [PATCH 1/7] =?UTF-8?q?Strategick=C3=A1:=20Line=C3=A1rn=C3=AD=20bu?= =?UTF-8?q?dget=20funkce?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit + None jako action opět znamená i zůstání na místě. --- server/hra/game.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/hra/game.py b/server/hra/game.py index c4358a1..5b1d296 100644 --- a/server/hra/game.py +++ b/server/hra/game.py @@ -27,6 +27,7 @@ class Logic: @add_logic class Occupy(Logic): MOVE_VECTORS = { + None: (0, 0), "stay": (0, 0), "left": (0, -1), "right": (0, 1), @@ -60,8 +61,10 @@ class Occupy(Logic): "remaining_rounds": 1 } + # zero_state spawnuje jednoho vojáka, poslední (použitelný, takže + # předposlední) spawn umožňuje budget na `last_spawn` nových vojáků def budget_for_round(self, round_id): - return (self.last_spawn * self.spawn_price * (round_id + 1)**2) // (self.rounds_total**2) + return self.spawn_price + ((round_id * self.spawn_price * self.last_spawn) // self.rounds_total) # Počáteční stav def zero_state(self) -> Any: From c68d624620ce66952f568e036ed4767e318424a6 Mon Sep 17 00:00:00 2001 From: Jiri Kalvoda Date: Sun, 18 Sep 2022 23:37:26 +0200 Subject: [PATCH 2/7] =?UTF-8?q?Strategick=C3=A1:=20Dokon=C4=8Den=C3=AD=20l?= =?UTF-8?q?ogov=C3=A1tka?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/hra/web/__init__.py | 2 +- server/hra/web/pages.py | 104 +++++++++++++++++++++++++++++-------- server/static/ksp-mhd.css | 19 +++++++ 3 files changed, 103 insertions(+), 22 deletions(-) diff --git a/server/hra/web/__init__.py b/server/hra/web/__init__.py index a78596e..426cfe4 100644 --- a/server/hra/web/__init__.py +++ b/server/hra/web/__init__.py @@ -83,7 +83,7 @@ def init_request(): g.menu += [ MenuItem(app.url_for(pages.web_org_games.__name__), "Hry"), MenuItem(app.url_for(pages.web_org_users.__name__), "Uživatelé"), - MenuItem(app.url_for(pages.web_org_logs.__name__), "Logy"), + MenuItem(app.url_for(pages.web_org_logs.__name__)+"#main_table" , "Logy"), ] else: g.menu += [ diff --git a/server/hra/web/pages.py b/server/hra/web/pages.py index b10dbd4..68519c9 100644 --- a/server/hra/web/pages.py +++ b/server/hra/web/pages.py @@ -1,5 +1,5 @@ from flask import Flask, redirect, flash, session, g, request, get_flashed_messages -from wtforms import Form, BooleanField, StringField, PasswordField, validators, SubmitField, IntegerField, DateTimeField +from wtforms import Form, BooleanField, StringField, PasswordField, validators, SubmitField, IntegerField, DateTimeField, SelectMultipleField from wtforms.validators import ValidationError from flask_wtf import FlaskForm from flask_bootstrap import Bootstrap @@ -84,6 +84,12 @@ def user_link(user): return b.root_tag +def ip_link(ip): + b = html.Builder() + with b.line(): + b.a(href=app.url_for(web_org_logs.__name__, ip=ip)+"#main_table")(ip) + return b.root_tag + def right_for_game(game): if g.org: return True @@ -261,14 +267,16 @@ def web_game(game_id): with b.div(_class="btn-group", role="group"): if right_for_team(team): b.a(_class="btn btn-xs btn-primary", href=app.url_for(web_game_view.__name__, game_id=game.game_id, team_id=team.team_id))("Zobrazit hru") + b.a(_class="btn btn-xs btn-default", href=app.url_for(web_org_logs.__name__, game_id=game_id, team_id=team.team_id)+"#main_table")("Log") if g.org: b.a(_class="btn btn-xs btn-default", href=app.url_for(web_org_game_userchange.__name__, game_id=game.game_id, team_id=team.team_id))("Změnit uživatele") - with b.div(_class="btn-gruser_idoup", role="group"): + with b.div(_class="btn-group", role="group"): if right_for_step(game): with b.form(method="POST", _class="btn-group", action=app.url_for(web_game_step.__name__, game_id=game_id)): b.button(_class="btn btn-primary", type="submit", name="su", value="yes")("Krok") if g.org: + b.a(_class="btn btn-primary", href=app.url_for(web_org_logs.__name__, game_id=game_id)+"#main_table")("Log") b.a(_class="btn btn-default", href=app.url_for(web_org_game_round_inspect.__name__, game_id=game_id))("Inspekce kola") if g.org: @@ -379,13 +387,18 @@ def web_org_game_round_inspect(game_id, round_id=None): b.line().th()("Bodů") b.line().th()("Stažení") b.line().th()("Nahrání") + b.line().th()("Akce") for team, move in zip(teams, moves): with b.tr(): b.line().td()(team.team_id) b.line().td()(user_link(team.user),": ", team.name) b.line().td()(move.points) b.line().td()(move.reads_count) - b.line().td()(f"{move.ok_pushs_count} {move.warnings_pushs_count} {move.err_pushs_count}") + b.line().td()(f"ok: {move.ok_pushs_count} w: {move.warnings_pushs_count} e: {move.err_pushs_count}") + with b.td(): + with b.div(_class="btn-group", role="group"): + if right_for_team(team): + b.a(_class="btn btn-xs btn-primary", href=app.url_for(web_org_logs.__name__, game_id=game_id, team_id=team.team_id, round_id=round_id)+"#main_table")("Log") return b.print_file() @@ -482,6 +495,7 @@ def web_org_user(user_id): b.line().h2("Uživatel ", user.print()) with b.div(_class="btn-group", role="group"): with b.form(method="POST", _class="btn-group", action=app.url_for(web_org_user_su.__name__, user_id=user.id)): + b.a(_class="btn btn-primary", href=app.url_for(web_org_logs.__name__, user_id=user.id)+"#main_table")("Log") b.button(_class="btn btn-default", type="submit", name="su", value="yes")("Převtělit") b.h3("Týmy") with b.p().table(_class="data full"): @@ -503,8 +517,8 @@ def web_org_user_su(user_id): flash("Uživatel vtělen!") return redirect('/') -@app.route("/org/log/", methods=['GET']) -def web_org_log(log_id): +@app.route("/org/log//data", methods=['GET']) +def web_org_log_data(log_id): l = db.get_session().query(db.Log).filter_by(log_id=log_id).one_or_none() if l is None: raise werkzeug.exceptions.NotFound() @@ -513,31 +527,74 @@ def web_org_log(log_id): b.p().pre(pprint.pformat(l.get_data())) return b.print_file() +class LogsFindForm(FlaskForm): + username = StringField('Jméno uživatele') + user_id = OptionalIntField('ID uživatele') + game_id = OptionalIntField('ID hry') + team_id = OptionalIntField('Tým') + round_id = OptionalIntField('Kolo') + limit = OptionalIntField('Limit') + ip = StringField('IP') + status = SelectMultipleField('Status', choices=[("", "*")]+[(x,x) for x in ["ok", "warning", "too_early", "too_late", "error", "http-error"]], render_kw={"size":7}) + endpoint = SelectMultipleField('Stránky', choices=db.Endpoint.choices()) + + submit = SubmitField("Najít") + + + def validate_captcha(form, field): + if field.data != config.CAPTCHA: + raise ValidationError("Chyba!") @app.route("/org/logs", methods=['GET']) def web_org_logs(): - logs = db.get_session().query(db.Log).order_by(db.Log.log_id.desc()).all() + f = LogsFindForm(formdata=request.args, csrf_enabled=False) + q = db.get_session().query(db.Log).order_by(db.Log.log_id.desc()) + if request.args: + f.validate() + if f.username.data: + q = q.filter(db.User.username.ilike(f"%{f.username.data}%")) + if f.user_id.data is not None: + q = q.filter_by(user_id=f.user_id.data or None) + if f.game_id.data is not None: + q = q.filter_by(game_id=f.game_id.data or None) + if f.team_id.data is not None: + q = q.filter_by(team_id=f.team_id.data or None) + if f.round_id.data is not None: + q = q.filter_by(round=f.round_id.data) + if f.ip.data: + q = q.filter(db.Log.source_ip.ilike(f"%{f.ip.data}%")) + if f.status.data and "" not in f.status.data: + q = q.filter(db.Log.status.in_(f.status.data)) + if f.endpoint.data: + q = q.filter(db.Log.endpoint.in_(f.endpoint.data)) + + + + limit = f.limit.data or 20 + logs = q.limit(limit).all() + count = q.count() + b = BasePage() + del f.csrf_token b.line().h2("Logy") + b(jinja_mac.quick_form(f, form_type='horizontal', method="GET")) with b.p().table(_class="data full"): - with b.thead(): - b.line().th()("Čas") - b.line().th()("Uživatel") - b.line().th()("URL") - b.line().th()("Hra") - b.line().th()("Tým") - b.line().th()("Status") - b.line().th()("Popis") - b.line().th()("GET") - b.line().th()("Akce") + with b.thead(id="main_table"): + b.line().th("Čas") + b.line().th("Uživatel") + b.line().th("URL") + b.line().th("Hra") + b.line().th("Status") + b.line().th("Popis") + b.line().th("GET") + b.line().th("Akce") for l in logs: - with b.tr(): + with b.tr(_class="log_"+l.status): b.line().td(l.time.strftime("%H:%M:%S")) - b.line().td(l.user.print() if l.user else None, b._br(), l.source_ip) + b.line().td(user_link(l.user) if l.user else None, b._br(), ip_link(l.source_ip)) b.line().td(l.endpoint._name_, b._br(), l.url) - b.line().td(l.game.print() if l.game else None) - b.line().td(l.team_id) + b.line().td(l.game.print() if l.game else None, b._br(), "tým: ", l.team_id, " kolo: ", l.round) b.line().td(l.status) b.line().td(l.text) with b.line().td(): @@ -545,7 +602,12 @@ def web_org_logs(): b(f"{k}: {v}").br() with b.line().td(): with b.div(_class="btn-group", role="group"): - b.a(href=app.url_for(web_org_log.__name__, log_id=l.log_id), _class="btn btn-xs btn-primary")("Podrobnosti") + if l.data is not None: + b.a(href=app.url_for(web_org_log_data.__name__, log_id=l.log_id), _class="btn btn-xs btn-primary")("Data") + if count <= limit: + b.line().p(f"Toť vše ({count})") + else: + b.line().p().b(f"Zobrazeno prvních {limit} logů z {count}") return b.print_file() from hra.web.game import get_wlogic, wlogic_by_mode diff --git a/server/static/ksp-mhd.css b/server/static/ksp-mhd.css index c794054..9a3d271 100644 --- a/server/static/ksp-mhd.css +++ b/server/static/ksp-mhd.css @@ -300,3 +300,22 @@ nav#main-menu a.active { .collapsible input[type="checkbox"].toggle:checked ~ .collapsible-inner { max-height: 100vh; } + +.log_ok { + background-color: #BBFFBB; +} +.log_warning { + background-color: #FFFFBB; +} +.log_error { + background-color: #FFBBBB; +} +.log_http-error { + background-color: #FF0000; +} +.log_too_late { + background-color: #BBFFFF; +} +.log_too_early { + background-color: #BBBBFF; +} From 36d8b772e17608aad5fe102d8147bdf11a446319 Mon Sep 17 00:00:00 2001 From: Jiri Kalvoda Date: Sun, 18 Sep 2022 23:38:42 +0200 Subject: [PATCH 3/7] =?UTF-8?q?Strategick=C3=A1:=20Oprava=20escapov=C3=A1n?= =?UTF-8?q?=C3=AD=20&?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/hra/web/html.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/hra/web/html.py b/server/hra/web/html.py index dffd8ba..88901ce 100644 --- a/server/hra/web/html.py +++ b/server/hra/web/html.py @@ -11,7 +11,7 @@ class EscapeError(RuntimeError): def escape_attribute(x:str) -> str: - return x.replace("&", "&ersand;").replace('"', """) + return x.replace("&", "&").replace('"', """) def escape_attribute_name(x:str) -> str: From 54f87f268408d793e00d57bad6f38f283c4f9ded Mon Sep 17 00:00:00 2001 From: Jiri Kalvoda Date: Sun, 18 Sep 2022 23:39:10 +0200 Subject: [PATCH 4/7] =?UTF-8?q?Strategick=C3=A1:=20Konfigurace=20hry?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/bin/create_game | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/server/bin/create_game b/server/bin/create_game index d11890c..a226346 100755 --- a/server/bin/create_game +++ b/server/bin/create_game @@ -12,14 +12,14 @@ ses = db.get_session() name="Hlavní hra" mode = "occupy" -teams_count = 16 +teams_count = 12 configuration = { "teams_width": 4, - "teams_height": 4, - "width_per_team": 30, - "height_per_team": 30, - "born_per_round": [1], - "initial_remaining_rounds": 1000, + "teams_height": 3, + "width_per_team": 24, + "height_per_team": 24, + # "born_per_round": [1], + "initial_remaining_rounds": 540, "spawn_price": 10, "last_spawn": 100, # seed=5 From 576a7418b9dd628039fa0bb91afb7f887573e5cd Mon Sep 17 00:00:00 2001 From: Jiri Kalvoda Date: Tue, 20 Sep 2022 03:04:44 +0200 Subject: [PATCH 5/7] =?UTF-8?q?Strategick=C3=A1:=20Vylep=C5=A1en=C3=AD=20z?= =?UTF-8?q?obrazen=C3=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/hra/web/game.py | 137 +++++++++++++++++++++++++++++--------- server/static/ksp-mhd.css | 1 + server/static/occupy.css | 8 ++- 3 files changed, 112 insertions(+), 34 deletions(-) diff --git a/server/hra/web/game.py b/server/hra/web/game.py index e6fc55e..3748126 100644 --- a/server/hra/web/game.py +++ b/server/hra/web/game.py @@ -1,5 +1,5 @@ from flask import Flask, redirect, flash, session, g, request, get_flashed_messages, Markup -from wtforms import Form, BooleanField, StringField, PasswordField, validators, SubmitField, IntegerField, DateTimeField +from wtforms import Form, BooleanField, StringField, PasswordField, validators, SubmitField, IntegerField, DateTimeField, RadioField from wtforms.validators import ValidationError from flask_wtf import FlaskForm from flask_bootstrap import Bootstrap @@ -11,6 +11,7 @@ import wtforms from wtforms.fields import EmailField from wtforms.widgets import NumberInput from typing import Optional, Any, List +from datetime import datetime import hra.config as config import hra.web.html as html @@ -18,7 +19,7 @@ import hra.db as db from hra.web import app import hra.web.jinja_mac as jinja_mac from hra.util import hash_passwd -from hra.web.pages import BasePage +from hra.web.pages import BasePage, web_game_view wlogic_by_mode = {} @@ -39,17 +40,49 @@ class WLogic: self.game = game self.logic = game.get_logic() +class OccupyViewConfig(FlaskForm): + refresh_none = "Bez automatické aktualizace" + refresh_meta = "Aktualizace pomocí HTML META" + refresh_js = "Aktualizace pomocí java scriptu" + + font_size = IntegerField("Velikost fontu", default=8) + refresh = RadioField("Aktualizace", choices=(refresh_none, refresh_meta), default=refresh_none) + not_clickable = BooleanField("Vypnout podrobnosti kliknutím (zmenší nároky na sít a procesor)") + big_fields = BooleanField("Široká políčka (trojciferné počty)") + + submit = SubmitField("Refresh/Potvrdit") @add_wlogic class Occupy(WLogic): def view(self, state: db.State, team: Optional[db.Team], teams: List[db.Team]): + meta_refresh_without_reaming = 15 + + game = state.game + if game.step_mode == db.StepMode.automatic: + time_reaming = (state.create_time - datetime.now()).total_seconds() + game.step_every_s + else: + time_reaming = None s = state.get_state() - if team is not None: - s = self.logic.personalize_state(s, team.team_id, state.round) + conff = OccupyViewConfig(formdata=request.args, csrf_enabled=False) + q = db.get_session().query(db.Log).order_by(db.Log.log_id.desc()) + conff.validate() + + max_num = 999 if conff.big_fields.data else 99 + b = BasePage() b.h2(f"Hra {self.game.print()} kolo {state.round}") - b.p().b(_class=f"game_team_{team.team_id}")(f"Pohled týmu {team.print()}") - with b.table(_class="game_table"): + if not conff.not_clickable.data: + b.p("Po kliknutí na buňku se zobrazí další informace.") + if conff.refresh.data == conff.refresh_meta and time_reaming is None: + b.p(f"Není známo, kdy nastane další kolo, proto může být aktualizace až o {meta_refresh_without_reaming} sekund opožděna.") + with b.p(): + b.line().b(_class=f"game_team_{team.team_id}")(f"Pohled týmu {team.print()}") + with b.line().b(_class="pull-right"): + if time_reaming is not None: + b("Čas do konce kola: ", b._span(id="time")(int(time_reaming)), " sekund") + else: + b("Konec kola není stanoven") + with b.p(style="text-align:center;").table(_class="game_table"): for i, row in enumerate(s["world"]): with b.tr(): for j, x in enumerate(row): @@ -58,7 +91,7 @@ class Occupy(WLogic): members = x["members"] with b.td(): classes = [] - with b.a(href=f"#cell_{i}_{j}"): + with b.a(href=f"#cell_{i}_{j}") if not conff.not_clickable.data else b.bucket(): if x["protected_for_team"] is not None: classes.append("game_protected") if x["hill"]: @@ -70,38 +103,80 @@ class Occupy(WLogic): if occupied_by_team is not None: classes.append(f'game_occupied') classes.append(f'game_occupied_by_{occupied_by_team}') - if len(members): - b(len(members)) + num = len(members) + if num: + b(num if num <= max_num else ("+++" if conff.big_fields.data else "++")) else: b(Markup(" ")) b(_class=" ".join(classes)) - for i, row in enumerate(s["world"]): - for j, x in enumerate(row): - occupied_by_team = x["occupied_by_team"] - protected_for_team = x["protected_for_team"] - home_for_team = x["home_for_team"] - members = x["members"] - with b.div(id=f"cell_{i}_{j}", _class="game_tab"): - b.h4(f"Políčko {i} {j}") - if x["hill"]: - b.p().b("Pohoří") - else: - if occupied_by_team is not None: - b.p(_class=f"game_team_{occupied_by_team}").b(f"Obsazeno týmem: {teams[occupied_by_team].print()}") - if protected_for_team is not None: - b.p(_class=f"game_team_{protected_for_team}").b(f"Ochranné území týmu: {teams[protected_for_team].print()}") - if home_for_team is not None: - b.p(_class=f"game_team_{home_for_team}").b(f"Domov týmu: {teams[home_for_team].print()}") - b.p().b(f"Počet osob: {len(members)}") - with b.ul(): - for m in members: - b.li(_class=f"game_team_{home_for_team}")(f"Voják {m['id']} týmu {teams[m['team']].print()}") + if not conff.not_clickable.data: + for i, row in enumerate(s["world"]): + for j, x in enumerate(row): + occupied_by_team = x["occupied_by_team"] + protected_for_team = x["protected_for_team"] + home_for_team = x["home_for_team"] + members = x["members"] + with b.div(id=f"cell_{i}_{j}", _class="game_tab form-frame"): + b.h4(f"Políčko {i} {j}", b._a(href=f"#", _class="btn btn-primary pull-right")("Skrýt")) + if x["hill"]: + b.p().b("Pohoří") + else: + if occupied_by_team is not None: + b.p(_class=f"game_team_{occupied_by_team}").b(f"Obsazeno týmem: {teams[occupied_by_team].print()}") + if protected_for_team is not None: + b.p(_class=f"game_team_{protected_for_team}").b(f"Ochranné území týmu: {teams[protected_for_team].print()}") + if home_for_team is not None: + b.p(_class=f"game_team_{home_for_team}").b(f"Domov týmu: {teams[home_for_team].print()}") + b.p().b(f"Počet osob: {len(members)}") + with b.ul(): + for m in members: + b.li(_class=f"game_team_{home_for_team}")(f"Voják {m['id']} týmu {teams[m['team']].print()}") + + del conff.csrf_token + b.p() + b.div(_class="form-frame")(jinja_mac.quick_form(conff, form_type="horizontal", method="GET")) + + b.script(Markup(f""" + var intervalID = window.setInterval(update_time, 100); + var t = { time_reaming*1000 }; + function update_time() {{ + document.getElementById("time").innerHTML = Math.floor(t/1000);; + t -= 100; + }} + """)) + + def head(x): + refresh_time = time_reaming + 1 if time_reaming is not None else 15 + font_size = int(conff.font_size.data) + box_size = int(font_size * 1.3 + 1) + box_w = box_size + if conff.big_fields.data: + box_w *= 1.5 + table_w = len(s["world"][0]) * box_w + margin_cmd = "" + if table_w > 1000: + margin_cmd = f"margin-left: {(1000-table_w) / 2}px;" + if conff.refresh.data == conff.refresh_meta: + b.meta(**{"HTTP-EQUIV": "refresh", "CONTENT": f"{refresh_time};{app.url_for(web_game_view.__name__, game_id=game.game_id, team_id=team.team_id, **request.args)}"}) + b.link(rel="stylesheet", href=app.url_for('static', filename='occupy.css'), type='text/css', media="all") + b.style(Markup(f""" + table.game_table tr td {{ + width: {box_w}px; + max-width: {box_w}px; + height: {box_size}px; + font-size: {font_size}px; + }} + table.game_table {{ + width: {table_w}px; + {margin_cmd} + }} + """)) b.wrap( limited_size=False, sticky_head=False, - head=lambda x:x.link(rel="stylesheet", href=app.url_for('static', filename='occupy.css'), type='text/css', media="all") + head=head, ) return b.print_file() diff --git a/server/static/ksp-mhd.css b/server/static/ksp-mhd.css index 9a3d271..6dc0821 100644 --- a/server/static/ksp-mhd.css +++ b/server/static/ksp-mhd.css @@ -12,6 +12,7 @@ body { margin: 0 auto; padding: 0 1em; width: 100%; + max-width: 1000px; } .content_limited, main { diff --git a/server/static/occupy.css b/server/static/occupy.css index d4b58c7..2d0d0ae 100644 --- a/server/static/occupy.css +++ b/server/static/occupy.css @@ -1,15 +1,17 @@ +table.game_table{ + margin-left: auto; + margin-right: auto; +} table.game_table, table.game_table tr, table.game_table tr td { border: thin solid black; border-collapse: collapse; table-layout: fixed; + overflow: hidden; } table.game_table tr td { - width: 10pt; - height: 10px; - font-size: 8px; text-align: end; } From 35270cd65d2dc687ea02f413109e15dd49dad0c6 Mon Sep 17 00:00:00 2001 From: Jiri Kalvoda Date: Tue, 20 Sep 2022 03:20:04 +0200 Subject: [PATCH 6/7] =?UTF-8?q?Strategick=C3=A1:=20stringifikace=20v=20atr?= =?UTF-8?q?ibutech?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/hra/web/html.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/hra/web/html.py b/server/hra/web/html.py index 88901ce..d422e1d 100644 --- a/server/hra/web/html.py +++ b/server/hra/web/html.py @@ -11,7 +11,7 @@ class EscapeError(RuntimeError): def escape_attribute(x:str) -> str: - return x.replace("&", "&").replace('"', """) + return str(x).replace("&", "&").replace('"', """) def escape_attribute_name(x:str) -> str: From f64ab8a7912417bd6cbe56703e2056a36677c3fd Mon Sep 17 00:00:00 2001 From: Jiri Kalvoda Date: Tue, 20 Sep 2022 03:26:13 +0200 Subject: [PATCH 7/7] =?UTF-8?q?Strategick=C3=A1:=20Html:=20bucket=20creato?= =?UTF-8?q?r?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/hra/web/html.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/server/hra/web/html.py b/server/hra/web/html.py index d422e1d..db69114 100644 --- a/server/hra/web/html.py +++ b/server/hra/web/html.py @@ -72,6 +72,14 @@ class Bucket: def _line(self): return Line(self) + def bucket(self): + t = Bucket(self.builder) + self.add(t) + return t + + def _bucket(self): + return Bucket(self) + def print(self): out = [] self.serialize_append_to_list(out, 0) @@ -160,6 +168,12 @@ class Builder: def _line(self): return Line(self) + def bucket(self): + return self.current_tag.bucket() + + def _bucket(self): + return Bucket(self) + def __call__(self, *arg, **kvarg): self.current_tag(*arg, **kvarg) return self