Skip to content

Commit

Permalink
Merge pull request #38 from matejak/priorities
Browse files Browse the repository at this point in the history
Add support for advanced prioritization
  • Loading branch information
matejak authored Sep 23, 2024
2 parents f284faf + 38c0b64 commit 4425a44
Show file tree
Hide file tree
Showing 30 changed files with 987 additions and 403 deletions.
40 changes: 20 additions & 20 deletions estimage/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,28 +25,28 @@ def add_known_extendable_classes(self):
def get_class(self, name):
return self.class_dict[name]

def resolve_extension(self, plugin):
self.resolve_class_extension(plugin)
def resolve_extension(self, plugin, override=None):
if override:
exposed_exports = override
else:
exposed_exports = getattr(plugin, "EXPORTS", dict())

def resolve_class_extension(self, plugin):
for class_type in self.class_dict:
self._resolve_possible_class_extension(class_type, plugin)
for class_name in self.class_dict:
self.resolve_class_extension(class_name, plugin, exposed_exports)

def _resolve_possible_class_extension(self, class_type, plugin):
exposed_exports = getattr(plugin, "EXPORTS", dict())

plugin_doesnt_export_current_symbol = class_type not in exposed_exports
def resolve_class_extension(self, class_name, plugin, exposed_exports):
plugin_doesnt_export_current_symbol = class_name not in exposed_exports
if plugin_doesnt_export_current_symbol:
return

plugin_local_symbol_name = exposed_exports[class_type]
extension = getattr(plugin, plugin_local_symbol_name, None)
if extension is None:
plugin_local_symbol_name = exposed_exports[class_name]
class_extension = getattr(plugin, plugin_local_symbol_name, None)
if class_extension is None:
msg = (
f"Looking for exported symbol '{plugin_local_symbol_name}', "
"which was not found")
raise ValueError(msg)
self._update_class_with_extension(class_type, extension)
self._update_class_with_extension(class_name, class_extension)

def _update_class_io_with_extension(self, new_class, original_class, extension):
for backend, loader in persistence.LOADERS[original_class].items():
Expand All @@ -61,13 +61,13 @@ def _update_class_io_with_extension(self, new_class, original_class, extension):
fused_saver = type("saver", (extension_saver, saver), dict())
persistence.SAVERS[new_class][backend] = fused_saver

def _update_class_with_extension(self, class_type, extension):
our_value = self.class_dict[class_type]
def _update_class_with_extension(self, class_name, extension):
our_value = self.class_dict[class_name]
extension_module_name = extension.__module__.split('.')[-1]
class_name = f"{our_value.__name__}_{extension_module_name}"
new_class_name = f"{our_value.__name__}_{extension_module_name}"
if self.global_symbol_prefix:
class_name = f"{self.global_symbol_prefix}__{class_name}"
new_class = type(class_name, (extension, our_value), dict())
globals()[class_name] = new_class
self.class_dict[class_type] = new_class
new_class_name = f"{self.global_symbol_prefix}__{new_class_name}"
new_class = type(new_class_name, (extension, our_value), dict())
globals()[new_class_name] = new_class
self.class_dict[class_name] = new_class
self._update_class_io_with_extension(new_class, our_value, extension)
6 changes: 6 additions & 0 deletions estimage/entities/card.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ def _convert_into_single_result(self, statuses):

return ret

def get_direct_dependencies(self) -> typing.Iterable["BaseCard"]:
return tuple(self.depends_on) + tuple(self.children)

def register_direct_dependency(self, dependency: "BaseCard"):
self.depends_on.append(dependency)

def add_element(self, what: "BaseCard"):
if what in self:
return
Expand Down
39 changes: 27 additions & 12 deletions estimage/persistence/card/ini.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ def save_costs(self, t):
self._store_our(t, "point_cost", str(t.point_cost))

def save_family_records(self, t):
depnames_str = self._pack_list([dep.name for dep in t.children])
self._store_our(t, "depnames", depnames_str)
depnames_str = self._pack_list([dep.name for dep in t.depends_on])
self._store_our(t, "direct_depnames", depnames_str)
children_str = self._pack_list([dep.name for dep in t.children])
self._store_our(t, "depnames", children_str)
parent_str = ""
if t.parent:
parent_str = t.parent.name
Expand Down Expand Up @@ -85,24 +87,37 @@ def load_title_and_desc(self, t):
def load_costs(self, t):
t.point_cost = float(self._get_our(t, "point_cost"))

def _get_or_create_card_named(self, name, parent=None):
def _get_or_create_card_named(self, name):
if name in self._card_cache:
c = self._card_cache[name]
else:
c = self.card_class(name)
if parent:
c.parent = parent
c.load_data_by_loader(self)
self._card_cache[name] = c
c.load_data_by_loader(self)
return c

def load_family_records(self, t):
all_deps = self._get_our(t, "depnames", "")
for n in self._unpack_list(all_deps):
if not n:
def _load_list_of_cards_from_entry(self, t, entry_name):
entry_contents = self._get_our(t, entry_name, "")
all_entries = self._load_list_of_cards(entry_contents)
return all_entries

