예제 #1
0
def _read_validated_scopes() -> List[str]:
    """
    Prompts the user to enter a list of space-separated TimeScopes

    - if nothing is entered, fall back to the "current" scope
    - if whitespace is entered, return an empty list
    - TimeScopes are checked for validity
    """
    today_scope = TimeScope(datetime.now().strftime("%G-ww%V.%u"))
    entered_scopes = input(f"Enter scopes to add [{today_scope}]: {Color.RED}")
    print(Color.END, end='', flush=True)

    if not entered_scopes:
        requested_scopes = [today_scope]
    elif not entered_scopes.strip():
        requested_scopes = []
    else:
        requested_scopes = [TimeScope(s) for s in entered_scopes.split()]
        try:
            [s.get_type() for s in requested_scopes]
        except ValueError as e:
            print(e)
            return

    return requested_scopes
예제 #2
0
def test_invalid():
    with pytest.raises(ValueError):
        s = TimeScope("invalid scope string")
        s.type

    with pytest.raises(ValueError):
        s = TimeScope("2020-w35")
        s.type
예제 #3
0
def test_latest_scope(task_v1_session):
    csv_test_file = """desc,scopes
task 1,2020-ww39.1 2020-ww39.2 2020-ww39.3
"""

    import_from_csv(io.StringIO(csv_test_file), task_v1_session)
    t: Task = Task.query.all()[0]

    assert len(list(matching_scopes(t.task_id))) == 3
    assert latest_scope(t.task_id, TimeScope("2020-ww39.1")) == "2020-ww39.3"
    assert not latest_scope(t.task_id, TimeScope("2020-ww39.3"))
예제 #4
0
    def report_notes_all():
        page_scope = None
        try:
            parsed_scope = TimeScope(escape(request.args.get('scope')))
            parsed_scope.get_type()
            page_scope = parsed_scope
        except ValueError:
            pass

        page_domain = request.args.get('domain')

        return report.report_notes(page_scope=page_scope, page_domain=page_domain)
예제 #5
0
    def edit_tasks_in_scope(scope_id):
        if scope_id == 'week':
            return redirect(
                url_for(".edit_tasks_in_scope",
                        scope_id=datetime.now().strftime("%G-ww%V")))

        page_scope = None
        try:
            parsed_scope = TimeScope(scope_id)
            parsed_scope.get_type()
            page_scope = parsed_scope
        except ValueError:
            pass

        return report.edit_tasks_in_scope(page_scope=page_scope)
예제 #6
0
def update(session, requested_scopes, category, resolution, task_id):
    t: Task = Task.query.filter(Task.task_id == task_id).one()

    if requested_scopes:
        # TODO: This is redundant, should let sqlalchemy enforce the no-dupes
        existing_scopes = tasks_v1.report.matching_scopes(task_id)

        for requested_scope in requested_scopes:
            if TimeScope(requested_scope) not in existing_scopes:
                session.add(
                    TaskTimeScope(task_id=t.task_id,
                                  time_scope_id=requested_scope))

    if category:
        t.category = category
        session.add(t)

    if resolution:
        t.resolution = resolution
        session.add(t)

    try:
        session.commit()
        print()
        print(f"Updated task {t.task_id}")
        print(json.dumps(t.to_json_dict(), indent=4))
    except OperationalError as e:
        print()
        print(e)
        return
예제 #7
0
def _to_summary_html(t: Task,
                     ref_scope: Optional[TimeScope],
                     print_task_id=False) -> str:
    def _link_replacer(mdown: str):
        return re.sub(r'\[(.+?)\]\((.+?)\)', r"""[\1](<a href="\2">\2</a>)""",
                      mdown)

    def _to_time_html(t: Task) -> str:
        if t.time_estimate is not None and t.time_actual is not None:
            return f"`{t.time_estimate}h => {t.time_actual}h`"
        elif t.time_estimate is not None:
            return f"`{t.time_estimate}h`"
        elif t.time_actual is not None:
            return f"`=> {t.time_actual}`"
        else:
            return f"{t.time_estimate} => {t.time_actual}"

    response_html = ""

    if ref_scope:
        short_scope = TimeScope(t.first_scope).shorten(ref_scope)
        if short_scope:
            response_html += f'\n<span class="time-scope">{short_scope}</span>'

    response_html += f'\n<span class="desc">{_link_replacer(t.desc)}</span>'

    if t.time_estimate is not None or t.time_actual is not None:
        response_html += f'\n<span class="task-time">{_to_time_html(t)}</span>'

    if print_task_id:
        response_html += f'\n<span class="task-id"><a href="/task/{t.task_id}">#{t.task_id}</a></span>'

    return response_html
예제 #8
0
def add_from_cli(session):
    # Read description for the task
    desc = input(f"Enter description: {Color.RED}")
    print(Color.END, end='', flush=True)

    # Read relevant scopes
    requested_scopes = sorted(_read_validated_scopes())
    if not len(requested_scopes):
        requested_scopes = [TimeScope(datetime.now().strftime("%G-ww%V.%u"))]
        print(f"   => defaulting to {requested_scopes}")
    if len(requested_scopes) > 1:
        print(f"   => parsed as {requested_scopes}")

    # Create a Task, now that we have all required fields
    t = Task(desc=desc,
             first_scope=requested_scopes[0],
             created_at=datetime.now())

    # And a category
    category = input(f"Enter category: {Color.RED}")
    if category is not None and category != "":
        t.category = category
    print(Color.END, end='', flush=True)

    # And a time_estimate
    time_estimate = input(f"Enter time_estimate: {Color.RED}")
    if time_estimate is not None and time_estimate != "":
        if not re.fullmatch(r"\d+\.\d", time_estimate):
            print(Color.END, end='')
            print(
                "time_estimate must be in format like `12.0` (\"\\d+\\.\\d\"), exiting"
            )
            print()
            return
        t.time_estimate = time_estimate
    print(Color.END, end='', flush=True)

    # Try committing the Task
    try:
        session.add(t)
        session.commit()
    except StatementError as e:
        print()
        print("Hit exception when parsing:")
        print(json.dumps(t.to_json_dict(), indent=4))
        session.rollback()
        return

    # Try creating the TaskTimeScopes
    for requested_scope in requested_scopes:
        session.add(
            TaskTimeScope(task_id=t.task_id, time_scope_id=requested_scope))
    session.commit()

    print()
    print(f"Created task {t.task_id}")
    print(json.dumps(t.to_json_dict(), indent=4))
예제 #9
0
def test_blank_domains_reporting(note_session):
    csv_test_file = """scope,desc,short_desc,domains
2020-ww48.4,,day-long event,
"""

    import_from_csv(io.StringIO(csv_test_file), note_session)

    dict = _report_notes_for(scope=TimeScope("2020—Q4"), domain=None)
    assert dict
    assert "2020—Q4" in dict
    assert '2020-ww48' in dict["2020—Q4"]["child_scopes"]
예제 #10
0
def _force_day_scope(scope_str):
    maybe_scope = TimeScope(scope_str)
    if maybe_scope.type == TimeScope.Type.quarter:
        # Convert "quarter" scopes into their first day
        scope = maybe_scope.start.strftime("%G-ww%V.%u")
        return scope
    elif maybe_scope.type == TimeScope.Type.week:
        # Convert "week" scopes into their first day
        scope = maybe_scope.start.strftime("%G-ww%V.%u")
        return scope

    return scope_str
예제 #11
0
        def print_sort_time(n: Note) -> str:
            scope_id_to_print = TimeScope(n.time_scope_id)

            # For within-day notes, print something more detailed
            if parent_scope_id.type == TimeScope.Type.day:
                # Override with sort_time as needed
                if n.sort_time:
                    scope_id_to_print = TimeScope(
                        n.sort_time.strftime("%G-ww%V.%u"))

                    # If the days are identical, just print the time
                    if parent_scope_id == scope_id_to_print:
                        return f'<span class="time">{str(n.sort_time)[11:16]}</span>'
                    else:
                        return f'''<span class="time">{
                            scope_id_to_print.shorten(parent_scope_id)
                        } {
                            str(n.sort_time)[11:16]
                        }</span>'''

            return f'<span class="time">{scope_id_to_print.shorten(parent_scope_id)}</span>'
예제 #12
0
def test_report_emdash(note_session):
    csv_test_file = """scope,desc,short_desc,domains
2020-ww48.4,,day-long event,d1
2020-ww48,,week-long,d1
2020—Q4,,quarter-long,d1
"""

    import_from_csv(io.StringIO(csv_test_file), note_session)

    dict = _report_notes_for(scope=TimeScope("2020—Q4"), domain=None)
    assert dict
    assert "2020—Q4" in dict
    assert '2020-ww48' in dict["2020—Q4"]["child_scopes"]
