Skip to content

Commit

Permalink
Merge pull request #26 from matejak/problems
Browse files Browse the repository at this point in the history
Introduce first Problems
  • Loading branch information
matejak committed Apr 22, 2024
2 parents 8d3ad13 + 9711e38 commit e85a165
Show file tree
Hide file tree
Showing 31 changed files with 1,054 additions and 230 deletions.
33 changes: 31 additions & 2 deletions estimage/entities/card.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import re
import typing
import enum
import dataclasses
import datetime

Expand Down Expand Up @@ -77,6 +75,8 @@ def _convert_into_single_result(self, statuses):
return ret

def add_element(self, what: "BaseCard"):
if what in self:
return
self.children.append(what)
what.parent = self

Expand Down Expand Up @@ -142,3 +142,32 @@ def to_tree(cls, cards: typing.List["BaseCard"], statuses: status.Statuses=None)

def get_tree(self, statuses: status.Statuses=None):
return self.to_tree([self], statuses)


@PluginResolver.class_is_extendable("CardSynchronizer")
class CardSynchronizer:
ABSOLUTE_TOLERABLE_DIFFERENCE = 0.1
def get_tracker_points_of(self, card: BaseCard) -> float:
raise NotImplementedError

def set_tracker_points_of(self, card: BaseCard, target_points: float, card_io=None):
real_points = self.get_tracker_points_of(card)
self._synchronize_or_raise(card, real_points, target_points)
card.point_cost = target_points
if card_io:
card.save_metadata(card_io)

def _synchronize_or_raise(self, card: BaseCard, real_points: float, target_points: float):
if real_points == target_points:
return
if abs(real_points - card.point_cost) > self.ABSOLUTE_TOLERABLE_DIFFERENCE:
msg = (
f"Value of card '{card.name}' differs from expected value: "
f"{real_points} is not {card.point_cost} respectively "
f"by more than {self.ABSOLUTE_TOLERABLE_DIFFERENCE:.2g}."
)
raise ValueError(msg)
self.insert_points_into_tracker(card, target_points)

def insert_points_into_tracker(self, card: BaseCard, target_points: float):
raise NotImplementedError
8 changes: 6 additions & 2 deletions estimage/entities/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@ def __init__(self):
self.name_composition_map = dict()

def use_composition(self, composition: Composition):
self.main_composition = composition
self.main_composition.name = ""
if composition.name:
self.main_composition = Composition("")
self.main_composition.add_composition(composition)
else:
self.main_composition = composition
self.main_composition.name = ""

self.name_result_map = dict()
self.name_composition_map = dict()
Expand Down
Empty file.
7 changes: 7 additions & 0 deletions estimage/plugins/base/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from flask_wtf import FlaskForm


class BaseForm(FlaskForm):
def __init__(self, ** kwargs):
self.extending_fields = []
super().__init__(** kwargs)
44 changes: 37 additions & 7 deletions estimage/plugins/crypto/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@
from ...entities import status
from ...visualize.burndown import StatusStyle
from .forms import CryptoForm
from ..jira.forms import AuthoritativeForm, ProblemForm


JiraFooter = jira.JiraFooter

