Exemple #1
0
 def _sum_cell(self, show_sum: bool, rows: List[List[str]]):
     if not show_sum:
         return [""]
     sum_ = timedelta()
     for row in rows:
         sum_ += sum(row[1:], timedelta())
     return ["Sum: %s" % self._timedelta_conversion_function(sum_)]
Exemple #2
0
    def to_date(self, str_: str) -> Union[date, None]:
        """Parses a string to a datetime.date.

        :param str_: a string on the form on the form YYYY-MM-DD, 'yesterday',
                     'monday' or 'Monday'
        :return: a datetime.date() object for the supplied date, or None if
                 the date could not be parsed.
        """
        if self._is_date(str_):
            return datetime.strptime(str_, "%Y-%m-%d").date()
        elif str_ == "yesterday":
            return self.today - timedelta(days=1)

        try:
            index = "monday tuesday wednesday thursday friday".split().index(
                str_.lower())
            date_ = self.today + timedelta(days=-self.today.weekday() + index)
            if self.contains_next:
                date_ += timedelta(weeks=1)
                self.next_shall_be_removed = True
            if self.contains_last:
                date_ -= timedelta(weeks=1)
                self.last_shall_be_removed = True
            return date_

        except ValueError:
            pass
        return None
Exemple #3
0
def test_minute():
    assert TimeParser.as_timedelta("34m") == timedelta(minutes=34)
    assert TimeParser.as_timedelta("3m") == timedelta(minutes=3)
    assert TimeParser.as_timedelta("34 m") == timedelta(minutes=34)
    assert TimeParser.as_timedelta("75m") == timedelta(hours=1, minutes=15)

    with pytest.raises(TimeParserError):
        TimeParser.as_timedelta("m")
Exemple #4
0
 def working_time(self) -> timedelta:
     if self.came and self.left:
         lunch = self.lunch if self.lunch else timedelta()
         seconds_at_work = self.to_seconds(self.left) - self.to_seconds(self.came)
         working_time_excluding_lunch = timedelta(seconds=seconds_at_work)
         return working_time_excluding_lunch - lunch
     else:
         return timedelta()
Exemple #5
0
 def _mondays(self):
     first_monday = self._closest_monday_to_first_day_of_target_month()
     mondays = [
         first_monday + timedelta(days=days)
         for days in range(0, 38, 7)  # Max 31+7 days first-last Mon
         if (first_monday +
             timedelta(days=days)).month in (self.month_index,
                                             self.last_month_index)
     ]
     return mondays
Exemple #6
0
 def show(self, calendar):
     total_flex = timedelta()
     date_ = self.from_
     while True:
         if calendar.flex(date_):
             total_flex += calendar.flex(date_)
         if date_ == self.to:
             break
         date_ += timedelta(days=1)
     return f"Total flex from {self.from_} to {self.to}: " f"{total_flex}"
Exemple #7
0
    def default_project_time(self, date_):
        project_time_sum = timedelta()
        for project_name in self.days[date_].projects:
            project = [
                project for project in self.projects
                if project.name == project_name
            ][0]
            if project.work:
                project_time_sum += self.days[date_].projects[project_name]

        default_project_time = self.days[date_].working_time - project_time_sum

        # Set to 0 hours if less than 0 hours
        return max(default_project_time, timedelta())
Exemple #8
0
class Command:
    TIMEDELTA = timedelta(weeks=1)
    WRITE_TO_DISK = True

    def __init__(self, calendar: Calendar, date_: date, args: Union[list,
                                                                    str]):
        self.calendar = calendar
        self.date = date_
        self.args = args

        # TODO: use the new argument splitter method instead
        if isinstance(self.args, str):
            self.args = self.args.split()

        if "last" in self.args:
            self.date -= self.TIMEDELTA
        elif "next" in self.args:
            self.date += self.TIMEDELTA
        self.options = self._parse_options()

    def _parse_options(self) -> Dict[str, str]:
        options = {}
        new_args = []
        assert isinstance(self.args, list)
        for arg in self.args:
            if arg.startswith("--"):
                name = arg.split("=")[0]
                value = arg.split("=")[1] if "=" in arg else True
                options[name] = value

                if name not in self.valid_options():
                    raise UnexpectedOptionError(name)
            else:
                new_args.append(arg)
        self.args = new_args
        return options

    @classmethod
    def can_handle(cls, args) -> bool:
        args = [arg for arg in args if not arg.startswith("--")]
        args = [arg for arg in args if arg not in ("last", "next")]
        return cls._can_handle(args)

    @classmethod
    def _can_handle(cls, args: List[str]) -> bool:
        raise NotImplementedError

    def valid_options(self) -> List[str]:
        return []

    def execute(
        self, created_at: datetime.datetime = datetime.datetime.now()
    ) -> Tuple[Calendar, View]:
        return self.new_calendar(created_at), self.view()

    def view(self) -> View:
        return ConsoleWeekView(self.date)

    def new_calendar(self, created_at: datetime.datetime) -> Calendar:
        return self.calendar