예제 #13
0
def _migrate_simple(session, t1: Task_v1) -> Task_v2:
    """
    Migrate an orphan task

    Task_v1 only has a few fields that will turn into linkages:

    - for first_scope, we can add the `created_at` field,
      and possibly an `as_json()` dump
    - for the last scope, we set `resolution` + `time_elapsed`
    - every other linkage has no info, a `roll => wwXX.Y` resolution at most

    Note that first_scope isn't guaranteed to be the _earliest_ scope,
    it's intended to be the one where the task was created.
    """
    t2 = Task_v2(desc=t1.desc,
                 category=t1.category,
                 time_estimate=t1.time_estimate)
    session.add(t2)
    session.flush()
    #print(f"DEBUG: {t2} <= _migrate_simple({t1})")

    created_at_linkage = None
    prior_linkage = None
    for scope_id in _get_scopes(t1):
        linkage = TaskLinkage(task_id=t2.task_id, time_scope_id=scope_id)
        linkage.created_at = datetime.now()
        session.add(linkage)

        # Touch up created_at_linkage
        if scope_id == _force_day_scope(t1.first_scope):
            created_at_linkage = linkage
            created_at_linkage.created_at = t1.created_at if t1.created_at else datetime.now(
            )
            created_at_linkage.detailed_resolution = json.dumps(t1.as_json(),
                                                                indent=4)

        # For "ordinary" linkages, append `roll => wwXX.Y` resolution
        if prior_linkage:
            min_scope_id = TimeScope(t1.first_scope).minimize(scope_id)
            prior_linkage.resolution = f"roll => {min_scope_id}"

        prior_linkage = linkage

    # For the final linkage, inherit any/all Task_v1 fields
    final_linkage = prior_linkage if prior_linkage else created_at_linkage
    final_linkage.resolution = t1.resolution
    final_linkage.time_elapsed = t1.time_actual

    return t2
예제 #14
0
def report_open_tasks(hide_future_tasks: bool):
    query: Query = Task.query \
        .filter(Task.resolution == None) \
        .order_by(Task.category, Task.created_at)

    future_tasks_cutoff = datetime.now()
    ref_scope = TimeScope(future_tasks_cutoff.date().strftime("%G-ww%V.%u"))
    if hide_future_tasks:
        # TODO: This is a very simple filter that barely does what's requested.
        query = query.filter(Task.first_scope < ref_scope)

    return render_template('task.html',
                           short_scope=_short_scope,
                           tasks_by_scope={ref_scope: query.all()},
                           to_details_html=to_details_html,
                           to_summary_html=to_summary_html)
예제 #15
0
def update_from_cli(session, task_id):
    # Open relevant task
    t: Task = Task.query.filter(Task.task_id == task_id).one()
    if len(t.desc) <= 70:
        print(t.desc)
        print()
    else:
        print(t.desc[:60] + "…")
        print()

    matching_scopes = tasks_v1.report.matching_scopes(task_id)
    print(f"Existing scopes => {', '.join(matching_scopes)}")

    # Read relevant scopes
    requested_scopes = _read_validated_scopes()

    # Decide whether we're resolving it
    if t.resolution:
        print(f"Task already has a resolution => {t.resolution}")
        requested_resolution = input(
            f"Enter resolution to set [{t.resolution}]: {Color.RED}")
        print(Color.END, end='', flush=True)
    else:
        requested_resolution = input(f"Enter resolution to set: {Color.RED}")
        print(Color.END, end='', flush=True)

    if requested_resolution:
        t.resolution = requested_resolution
        session.add(t)

    # And update the scopes, maybe
    for requested_scope in requested_scopes:
        if TimeScope(requested_scope) not in matching_scopes:
            session.add(
                TaskTimeScope(task_id=t.task_id,
                              time_scope_id=requested_scope))

    try:
        session.commit()
        print()
        print(f"Updated task {t.task_id}")
        print(json.dumps(t.to_json_dict(), indent=4))
    except OperationalError as e:
        print()
        print(e)
        return
예제 #16
0
    def add_note(self, n: Note):
        """
        This is the only time Notes are sorted into special summary/event types.
        """
        # Add the note to its dict
        s = TimeScope(n.time_scope_id)

        # Note that summaries go into their _enclosing_ scope.
        # Except for quarters, which they don't have such a thing.
        if n.type == "summary":
            enclosing_scope = TimeScopeUtils.enclosing_scope(s)[0]
            self.scope_to_summaries_dict[enclosing_scope].append(n)
            self.add_scope(enclosing_scope)
        elif n.type == "event" or (n.type and n.type[0:7] == "event: "):
            self.scope_to_events_dict[s].append(n)
            self.add_scope(s)
        else:
            self.scope_to_notes_dict[s].append(n)
            self.add_scope(s)
