From 665002671abd35f672a9f6d51049783dba81eb22 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mat=C4=9Bj=20T=C3=BD=C4=8D?=
Date: Fri, 31 May 2024 01:06:01 +0200
Subject: [PATCH 1/5] Generally simplify things without revolution
---
estimage/plugins/base/forms.py | 4 +
estimage/webapp/main/routes.py | 116 +++++-------------
estimage/webapp/routers.py | 26 +++-
...ic_view.html => epic_view_projective.html} | 2 +-
estimage/webapp/templates/issue_view.html | 2 +-
estimage/webapp/templates/utils.j2 | 2 +-
estimage/webapp/vis/routes.py | 34 ++---
estimage/webapp/web_utils.py | 4 +
8 files changed, 80 insertions(+), 110 deletions(-)
rename estimage/webapp/templates/{epic_view.html => epic_view_projective.html} (85%)
diff --git a/estimage/plugins/base/forms.py b/estimage/plugins/base/forms.py
index 104d787..8f43ee3 100644
--- a/estimage/plugins/base/forms.py
+++ b/estimage/plugins/base/forms.py
@@ -5,3 +5,7 @@ class BaseForm(FlaskForm):
def __init__(self, ** kwargs):
self.extending_fields = []
super().__init__(** kwargs)
+
+ @classmethod
+ def supporting_js(cls, forms):
+ return ""
diff --git a/estimage/webapp/main/routes.py b/estimage/webapp/main/routes.py
index fb112e0..223b2e7 100644
--- a/estimage/webapp/main/routes.py
+++ b/estimage/webapp/main/routes.py
@@ -6,7 +6,7 @@
from . import bp
from . import forms
-from .. import web_utils
+from .. import web_utils, routers
from ... import data
from ... import utilities, statops
from ...statops import summary
@@ -116,12 +116,9 @@ def move_consensus_estimate_to_authoritative(task_name):
@bp.route('/estimate/', methods=['POST'])
@flask_login.login_required
def estimate(task_name):
- user = flask_login.current_user
-
- user_id = user.get_id()
-
+ r = routers.PollsterRouter()
+ pollster = r.private_pollster
form = forms.NumberEstimationForm()
- pollster = webdata.UserPollster(user_id)
if form.validate_on_submit():
if form.submit.data:
@@ -188,8 +185,8 @@ def view_projective_task(task_name, known_forms=None):
request_forms.update(known_forms)
breadcrumbs = get_projective_breadcrumbs()
- append_card_to_breadcrumbs(breadcrumbs, t, lambda n: web_utils.head_url_for("main.view_epic", epic_name=n))
- return view_task(t, breadcrumbs, request_forms)
+ append_card_to_breadcrumbs(breadcrumbs, t, lambda n: web_utils.head_url_for("main.view_epic_proj", epic_name=n))
+ return view_task(t, breadcrumbs, "proj", request_forms)
@bp.route('/retrospective/task/')
@@ -199,7 +196,7 @@ def view_retro_task(task_name):
breadcrumbs = get_retro_breadcrumbs()
append_card_to_breadcrumbs(breadcrumbs, t, lambda n: web_utils.head_url_for("main.view_epic_retro", epic_name=n))
- return view_task(t, breadcrumbs)
+ return view_task(t, breadcrumbs, "retro")
def _setup_forms_according_to_context(request_forms, context):
@@ -219,12 +216,13 @@ def _setup_forms_according_to_context(request_forms, context):
feed_estimation_to_form(context.estimation, request_forms["estimation"])
-def view_task(task, breadcrumbs, request_forms=None):
+def view_task(task, breadcrumbs, mode, request_forms=None):
user = flask_login.current_user
user_id = user.get_id()
- pollster = webdata.UserPollster(user_id)
+ pollster = webdata.UserPollster(user_id)
c_pollster = webdata.AuthoritativePollster()
+
context = webdata.Context(task)
give_data_to_context(context, pollster, c_pollster)
@@ -236,7 +234,7 @@ def view_task(task, breadcrumbs, request_forms=None):
similar_cards = get_similar_cards_with_estimations(user_id, task.name)
return web_utils.render_template(
- 'issue_view.html', title='Estimate Issue', breadcrumbs=breadcrumbs,
+ 'issue_view.html', title='Estimate Issue', breadcrumbs=breadcrumbs, mode=mode,
user=user, forms=request_forms, task=task, context=context, similar_sized_cards=similar_cards)
@@ -265,24 +263,18 @@ def append_card_to_breadcrumbs(breadcrumbs, card, name_to_url):
@bp.route('/projective/epic/')
@flask_login.login_required
-def view_epic(epic_name):
- user = flask_login.current_user
+def view_epic_proj(epic_name):
+ r = routers.ModelRouter(mode="proj")
- user_id = user.get_id()
- cls, loader = web_utils.get_proj_loader()
- all_cards = loader.load_all_cards(cls)
- cards_tree_without_duplicates = utilities.reduce_subsets_from_sets(all_cards)
- model = web_utils.get_user_model(user_id, cards_tree_without_duplicates)
-
- estimate = model.nominal_point_estimate_of(epic_name)
+ estimate = r.model.nominal_point_estimate_of(epic_name)
t = projective_retrieve_task(epic_name)
breadcrumbs = get_projective_breadcrumbs()
- append_card_to_breadcrumbs(breadcrumbs, t, lambda n: web_utils.head_url_for("main.view_epic", epic_name=n))
+ append_card_to_breadcrumbs(breadcrumbs, t, lambda n: web_utils.head_url_for("main.view_epic_proj", epic_name=n))
return web_utils.render_template(
- 'epic_view.html', title='View epic', epic=t, estimate=estimate, model=model, breadcrumbs=breadcrumbs,
+ 'epic_view_projective.html', title='View epic', epic=t, estimate=estimate, model=r.model, breadcrumbs=breadcrumbs,
)
@@ -303,15 +295,10 @@ def index():
@bp.route('/projective')
@flask_login.login_required
def tree_view():
- user = flask_login.current_user
- user_id = user.get_id()
-
- all_cards_by_id, model = web_utils.get_all_tasks_by_id_and_user_model("proj", user_id)
- all_cards = list(all_cards_by_id.values())
- cards_tree_without_duplicates = utilities.reduce_subsets_from_sets(all_cards)
+ r = routers.ModelRouter(mode="proj")
return web_utils.render_template(
"tree_view.html", title="Tasks tree view",
- cards=cards_tree_without_duplicates, model=model)
+ cards=r.cards_tree_without_duplicates, model=r.model)
def executive_summary_of_points_and_velocity(cards, cls=history.Summary):
@@ -331,11 +318,9 @@ def executive_summary_of_points_and_velocity(cards, cls=history.Summary):
@bp.route('/retrospective')
@flask_login.login_required
def overview_retro():
- user = flask_login.current_user
- user_id = user.get_id()
+ r = routers.ModelRouter(mode="retro")
- all_cards_by_id, model = web_utils.get_all_tasks_by_id_and_user_model("retro", user_id)
- tier0_cards = [t for t in all_cards_by_id.values() if t.tier == 0]
+ tier0_cards = [t for t in r.all_cards_by_id.values() if t.tier == 0]
tier0_cards_tree_without_duplicates = utilities.reduce_subsets_from_sets(tier0_cards)
summary = executive_summary_of_points_and_velocity(tier0_cards_tree_without_duplicates)
@@ -349,11 +334,9 @@ def overview_retro():
@bp.route('/completion')
@flask_login.login_required
def completion():
- user = flask_login.current_user
- user_id = user.get_id()
+ r = routers.ModelRouter(mode="retro")
- all_cards_by_id, model = web_utils.get_all_tasks_by_id_and_user_model("retro", user_id)
- tier0_cards = [t for t in all_cards_by_id.values() if t.tier == 0]
+ tier0_cards = [t for t in r.all_cards_by_id.values() if t.tier == 0]
tier0_cards_tree_without_duplicates = utilities.reduce_subsets_from_sets(tier0_cards)
summary = executive_summary_of_points_and_velocity(tier0_cards_tree_without_duplicates, statops.summary.StatSummary)
@@ -367,36 +350,28 @@ def completion():
@bp.route('/retrospective_tree')
@flask_login.login_required
def tree_view_retro():
- user = flask_login.current_user
- user_id = user.get_id()
-
- all_cards_by_id, model = web_utils.get_all_tasks_by_id_and_user_model("retro", user_id)
- all_cards = list(all_cards_by_id.values())
- cards_tree_without_duplicates = utilities.reduce_subsets_from_sets(all_cards)
+ r = routers.ModelRouter(mode="retro")
- tier0_cards = [t for t in all_cards_by_id.values() if t.tier == 0]
+ tier0_cards = [t for t in r.all_cards_by_id.values() if t.tier == 0]
tier0_cards_tree_without_duplicates = utilities.reduce_subsets_from_sets(tier0_cards)
- cards_tree_without_duplicates = utilities.reduce_subsets_from_sets(all_cards)
summary = executive_summary_of_points_and_velocity(tier0_cards_tree_without_duplicates)
- priority_sorted_cards = sorted(cards_tree_without_duplicates, key=lambda x: - x.priority)
+ priority_sorted_cards = sorted(r.cards_tree_without_duplicates, key=lambda x: - x.priority)
statuses = flask.current_app.get_final_class("Statuses")()
return web_utils.render_template(
"tree_view_retrospective.html",
title="Retrospective Tasks tree view",
- cards=priority_sorted_cards, today=datetime.datetime.today(), model=model,
+ cards=priority_sorted_cards, today=datetime.datetime.today(), model=r.model,
summary=summary, status_of=lambda c: statuses.get(c.status))
@bp.route('/retrospective/epic/')
@flask_login.login_required
def view_epic_retro(epic_name):
- user = flask_login.current_user
- user_id = user.get_id()
+ r = routers.ModelRouter(mode="retro")
- all_cards_by_id, model = web_utils.get_all_tasks_by_id_and_user_model("retro", user_id)
- t = all_cards_by_id[epic_name]
+ t = r.all_cards_by_id[epic_name]
summary = executive_summary_of_points_and_velocity(t.children)
breadcrumbs = get_retro_breadcrumbs()
@@ -404,32 +379,18 @@ def view_epic_retro(epic_name):
return web_utils.render_template(
'epic_view_retrospective.html', title='View epic', breadcrumbs=breadcrumbs,
- today=datetime.datetime.today(), epic=t, model=model, summary=summary)
-
-
-class RetroProblem(problems.Problem):
- def format_task_name(self):
- return f"{self.affected_card_name}"
+ today=datetime.datetime.today(), epic=t, model=r.model, summary=summary)
@bp.route('/problems')
@flask_login.login_required
def view_problems():
- user = flask_login.current_user
- user_id = user.get_id()
-
- all_cards_by_id, model = web_utils.get_all_tasks_by_id_and_user_model("proj", user_id)
- all_cards = list(all_cards_by_id.values())
-
- problem_detector = problems.ProblemDetector(model, all_cards, RetroProblem)
-
- classifier = problems.groups.ProblemClassifier()
- classifier.classify(problem_detector.problems)
- categories = classifier.get_categories_with_problems()
+ r = routers.ProblemRouter(mode="proj")
+ categories = r.classifier.get_categories_with_problems()
cat_forms = []
for cat in categories:
- probs = classifier.classified_problems[cat.name].values()
+ probs = r.classifier.classified_problems[cat.name].values()
form = flask.current_app.get_final_class("ProblemForm")(prefix=cat.name)
form.add_problems_and_cat(cat, probs)
@@ -457,21 +418,12 @@ def _solve_problem(form, classifier, all_cards_by_id):
@bp.route('/problems/fix/', methods=['POST'])
@flask_login.login_required
def fix_problems(category):
- user = flask_login.current_user
- user_id = user.get_id()
-
- all_cards_by_id, model = web_utils.get_all_tasks_by_id_and_user_model("retro", user_id)
- all_cards = list(all_cards_by_id.values())
-
- problem_detector = problems.ProblemDetector(model, all_cards)
-
- classifier = problems.groups.ProblemClassifier()
- classifier.classify(problem_detector.problems)
+ r = routers.ProblemRouter(mode="proj")
form = flask.current_app.get_final_class("ProblemForm")(prefix=category)
- form.add_problems(problem_detector.problems)
+ form.add_problems(r.problem_detector.problems)
if form.validate_on_submit():
- _solve_problem(form, classifier, all_cards_by_id)
+ _solve_problem(form, r.classifier, r.all_cards_by_id)
else:
flask.flash(f"Error handing over solution: {form.errors}")
return flask.redirect(
diff --git a/estimage/webapp/routers.py b/estimage/webapp/routers.py
index 554af53..6f2c801 100644
--- a/estimage/webapp/routers.py
+++ b/estimage/webapp/routers.py
@@ -1,7 +1,7 @@
import flask
import flask_login
-from .. import data, simpledata, persistence, utilities, history
+from .. import data, simpledata, persistence, utilities, history, problems
from . import web_utils, CACHE
@@ -71,14 +71,34 @@ def get_all_cards_by_id(self):
return ret
-class ModelRouter(UserRouter, CardRouter):
+class PollsterRouter(UserRouter):
def __init__(self, ** kwargs):
super().__init__(** kwargs)
- self.model = web_utils.get_user_model(self.user_id, self.cards_tree_without_duplicates)
+ self.private_pollster = simpledata.AuthoritativePollster()
+ self.global_pollster = simpledata.UserPollster(self.user_id)
+
+
+class ModelRouter(PollsterRouter, CardRouter):
+ def __init__(self, ** kwargs):
+ super().__init__(** kwargs)
+
+ self.model = web_utils.get_user_model_given_pollsters(
+ self.private_pollster, self.global_pollster, self.cards_tree_without_duplicates)
self.model.update_cards_with_values(self.cards_tree_without_duplicates)
+class ProblemRouter(ModelRouter):
+ def __init__(self, ** kwargs):
+ super().__init__(** kwargs)
+
+ all_cards = list(self.all_cards_by_id.values())
+ self.problem_detector = problems.ProblemDetector(self.model, all_cards)
+
+ self.classifier = problems.groups.ProblemClassifier()
+ self.classifier.classify(self.problem_detector.problems)
+
+
class AggregationRouter(ModelRouter):
def __init__(self, ** kwargs):
super().__init__(** kwargs)
diff --git a/estimage/webapp/templates/epic_view.html b/estimage/webapp/templates/epic_view_projective.html
similarity index 85%
rename from estimage/webapp/templates/epic_view.html
rename to estimage/webapp/templates/epic_view_projective.html
index 6d2a396..694b152 100644
--- a/estimage/webapp/templates/epic_view.html
+++ b/estimage/webapp/templates/epic_view_projective.html
@@ -27,7 +27,7 @@ Sum of subtasks
Remaining point cost: {{ utils.render_estimate(model.remaining_point_estimate_of(epic.name)) }}
Nominal point cost: {{ utils.render_estimate(model.nominal_point_estimate_of(epic.name)) }}
-
+
{%- if similar_sized_epics %}
diff --git a/estimage/webapp/templates/issue_view.html b/estimage/webapp/templates/issue_view.html
index 995cbd6..3b21d02 100644
--- a/estimage/webapp/templates/issue_view.html
+++ b/estimage/webapp/templates/issue_view.html
@@ -64,7 +64,7 @@ Estimates
{%- endif %}
-
+
{% if not forms %}
diff --git a/estimage/webapp/templates/utils.j2 b/estimage/webapp/templates/utils.j2
index 8869db3..7853ade 100644
--- a/estimage/webapp/templates/utils.j2
+++ b/estimage/webapp/templates/utils.j2
@@ -74,7 +74,7 @@
} %}
{% set epic_type_to_function = {
- "projective": "main.view_epic",
+ "projective": "main.view_epic_proj",
"retrospective": "main.view_epic_retro",
} %}
diff --git a/estimage/webapp/vis/routes.py b/estimage/webapp/vis/routes.py
index b4f1b10..39fc316 100644
--- a/estimage/webapp/vis/routes.py
+++ b/estimage/webapp/vis/routes.py
@@ -175,33 +175,23 @@ def visualize_all_projective_tasks(nominal_or_remaining):
return send_figure_as(fig, "all", "svg")
-@bp.route('/
--pert.svg')
+@bp.route('/--remaining-pert.svg')
@flask_login.login_required
-def visualize_task(task_name, nominal_or_remaining):
- allowed_modes = ("nominal", "remaining")
- if nominal_or_remaining not in allowed_modes:
- msg = (
- f"Attempt to visualize {task_name} "
- "and not setting mode to one of {allowed_modes}, "
- f"but to '{nominal_or_remaining}'."
- )
- flask.flash(msg)
- raise ValueError(msg)
+def visualize_task_remaining(task_name, mode):
+ r = routers.ModelRouter(mode=mode)
+ estimation = r.model.remaining_point_estimate_of(task_name)
+ return visualize_estimation(task_name, estimation)
- user = flask_login.current_user
- user_id = user.get_id()
- tasks, model = web_utils.get_all_tasks_by_id_and_user_model("proj", user_id)
- if task_name not in tasks:
- tasks, model = web_utils.get_all_tasks_by_id_and_user_model("retro", user_id)
- if task_name not in tasks:
- return (f"Unable to find task '{markupsafe.escape(task_name)}'", 500)
+@bp.route('/--nominal-pert.svg')
+@flask_login.login_required
+def visualize_task_nominal(task_name, mode):
+ r = routers.ModelRouter(mode=mode)
+ estimation = r.model.nominal_point_estimate_of(task_name)
+ return visualize_estimation(task_name, estimation)
- if nominal_or_remaining == "nominal":
- estimation = model.nominal_point_estimate_of(task_name)
- else:
- estimation = model.remaining_point_estimate_of(task_name)
+def visualize_estimation(task_name, estimation):
matplotlib.use("svg")
fig = get_pert_in_figure(estimation, task_name)
diff --git a/estimage/webapp/web_utils.py b/estimage/webapp/web_utils.py
index 0d1e5bd..f5ae952 100644
--- a/estimage/webapp/web_utils.py
+++ b/estimage/webapp/web_utils.py
@@ -64,6 +64,10 @@ def get_all_tasks_by_id_and_user_model(spec, user_id):
def get_user_model(user_id, cards_tree_without_duplicates):
authoritative_pollster = webdata.AuthoritativePollster()
user_pollster = webdata.UserPollster(user_id)
+ return get_user_model_given_pollsters(user_pollster, authoritative_pollster, cards_tree_without_duplicates)
+
+
+def get_user_model_given_pollsters(user_pollster, authoritative_pollster, cards_tree_without_duplicates):
statuses = flask.current_app.get_final_class("Statuses")()
model = webdata.get_model(cards_tree_without_duplicates, None, statuses)
try:
From 3246752b29c93102b40f1b5bff5f283241b6d1df Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mat=C4=9Bj=20T=C3=BD=C4=8D?=
Date: Tue, 4 Jun 2024 00:26:44 +0200
Subject: [PATCH 2/5] Very serious cleanup
---
estimage/plugins/demo/routes.py | 29 +++--
estimage/plugins/demo/templates/demo.html | 4 +-
estimage/webapp/main/routes.py | 120 ++++++++----------
estimage/webapp/persons/routes.py | 20 +--
estimage/webapp/routers.py | 4 +-
.../templates/epic_view_projective.html | 2 +-
estimage/webapp/templates/issue_view.html | 8 +-
estimage/webapp/templates/utils.j2 | 5 +-
estimage/webapp/vis/routes.py | 1 -
estimage/webapp/web_utils.py | 36 ------
10 files changed, 94 insertions(+), 135 deletions(-)
diff --git a/estimage/plugins/demo/routes.py b/estimage/plugins/demo/routes.py
index b337cc4..ab334ef 100644
--- a/estimage/plugins/demo/routes.py
+++ b/estimage/plugins/demo/routes.py
@@ -3,23 +3,36 @@
import flask
import flask_login
-from ...webapp import web_utils
+from ... import simpledata, persistence
+from ...webapp import web_utils, routers
from . import forms
from .. import demo
+
bp = flask.Blueprint("demo", __name__, template_folder="templates")
+def _get_card_loader(flavor, backend):
+ card_class = flask.current_app.get_final_class("BaseCard")
+ loader = type("loader", (flavor, persistence.SAVERS[card_class][backend], persistence.LOADERS[card_class][backend]), dict())
+ return card_class, loader
+
+
+def get_retro_loader():
+ return _get_card_loader(simpledata.RetroCardIO, "ini")
+
+
+def get_proj_loader():
+ return _get_card_loader(simpledata.ProjCardIO, "ini")
+
+
+@web_utils.is_primary_menu_of("demo", bp, "Estimagus Demo")
@bp.route('/demo', methods=("GET", "POST"))
@flask_login.login_required
def next_day():
- user = flask_login.current_user
- user_id = user.get_id()
-
- cls, loader = web_utils.get_retro_loader()
- cards_by_id, model = web_utils.get_all_tasks_by_id_and_user_model("retro", user_id)
+ _, loader = get_retro_loader()
- start_date = flask.current_app.config["RETROSPECTIVE_PERIOD"][0]
+ start_date = flask.current_app.get_config_option("RETROSPECTIVE_PERIOD")[0]
doer = demo.Demo(loader, start_date)
doer.start_if_on_start()
@@ -41,4 +54,4 @@ def reset():
reset_form = forms.ResetForm()
if reset_form.validate_on_submit():
demo.reset_data()
- return flask.redirect(flask.url_for("demo.next_day"))
+ return flask.redirect(web_utils.head_url_for("demo.next_day"))
diff --git a/estimage/plugins/demo/templates/demo.html b/estimage/plugins/demo/templates/demo.html
index c29ea6f..6373901 100644
--- a/estimage/plugins/demo/templates/demo.html
+++ b/estimage/plugins/demo/templates/demo.html
@@ -8,8 +8,8 @@
Execution Demo
Day {{ day_index + 1 }}
- {{ render_form(plugin_form, action=url_for("demo.next_day")) }}
- {{ render_form(reset_form, button_map={"reset": "danger"}, action=url_for("demo.reset")) }}
+ {{ render_form(plugin_form, action=head_url_for("demo.next_day")) }}
+ {{ render_form(reset_form, button_map={"reset": "danger"}, action=head_url_for("demo.reset")) }}
{% endblock %}
diff --git a/estimage/webapp/main/routes.py b/estimage/webapp/main/routes.py
index 223b2e7..5a6be3a 100644
--- a/estimage/webapp/main/routes.py
+++ b/estimage/webapp/main/routes.py
@@ -41,18 +41,6 @@ def feed_estimation_to_form(estimation, form_data):
form_data.pessimistic.data = estimation.source.pessimistic
-def projective_retrieve_task(task_id):
- cls, loader = web_utils.get_proj_loader()
- ret = cls.load_metadata(task_id, loader)
- return ret
-
-
-def retro_retrieve_task(task_id):
- cls, loader = web_utils.get_retro_loader()
- ret = cls.load_metadata(task_id, loader)
- return ret
-
-
@bp.route('/consensus/', methods=['POST'])
@flask_login.login_required
def move_issue_estimate_to_consensus(task_name):
@@ -155,25 +143,30 @@ def give_data_to_context(context, user_pollster, global_pollster):
flask.flash(msg)
-def get_similar_cards_with_estimations(user_id, task_name):
- cls, loader = web_utils.get_proj_loader()
- all_cards = loader.get_loaded_cards_by_id(cls)
- cls, loader = web_utils.get_retro_loader()
- all_cards.update(loader.get_loaded_cards_by_id(cls))
-
- similar_cards = []
- similar_tasks = get_similar_tasks(user_id, task_name, all_cards)
- for task in similar_tasks:
- card = all_cards[task.name]
- card.point_estimate = task.nominal_point_estimate
- similar_cards.append(card)
- return similar_cards
+def get_similar_cards_with_estimations(task_name):
+ rs = dict(
+ proj=routers.ModelRouter(mode="proj"),
+ retro=routers.ModelRouter(mode="retro"),
+ )
+ ref_task = rs["proj"].model.get_element(task_name)
+
+ ret = dict()
+ for mode in ("proj", "retro"):
+ similar_cards = []
+
+ r = rs[mode]
+ similar_tasks = get_similar_tasks(r, ref_task)
+ for task in similar_tasks:
+ card = r.all_cards_by_id[task.name]
+ card.point_estimate = task.nominal_point_estimate
+ similar_cards.append(card)
+ ret[mode] = similar_cards
+ return ret
@bp.route('/projective/task/')
@flask_login.login_required
def view_projective_task(task_name, known_forms=None):
- t = projective_retrieve_task(task_name)
if known_forms is None:
known_forms = dict()
@@ -185,18 +178,14 @@ def view_projective_task(task_name, known_forms=None):
request_forms.update(known_forms)
breadcrumbs = get_projective_breadcrumbs()
- append_card_to_breadcrumbs(breadcrumbs, t, lambda n: web_utils.head_url_for("main.view_epic_proj", epic_name=n))
- return view_task(t, breadcrumbs, "proj", request_forms)
+ return view_task(task_name, breadcrumbs, "proj", request_forms)
@bp.route('/retrospective/task/')
@flask_login.login_required
def view_retro_task(task_name):
- t = retro_retrieve_task(task_name)
-
breadcrumbs = get_retro_breadcrumbs()
- append_card_to_breadcrumbs(breadcrumbs, t, lambda n: web_utils.head_url_for("main.view_epic_retro", epic_name=n))
- return view_task(t, breadcrumbs, "retro")
+ return view_task(task_name, breadcrumbs, "retro")
def _setup_forms_according_to_context(request_forms, context):
@@ -216,12 +205,16 @@ def _setup_forms_according_to_context(request_forms, context):
feed_estimation_to_form(context.estimation, request_forms["estimation"])
-def view_task(task, breadcrumbs, mode, request_forms=None):
- user = flask_login.current_user
- user_id = user.get_id()
+def view_task(task_name, breadcrumbs, mode, request_forms=None):
+ card_r = routers.CardRouter(mode=mode)
+ task = card_r.all_cards_by_id[task_name]
+
+ name_to_url = lambda n: web_utils.head_url_for(f"main.view_epic_{mode}", epic_name=n)
+ append_card_to_breadcrumbs(breadcrumbs, task, name_to_url)
- pollster = webdata.UserPollster(user_id)
- c_pollster = webdata.AuthoritativePollster()
+ poll_r = routers.PollsterRouter()
+ pollster = poll_r.private_pollster
+ c_pollster = poll_r.global_pollster
context = webdata.Context(task)
give_data_to_context(context, pollster, c_pollster)
@@ -231,11 +224,14 @@ def view_task(task, breadcrumbs, mode, request_forms=None):
similar_cards = []
if context.estimation_source != "none":
- similar_cards = get_similar_cards_with_estimations(user_id, task.name)
+ similar_cards = get_similar_cards_with_estimations(task_name)
+ LIMIT = 8
+ similar_cards["proj"] = similar_cards["proj"][:LIMIT]
+ similar_cards["retro"] = similar_cards["retro"][:LIMIT - len(similar_cards["proj"])]
return web_utils.render_template(
'issue_view.html', title='Estimate Issue', breadcrumbs=breadcrumbs, mode=mode,
- user=user, forms=request_forms, task=task, context=context, similar_sized_cards=similar_cards)
+ user=poll_r.user, forms=request_forms, task=task, context=context, similar_sized_cards=similar_cards)
def get_projective_breadcrumbs():
@@ -268,7 +264,7 @@ def view_epic_proj(epic_name):
estimate = r.model.nominal_point_estimate_of(epic_name)
- t = projective_retrieve_task(epic_name)
+ t = r.all_cards_by_id[epic_name]
breadcrumbs = get_projective_breadcrumbs()
append_card_to_breadcrumbs(breadcrumbs, t, lambda n: web_utils.head_url_for("main.view_epic_proj", epic_name=n))
@@ -278,13 +274,10 @@ def view_epic_proj(epic_name):
)
-def get_similar_tasks(user_id, task_name, all_cards_by_id):
- all_tasks = []
- all_cards = list(all_cards_by_id.values())
- cards_tree_without_duplicates = utilities.reduce_subsets_from_sets(all_cards)
- model = web_utils.get_user_model(user_id, cards_tree_without_duplicates)
- all_tasks.extend(model.get_all_task_models())
- return webdata.order_nearby_tasks(model.get_element(task_name), all_tasks, 0.5, 2)
+def get_similar_tasks(model_router, ref_task):
+ model = model_router.model
+ all_tasks = model.get_all_task_models()
+ return webdata.order_nearby_tasks(ref_task, all_tasks, 0.5, 2)
@bp.route('/')
@@ -301,15 +294,9 @@ def tree_view():
cards=r.cards_tree_without_duplicates, model=r.model)
-def executive_summary_of_points_and_velocity(cards, cls=history.Summary):
- all_events = data.EventManager()
- all_events.load(webdata.IOs["events"]["ini"])
-
- start, end = flask.current_app.get_config_option("RETROSPECTIVE_PERIOD")
- cutoff_date = min(datetime.datetime.today(), end)
- statuses = flask.current_app.get_final_class("Statuses")()
- aggregation = history.Aggregation.from_cards(cards, start, end, statuses)
- aggregation.process_event_manager(all_events)
+def executive_summary_of_points_and_velocity(agg_router, cards, cls=history.Summary):
+ aggregation = agg_router.get_aggregation_of_cards(cards)
+ cutoff_date = min(datetime.datetime.today(), aggregation.end)
summary = cls(aggregation, cutoff_date)
return summary
@@ -318,12 +305,12 @@ def executive_summary_of_points_and_velocity(cards, cls=history.Summary):
@bp.route('/retrospective')
@flask_login.login_required
def overview_retro():
- r = routers.ModelRouter(mode="retro")
+ r = routers.AggregationRouter(mode="retro")
tier0_cards = [t for t in r.all_cards_by_id.values() if t.tier == 0]
tier0_cards_tree_without_duplicates = utilities.reduce_subsets_from_sets(tier0_cards)
- summary = executive_summary_of_points_and_velocity(tier0_cards_tree_without_duplicates)
+ summary = executive_summary_of_points_and_velocity(r, tier0_cards_tree_without_duplicates)
return web_utils.render_template(
"retrospective_overview.html",
@@ -334,12 +321,12 @@ def overview_retro():
@bp.route('/completion')
@flask_login.login_required
def completion():
- r = routers.ModelRouter(mode="retro")
+ r = routers.AggregationRouter(mode="retro")
tier0_cards = [t for t in r.all_cards_by_id.values() if t.tier == 0]
tier0_cards_tree_without_duplicates = utilities.reduce_subsets_from_sets(tier0_cards)
- summary = executive_summary_of_points_and_velocity(tier0_cards_tree_without_duplicates, statops.summary.StatSummary)
+ summary = executive_summary_of_points_and_velocity(r, tier0_cards_tree_without_duplicates, statops.summary.StatSummary)
return web_utils.render_template(
"completion.html",
@@ -350,30 +337,29 @@ def completion():
@bp.route('/retrospective_tree')
@flask_login.login_required
def tree_view_retro():
- r = routers.ModelRouter(mode="retro")
+ r = routers.AggregationRouter(mode="retro")
tier0_cards = [t for t in r.all_cards_by_id.values() if t.tier == 0]
tier0_cards_tree_without_duplicates = utilities.reduce_subsets_from_sets(tier0_cards)
- summary = executive_summary_of_points_and_velocity(tier0_cards_tree_without_duplicates)
+ summary = executive_summary_of_points_and_velocity(r, tier0_cards_tree_without_duplicates)
priority_sorted_cards = sorted(r.cards_tree_without_duplicates, key=lambda x: - x.priority)
- statuses = flask.current_app.get_final_class("Statuses")()
return web_utils.render_template(
"tree_view_retrospective.html",
title="Retrospective Tasks tree view",
cards=priority_sorted_cards, today=datetime.datetime.today(), model=r.model,
- summary=summary, status_of=lambda c: statuses.get(c.status))
+ summary=summary, status_of=lambda c: r.statuses.get(c.status))
@bp.route('/retrospective/epic/')
@flask_login.login_required
def view_epic_retro(epic_name):
- r = routers.ModelRouter(mode="retro")
+ r = routers.AggregationRouter(mode="retro")
t = r.all_cards_by_id[epic_name]
- summary = executive_summary_of_points_and_velocity(t.children)
+ summary = executive_summary_of_points_and_velocity(r, t.children)
breadcrumbs = get_retro_breadcrumbs()
append_card_to_breadcrumbs(breadcrumbs, t, lambda n: web_utils.head_url_for("main.view_epic_retro", epic_name=n))
@@ -399,7 +385,7 @@ def view_problems():
return web_utils.render_template(
'problems.html', title='Problems', category_forms=[cf[1] for cf in cat_forms],
- all_cards_by_id=all_cards_by_id, catforms=cat_forms)
+ all_cards_by_id=r.all_cards_by_id, catforms=cat_forms)
def _solve_problem(form, classifier, all_cards_by_id):
diff --git a/estimage/webapp/persons/routes.py b/estimage/webapp/persons/routes.py
index 02f0674..991c005 100644
--- a/estimage/webapp/persons/routes.py
+++ b/estimage/webapp/persons/routes.py
@@ -5,7 +5,7 @@
import flask_login
from . import bp
-from .. import web_utils
+from .. import web_utils, routers
from ... import persons, utilities
@@ -39,22 +39,12 @@ def render_workload(title, mode, cards_tree, model):
@bp.route('/retrospective_workload')
@flask_login.login_required
def retrospective_workload():
- user = flask_login.current_user
- user_id = user.get_id()
-
- all_cards_by_id, model = web_utils.get_all_tasks_by_id_and_user_model("retro", user_id)
- all_cards = list(all_cards_by_id.values())
- cards_tree_without_duplicates = utilities.reduce_subsets_from_sets(all_cards)
- return render_workload('Retrospective Workloads', "retro", cards_tree_without_duplicates, model)
+ r = routers.ModelRouter(mode="retro")
+ return render_workload('Retrospective Workloads', "retro", r.cards_tree_without_duplicates, r.model)
@bp.route('/planning_workload')
@flask_login.login_required
def planning_workload():
- user = flask_login.current_user
- user_id = user.get_id()
-
- all_cards_by_id, model = web_utils.get_all_tasks_by_id_and_user_model("proj", user_id)
- all_cards = list(all_cards_by_id.values())
- cards_tree_without_duplicates = utilities.reduce_subsets_from_sets(all_cards)
- return render_workload('Planning Workloads', "plan", cards_tree_without_duplicates, model)
+ r = routers.ModelRouter(mode="proj")
+ return render_workload('Planning Workloads', "proj", r.cards_tree_without_duplicates, r.model)
diff --git a/estimage/webapp/routers.py b/estimage/webapp/routers.py
index 6f2c801..a62ea81 100644
--- a/estimage/webapp/routers.py
+++ b/estimage/webapp/routers.py
@@ -105,6 +105,7 @@ def __init__(self, ** kwargs):
self.start, self.end = flask.current_app.get_config_option("RETROSPECTIVE_PERIOD")
self.all_events = self.get_all_events()
+ self.statuses = flask.current_app.get_final_class("Statuses")()
@CACHE.cached(timeout=60, key_prefix=lambda: gen_cache_key("get_all_events"))
def get_all_events(self):
@@ -122,7 +123,6 @@ def get_aggregation_of_names(self, names):
return self.get_aggregation_of_cards(cards)
def get_aggregation_of_cards(self, cards):
- statuses = flask.current_app.get_final_class("Statuses")()
- ret = history.Aggregation.from_cards(cards, self.start, self.end, statuses)
+ ret = history.Aggregation.from_cards(cards, self.start, self.end, self.statuses)
ret.process_event_manager(self.all_events)
return ret
diff --git a/estimage/webapp/templates/epic_view_projective.html b/estimage/webapp/templates/epic_view_projective.html
index 694b152..0b689e0 100644
--- a/estimage/webapp/templates/epic_view_projective.html
+++ b/estimage/webapp/templates/epic_view_projective.html
@@ -1,4 +1,4 @@
-{% extends "base.html" %}
+{% extends "general_plan.html" %}
{% import "utils.j2" as utils with context %}
diff --git a/estimage/webapp/templates/issue_view.html b/estimage/webapp/templates/issue_view.html
index 3b21d02..f1071e4 100644
--- a/estimage/webapp/templates/issue_view.html
+++ b/estimage/webapp/templates/issue_view.html
@@ -1,4 +1,8 @@
-{% extends base %}
+{%- if mode == "proj" %}
+{% extends "general_plan.html" %}
+{%- else %}
+{% extends "general_retro.html" %}
+{%- endif %}
{% import "utils.j2" as utils with context %}
{% from 'bootstrap5/form.html' import render_form %}
@@ -98,7 +102,7 @@ Our values
{%- if similar_sized_cards %}
- {{ utils.render_similar_sized_tasks(similar_sized_cards[:8]) }}
+ {{ utils.render_similar_sized_tasks(similar_sized_cards) }}
{%- endif %}
{%- endif %}
diff --git a/estimage/webapp/templates/utils.j2 b/estimage/webapp/templates/utils.j2
index 7853ade..fe5ba97 100644
--- a/estimage/webapp/templates/utils.j2
+++ b/estimage/webapp/templates/utils.j2
@@ -198,9 +198,12 @@
Tasks of similar sizes
- {%- for card in similar_sized_cards %}
+ {%- for card in similar_sized_cards["proj"] %}
- {{ render_task_basic(card, "projective", None) }}: {{ render_estimate(card.point_estimate) }}
{%- endfor %}
+ {%- for card in similar_sized_cards["retro"] %}
+ - {{ render_task_basic(card, "retrospective", None) }}: {{ render_estimate(card.point_estimate) }}
+ {%- endfor %}
diff --git a/estimage/webapp/vis/routes.py b/estimage/webapp/vis/routes.py
index 39fc316..855e5f7 100644
--- a/estimage/webapp/vis/routes.py
+++ b/estimage/webapp/vis/routes.py
@@ -13,7 +13,6 @@
from .. import web_utils, routers
from ... import history, utilities
from ...statops import func
-from ... import simpledata as webdata
from ...visualize import utils, pert
# need to import those to ensure that they are discovered by the app
from ...visualize import velocity, completion
diff --git a/estimage/webapp/web_utils.py b/estimage/webapp/web_utils.py
index f5ae952..a474d72 100644
--- a/estimage/webapp/web_utils.py
+++ b/estimage/webapp/web_utils.py
@@ -25,48 +25,12 @@ def head_url_for(endpoint, * args, ** kwargs):
return flask.url_for(endpoint, * args, ** kwargs)
-def _get_card_loader(flavor, backend):
- card_class = flask.current_app.get_final_class("BaseCard")
- loader = type("loader", (flavor, persistence.SAVERS[card_class][backend], persistence.LOADERS[card_class][backend]), dict())
- return card_class, loader
-
-
-def get_retro_loader():
- return _get_card_loader(webdata.RetroCardIO, "ini")
-
-
-def get_proj_loader():
- return _get_card_loader(webdata.ProjCardIO, "ini")
-
-
def get_workloads(workload_type):
if workloads := flask.current_app.get_final_class("Workloads"):
workload_type = type(f"ext_{workload_type.__name__}", (workload_type, workloads), dict())
return workload_type
-def get_all_tasks_by_id_and_user_model(spec, user_id):
- if spec == "retro":
- cls, loader = get_retro_loader()
- elif spec == "proj":
- cls, loader = get_proj_loader()
- else:
- msg = "Unknown specification of source: {spec}"
- raise KeyError(msg)
- all_cards_by_id = loader.get_loaded_cards_by_id(cls)
- cards_list = list(all_cards_by_id.values())
- cards_tree_without_duplicates = utilities.reduce_subsets_from_sets(cards_list)
- model = get_user_model(user_id, cards_tree_without_duplicates)
- model.update_cards_with_values(cards_tree_without_duplicates)
- return all_cards_by_id, model
-
-
-def get_user_model(user_id, cards_tree_without_duplicates):
- authoritative_pollster = webdata.AuthoritativePollster()
- user_pollster = webdata.UserPollster(user_id)
- return get_user_model_given_pollsters(user_pollster, authoritative_pollster, cards_tree_without_duplicates)
-
-
def get_user_model_given_pollsters(user_pollster, authoritative_pollster, cards_tree_without_duplicates):
statuses = flask.current_app.get_final_class("Statuses")()
model = webdata.get_model(cards_tree_without_duplicates, None, statuses)
From f6c97f9095f45fcd2246a5b886c7b344e6ea33a0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mat=C4=9Bj=20T=C3=BD=C4=8D?=
Date: Tue, 4 Jun 2024 18:02:04 +0200
Subject: [PATCH 3/5] General refactor
---
estimage/persistence/event/ini.py | 4 ----
estimage/statops/func.py | 29 +++++++++++++----------------
estimage/webapp/main/routes.py | 18 +++++++++++-------
3 files changed, 24 insertions(+), 27 deletions(-)
diff --git a/estimage/persistence/event/ini.py b/estimage/persistence/event/ini.py
index 473143e..bfaaeaf 100644
--- a/estimage/persistence/event/ini.py
+++ b/estimage/persistence/event/ini.py
@@ -21,12 +21,8 @@ def _event_to_string_dict(self, event):
task_name=event.task_name
)
if (val := event.value_before) is not None:
- if event.quantity == "state":
- val = val
to_save["value_before"] = str(val)
if (val := event.value_after) is not None:
- if event.quantity == "state":
- val = val
to_save["value_after"] = str(val)
return to_save
diff --git a/estimage/statops/func.py b/estimage/statops/func.py
index a666743..e1034cc 100644
--- a/estimage/statops/func.py
+++ b/estimage/statops/func.py
@@ -133,14 +133,8 @@ def get_time_to_completion(velocity_mean, velocity_stdev, distance, confidence=0
def get_prob_of_completion(velocity_mean, velocity_stdev, distance, time):
- if distance == 0:
- return 1
- if velocity_stdev == 0:
- return 0 if velocity_mean * time < distance else 1
- if time == 0:
- return 0
- dist = sp.stats.norm(loc=velocity_mean * time, scale=np.sqrt(velocity_stdev ** 2 * time))
- return 1 - dist.cdf(distance)
+ times = np.ones(1) * time
+ return get_prob_of_completion_vector(velocity_mean, velocity_stdev, distance, times)[0]
def get_prob_of_completion_vector(velocity_mean, velocity_stdev, distance, times):
@@ -212,17 +206,20 @@ def estimate_lognorm(grids, samples):
return result
-def autoestimate_lognorm(samples):
- grids = get_1d_lognorm_grid(0.01, 5.0, samples.mean(), 10)
- res = estimate_lognorm(grids, samples)
- grids = get_1d_lognorm_grid(res[1] - 0.5, res[1] + 0.5, samples.mean(), 10)
- res2 = estimate_lognorm(grids, samples)
+def autoestimage_lognorm_general(samples, first_grid, radii, counts):
+ mean = samples.mean()
+ res = estimate_lognorm(first_grid, samples)
+ for (radius, count) in zip(radii, counts):
+ grid = get_1d_lognorm_grid(res[1] - radius, res[1] + radius, mean, count)
+ res = estimate_lognorm(grid, samples)
+ return res
- grids = get_1d_lognorm_grid(res2[1] - 0.2, res2[1] + 0.2, samples.mean(), 20)
- res3 = estimate_lognorm(grids, samples)
- return res3
+def autoestimate_lognorm(samples):
+ grids = get_1d_lognorm_grid(0.01, 5.0, samples.mean(), 10)
+ res = autoestimage_lognorm_general(samples, grids, (0.5, 0.2), (10, 20))
+ return res
def get_nonzero_velocity(velocity):
diff --git a/estimage/webapp/main/routes.py b/estimage/webapp/main/routes.py
index 5a6be3a..10d9db2 100644
--- a/estimage/webapp/main/routes.py
+++ b/estimage/webapp/main/routes.py
@@ -101,6 +101,16 @@ def move_consensus_estimate_to_authoritative(task_name):
return view_projective_task(task_name, dict(authoritative=form))
+def _attempt_record_of_estimate(task_name, form, pollster):
+ if form.submit.data:
+ tell_pollster_about_obtained_data(pollster, task_name, form)
+ elif form.delete.data:
+ if pollster.knows_points(task_name):
+ pollster.forget_points(task_name)
+ else:
+ flask.flash("Told to forget something that we don't know")
+
+
@bp.route('/estimate/', methods=['POST'])
@flask_login.login_required
def estimate(task_name):
@@ -109,13 +119,7 @@ def estimate(task_name):
form = forms.NumberEstimationForm()
if form.validate_on_submit():
- if form.submit.data:
- tell_pollster_about_obtained_data(pollster, task_name, form)
- elif form.delete.data:
- if pollster.knows_points(task_name):
- pollster.forget_points(task_name)
- else:
- flask.flash("Told to forget something that we don't know")
+ _attempt_record_of_estimate(task_name, form, pollster)
else:
msg = "There were following errors: "
msg += ", ".join(form.get_all_errors())
From 97e8c0e3b20275766428121b620735eb1353f1cb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mat=C4=9Bj=20T=C3=BD=C4=8D?=
Date: Wed, 5 Jun 2024 14:50:20 +0200
Subject: [PATCH 4/5] Login tweak
---
estimage/webapp/__init__.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/estimage/webapp/__init__.py b/estimage/webapp/__init__.py
index 2605e60..e493e5f 100644
--- a/estimage/webapp/__init__.py
+++ b/estimage/webapp/__init__.py
@@ -155,6 +155,9 @@ def create_app_common(app):
LOGIN.init_app(app)
LOGIN.user_loader(users.load_user)
LOGIN.login_view = "login.auto_login"
+ # Don't display the "log in to proceed" message, as it is often more confusing than helpful
+ # in connection with random logouts and autologins
+ LOGIN.login_message = ""
CACHE.init_app(app, config=app.config)
From 1c38669e592e5a126edcbda503ce152f1f8f97ec Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mat=C4=9Bj=20T=C3=BD=C4=8D?=
Date: Wed, 5 Jun 2024 17:02:15 +0200
Subject: [PATCH 5/5] Improve structure to reduce complexity
---
estimage/webapp/main/routes.py | 38 +++++++++++++----------
estimage/webapp/templates/issue_view.html | 2 +-
2 files changed, 22 insertions(+), 18 deletions(-)
diff --git a/estimage/webapp/main/routes.py b/estimage/webapp/main/routes.py
index 10d9db2..690449c 100644
--- a/estimage/webapp/main/routes.py
+++ b/estimage/webapp/main/routes.py
@@ -41,29 +41,33 @@ def feed_estimation_to_form(estimation, form_data):
form_data.pessimistic.data = estimation.source.pessimistic
+def _move_estimate_from_private_to_global(form, task_name, pollster_router):
+ user_point = pollster_router.private_pollster.ask_points(task_name)
+ pollster_router.global_pollster.tell_points(task_name, user_point)
+
+ if form.forget_own_estimate.data:
+ pollster_router.private_pollster.forget_points(task_name)
+
+
+def _delete_global_estimate(task_name, pollster_router):
+ pollster_cons = pollster_router.global_pollster
+
+ if pollster_cons.knows_points(task_name):
+ pollster_cons.forget_points(task_name)
+ else:
+ flask.flash("Told to forget something that we don't know")
+
+
@bp.route('/consensus/', methods=['POST'])
@flask_login.login_required
-def move_issue_estimate_to_consensus(task_name):
- user = flask_login.current_user
- user_id = user.get_id()
+def act_on_global_estimate(task_name):
+ r = routers.PollsterRouter()
form = forms.ConsensusForm()
if form.validate_on_submit():
if form.submit.data and form.i_kid_you_not.data:
- pollster_user = webdata.UserPollster(user_id)
- pollster_cons = webdata.AuthoritativePollster()
-
- user_point = pollster_user.ask_points(task_name)
- pollster_cons.tell_points(task_name, user_point)
-
- if form.forget_own_estimate.data:
- pollster_user.forget_points(task_name)
+ _move_estimate_from_private_to_global(form, task_name, r)
elif form.delete.data:
- pollster_cons = webdata.AuthoritativePollster()
-
- if pollster_cons.knows_points(task_name):
- pollster_cons.forget_points(task_name)
- else:
- flask.flash("Told to forget something that we don't know")
+ _delete_global_estimate(task_name, r)
else:
flask.flash("Consensus not updated, request was not serious")
diff --git a/estimage/webapp/templates/issue_view.html b/estimage/webapp/templates/issue_view.html
index f1071e4..4356e63 100644
--- a/estimage/webapp/templates/issue_view.html
+++ b/estimage/webapp/templates/issue_view.html
@@ -93,7 +93,7 @@ Consensus values
Point cost: {{ utils.render_estimate(context.global_estimate) }}
{% endif %}
{% if "consensus" in forms %}
- {{ render_form(forms["consensus"], button_map={"submit": "primary", "delete": "danger"}, action=head_url_for("main.move_issue_estimate_to_consensus", task_name=task.name)) }}
+ {{ render_form(forms["consensus"], button_map={"submit": "primary", "delete": "danger"}, action=head_url_for("main.act_on_global_estimate", task_name=task.name)) }}
{% endif %}