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

Add support for advanced prioritization #38

Merged
merged 14 commits into from
Sep 23, 2024
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
Loading