Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce first Problems #26

Merged
merged 21 commits into from
Apr 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading