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

Easy estimation #35

Merged
merged 18 commits into from
Jul 1, 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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ Following environmental variables are recognized and used:
- `LOGIN_PROVIDER_NAME`: one of `autologin` or `google`. Autologin logs in any user, google allows Google login when set up properly.
- `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`: When using google login, you need to obtain those from Google to have the Google login working.
- `PLUGINS`: An ordered, comma-separated list of plugin names to load. Plugins are Python packages located in the `plugins` directory of Estimagus.
- `CACHE_...`: Environment variables used by [flask-caching](https://flask-caching.readthedocs.io/en/latest/#configuring-flask-caching), e.g. `CACHE_TYPE=SimpleCache`.


## Assumptions
Expand Down
4 changes: 3 additions & 1 deletion estimage/entities/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,5 +81,7 @@ def load(self, io_cls):
for name in events_task_names:
self._events[name] = loader.load_events_of(name)

def erase(self):
def erase(self, io_cls):
self._events.clear()
with io_cls.get_saver() as saver:
saver.erase()
8 changes: 4 additions & 4 deletions estimage/inidata.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ def get_canonical_status(name_or_index):
class IniStorage:
CONFIG_FILENAME = ""

def __init__(self, * args, ** kwargs):
super().__init__(* args, ** kwargs)
def __init__(self, ** kwargs):
super().__init__(** kwargs)

@staticmethod
def _pack_list(string_list: typing.Container[str]):
Expand Down Expand Up @@ -77,8 +77,8 @@ def callback(d):
class IniSaverBase(IniStorage):
WHAT_IS_THIS = "entity"

def __init__(self, * args, ** kwargs):
super().__init__(* args, ** kwargs)
def __init__(self, ** kwargs):
super().__init__(** kwargs)
self._data_to_save = collections.defaultdict(dict)

def _write_items_attribute(self, item_id, attribute_id, value):
Expand Down
3 changes: 3 additions & 0 deletions estimage/persistence/event/memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,6 @@ def load_event_names(self):

def load_events_of(self, name):
return self._memory[name]

def erase(self):
self._memory.clear()
3 changes: 1 addition & 2 deletions estimage/persistence/pollster/memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,4 @@ def load_points(self, ns, name):


class MemoryPollsterIO(MemoryPollsterLoader, MemoryPollsterSaver):
def __init__(self, * args, ** kwargs):
super().__init__(* args, ** kwargs)
pass
147 changes: 105 additions & 42 deletions estimage/plugins/crypto/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@

TEMPLATE_OVERRIDES = {
"tree_view_retrospective.html": "crypto-retrotree.html",
"issue_view.html": "crypto-issue_view.html",
}


Expand All @@ -39,6 +40,7 @@ class InputSpec(redhat_jira.InputSpec):
def from_form_and_app(cls, form, app) -> "InputSpec":
ret = super().from_form_and_app(form, app)
ret.cutoff_date = app.get_config_option("RETROSPECTIVE_PERIOD")[0]
ret.import_method = form.import_method.data
return ret

def set_cutoff_date(self, input_form):
Expand All @@ -47,47 +49,14 @@ def set_cutoff_date(self, input_form):
def set_queries(self, input_form):
sprint = "openSprints()"
query_tpl = "filter = 12350823 AND Sprint in {sprint} AND issuetype in (task, bug, Story)"
# query_tpl = "key in (CRYPTO-7890, CRYPTO-9482, CRYPTO-6349) AND issuetype in (task, bug, Story)"
self.retrospective_query = query_tpl.format(sprint=sprint)
if input_form.project_next.data:
sprint = "futureSprints()"
self.projective_query = query_tpl.format(sprint=sprint)


class Importer(redhat_jira.Importer):
def _get_owner_epic_of(self, assignee, committed):
if not assignee:
assignee = "nobody"
name = f"{PROJECT_NAME}-{assignee}"
if committed:
name += "-C"
if self._import_context == "proj":
name += "-future"

epic = self._cards_by_id.get(name)

if not epic:
epic = self.item_class(name)
epic.assignee = assignee
epic.title = f"Issues of {assignee}"
if self._import_context == "proj":
epic.title = f"Future issues of {assignee}"
epic.tier = 1
epic.status = "in_progress"
if committed:
epic.title = "Committed " + epic.title
epic.tier = 0
self._cards_by_id[name] = epic
return epic

def put_cards_under_artificial_epics(self, tasks):
epic_names = set()
for task_name in tasks:
task = self._cards_by_id[task_name]
epic = self._get_owner_epic_of(task.assignee, "label:Committed" in task.tags)
epic.add_element(task)
epic_names.add(epic.name)
return epic_names

class CryptoImporter(redhat_jira.Importer):
def _parent_has_only_unestimated_children(self, pname):
children = self._cards_by_id[pname].children
rolling_sum = 0
Expand All @@ -108,7 +77,7 @@ def _propagate_estimates_of_estimated_task_to_unestimated_subtasks(self, pname):
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:
if not c.children and c.parent and c.name.startswith(PROJECT_NAME):
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:
Expand All @@ -117,11 +86,6 @@ def distribute_subtasks_points_to_tasks(self):
def _query_children_to_get_children(self, parent_name, query_order):
return False

def _export_jira_tree_to_cards(self, root_results):
new_cards = super()._export_jira_tree_to_cards(root_results)
new_epic_names = self.put_cards_under_artificial_epics(root_results)
return new_cards.union(new_epic_names)

def import_data(self):
super().import_data()
self.distribute_subtasks_points_to_tasks()
Expand Down Expand Up @@ -162,8 +126,107 @@ def _status_to_state(cls, item, jira_string):
return super()._status_to_state(item, jira_string)


class ArtificialCryptoImporter(CryptoImporter):
def _get_owner_epic_of(self, assignee, committed):
if not assignee:
assignee = "nobody"
name = f"{PROJECT_NAME}-{assignee}"
if committed:
name += "-C"
if self._import_context == "proj":
name += "-future"

epic = self._cards_by_id.get(name)

if not epic:
epic = self.item_class(name)
epic.assignee = assignee
epic.title = f"Issues of {assignee}"
if self._import_context == "proj":
epic.title = f"Future issues of {assignee}"
epic.tier = 1
epic.status = "in_progress"
if committed:
epic.title = "Committed " + epic.title
epic.tier = 0
self._cards_by_id[name] = epic
return epic

def put_cards_under_artificial_epics(self, tasks):
epic_names = set()
for task_name in tasks:
task = self._cards_by_id[task_name]
epic = self._get_owner_epic_of(task.assignee, "label:Committed" in task.tags)
epic.add_element(task)
epic_names.add(epic.name)
return epic_names

def _export_jira_tree_to_cards(self, root_results):
new_cards = super()._export_jira_tree_to_cards(root_results)
new_epic_names = self.put_cards_under_artificial_epics(root_results)
return new_cards.union(new_epic_names)


class FeatureCryptoImporter(CryptoImporter):
EPIC_LINK = "customfield_12311140"
PARENT_LINK = "customfield_12313140"

def _find_parent_epic_of(self, issue):
if epic := self._all_issues_by_name.get(epic_name):
return epic
epic = self.find_card(epic_name)
self._all_issues_by_name[epic_name] = epic
return epic

def _find_parent_of(self, epic):
return epic

def _find_preferably_grandparent_of(self, task_name):
task = self._all_issues_by_name[task_name]
expand = "changelog,renderedFields"
epic_name = self._get_contents_of_field(task, self.EPIC_LINK)
if not epic_name:
return
epic = self.just_get_or_find_and_store(epic_name, expand)
ancestor_name = self._get_contents_of_field(epic, self.PARENT_LINK)
if not ancestor_name:
return epic
granparent = self.just_get_or_find_and_store(ancestor_name, expand)
return granparent

def _expand_primary_query_results(self, result_names, order=1):
self.root_ancestors = set()
super()._expand_primary_query_results(result_names, order)
return self.root_ancestors

def _add_child_to_parent(self, parent_name, child_name):
if parent_name not in self._parent_name_to_children_names:
self._parent_name_to_children_names[parent_name] = [child_name]
else:
self._parent_name_to_children_names[parent_name].append(child_name)

def _expand_primary_query_result(self, result_name, order=1):
self._find_children_by_examining_parent(result_name)
if granparent := self._find_preferably_grandparent_of(result_name):
ancestor_name = f"{self._import_context}-{granparent.key}"
self._all_issues_by_name[ancestor_name] = granparent
self.root_ancestors.add(ancestor_name)
self._add_child_to_parent(ancestor_name, result_name)

def merge_jira_item_without_children(self, item):

result = super().merge_jira_item_without_children(item)
altname = f"{self._import_context}-{result.name}"
if altname in self._all_issues_by_name:
result.name = altname

return result

def do_stuff(spec, ios_by_target):
importer = Importer(spec)
if spec.import_method == "product-centric":
importer = FeatureCryptoImporter(spec)
else:
importer = ArtificialCryptoImporter(spec)
importer.import_data()
importer.save(ios_by_target)
return importer.get_collected_stats()
Expand Down
3 changes: 3 additions & 0 deletions estimage/plugins/crypto/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@

class CryptoFormEnd(BaseForm):
project_next = wtforms.BooleanField('Plan for the Next Iteration')
import_method = wtforms.RadioField("Import Method", choices=(
("product-centric", "Product-Centric"),
("person-centric", "Person-Centric")))
submit = wtforms.SubmitField("Import Data")


Expand Down
1 change: 1 addition & 0 deletions estimage/plugins/crypto/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@ def sync():
else:
next_starts_soon = redhat_compliance.days_to_next_epoch(datetime.datetime.today()) < 30
form.project_next.data = next_starts_soon
form.import_method.data = "product-centric"
return web_utils.render_template(
'crypto.html', title='Red Hat Crypto Plugin', plugin_form=form, )
17 changes: 17 additions & 0 deletions estimage/plugins/crypto/templates/crypto-issue_view.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{% extends "issue_view.html" %}

{% block forms %}
<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)) }}
{%- endif %}
</p>
</div>
<div class="col">
<h3>Estimagus values</h3>
{{ estimation_form_in_accordion(context.own_estimation_exists) }}
</div>
{% endblock %}
1 change: 1 addition & 0 deletions estimage/plugins/demo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ def get_date_of_dday(self):
def start(cards, loader, start_date):
date = start_date - datetime.timedelta(days=20)
mgr = data.EventManager()
mgr.erase(simpledata.IOs["events"]["ini"])
for t in cards:
evt = data.Event(t.name, "state", date)
evt.value_before = "irrelevant"
Expand Down
9 changes: 6 additions & 3 deletions estimage/plugins/jira/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,9 +206,11 @@ def _expand_primary_query_results(self, result_names, order=1):
new_results = self._expand_primary_query_result(name, order)
else:
new_results = self._parent_name_to_children_names[name]
return result_names