def _load_list_of_cards(self, list_string):
ret = []
for name in self._unpack_list(list_string):
if not name:
continue
new = self._get_or_create_card_named(n, t)
t.add_element(new)
ret.append(self._get_or_create_card_named(name))
return ret

def load_family_records(self, t):
all_children = self._load_list_of_cards_from_entry(t, "depnames")
for c in all_children:
t.add_element(c)

all_direct_deps = self._load_list_of_cards_from_entry(t, "direct_depnames")
for c in all_direct_deps:
t.register_direct_dependency(c)

parent_id = self._get_our(t, "parent", "")
parent_known_notyet_fetched = parent_id and t.parent is None
if parent_known_notyet_fetched:
Expand Down
2 changes: 2 additions & 0 deletions estimage/persistence/card/memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ def save_costs(self, t):
def save_family_records(self, t):
self._save(t, "children")
self._save(t, "parent")
self._save(t, "depends_on")

def save_assignee_and_collab(self, t):
self._save(t, "assignee")
Expand Down Expand Up @@ -64,6 +65,7 @@ def load_costs(self, t):
def load_family_records(self, t):
self._load(t, "children")
self._load(t, "parent")
self._load(t, "depends_on")

def load_assignee_and_collab(self, t):
self._load(t, "assignee")
Expand Down
5 changes: 4 additions & 1 deletion estimage/plugins/base/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,8 @@ def __init__(self, ** kwargs):
super().__init__(** kwargs)

@classmethod
def supporting_js(cls, forms):
def bulk_supporting_js(cls, forms):
return ""

def supporting_js(self):
return self.bulk_supporting_js([self])
8 changes: 4 additions & 4 deletions estimage/plugins/crypto/templates/crypto-issue_view.html
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
{% extends "issue_view.html" %}

{% block forms %}
{% block estimation %}
<div class="col">
<h3>Jira values</h3>
<p>
{{ format_tracker_task_size() | indent(8) -}}
{% if "authoritative" in forms -%}
{{ render_form(forms["authoritative"], action=head_url_for("main.move_consensus_estimate_to_authoritative", task_name=task.name)) }}
{% if "authoritative" in card_details.forms -%}
{{ render_form(card_details.forms["authoritative"], action=head_url_for("main.move_consensus_estimate_to_authoritative", task_name=task.name)) }}
{%- endif %}
</p>
</div>
<div class="col">
<h3>Estimagus values</h3>
{{ estimation_form_in_accordion(context.own_estimation_exists) }}
</div>
{% endblock %}
{% endblock estimation %}
2 changes: 1 addition & 1 deletion estimage/plugins/crypto/templates/crypto.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@ <h1>Crypto Plugin</h1>

{% block footer %}
{{ super() }}
{{ plugin_form.supporting_js([plugin_form]) | safe }}
{{ plugin_form.supporting_js() | safe }}
{% endblock %}
2 changes: 1 addition & 1 deletion estimage/plugins/jira/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def _perform_work_with_token_encryption(self):
return True