EXPORTS = dict(
Footer="JiraFooter",
AuthoritativeForm="AuthoritativeForm",
ProblemForm="ProblemForm",
MPLPointPlot="MPLPointPlot",
Statuses="Statuses",
Workloads="Workloads",
Expand Down Expand Up @@ -55,11 +55,14 @@ def from_form_and_app(cls, form, app) -> "InputSpec":
ret.server_url = "https://issues.redhat.com"
ret.token = form.token.data
ret.cutoff_date = app.get_config_option("RETROSPECTIVE_PERIOD")[0]
sprint = "openSprints()"
query = f"project = {PROJECT_NAME} AND type = Task AND sprint in openSprints()"
query = "filter = 12350823 AND Sprint in openSprints() AND labels = committed AND issuetype in (task, bug, Story)"
ret.retrospective_query = query
query = "filter = 12350823 AND Sprint in futureSprints() AND issuetype in (task, bug, Story)"
ret.projective_query = query
query_tpl = "filter = 12350823 AND Sprint in {sprint} AND issuetype in (task, bug, Story)"
ret.retrospective_query = query_tpl.format(sprint=sprint)
if form.project_next.data:
sprint = "futureSprints()"
ret.projective_query = query_tpl.format(sprint=sprint)
ret.item_class = app.get_final_class("BaseCard")
return ret

Expand Down Expand Up @@ -103,6 +106,7 @@ def _get_owner_epic_of(self, assignee, committed):
epic.assignee = assignee
epic.title = f"Issues of {assignee}"
epic.tier = 1
epic.status = "in_progress"
if committed:
epic.title = "Committed " + epic.title
epic.tier = 0
Expand All @@ -118,6 +122,31 @@ def put_tasks_under_artificial_epics(self, tasks):
epic_names.add(epic.name)
return epic_names

def _parent_has_only_unestimated_children(self, pname):
children = self._cards_by_id[pname].children
rolling_sum = 0
for c in children:
rolling_sum += abs(c.point_cost)
return rolling_sum == 0

def _propagate_estimates_of_estimated_task_to_unestimated_subtasks(self, pname):
parent = self._cards_by_id[pname]
if parent.point_cost == 0:
return
if not self._parent_has_only_unestimated_children(pname):
return
points_per_child = parent.point_cost / len(parent.children)
for c in parent.children:
c.point_cost = points_per_child

def distribute_subtasks_points_to_tasks(self):
names_of_not_parents = set()
for c in self._cards_by_id.values():
if not c.children and c.parent:
names_of_not_parents.add(c.name)
names_of_parents_of_not_parents = {self._cards_by_id[cname].parent.name for cname in names_of_not_parents}
for pn in names_of_parents_of_not_parents:
self._propagate_estimates_of_estimated_task_to_unestimated_subtasks(pn)

def import_data(self, spec):
retro_tasks = set()
Expand Down Expand Up @@ -148,7 +177,8 @@ def import_data(self, spec):
self._projective_cards.update(new_epic_names)
self._cards_by_id.update(new_cards)

self.resolve_inheritance(proj_tasks.union(retro_tasks))
self.resolve_inheritance(set(self._cards_by_id.keys()))
self.distribute_subtasks_points_to_tasks()

for name in issue_names_requiring_events:
extractor = jira.EventExtractor(self._all_issues_by_name[name], spec.cutoff_date)
Expand Down
5 changes: 3 additions & 2 deletions estimage/plugins/crypto/forms.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from flask_wtf import FlaskForm
from ..base.forms import BaseForm
import wtforms


from ..jira import forms


class CryptoFormEnd(FlaskForm):
class CryptoFormEnd(BaseForm):
project_next = wtforms.BooleanField('Plan for the Next Iteration')
submit = wtforms.SubmitField("Import Data")


Expand Down
5 changes: 4 additions & 1 deletion estimage/plugins/crypto/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import flask_login

from ...webapp import web_utils
from ...plugins import crypto, jira
from ...plugins import crypto, jira, redhat_compliance
from ...plugins.jira import routes
from . import forms

Expand All @@ -19,5 +19,8 @@ def sync():
if form.validate_on_submit():
task_spec = crypto.InputSpec.from_form_and_app(form, flask.current_app)
jira.routes.do_stuff_and_flash_messages(task_spec, crypto.do_stuff)
else:
next_starts_soon = redhat_compliance.days_to_next_epoch(datetime.datetime.today()) < 30
form.project_next.data = next_starts_soon
return web_utils.render_template(
'crypto.html', title='Red Hat Crypto Plugin', plugin_form=form, )
5 changes: 5 additions & 0 deletions estimage/plugins/crypto/templates/crypto.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,8 @@ <h1>Crypto Plugin</h1>
</div>

{% endblock %}

{% block footer %}
{{ super() }}
{{ plugin_form.supporting_js([plugin_form]) | safe }}
{% endblock %}
7 changes: 4 additions & 3 deletions estimage/plugins/demo/forms.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
from flask_wtf import FlaskForm
import wtforms

from ..base.forms import BaseForm

class DemoForm(FlaskForm):

class DemoForm(BaseForm):
issues = wtforms.SelectMultipleField('Issues')
progress = wtforms.FloatField('Progress')
submit = wtforms.SubmitField("Next Day")


class ResetForm(FlaskForm):
class ResetForm(BaseForm):
reset = wtforms.SubmitField("Reset Simulation")
88 changes: 36 additions & 52 deletions estimage/plugins/jira/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import dataclasses
import datetime
import collections
import textwrap
import time
import typing

Expand Down Expand Up @@ -43,52 +42,35 @@
Collected = collections.namedtuple("Collected", ("Retrospective", "Projective", "Events"))


EXPORTS = dict(
Footer="JiraFooter",
)


class JiraFooter:
def get_footer_html(self):
strings = [super().get_footer_html()]
strings.append(textwrap.dedent(
"""
<script type="text/javascript">
function tokenName() {
return "estimagus." + location.hostname + ".jira_ePAT";
}
function getPAT() {
const token_name = tokenName();
return localStorage.getItem(token_name);
}
function updatePAT(with_what) {
const token_name = tokenName();
return localStorage.setItem(token_name, with_what);
}
function supplyEncryptedToken(encrypted_field, normal_field, store_checkbox, token_str) {
store_checkbox.checked = false;
encrypted_field.value = token_str;
normal_field.placeholder = "Optional, using stored locally stored token by default";
}
let update_store = document.getElementById('encrypted_meant_for_storage');
let enc_field = document.getElementById('encrypted_token');
if (update_store.value == "yes" && enc_field.value) {
updatePAT(enc_field.value);
}
let pat = getPAT();
if (pat) {
let normal_field = document.getElementById('token');
let store_checkbox = document.getElementById('store_token');
supplyEncryptedToken(enc_field, normal_field, store_checkbox, pat);
}
</script>
"""))
return "\n".join(strings)
class CardSynchronizer:
def __init__(self, server_url, token, importer_cls, ** kwargs):
self.server_url = server_url
self.token = token
self.importer_cls = importer_cls
super().__init__(** kwargs)

@classmethod
def from_form(cls, form):
raise NotImplementedError

def _get_spec(self):
ret = InputSpec()
ret.server_url = self.server_url
ret.token = self.token
ret.item_class = card.BaseCard
return ret

def get_tracker_points_of(self, c: card.BaseCard) -> float:
spec = self._get_spec()
spec.item_class = c.__class__
importer = self.importer_cls(spec)
return importer.get_points_of(c)

def insert_points_into_tracker(self, c: card.BaseCard, target_points: float):
spec = self._get_spec()
spec.item_class = c.__class__
importer = self.importer_cls(spec)
importer.update_points_of(c, target_points)


@dataclasses.dataclass(init=False)
Expand Down Expand Up @@ -336,14 +318,13 @@ def find_cards(self, card_names: typing.Iterable[str]):
return [cards_by_id[name] for name in card_names]

def find_card(self, name: str):
query = f"id = {name}"
card = self.jira.search_issues(query)
card = self.jira.issue(name)
if not card:
msg = (
f"{card} not found"
)
raise ValueError(msg)
return card[0]
return card

def refresh_cards(self, real_cards: typing.Iterable[card.BaseCard], io_cls):
if not real_cards:
Expand All @@ -364,8 +345,11 @@ def _apply_refresh(self, real_card: card.BaseCard, jira_card):
def export_jira_epic_chain_to_cards(self, root_names: typing.Iterable[str]) -> dict[str, card.BaseCard]:
exported_cards_by_id = dict()
for name in root_names:
issue = self._all_issues_by_name[name]
card = self.merge_jira_item_without_children(issue)
if name in self._cards_by_id:
card = self._cards_by_id[name]
else:
issue = self._all_issues_by_name[name]
card = self.merge_jira_item_without_children(issue)
exported_cards_by_id[name] = card
children = self._parents_child_keymap[name]
if not children:
Expand Down
Loading

0 comments on commit e85a165

Please sign in to comment.