Exemple #9
0
 def test_serialize(self):
     c = Calendar(target_hours_per_day=timedelta(hours=8.00))
     c = c.add(Day("came 9 left 18", today))
     data = c.dump()
     c2 = Calendar.load(data)
     s = ConsoleDayShower(c2).show_days(today, 1)
     assert re.search("Flex *01:00", s)
Exemple #10
0
 def as_timedelta(cls, str_: str) -> timedelta:
     if "m" in str_:
         try:
             minutes = int(str_.split("m")[0])
             hours = minutes // 60
             minutes = minutes % 60
             return timedelta(hours=hours, minutes=minutes)
         except ValueError:
             pass
     try:
         hours, minutes = cls._to_hours_and_minutes(str_)
         return timedelta(hours=hours, minutes=minutes)
     except TimeParserError:
         raise TimeParserError(
             f'Error: Could not parse "{str_}" as time interval. Time intervals must be on the '
             f"form H, HH:MM, H:MM, HHMM, HMM, M m, MM m, M min or MM min")
Exemple #11
0
 def test_show_last_week_html(self, mock_browser):
     s, _ = main("show last week html")
     last_monday = str(
         timereporter.__main__.today() +
         timedelta(days=-timereporter.__main__.today().weekday(), weeks=-1))
     with open(mock_browser.url) as f:
         s = f.read()
         assert last_monday in s
Exemple #12
0
 def _flex_row(self, dates) -> List[str]:
     flex_times = [self.calendar.flex(date_) for date_ in dates]
     flex_times = list(
         map(
             lambda x: timedelta()
             if x is None else x * self.FLEX_MULTIPLIER,
             flex_times,
         ))
     if not self.SHOW_EARNED_FLEX:
         flex_times = list(
             map(
                 lambda x: timedelta()
                 if x is None or x <= timedelta() else x,
                 flex_times,
             ))
     flex_times = [
         self._timedelta_conversion_function(flex) for flex in flex_times
     ]
     return ["Flex"] + flex_times
 def test_added(self):
     c = Calendar()
     wednesday = today + timedelta(days=-today.weekday() + 2)
     c = c.add(Day("came 8 left 18 lunch 45m", wednesday))
     s = ConsoleDayShower(c).show_days(today, 5)
     assert "08:00" in s
     assert "18:00" in s
     assert "0:45" in s
     assert "Came" in s
     assert "Left" in s
     assert "Lunch" in s
Exemple #14
0
class ShowMonthCommand(ShowWeekendCommand):
    # Close enough to a month
    TIMEDELTA = timedelta(days=30)

    @classmethod
    def _can_handle(cls, args) -> bool:
        return args == "show month".split()

    def view(self):
        return ConsoleMonthView(
            self.date,
            ConsoleMonthView.MONTHS[self.date.month - 1],
            "--show-weekend" in self.options,
        )
Exemple #15
0
    def flex(self, date_: date) -> Union[timedelta, None]:
        """Calculates the flex time earned or spent on a certain day.

        The flex time is equal to the working time plus the no-work project time
        minus the target hours per day.
        """
        working_time = self.days[date_].working_time
        no_work_projects_names = [
            project.name for project in self.projects if not project.work
        ]
        no_work_project_time = sum(
            [
                self.days[date_].projects[project_name]
                for project_name in no_work_projects_names
            ],
            timedelta(),
        )
        if working_time:
            return working_time - self.target_hours_per_day + no_work_project_time
        else:
            return None
Exemple #16
0
    def show_days(
        self,
        first_date: date,
        day_count,
    ):
        """Shows a number of days from the calendar in table format.

        :param first_date: the first day to show.
        :param day_count: the number of days to show, including the first day.
        :param table_format: the table format, see
        https://bitbucket.org/astanin/python-tabulate for the alternatives.
        """
        dates = [first_date + timedelta(days=i) for i in range(day_count)]

        weekdays = "Monday Tuesday Wednesday Thursday Friday Saturday " "Sunday".split(
        )
        weekdays_to_show = [weekdays[date_.weekday() % 7] for date_ in dates]

        came_times = [self.calendar.days[date_].came for date_ in dates]
        leave_times = [self.calendar.days[date_].left for date_ in dates]
        lunch_times = [self.calendar.days[date_].lunch for date_ in dates]

        rows = self._rows(dates)

        table = tabulate(
            [
                self._sum_cell(self.SHOW_SUM, rows) + dates,
                [""] + weekdays_to_show,
                ["Came"] + came_times,
                ["Left"] + leave_times,
                ["Lunch"] + lunch_times,
                *rows,
            ],
            tablefmt=self.TABLE_FORMAT,
        )

        return html.unescape(table)
Exemple #17
0
 def _closest_monday(self):
     closest_monday = self.date + timedelta(days=-self.date.weekday())
     return closest_monday
Exemple #18
0
 def test_day_fix_wrong_lunch_format(self):
     d = Day("lunch 01:00")
     assert d.lunch == timedelta(minutes=60)
Exemple #19
0
 def _to_timedelta(cls, t: Union[time, timedelta, None]) -> Union[timedelta, None]:
     if t is None:
         return None
     if isinstance(t, timedelta):
         return t
     return timedelta(seconds=t.hour * 3600 + t.minute * 60)
Exemple #20
0
 def show(self, calendar):
     closest_monday = self.date + timedelta(days=-self.date.weekday())
     return ConsoleDayShower(calendar).show_days(closest_monday,
                                                 self.day_count)
Exemple #21
0
 def test_basic(self):
     c = Calendar(target_hours_per_day=timedelta(hours=8.00))
     c = c.add(Day("came 9 left 18", today))
     s = ConsoleDayShower(c).show_days(today, 1)
     assert re.search("Flex *01:00", s)
 def test_positive(self):
     assert str(timedelta(seconds=60)) == "00:01"
 def test_positive_hours(self):
     assert str(timedelta(seconds=3660)) == "01:01"
 def test_convert_from_timedelta(self):
     td = timedelta(seconds=3600 + 3600 * 0.25)
     tdd = timedeltaDecimal.from_timedelta(td)
     assert str(tdd) == "1,25"
 def test_convert_from_timedelta_negative(self):
     td = timedelta(seconds=-3600 - 3600 * 0.25)
     tdd = timedeltaDecimal.from_timedelta(td)
     assert str(tdd) == "-1,25"
 def test_negative_hours(self):
     assert str(timedelta(seconds=-3660)) == "-01:01"
 def test_negative(self):
     assert str(timedelta(seconds=-60)) == "-00:01"
Exemple #28
0
 def test_overwrite_lunch(self):
     c = Calendar()
     c = c.add(Day("lunch 45m", today))
     c = c.add(Day("lunch 35m", today))
     assert c.days[today].lunch == timedelta(minutes=35)
Exemple #29
0
 def _closest_monday_to_first_day_of_target_month(self):
     first_day_of_month = self._first_day_of_month(
         self._date_in_correct_year())
     closest_monday = first_day_of_month + timedelta(
         days=-first_day_of_month.weekday())
     return closest_monday
