Skip to content

Commit

Permalink
Consolidation of problems and solutions
Browse files Browse the repository at this point in the history
  • Loading branch information
matejak committed Apr 4, 2024
1 parent fb851d4 commit 2cccea5
Show file tree
Hide file tree
Showing 10 changed files with 106 additions and 54 deletions.
3 changes: 2 additions & 1 deletion estimage/plugins/crypto/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@
from ...webapp import web_utils
from ...entities import status
from ...visualize.burndown import StatusStyle
from .forms import CryptoForm
from .forms import CryptoForm, ProblemForm


JiraFooter = jira.JiraFooter

EXPORTS = dict(
ProblemForm="ProblemForm",
Footer="JiraFooter",
MPLPointPlot="MPLPointPlot",
Statuses="Statuses",
Expand Down
4 changes: 4 additions & 0 deletions estimage/plugins/crypto/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@ class CryptoFormEnd(FlaskForm):

class CryptoForm(forms.EncryptedTokenForm, CryptoFormEnd):
pass


class ProblemForm(forms.EncryptedTokenForm):
pass
3 changes: 2 additions & 1 deletion estimage/plugins/redhat_compliance/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@
from ...webapp import web_utils
from ...visualize.burndown import StatusStyle
from .. import jira
from .forms import AuthoritativeForm
from .forms import AuthoritativeForm, ProblemForm

JiraFooter = jira.JiraFooter

EXPORTS = dict(
AuthoritativeForm="AuthoritativeForm",
ProblemForm="ProblemForm",
Footer="JiraFooter",
BaseCard="BaseCard",
MPLPointPlot="MPLPointPlot",
Expand Down
4 changes: 4 additions & 0 deletions estimage/plugins/redhat_compliance/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,7 @@ def __iter__(self):
)
ret = (a for a in attributes)
return ret


class ProblemForm(forms.EncryptedTokenForm):
pass
6 changes: 3 additions & 3 deletions estimage/problems/groups.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import collections
import typing

from .problem import Problem, get_problem
from .problem import Problem
from . import solutions


Expand All @@ -14,9 +14,9 @@ class ProblemCategory:
unwanted_tags: typing.FrozenSet[str] = frozenset()

def matches(self, p: Problem):
if p.tags.intersection(self.required_tags) != self.required_tags:
if set(p.tags).intersection(self.required_tags) != self.required_tags:
return False
if p.tags.intersection(self.unwanted_tags):
if set(p.tags).intersection(self.unwanted_tags):
return False
return True

Expand Down
93 changes: 65 additions & 28 deletions estimage/problems/problem.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,11 @@

TAG_TO_PROBLEM_TYPE = dict()

def get_problem(** data):
data["tags"] = frozenset(data.get("tags", frozenset()))
problem_tags = list(data["tags"].intersection(set(TAG_TO_PROBLEM_TYPE.keys())))
def _get_problem_t_tags(tags):
problem_tags = list(tags.intersection(set(TAG_TO_PROBLEM_TYPE.keys())))
if not problem_tags:
return Problem(** data)
return TAG_TO_PROBLEM_TYPE[problem_tags[0]](** data)
return ""
return problem_tags[0]


def problem_associated_with_tag(tag):
Expand All @@ -27,12 +26,32 @@ def wrapper(wrapped):

@dataclasses.dataclass(frozen=True)
class Problem:
description: str = ""
description_template: str = ""
affected_card_name: str = ""
tags: typing.FrozenSet[str] = frozenset()

def get_formatted_description(self):
return self.description.format(formatted_task_name=self.format_task_name())

def format_task_name(self):
return self.affected_card_name
return f"'{self.affected_card_name}'"

@property
def description(self):
return self.description_template.format(formatted_task_name=self.format_task_name())

@classmethod
def get_problem(cls, ** data):
tags = frozenset(data.get("tags", set()))
data["tags"] = tags

problem_tag = _get_problem_t_tags(tags)
if not problem_tag:
problem_t_final = cls
else:
problem_t_special = TAG_TO_PROBLEM_TYPE[problem_tag]
problem_t_final = type("someProblem", (problem_t_special, cls), dict())
return problem_t_final(** data)


@problem_associated_with_tag("inconsistent_estimate")
Expand All @@ -42,13 +61,30 @@ class ValueProblem(Problem):
value_found: float = None


class Analysis:
card: BaseCard
recorded_cost: float
computed_nominal_cost: float
expected_computed_cost: float

def __init__(self, card, computed_remaining_cost, computed_nominal_cost):
self.card = card
self.recorded_cost = card.point_cost
self.computed_nominal_cost = computed_nominal_cost

self.expected_computed_cost = computed_nominal_cost
if card.children:
self.expected_computed_cost = computed_remaining_cost


class ProblemDetector:
POINT_THRESHOLD = 0.4

def __init__(self, model: EstiModel, cards: typing.Iterable[BaseCard]):
def __init__(self, model: EstiModel, cards: typing.Iterable[BaseCard], base_problem_t=Problem):
self.model = model
self.cards = cards
self.problems = []
self.base_problem_t = base_problem_t

self._get_problems()

Expand All @@ -72,16 +108,17 @@ def _create_card_problem_data(self, card):
def _card_is_not_estimated_but_has_children(self, card):
return card.children and card.point_cost == 0

def _treat_inconsistent_estimate(self, card, computed_nominal_cost, recorded_cost, expected_computed_cost):
def _treat_inconsistent_estimate(self, analysis: Analysis):
card = analysis.card
data = self._create_card_problem_data(card)
data["tags"].add("inconsistent_estimate")
data["value_found"] = recorded_cost
if computed_nominal_cost == 0:
data["value_found"] = analysis.recorded_cost
if analysis.computed_nominal_cost == 0:
self._inconsistent_card_missing_children_estimates(data, card)
else:
self._inconsistent_card_differing_estimate(data, card, expected_computed_cost, computed_nominal_cost)
self._inconsistent_card_differing_estimate(data, card, analysis)
data["tags"] = frozenset(data["tags"])
self.problems.append(get_problem(** data))
self.problems.append(self.base_problem_t.get_problem(** data))

def _card_has_no_children_with_children(self, card):
for child in card.children:
Expand All @@ -91,39 +128,39 @@ def _card_has_no_children_with_children(self, card):

def _inconsistent_card_missing_children_estimates(self, data, card):
recorded_cost = data['value_found']
data["description"] = (
f"'{card.name}' has inconsistent recorded point cost of {recorded_cost:.2g}, "
data["description_template"] = (
"{formatted_task_name} "
f"has inconsistent recorded point cost of {recorded_cost:.2g}, "
f"as its children appear to lack estimations."
)
data["tags"].add("missing_children_estimates")
if self._card_has_no_children_with_children(card):
data["tags"].add("has_only_childless_children")

def _inconsistent_card_differing_estimate(self, data, card, expected_computed_cost, computed_nominal_cost):
def _inconsistent_card_differing_estimate(self, data, card, analysis):
recorded_cost = data['value_found']
if expected_computed_cost < recorded_cost:
if analysis.expected_computed_cost < analysis.recorded_cost:
data["tags"].add("sum_of_children_lower")
if recorded_cost <= computed_nominal_cost:
if analysis.recorded_cost <= analysis.computed_nominal_cost:
data["tags"].add("estimate_within_nominal")
data["description"] = (
f"'{card.name}' has inconsistent recorded point cost of {recorded_cost:.2g}, "
f"while the deduced cost is {expected_computed_cost:.2g}"
data["description_template"] = (
"{formatted_task_name} "
f"has inconsistent recorded point cost of {analysis.recorded_cost:.2g}, "
f"while the deduced cost is {analysis.expected_computed_cost:.2g}"
)
data["value_expected"] = expected_computed_cost
data["value_expected"] = analysis.expected_computed_cost

def _analyze_inconsistent_record(self, card):
recorded_cost = card.point_cost

computed_remaining_cost = self.model.remaining_point_estimate_of(card.name).expected
computed_nominal_cost = self.model.nominal_point_estimate_of(card.name).expected
expected_computed_cost = computed_nominal_cost
if card.children:
expected_computed_cost = computed_remaining_cost
analysis = Analysis(card, computed_remaining_cost, computed_nominal_cost)

if self._card_is_not_estimated_but_has_children(card):
return

if not self._numbers_differ_significantly(recorded_cost, expected_computed_cost):
if not self._numbers_differ_significantly(recorded_cost, analysis.expected_computed_cost):
return

self._treat_inconsistent_estimate(card, computed_nominal_cost, recorded_cost, expected_computed_cost)

self._treat_inconsistent_estimate(analysis)
17 changes: 9 additions & 8 deletions estimage/webapp/main/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,22 +110,23 @@ class MultiCheckboxField(wtforms.SelectMultipleField):
widget = wtforms.widgets.ListWidget(prefix_label=False)
option_widget = wtforms.widgets.CheckboxInput()

def get_selected(self):
ret = []
for x in self:
print(11, x, 22)


@PluginResolver.class_is_extendable("ProblemForm")
class ProblemForm(FlaskForm):

def add_problems(self, problems_category, problems):
def add_problems(self, all_problems):
for p in all_problems:
self.problems.choices.append((p.affected_card_name, ""))

def add_problems_and_cat(self, problems_category, problems):
for p in problems:
self.problems.choices.append((p.affected_card_name, p.description))
self.problems.choices.append((p.affected_card_name, p.get_formatted_description()))

self.problem_category.data = problems_category.name
if s := problems_category.solution:
self.solution.data = s.description

problem = wtforms.HiddenField("problem")
problem_category = wtforms.HiddenField("problem_cat")
problems = MultiCheckboxField("Problems", choices=[])
solution = wtforms.StringField("Solution", render_kw={'readonly': True})
submit = SubmitField("Solve Problems")
25 changes: 15 additions & 10 deletions estimage/webapp/main/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from ... import utilities, statops
from ...statops import summary
from ... import simpledata as webdata
from ... import history, problems, solutions
from ... import history, problems
from ...plugins import redhat_compliance


Expand Down Expand Up @@ -438,6 +438,11 @@ def view_epic_retro(epic_name):
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}"


@bp.route('/problems')
@flask_login.login_required
def view_problems():
Expand All @@ -447,7 +452,7 @@ def view_problems():
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)
problem_detector = problems.ProblemDetector(model, all_cards, RetroProblem)

classifier = problems.groups.ProblemClassifier()
classifier.classify(problem_detector.problems)
Expand All @@ -457,15 +462,11 @@ def view_problems():
for cat in categories:
probs = classifier.classified_problems[cat.name]

form = forms.ProblemForm()
form.add_problems(cat, probs)
form.problem.data = "Missing Update"
form = flask.current_app.get_final_class("ProblemForm")()
form.add_problems_and_cat(cat, probs)

cat_forms.append((cat, form))

# solver = solutions.ProblemSolver()
# sols = {p.description: solver.get_solution_of(p) for p in probs}

return web_utils.render_template(
'problems.html', title='Problems',
all_cards_by_id=all_cards_by_id, problems=probs, catforms=cat_forms)
Expand All @@ -482,11 +483,15 @@ def fix_problems():

problem_detector = problems.ProblemDetector(model, all_cards)

classifier = problems.groups.ProblemClassifier()
classifier.classify(problem_detector.problems)
categories = classifier.get_categories_with_problems()

form = forms.ProblemForm()
form.add_problems(problem_detector.problems)
if form.validate_on_submit():
print(f"Fix {form.problem.data} by {form.solution.data}")
print(f"Fix: {form.problems.data}")
problems_cat = classifier.CATEGORIES[form.problem_category.data]
print(f"Fix {form.problems.data}: {problems_cat.solution.description}")
else:
flask.flash(f"Error handing over solution: {form.errors}")
return flask.redirect(
Expand Down
1 change: 0 additions & 1 deletion estimage/webapp/templates/general_retro.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,4 @@
{{ render_nav_item(get_head_absolute_endpoint('main.tree_view_retro'), 'Tree View') }}
{{ render_nav_item(get_head_absolute_endpoint('persons.retrospective_workload'), 'Workloads') }}
{{ render_nav_item(get_head_absolute_endpoint('main.completion'), 'Completion') }}
{{ render_nav_item(get_head_absolute_endpoint('main.view_problems'), 'Problems') }}
{% endblock %}
4 changes: 2 additions & 2 deletions tests/test_problems_groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

def test_problem_categories_trivial():
dumb_classifier = tm.ProblemClassifier()
p = tm.get_problem(tags=["unspecific"])
p = tm.Problem.get_problem(tags=["unspecific"])
dumb_classifier.classify([p])
others = dumb_classifier.not_classified
assert len(others) == 1
Expand Down Expand Up @@ -48,7 +48,7 @@ def test_problem_categories_no_duplication(classifier):

def test_problem_categories_basic(classifier):
classifier.add_category(Underestimation)
p = tm.get_problem(tags=["underestimated"])
p = tm.Problem.get_problem(tags=["underestimated"])
classifier.classify([p])
assert not classifier.not_classified
underestimation_problems = classifier.classified_problems["underestimation"]
Expand Down

0 comments on commit 2cccea5

Please sign in to comment.