예제 #17
0
def report_tasks(scope):
    tasks_by_scope = {}

    subscopes = TaskTimeScope.query \
        .filter(TaskTimeScope.time_scope_id.like(scope + "%")) \
        .order_by(TaskTimeScope.time_scope_id) \
        .all()

    sorted_scopes = [
        *TimeScopeUtils.enclosing_scope(scope, recurse=True), scope,
        *[s.time_scope_id for s in subscopes]
    ]

    for s in sorted_scopes:
        tasks = Task.query \
            .join(TaskTimeScope, Task.task_id == TaskTimeScope.task_id) \
            .filter(TaskTimeScope.time_scope_id == s) \
            .order_by(TaskTimeScope.time_scope_id, Task.category) \
            .all()
        tasks_by_scope[TimeScope(s)] = tasks

    prev_scope = TimeScopeUtils.prev_scope(scope)
    prev_scope_html = f'<a href="/report-tasks/{prev_scope}">{prev_scope}</a>'
    next_scope = TimeScopeUtils.next_scope(scope)
    next_scope_html = f'<a href="/report-tasks/{next_scope}">{next_scope}</a>'

    def mdown_desc_cleaner(desc: str):
        desc = re.sub(r'\[(.+?)]\((.+?)\)', r"""[\1](<a href="\2">\2</a>)""",
                      desc)
        return desc

    return render_template('task.html',
                           prev_scope=prev_scope_html,
                           next_scope=next_scope_html,
                           tasks_by_scope=tasks_by_scope,
                           link_replacer=mdown_desc_cleaner,
                           short_scope=_short_scope,
                           to_details_html=to_details_html,
                           to_summary_html=to_summary_html)
예제 #18
0
def test_enclosing_scopes():
    ref = TimeScope("2023-ww04.3")
    assert TimeScopeUtils.enclosing_scope(ref, recurse=False) == [TimeScope("2023-ww04")]
    assert set(TimeScopeUtils.enclosing_scope(ref, recurse=True)) \
           == {TimeScope("2023-ww04"), TimeScope("2023—Q1")}
예제 #19
0
def _short_scope(t: Task, ref_scope):
    short_scope_str = TimeScope(t.first_scope).shorten(ref_scope)
    if short_scope_str:
        return short_scope_str

    return None
예제 #20
0
            def note_sorter(n: Note):
                if not n.sort_time:
                    return TimeScope(n.time_scope_id).start

                return n.sort_time
예제 #21
0
def test_day_type():
    s = TimeScope("2020-ww35.5")
    assert s.type == TimeScope.Type.day
    assert s.start == _construct_dt(2020, 8, 28)
    assert s.end == _construct_dt(2020, 8, 29)
예제 #22
0
def test_quarter_type():
    s = TimeScope("2018—Q3")
    assert s.type == TimeScope.Type.quarter
    assert s.start == _construct_dt(2018, 7, 1)
    assert s.end == _construct_dt(2018, 10, 1)
예제 #23
0
def test_create_quarterly(task_v1_session):
    task = Task(desc="아니아", first_scope="2020—Q4")
    time = TimeScope(task.first_scope)
    assert time.type == TimeScope.Type.quarter
예제 #24
0
def test_shorten_today():
    ref = "2020-ww48.4"
    s = TimeScope("2020-ww48.4").shorten(ref)
    assert not s
예제 #25
0
def test_shorten_weeks():
    ref = "2020-ww47.3"
    s = TimeScope("2020-ww48.4").shorten(ref)
    assert s == "ww48.4"
예제 #26
0
def test_create():
    s = TimeScope("2020-ww35")
    assert s
예제 #27
0
def test_minimize():
    ref = TimeScope("2020-ww48.4")

    assert ref.minimize("2019-ww48.5") == "2019-ww48.5"
    assert ref.minimize("2020-ww48.5") == "ww48.5"
    assert ref.minimize("2020-ww50.4") == "ww50.4"
예제 #28
0
def test_shorten_years():
    ref = "2010-ww48.4"
    s = TimeScope("2020-ww48.4").shorten(ref)
    assert s == "2020-ww48.4"
예제 #29
0
def test_shorten_years_close():
    ref = "2020-ww52.4"
    s = TimeScope("2021-ww02.1").shorten(ref)
    assert s == "2021-ww02.1"
예제 #30
0
def test_prev_next_day():
    dref = TimeScope("2002-ww04.4")
    assert TimeScopeUtils.next_scope(dref) == "2002-ww04.5"