@classmethod
def supporting_js(cls, forms):
def bulk_supporting_js(cls, forms):
template = textwrap.dedent("""
<script type="text/javascript">
function tokenName() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
{% extends "issue_view.html" %}
{% extends ancestor_of_redhat_compliance %}

{% block forms %}
{% block estimation %}
<div class="col">
<h3>Jira values</h3>
<p>
{{ format_tracker_task_size() | indent(8) -}}
{% if "authoritative" in forms -%}
{{ render_form(forms["authoritative"], action=head_url_for("main.move_consensus_estimate_to_authoritative", task_name=task.name)) }}
{% if "authoritative" in card_details.forms -%}
{{ render_form(card_details.forms["authoritative"], action=head_url_for("main.move_consensus_estimate_to_authoritative", task_name=task.name)) }}
{%- endif %}
</p>
</div>
<div class="col">
<h3>Estimagus values</h3>
{{ estimation_form_in_accordion(context.own_estimation_exists) }}
</div>
{% endblock %}
{% endblock estimation %}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{% extends "tree_view_retrospective.html" %}
{% extends ancestor_of_redhat_compliance %}


{% block epics_wip %}
<h4>Committed</h4>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@ <h1>Red Hat Compliance Plugin</h1>

{% block footer %}
{{ super() }}
{{ plugin_form.supporting_js([plugin_form]) | safe }}
{{ plugin_form.supporting_js() | safe }}
{% endblock %}
2 changes: 1 addition & 1 deletion estimage/plugins/redhat_jira/templates/jira.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@ <h1>Jira Plugin</h1>

{% block footer %}
{{ super() }}
{{ plugin_form.supporting_js([plugin_form]) | safe }}
{{ plugin_form.supporting_js() | safe }}
{% endblock %}
Original file line number Diff line number Diff line change
@@ -1,30 +1,23 @@
import pytest

from estimage import plugins, PluginResolver, persistence
import estimage.plugins.redhat_compliance as tm
import estimage.plugins.redhat_jira as tm

from tests.test_card import base_card_load_save, fill_card_instance_with_stuff, assert_cards_are_equal
from tests.test_inidata import temp_filename, cardio_inifile_cls
from tests.test_card import base_card_load_save, fill_card_instance_with_stuff, assert_cards_are_equal, TesCardIO
from tests.test_inidata import temp_filename, inifile_temploc, cardio_inifile_cls


@pytest.fixture(params=("ini",))
def card_io(request, cardio_inifile_cls):
cls = tm.BaseCardWithStatus
choices = dict(
ini=cardio_inifile_cls,
)
generator = TesCardIO(tm.BaseCardWithStatus, ini_base=cardio_inifile_cls)
backend = request.param
appropriate_io = type(
"test_io",
(choices[backend], persistence.LOADERS[cls][backend], persistence.SAVERS[cls][backend]),
dict())
return appropriate_io
return generator(backend)


def plugin_fill(t):
fill_card_instance_with_stuff(t)
def plugin_fill(card):
fill_card_instance_with_stuff(card)

t.status_summary = "Lorem Ipsum and So On"
card.status_summary = "Lorem Ipsum and So On"


def plugin_test(lhs, rhs):
Expand All @@ -37,6 +30,6 @@ def test_card_load_and_save_values(card_io):
resolver = PluginResolver()
resolver.add_known_extendable_classes()
assert "BaseCard" in resolver.class_dict
resolver.resolve_extension(tm)
resolver.resolve_extension(tm, dict(BaseCard="BaseCardWithStatus"))
cls = resolver.class_dict["BaseCard"]
base_card_load_save(card_io, cls, plugin_fill, plugin_test)
101 changes: 101 additions & 0 deletions estimage/plugins/wsjf/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
from ... import persistence
from . import forms


TEMPLATE_OVERRIDES = {
"issue_view.html": "prio_issue_fields.html",
}

EXPORTS = {
"BaseCard": "WSJFCard",
"ProjectiveForms": "ProjectiveForms",
}


class ProjectiveForms:
def add_sections(self):
super().add_sections()
self._add_section(20, name="wsjf", title="Prioritization")

def instantiate_forms(self, app):
super().instantiate_forms(app)
form = forms.WSJFForm()
self.forms["wsjf"] = form

def setup_forms_according_to_context(self, context, card):
super().setup_forms_according_to_context(context, card)
self.forms["wsjf"].business_value.data = card.business_value
self.forms["wsjf"].time_sensitivity.data = card.time_sensitivity
self.forms["wsjf"].risk_and_opportunity.data = card.risk_and_opportunity


class WSJFCard:
business_value: float = 0
risk_and_opportunity: float = 0
time_sensitivity: float = 0

def _get_inherent_cost_of_delay(self):
return (
self.business_value
+ self.risk_and_opportunity
+ self.time_sensitivity)

@property
def cost_of_delay(self):
ret = self._get_inherent_cost_of_delay()
ret += sum(self.inherited_priority.values()) * self.point_cost
return ret

@property
def intrinsic_cost_of_delay(self):
return self.business_value + self.risk_and_opportunity + self.time_sensitivity

@property
def inherited_priority(self):
ret = self._shallow_inherited_priority()
for c in self.get_direct_dependencies():
new_prio = c.inherited_priority
ret.update(new_prio)
return ret

def _shallow_inherited_priority(self):
ret = dict()
for c in self.get_direct_dependencies():
prio = c.intrinsic_cost_of_delay / c.point_cost
if not prio:
continue
ret[c.name] = prio
return ret

@property
def wsjf_score(self):
if self.cost_of_delay == 0:
return 0
if self.point_cost == 0:
msg = f"Point Cost aka size of '{self.name}' is unknown, as is its priority."
raise ValueError(msg)
return self.cost_of_delay / self.point_cost

def pass_data_to_saver(self, saver):
super().pass_data_to_saver(saver)
saver.save_wsjf_fields(self)

def load_data_by_loader(self, loader):
super().load_data_by_loader(loader)
loader.load_wsjf_fields(self)


@persistence.loader_of(WSJFCard, "ini")
class IniCardStateLoader:
def load_wsjf_fields(self, card):
card.business_value = float(self._get_our(card, "wsjf_business_value", 0))
card.risk_and_opportunity = float(self._get_our(card, "wsjf_risk_and_opportunity", 0))
card.time_sensitivity = float(self._get_our(card, "time_sensitivity", 0))


@persistence.saver_of(WSJFCard, "ini")
class IniCardStateSaver:
def save_wsjf_fields(self, card):
self._store_our(card, "wsjf_business_value", str(card.business_value))
self._store_our(card, "wsjf_risk_and_opportunity", str(card.risk_and_opportunity))
self._store_our(card, "time_sensitivity", str(card.time_sensitivity))
Loading

0 comments on commit 4425a44

Please sign in to comment.