Exemple #30
0
class Calendar:
    """Contains a dictionary mapping Day objects to dates, and handles
    visualization of those days
    """

    DEFAULT_TARGET_HOURS_PER_DAY = timedelta(hours=7.75)
    DEFAULT_PROJECT_NAME = "EPG Program"

    def __init__(
        self,
        raw_days=None,
        projects=None,
        redo_list=None,
        target_hours_per_day=DEFAULT_TARGET_HOURS_PER_DAY,
        default_project_name=DEFAULT_PROJECT_NAME,
        aliases=None,
    ):
        self._raw_days = [] if raw_days is None else raw_days
        self.redo_list = [] if redo_list is None else redo_list
        self.projects = [] if projects is None else projects
        self.target_hours_per_day = target_hours_per_day
        self.default_project_name = default_project_name
        self._aliases = aliases or {}  # type: Dict[str, str]
        self._days = None

    @property
    def days(self) -> Dict[date, Day]:
        """Retrieve all Day objects in the calendar.

        :return: A dictionary (date, Day) with all the Days in the calendar.
        """
        if not self._days:
            self._days = defaultdict(Day)
            for day in self._raw_days:
                self._days[day.date] += day
        return self._days

    def add(self, day: Day):
        """Add a day to the calendar."""
        new_days = self._raw_days + [day]
        return Calendar(
            raw_days=new_days,
            redo_list=[],
            projects=self.projects[:],
            target_hours_per_day=self.target_hours_per_day,
            default_project_name=self.default_project_name,
            aliases=self.aliases.copy(),
        )

    def add_project(self, project_name: str, work=True):
        """Adds a project with the specified project name to the calendar"""
        return Calendar(
            raw_days=self._raw_days[:],
            redo_list=[],
            projects=self.projects + [Project(project_name, work)],
            target_hours_per_day=self.target_hours_per_day,
            default_project_name=self.default_project_name,
            aliases=self.aliases.copy(),
        )

    def undo(self) -> Tuple["Calendar", Optional[date]]:
        """Undo the last edits with the same created_at to the calendar."""
        if not self._raw_days:
            return self, None
        created_at = self._raw_days[-1].created_at
        raw_days = self._raw_days[:]
        days_to_undo = []
        new_redo_list = self.redo_list
        while raw_days and raw_days[-1].created_at == created_at:
            days_to_undo.append(raw_days.pop())
        new_redo_list.append(days_to_undo)
        return (
            Calendar(
                raw_days=raw_days,
                redo_list=new_redo_list,
                projects=self.projects[:],
                target_hours_per_day=self.target_hours_per_day,
                default_project_name=self.default_project_name,
                aliases=self.aliases.copy(),
            ),
            self._raw_days[-1].date,
        )

    def redo(self) -> Tuple["Calendar", Optional[date]]:
        """Redo the last undo made to the calendar.

        Returns None instead of a date if there is nothing to undo."""
        new_days = self._raw_days
        if self.redo_list:
            new_days.extend(self.redo_list[-1])
        return (
            Calendar(
                raw_days=new_days,
                redo_list=self.redo_list[:-1],
                projects=self.projects[:],
                target_hours_per_day=self.target_hours_per_day,
                default_project_name=self.default_project_name,
                aliases=self.aliases.copy(),
            ),
            self.redo_list[-1][-1].date if self.redo_list else None,
        )

    @property
    def aliases(self):
        return self._aliases

    def add_alias(self, short: str, full: str):
        """Add a new alias"""
        aliases = self.aliases.copy()
        aliases[short] = full
        return Calendar(
            raw_days=self._raw_days[:],
            redo_list=[],
            projects=self.projects,
            target_hours_per_day=self.target_hours_per_day,
            default_project_name=self.default_project_name,
            aliases=aliases,
        )

    def remove_alias(self, short: str):
        """Add a new alias"""
        aliases = self.aliases.copy()
        del aliases[short]
        return Calendar(
            raw_days=self._raw_days[:],
            redo_list=[],
            projects=self.projects,
            target_hours_per_day=self.target_hours_per_day,
            default_project_name=self.default_project_name,
            aliases=aliases,
        )

    def default_project_time(self, date_):
        project_time_sum = timedelta()
        for project_name in self.days[date_].projects:
            project = [
                project for project in self.projects
                if project.name == project_name
            ][0]
            if project.work:
                project_time_sum += self.days[date_].projects[project_name]

        default_project_time = self.days[date_].working_time - project_time_sum

        # Set to 0 hours if less than 0 hours
        return max(default_project_time, timedelta())

    def flex(self, date_: date) -> Union[timedelta, None]:
        """Calculates the flex time earned or spent on a certain day.

        The flex time is equal to the working time plus the no-work project time
        minus the target hours per day.
        """
        working_time = self.days[date_].working_time
        no_work_projects_names = [
            project.name for project in self.projects if not project.work
        ]
        no_work_project_time = sum(
            [
                self.days[date_].projects[project_name]
                for project_name in no_work_projects_names
            ],
            timedelta(),
        )
        if working_time:
            return working_time - self.target_hours_per_day + no_work_project_time
        else:
            return None

    def dump(self):
        return Camel([camelRegistry]).dump(self)

    @classmethod
    def load(cls, data, version=1):
        return Camel([camelRegistry]).load(data)