matejak/estimagus

View on GitHub
estimage/simpledata.py

Summary

Maintainability
A
0 mins
Test Coverage
A
92%
import typing
import collections
import pathlib
import datetime
import dataclasses
import dateutil.relativedelta

import flask

from . import data
from . import inidata
from .persistence import card, pollster, event
from .persistence.card import ini


class IniInDirMixin:
    @classmethod
    @property
    def CONFIG_FILENAME(cls):
        try:
            if "head" in flask.current_app.config:
                datadir = flask.current_app.get_config_option("DATA_DIR")
            else:
                datadir = pathlib.Path(flask.current_app.config["DATA_DIR"])
        except RuntimeError:
            datadir = pathlib.Path(".")
        ret = datadir / cls.CONFIG_BASENAME
        return ret


IOs = collections.defaultdict(dict)


class RetroCardIO(IniInDirMixin):
    CONFIG_BASENAME = "retrospective.ini"
    WHAT_IS_THIS = "retrospective card"


class ProjCardIO(IniInDirMixin):
    CONFIG_BASENAME = "projective.ini"
    WHAT_IS_THIS = "projective card"


class EventsIO(IniInDirMixin, event.ini.IniEventsIO):
    CONFIG_BASENAME = "events.ini"
    WHAT_IS_THIS = "events manager"


IOs["retro"]["ini"] = RetroCardIO
IOs["proj"]["ini"] = ProjCardIO
IOs["events"]["ini"] = EventsIO


class UserPollsterBase(data.Pollster):
    def __init__(self, username, * args, ** kwargs):
        class pollster_io_class(IniInDirMixin, pollster.ini.IniPollsterIO):
            CONFIG_BASENAME = self.CONFIG_BASENAME
            WHAT_IS_THIS = "user pollster"

        super().__init__(* args, io_cls=pollster_io_class, ** kwargs)
        self.username = username
        self.set_namespace(f"user-{username}-")


class UserPollster(UserPollsterBase):
    CONFIG_BASENAME = "pollsters.ini"


class AuthoritativePollsterBase(data.Pollster):
    def __init__(self, * args, ** kwargs):
        class pollster_io_class(IniInDirMixin, pollster.ini.IniPollsterIO):
            CONFIG_BASENAME = self.CONFIG_BASENAME
            WHAT_IS_THIS = "authoritative pollster"

        super().__init__(* args, io_cls=pollster_io_class, ** kwargs)
        self.set_namespace("***-")


class AuthoritativePollster(AuthoritativePollsterBase):
    CONFIG_BASENAME = "pollsters.ini"


@dataclasses.dataclass
class Context:
    task_name: str
    own_estimation_exists: bool = False
    global_estimation_exists: bool = False

    def __init__(self, of_task: data.BaseCard):
        self.task_name = of_task.name
        self.task_point_cost = of_task.point_cost
        self._own_estimate = None
        self._global_estimate = None
        self._authoritative_estimate = of_task.point_cost

    def process_own_pollster(self, pollster: data.Pollster):
        self.own_estimation_exists = False
        self._own_estimate = None
        if pollster.knows_points(self.task_name):
            points = pollster.ask_points(self.task_name)
            self._own_estimate = data.Estimate.from_input(points)
            self.own_estimation_exists = True

    def process_global_pollster(self, pollster: data.Pollster):
        self.global_estimation_exists = False
        self._global_estimate = None
        if pollster.knows_points(self.task_name):
            self.global_estimation_exists = False
            points = pollster.ask_points(self.task_name)
            self._global_estimate = data.Estimate.from_input(points)
            self.global_estimation_exists = True

    @property
    def estimation(self) -> data.Estimate:
        if self.estimation_source == "none":
            msg = "No estimation exists"
            raise ValueError(msg)
        elif self.estimation_source == "own":
            return self.own_estimation
        elif self.estimation_source == "global":
            return self.global_estimation

    @property
    def own_estimation(self) -> data.Estimate:
        if not self.own_estimation_exists:
            msg = "Own estimation doesn't exist"
            raise ValueError(msg)
        return self._own_estimate

    @property
    def global_estimation(self) -> data.Estimate:
        if not self.global_estimation_exists:
            msg = "Global estimation doesn't exist"
            raise ValueError(msg)
        return self._global_estimate

    @property
    def estimate_status(self) -> str:
        ret = "absent"
        if self.own_estimation_exists != self.global_estimation_exists:
            ret = "single"
        elif self.own_estimation_exists and self.global_estimation_exists:
            ret = self._get_status_of_existing_estimation()
        return ret

    def _get_status_of_existing_estimation(self):
        if self._own_estimate == self._global_estimate:
            ret = "duplicate"
        else:
            ret = "contradictory"
        return ret

    @property
    def authoritative_record_exists(self) -> bool:
        return self._authoritative_estimate > 0

    @property
    def authoritative_record_consistent(self) -> bool:
        return abs(self._authoritative_estimate - self.estimation.expected) < 0.5

    @property
    def estimation_source(self) -> str:
        ret = "none"
        if self.global_estimation_exists:
            ret = "global"
        if self.own_estimation_exists:
            ret = "own"
        return ret


class AppData(inidata.IniAppdata):
    CONFIG_BASENAME = "appdata.ini"

    def _get_default_retrospective_period(self):
        today = datetime.datetime.today()
        today_first_of_month = datetime.datetime(today.year, today.month, 1)
        beginning = today_first_of_month - dateutil.relativedelta.relativedelta(months=1)
        end = today_first_of_month + dateutil.relativedelta.relativedelta(months=2, days=-1)
        return (beginning, end)

    def _get_default_projective_quarter(self):
        return ""

    def _get_default_retrospective_quarter(self):
        return ""


def get_model(cards_tree_without_duplicates):
    model = data.EstiModel()
    if not cards_tree_without_duplicates:
        return model
    cls = cards_tree_without_duplicates[0].__class__
    main_composition = cls.to_tree(cards_tree_without_duplicates, statuses=None)
    model.use_composition(main_composition)
    return model


def _create_distance_task_tuples(
        relevant_tasks, reference_estimate,
        distance_threshold: float, rank_threshold: float):
    distance_task_tuples = list()
    for t in relevant_tasks:
        distance = abs(t.nominal_point_estimate.expected - reference_estimate.expected)
        rank = t.nominal_point_estimate.rank_distance(reference_estimate)
        if (distance <= distance_threshold or rank <= rank_threshold):
            distance_task_tuples.append((distance, t))
    return distance_task_tuples


def order_nearby_tasks(
        reference_task: data.TaskModel, all_tasks: typing.Iterable[data.TaskModel],
        distance_threshold: float, rank_threshold: float) -> typing.List[data.TaskModel]:
    reference_estimate = reference_task.nominal_point_estimate
    relevant_tasks = [t for t in all_tasks if t.name != reference_task.name]
    distance_task_tuples = _create_distance_task_tuples(
        relevant_tasks, reference_estimate, distance_threshold, rank_threshold)
    sorted_distance_tasks = sorted(distance_task_tuples, key=lambda x: x[0])
    return [dt[1] for dt in sorted_distance_tasks]