def _expand_primary_query_to_tree(self, names_obtained):
self._expand_primary_query_results(names_obtained, 1)
tree_results = self._expand_primary_query_results(names_obtained, 1)
return tree_results

def _get_or_create_card(self, name):
if name in self._cards_by_id:
Expand All @@ -232,8 +234,8 @@ def export_issue_tree_to_cards(self, root_names: typing.Iterable[str]) -> dict[s

def _get_and_record_jira_tree(self, query):
core_results = self._perform_and_process_query(query)
self._expand_primary_query_to_tree(core_results)
return core_results
tree_results = self._expand_primary_query_to_tree(core_results)
return tree_results

def _export_jira_tree_to_cards(self, root_results):
new_cards = self.export_issue_tree_to_cards(root_results)
Expand Down Expand Up @@ -325,6 +327,7 @@ def save(self, ios_by_target):
save_exported_jira_tasks(self._cards_by_id, self._projective_cards, proj_card_io_class)

storer = data.EventManager()
storer.erase(ios_by_target["events"])
for e in self._all_events:
storer.add_event(e)
storer.save(ios_by_target["events"])
Expand Down
4 changes: 3 additions & 1 deletion estimage/plugins/jira/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@ class JiraForm(JiraFormStart, EncryptedTokenForm, JiraFormEnd):

class AuthoritativeForm(EncryptedTokenForm):
token = wtforms.PasswordField('Jira Token')
def __init__(self, ** kwargs):
super().__init__(** kwargs)
self.submit.label.text = "Save Estimate to Jira"

def clear_to_go(self):
self.enable_submit_button()
Expand All @@ -131,7 +134,6 @@ def __iter__(self):
self.point_cost,
self.token,
self.store_token,
self.i_kid_you_not,
self.encrypted_token,
self.encrypted_meant_for_storage,
self.submit,
Expand Down
11 changes: 9 additions & 2 deletions estimage/plugins/jira/importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,20 @@ def __init__(self, spec):
def report(self, msg):
print(msg)

def find_card(self, name: str):
card = self.jira.issue(name)
def find_card(self, name: str, expand=""):
card = self.jira.issue(name, expand=expand)
if not card:
msg = f"{card} not found"
raise ValueError(msg)
return card

def just_get_or_find_and_store(self, name: str, expand=""):
if issue := self._all_issues_by_name.get(name):
return issue
issue = self.find_card(name, expand)
self._all_issues_by_name[name] = issue
return issue

@classmethod
def status_to_state(cls, item, jira_string=""):
if not jira_string:
Expand Down
1 change: 1 addition & 0 deletions estimage/plugins/jira/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ def try_callback_and_produce_error_msg(callback, * args):
try:
stats = callback(* args)
flask.flash(jira.stats_to_summary(stats))
web_utils.updated_cards_and_events_from_tracker()
except exceptions.JIRAError as exc:
if 500 <= exc.status_code < 600:
error_msg = f"Error {exc.status_code} when interacting with Jira, accessing URL {exc.url}"
Expand Down
Loading
Loading