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

- PERT prob density function for {{ epic.name }} - remaining work + PERT prob density function for {{ epic.name }} - remaining work
{%- 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 %}
- PERT prob density function for {{ task.name }} + PERT prob density function for {{ task.name }}
{% 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 %}