def __squeeze_in(self, start_time): """ tries to put task in the given date if there are conflicts, we will only truncate the ongoing task and replace it's end part with our activity """ # we are checking if our start time is in the middle of anything # or maybe there is something after us - so we know to adjust end time # in the latter case go only few hours ahead. everything else is madness, heh query = """ SELECT a.*, b.name FROM facts a LEFT JOIN activities b on b.id = a.activity_id WHERE ((start_time < ? and end_time > ?) OR (start_time > ? and start_time < ? and end_time is null) OR (start_time > ? and start_time < ?)) ORDER BY start_time LIMIT 1 """ fact = self.fetchone( query, (start_time, start_time, start_time - dt.timedelta(hours=12), start_time, start_time, start_time + dt.timedelta(hours=12))) end_time = None if fact: if start_time > fact["start_time"]: #we are in middle of a fact - truncate it to our start self.execute("UPDATE facts SET end_time=? WHERE id=?", (start_time, fact["id"])) else: #otherwise we have found a task that is after us end_time = fact["start_time"] return end_time
def test_comparison(self): fact1 = Fact.parse("12:25-13:25 case@cat,, description #tag #bäg") fact2 = fact1.copy() self.assertEqual(fact1, fact2) fact2 = fact1.copy() fact2.activity = "abcd" self.assertNotEqual(fact1, fact2) fact2 = fact1.copy() fact2.category = "abcd" self.assertNotEqual(fact1, fact2) fact2 = fact1.copy() fact2.description = "abcd" self.assertNotEqual(fact1, fact2) fact2 = fact1.copy() fact2.range.start = fact1.range.start + dt.timedelta(minutes=1) self.assertNotEqual(fact1, fact2) fact2 = fact1.copy() fact2.range.end = fact1.range.end + dt.timedelta(minutes=1) self.assertNotEqual(fact1, fact2) # wrong order fact2 = fact1.copy() fact2.tags = ["bäg", "tag"] self.assertNotEqual(fact1, fact2) # correct order fact2 = fact1.copy() fact2.tags = ["tag", "bäg"] self.assertEqual(fact1, fact2)
def show_popup(self): if not self._parent_click_watcher: self._parent_click_watcher = self.get_toplevel().connect("button-press-event", self._on_focus_out_event) # we will be adding things, need datetime i_time_0 = dt.datetime.combine(self.start_date or dt.date.today(), self.start_time or dt.time()) if self.start_time is None: # full 24 hours i_time = i_time_0 interval = dt.timedelta(minutes = 15) end_time = i_time_0 + dt.timedelta(days = 1) else: # from start time to start time + 12 hours interval = dt.timedelta(minutes = 15) i_time = i_time_0 + interval end_time = i_time_0 + dt.timedelta(hours = 12) time = self.figure_time(self.get_text()) focus_time = dt.datetime.combine(dt.date.today(), time) if time else None hours = gtk.ListStore(str) i, focus_row = 0, None while i_time < end_time: row_text = self._format_time(i_time) if self.start_time is not None: delta_text = (i_time - i_time_0).format() row_text += " (%s)" % delta_text hours.append([row_text]) if focus_time and i_time <= focus_time < i_time + interval: focus_row = i i_time += interval i += 1 self.time_tree.set_model(hours) #focus on row if focus_row != None: selection = self.time_tree.get_selection() selection.select_path(focus_row) self.time_tree.scroll_to_cell(focus_row, use_align = True, row_align = 0.4) #move popup under the widget alloc = self.get_allocation() w = alloc.width self.time_tree.set_size_request(w, alloc.height * 5) window = self.get_parent_window() dmmy, x, y= window.get_origin() self.popup.move(x + alloc.x,y + alloc.y + alloc.height) self.popup.resize(*self.time_tree.get_size_request()) self.popup.show_all()
def test_format_timedelta(self): delta = dt.timedelta(minutes=10) self.assertEqual(delta.format("human"), "10min") delta = dt.timedelta(hours=5, minutes=0) self.assertEqual(delta.format("human"), "5h") delta = dt.timedelta(hours=5, minutes=10) self.assertEqual(delta.format("human"), "5h 10min") delta = dt.timedelta(hours=5, minutes=10) self.assertEqual(delta.format("HH:MM"), "05:10")
def __get_facts(self, date, end_date=None, search_terms=""): split_time = conf.day_start datetime_from = dt.datetime.combine(date, split_time) end_date = end_date or date datetime_to = dt.datetime.combine(end_date, split_time) + dt.timedelta( days=1, seconds=-1) logger.info("searching for facts from {} to {}".format( datetime_from, datetime_to)) query = """ SELECT a.id AS id, a.start_time AS start_time, a.end_time AS end_time, a.description as description, b.name AS name, b.id as activity_id, coalesce(c.name, ?) as category, e.name as tag FROM facts a LEFT JOIN activities b ON a.activity_id = b.id LEFT JOIN categories c ON b.category_id = c.id LEFT JOIN fact_tags d ON d.fact_id = a.id LEFT JOIN tags e ON e.id = d.tag_id WHERE (a.end_time >= ? OR a.end_time IS NULL) AND a.start_time <= ? """ if search_terms: # check if we need changes to the index self.__check_index(datetime_from, datetime_to) # flip the query around when it starts with "not " reverse_search_terms = search_terms.lower().startswith("not ") if reverse_search_terms: search_terms = search_terms[4:] search_terms = search_terms.replace('\\', '\\\\').replace( '%', '\\%').replace('_', '\\_').replace("'", "''") query += """ AND a.id %s IN (SELECT id FROM fact_index WHERE fact_index MATCH '%s')""" % ( 'NOT' if reverse_search_terms else '', search_terms) query += " ORDER BY a.start_time, e.name" fact_rows = self.fetchall( query, (self._unsorted_localized, datetime_from, datetime_to)) #first let's put all tags in an array dbfacts = self.__group_tags(fact_rows) # ignore old on-going facts return [ self._dbfact_to_libfact(dbfact) for dbfact in dbfacts if dbfact["start_time"] >= datetime_from - dt.timedelta(days=30) ]
def test_datetime_patterns(self): p = dt.datetime.pattern(1) s = "12:03" m = re.fullmatch(p, s, re.VERBOSE) time = dt.datetime._extract_datetime(m, d="date1", h="hour1", m="minute1", r="relative1", default_day=dt.hday.today()) self.assertEqual(time.strftime("%H:%M"), "12:03") s = "2019-12-01 12:36" m = re.fullmatch(p, s, re.VERBOSE) time = dt.datetime._extract_datetime(m, d="date1", h="hour1", m="minute1", r="relative1") self.assertEqual(time.strftime("%Y-%m-%d %H:%M"), "2019-12-01 12:36") s = "-25" m = re.fullmatch(p, s, re.VERBOSE) relative = dt.datetime._extract_datetime(m, d="date1", h="hour1", m="minute1", r="relative1", default_day=dt.hday.today()) self.assertEqual(relative, dt.timedelta(minutes=-25)) s = "2019-12-05" m = re.search(p, s, re.VERBOSE) self.assertEqual(m, None)
def _draw(self, context, opacity, matrix): g = graphics.Graphics(context) g.save_context() g.translate(self.x, self.y) # arbitrary 3/4 total width for label, 1/4 for histogram hist_width = self.alloc_w // 4; margin = 10 # pixels label_width = self.alloc_w - hist_width - margin self.layout.set_width(label_width * pango.SCALE) label_h = self.label_height bar_start_x = label_width + margin for i, (label, value) in enumerate(self.values): g.set_color(self.label_color) duration_str = value.format(fmt="HH:MM") markup_label = stuff.escape_pango(str(label)) markup_duration = stuff.escape_pango(duration_str) self.layout.set_markup("{}, <i>{}</i>".format(markup_label, markup_duration)) y = int(i * label_h * 1.5) g.move_to(0, y) pangocairo.show_layout(context, self.layout) if self._max > dt.timedelta(0): w = ceil(hist_width * value.total_seconds() / self._max.total_seconds()) else: w = 1 g.rectangle(bar_start_x, y, int(w), int(label_h)) g.fill(self.bar_color) g.restore_context()
def load_suggestions(self): self.todays_facts = self.storage.get_todays_facts() # list of facts of last month now = dt.datetime.now() last_month = self.storage.get_facts(now - dt.timedelta(days=30), now) # naive recency and frequency rank # score is as simple as you get 30-days_ago points for each occurence suggestions = defaultdict(int) for fact in last_month: days = 30 - (now - dt.datetime.combine( fact.date, dt.time())).total_seconds() / 60 / 60 / 24 label = fact.activity if fact.category: label += "@%s" % fact.category suggestions[label] += days if fact.tags: label += " #%s" % (" #".join(fact.tags)) suggestions[label] += days for rec in self.storage.get_activities(): label = rec["name"] if rec["category"]: label += "@%s" % rec["category"] suggestions[label] += 0 # list of (label, score), higher scores first self.suggestions = sorted(suggestions.items(), key=lambda x: x[1], reverse=True)
def test_range(self): day = dt.hday(2020, 2, 2) time = dt.time(21, 20) base = dt.datetime.from_day_time(day, time) range = dt.Range(base, base + dt.timedelta(minutes=30)) range_str = range.format(default_day=day) self.assertEqual(range_str, "21:20 - 21:50") range = dt.Range(None, base) range_str = range.format(default_day=day) self.assertEqual(range_str, "-- - 21:20")
def _finish(self, facts): # group by date by_date = [] for date, date_facts in itertools.groupby(facts, lambda fact: fact.date): by_date.append((date, [fact.as_dict() for fact in date_facts])) by_date = dict(by_date) date_facts = [] date = min(by_date.keys()) while date <= self.end_date: str_date = date.strftime( # date column format for each row in HTML report # Using python datetime formatting syntax. See: # http://docs.python.org/library/time.html#time.strftime C_("html report", "%b %d, %Y")) date_facts.append([str_date, by_date.get(date, [])]) date += dt.timedelta(days=1) data = dict( title=self.title, totals_by_day_title=_("Totals by Day"), activity_log_title=_("Activity Log"), totals_title=_("Totals"), activity_totals_heading=_("activities"), category_totals_heading=_("categories"), tag_totals_heading=_("tags"), show_prompt=_("Distinguish:"), header_date=_("Date"), header_activity=_("Activity"), header_category=_("Category"), header_tags=_("Tags"), header_start=_("Start"), header_end=_("End"), header_duration=_("Duration"), header_description=_("Description"), data_dir=runtime.data_dir, show_template=_("Show template"), template_instructions= _("You can override it by storing your version in %(home_folder)s") % {'home_folder': runtime.home_data_dir}, start_date=timegm(self.start_date.timetuple()), end_date=timegm(self.end_date.timetuple()), facts=json_dumps([fact.as_dict() for fact in facts]), date_facts=json_dumps(date_facts), all_activities_rows="\n".join(self.fact_rows)) for key, val in data.items(): if isinstance(val, str): data[key] = val self.file.write(Template(self.main_template).safe_substitute(data)) return
def test_type_stability(self): dt1 = dt.datetime(2020, 1, 10, hour=13, minute=30) dt2 = dt.datetime(2020, 1, 10, hour=13, minute=40) delta = dt2 - dt1 self.assertEqual(type(delta), dt.timedelta) _sum = dt1 + delta self.assertEqual(_sum, dt.datetime(2020, 1, 10, hour=13, minute=40)) self.assertEqual(type(_sum), dt.datetime) _sub = dt1 - delta self.assertEqual(_sub, dt.datetime(2020, 1, 10, hour=13, minute=20)) self.assertEqual(type(_sub), dt.datetime) opposite = -delta self.assertEqual(opposite, dt.timedelta(minutes=-10)) self.assertEqual(type(opposite), dt.timedelta) _sum = delta + delta self.assertEqual(_sum, dt.timedelta(minutes=20)) self.assertEqual(type(_sum), dt.timedelta) _sub = delta - delta self.assertEqual(_sub, dt.timedelta()) self.assertEqual(type(_sub), dt.timedelta)
def __touch_fact(self, fact, end_time=None): end_time = end_time or dt.datetime.now() # tasks under one minute do not count if end_time - fact.start_time < dt.timedelta(minutes=1): self.__remove_fact(fact.id) else: query = """ UPDATE facts SET end_time = ? WHERE id = ? """ self.execute(query, (end_time, fact.id))
def check_fact(cls, fact, default_day=None): """Check Fact validity for inclusion in the storage. Raise FactError(message) on failure. """ if fact.start_time is None: raise FactError("Missing start time") if fact.end_time and (fact.delta < dt.timedelta(0)): fixed_fact = Fact(start_time=fact.start_time, end_time=fact.end_time + dt.timedelta(days=1)) suggested_range_str = fixed_fact.range.format(default_day=default_day) # work around cyclic imports from hamster.lib.configuration import conf raise FactError(dedent( """\ Duration would be negative. Working late ? This happens when the activity crosses the hamster day start time ({:%H:%M} from tracking settings). Suggestion: move the end to the next day; the range would become: {} (in civil local time) """.format(conf.day_start, suggested_range_str) )) if not fact.activity: raise FactError("Missing activity") if ',' in fact.category: raise FactError(dedent( """\ Forbidden comma in category: '{}' Note: The description separator changed from single comma to double comma ',,' (cf. PR #482). """.format(fact.category) ))
def set_facts(self, facts, scroll_to_top=False): # FactTree adds attributes to its facts. isolate these side effects # copy the id too; most of the checks are based on id here. self.facts = [fact.copy(id=fact.id) for fact in facts] del facts # make sure facts is not used by inadvertance below. # If we get an entirely new set of facts, scroll back to the top if scroll_to_top: self.y = 0 self.hover_fact = None if self.vadjustment: self.vadjustment.set_value(self.y) if self.facts: start = self.facts[0].date end = self.facts[-1].date else: start = end = dt.hday.today() by_date = defaultdict(list) delta_by_date = defaultdict(dt.timedelta) for fact in self.facts: by_date[fact.date].append(fact) delta_by_date[fact.date] += fact.delta # Add a TotalFact at the end of each day if we are # displaying more than one day. if len(by_date) > 1: for key in by_date: total_by_date = TotalFact(_("Total"), delta_by_date[key]) by_date[key].append(total_by_date) days = [] for i in range((end - start).days + 1): current_date = start + dt.timedelta(days=i) if current_date in by_date: days.append((current_date, by_date[current_date])) self.days = days self.set_row_heights() if (self.current_fact and self.current_fact.id in (fact.id for fact in self.facts) ): self.on_scroll() else: # will also trigger an on_scroll self.unset_current_fact()
def duration_minutes(duration): """Returns minutes from duration, otherwise we keep bashing in same math. Deprecated, use dt.timedelta.total_minutes instead. """ if isinstance(duration, dt.timedelta): return duration.total_seconds() / 60 elif isinstance(duration, (int, float)): return duration elif isinstance(duration, list): res = dt.timedelta() for entry in duration: res += entry return duration_minutes(res) else: raise NotImplementedError("received {}".format(type(duration)))
def locale_first_weekday(): """figure if week starts on monday or sunday""" first_weekday = 6 #by default settle on monday try: process = os.popen("locale first_weekday week-1stday") week_offset, week_start = process.read().split('\n')[:2] process.close() week_start = dt.date(*time.strptime(week_start, "%Y%m%d")[:3]) week_offset = dt.timedelta(int(week_offset) - 1) beginning = week_start + week_offset first_weekday = int(beginning.strftime("%w")) except: logger.warn("WARNING - Failed to get first weekday from locale") return first_weekday
def prev_range(self): start, end = self.start_date, self.end_date if self.current_range == "day": start, end = start - dt.timedelta(1), end - dt.timedelta(1) elif self.current_range == "week": start, end = start - dt.timedelta(7), end - dt.timedelta(7) elif self.current_range == "month": end = start - dt.timedelta(1) first_weekday, days_in_month = calendar.monthrange(end.year, end.month) start = end - dt.timedelta(days_in_month - 1) else: # manual range - just jump to the next window days = (end - start) + dt.timedelta(days = 1) start = start - days end = end - days self.emit_range(self.current_range, start, end)
def test_range(self): day = dt.hday(2020, 2, 2) time = dt.time(21, 20) base = dt.datetime.from_day_time(day, time) range = dt.Range(base, base + dt.timedelta(minutes=30)) range_str = range.format(default_day=day) self.assertEqual(range_str, "21:20 - 21:50") range = dt.Range(None, base) range_str = range.format(default_day=day) self.assertEqual(range_str, "-- - 21:20") # issue #576 start = dt.datetime(2020, 3, 8, 17, 7) end = dt.datetime(2020, 3, 8, 18, 6) range = dt.Range.from_start_end(start, end) self.assertEqual(range.start, start) self.assertEqual(range.end, end) # check passthrough range2 = dt.Range.from_start_end(range) self.assertEqual(range2.start, range.start) self.assertEqual(range2.end, range.end)
def __init__(self, **kwargs): graphics.Sprite.__init__(self, **kwargs) self.x_align = 0 self.y_align = 0 self.values = [] self._label_context = cairo.Context(cairo.ImageSurface(cairo.FORMAT_A1, 0, 0)) self.layout = pangocairo.create_layout(self._label_context) self.layout.set_font_description(pango.FontDescription(graphics._font_desc)) self.layout.set_markup("Hamster") # dummy # ellipsize the middle because depending on the use case, # the distinctive information can be either at the beginning or the end. self.layout.set_ellipsize(pango.EllipsizeMode.MIDDLE) self.layout.set_justify(True) self.layout.set_alignment(pango.Alignment.RIGHT) self.label_height = self.layout.get_pixel_size()[1] # should be updated by the parent self.label_color = gdk.RGBA() self.bar_color = gdk.RGBA() self._max = dt.timedelta(0)
def time(self): """Displayed time. None, or time type, or datetime if start_time() was given a datetime. """ time = self.figure_time(self.get_text()) if self.start_date and time: # recombine (since self.start_time contains only the time part) start = dt.datetime.combine(self.start_date, self.start_time) new = dt.datetime.combine(self.start_date, time) if new < start: # a bit hackish, valid only because # duration can not be negative if start_time was given, # and we accept that it can not exceed 24h. # For longer durations, # date will have to be changed subsequently. return new + dt.timedelta(days=1) else: return new else: return time
def set_facts(self, facts): # FactTree adds attributes to its facts. isolate these side effects # copy the id too; most of the checks are based on id here. self.facts = [fact.copy(id=fact.id) for fact in facts] del facts # make sure facts is not used by inadvertance below. self.y = 0 self.hover_fact = None if self.vadjustment: self.vadjustment.set_value(0) if self.facts: start = self.facts[0].date end = self.facts[-1].date else: start = end = dt.hday.today() by_date = defaultdict(list) for fact in self.facts: by_date[fact.date].append(fact) days = [] for i in range((end-start).days + 1): current_date = start + dt.timedelta(days=i) days.append((current_date, by_date[current_date])) self.days = days self.set_row_heights() if (self.current_fact and self.current_fact.id in (fact.id for fact in self.facts) ): self.on_scroll() else: # will also trigger an on_scroll self.unset_current_fact()
def test_parse_datetime_range(self): # only match clean s = "10.00@cat" (start, end), rest = dt.Range.parse(s, position="head") self.assertEqual(start, None) self.assertEqual(end, None) s = "12:02" (start, end), rest = dt.Range.parse(s) self.assertEqual(start.strftime("%H:%M"), "12:02") self.assertEqual(end, None) s = "12:03 13:04" (start, end), rest = dt.Range.parse(s) self.assertEqual(start.strftime("%H:%M"), "12:03") self.assertEqual(end.strftime("%H:%M"), "13:04") s = "12:35 activity" (start, end), rest = dt.Range.parse(s, position="head") self.assertEqual(start.strftime("%H:%M"), "12:35") self.assertEqual(end, None) s = "2019-12-01 12:33 activity" (start, end), rest = dt.Range.parse(s, position="head") self.assertEqual(start.strftime("%Y-%m-%d %H:%M"), "2019-12-01 12:33") self.assertEqual(end, None) ref = dt.datetime(2019, 11, 29, 13, 55) # 2019-11-29 13:55 s = "-25 activity" (start, end), rest = dt.Range.parse(s, position="head", ref=ref) self.assertEqual(start.strftime("%Y-%m-%d %H:%M"), "2019-11-29 13:30") self.assertEqual(end, None) s = "+25 activity" (start, end), rest = dt.Range.parse(s, position="head", ref=ref) self.assertEqual(start.strftime("%Y-%m-%d %H:%M"), "2019-11-29 14:20") self.assertEqual(end, None) s = "-55 -25 activity" (start, end), rest = dt.Range.parse(s, position="head", ref=ref) self.assertEqual(start.strftime("%Y-%m-%d %H:%M"), "2019-11-29 13:00") self.assertEqual(end.strftime("%Y-%m-%d %H:%M"), "2019-11-29 13:30") s = "+25 +55 activity" (start, end), rest = dt.Range.parse(s, position="head", ref=ref) self.assertEqual(start.strftime("%Y-%m-%d %H:%M"), "2019-11-29 14:20") self.assertEqual(end.strftime("%Y-%m-%d %H:%M"), "2019-11-29 14:50") s = "-55 -120 activity" (start, end), rest = dt.Range.parse(s, position="head", ref=ref) self.assertEqual(start.strftime("%Y-%m-%d %H:%M"), "2019-11-29 13:00") self.assertEqual(end.strftime("%Y-%m-%d %H:%M"), "2019-11-29 11:55") s = "-50 20 activity" (start, end), rest = dt.Range.parse(s, position="head", ref=ref) self.assertEqual(start.strftime("%Y-%m-%d %H:%M"), "2019-11-29 13:05") self.assertEqual(end.strftime("%Y-%m-%d %H:%M"), "2019-11-29 13:25") s = "2019-12-05" # single hamster day (start, end), rest = dt.Range.parse(s, ref=ref) just_before = start - dt.timedelta(seconds=1) just_after = end + dt.timedelta(seconds=1) self.assertEqual(just_before.hday(), pdt.date(2019, 12, 4)) self.assertEqual(just_after.hday(), pdt.date(2019, 12, 6)) s = "2019-12-05 2019-12-07" # hamster days range (start, end), rest = dt.Range.parse(s, ref=ref) just_before = start - dt.timedelta(seconds=1) just_after = end + dt.timedelta(seconds=1) self.assertEqual(just_before.hday(), dt.date(2019, 12, 4)) self.assertEqual(just_after.hday(), dt.date(2019, 12, 8)) s = "14:30 - --" (start, end), rest = dt.Range.parse(s, ref=ref) self.assertEqual(start.strftime("%H:%M"), "14:30") self.assertEqual(end, None)
def test_timedelta(self): delta = dt.timedelta(seconds=90) self.assertEqual(delta.total_minutes(), 1.5)
def __add_fact(self, fact, temporary=False): """Add fact to database. Args: fact (Fact) Returns: int, the new fact id in the database (> 0) on success, 0 if nothing needed to be done (e.g. if the same fact was already on-going), note: a sanity check on the given fact is performed first, that would raise an AssertionError. Other errors would also be handled through exceptions. """ logger.info("adding fact {}".format(fact)) start_time = fact.start_time end_time = fact.end_time # get tags from database - this will create any missing tags too tags = [(tag['id'], tag['name'], tag['autocomplete']) for tag in self.get_tag_ids(fact.tags)] # now check if maybe there is also a category category_id = self.__get_category_id(fact.category) if not category_id: category_id = self.__add_category(fact.category) # try to find activity, resurrect if not temporary activity_id = self.__get_activity_by_name(fact.activity, category_id, resurrect=not temporary) if not activity_id: activity_id = self.__add_activity(fact.activity, category_id, temporary) else: activity_id = activity_id['id'] # if we are working on +/- current day - check the last_activity if (dt.timedelta(days=-1) <= dt.datetime.now() - start_time <= dt.timedelta(days=1)): # pull in previous facts facts = self.__get_todays_facts() previous = None if facts and facts[-1].end_time is None: previous = facts[-1] if previous and previous.start_time <= start_time: # check if maybe that is the same one, in that case no need to restart if (previous.activity_id == activity_id and set(previous.tags) == set([tag[1] for tag in tags]) and (previous.description or "") == (fact.description or "")): logger.info("same fact, already on-going, nothing to do") return 0 # if no description is added # see if maybe previous was too short to qualify as an activity if (not previous.description and 60 >= (start_time - previous.start_time).seconds >= 0): self.__remove_fact(previous.id) # now that we removed the previous one, see if maybe the one # before that is actually same as the one we want to start # (glueing) if len(facts) > 1 and 60 >= ( start_time - facts[-2].end_time).seconds >= 0: before = facts[-2] if (before.activity_id == activity_id and set( before.tags) == set([tag[1] for tag in tags])): # resume and return update = """ UPDATE facts SET end_time = null WHERE id = ? """ self.execute(update, (before.id, )) return before.id else: # otherwise stop update = """ UPDATE facts SET end_time = ? WHERE id = ? """ self.execute(update, (start_time, previous.id)) # done with the current activity, now we can solve overlaps if not end_time: end_time = self.__squeeze_in(start_time) else: self.__solve_overlaps(start_time, end_time) # finally add the new entry insert = """ INSERT INTO facts (activity_id, start_time, end_time, description) VALUES (?, ?, ?, ?) """ self.execute(insert, (activity_id, start_time, end_time, fact.description)) fact_id = self.__last_insert_rowid() #now link tags insert = ["insert into fact_tags(fact_id, tag_id) values(?, ?)" ] * len(tags) params = [(fact_id, tag[0]) for tag in tags] self.execute(insert, params) self.__remove_index([fact_id]) logger.info("fact successfully added, with id #{}".format(fact_id)) return fact_id
def set_values(self, values): """expects a list of 2-tuples""" self.values = values self.height = len(self.values) * 14 self._max = max(rec[1] for rec in values) if values else dt.timedelta(0)
def increment_date(self, days): delta = dt.timedelta(days=days) self.date += delta self.update_fields()
def _list(self, start_time, end_time, search=""): """Print a listing of activities""" facts = self.storage.get_facts(start_time, end_time, search) headers = { 'activity': _("Activity"), 'category': _("Category"), 'tags': _("Tags"), 'description': _("Description"), 'start': _("Start"), 'end': _("End"), 'duration': _("Duration") } # print date if it is not the same day print_with_date = start_time.date() != end_time.date() cols = 'start', 'end', 'duration', 'activity', 'category' widths = dict([(col, len(headers[col])) for col in cols]) for fact in facts: fact = fact_dict(fact, print_with_date) for col in cols: widths[col] = max(widths[col], len(fact[col])) cols = [ "{{{col}: <{len}}}".format(col=col, len=widths[col]) for col in cols ] fact_line = " | ".join(cols) row_width = sum(val + 3 for val in list(widths.values())) print() print(fact_line.format(**headers)) print("-" * min(row_width, 80)) by_cat = {} for fact in facts: cat = fact.category or _("Unsorted") by_cat.setdefault(cat, dt.timedelta(0)) by_cat[cat] += fact.delta pretty_fact = fact_dict(fact, print_with_date) print(fact_line.format(**pretty_fact)) if pretty_fact['description']: for line in word_wrap(pretty_fact['description'], 76): print(" {}".format(line)) if pretty_fact['tags']: for line in word_wrap(pretty_fact['tags'], 76): print(" {}".format(line)) print("-" * min(row_width, 80)) cats = [] total_duration = dt.timedelta() for cat, duration in sorted(by_cat.items(), key=lambda x: x[1], reverse=True): cats.append("{}: {}".format(cat, duration.format())) total_duration += duration for line in word_wrap(", ".join(cats), 80): print(line) print("Total: ", total_duration.format()) print()
def on_enter_frame(self, scene, context): g = graphics.Graphics(context) self.plot_area.height = self.height - 30 vertical = min(self.plot_area.height / 5, 7) minute_pixel = (self.scope_hours * 60.0 - 15) / self.width g.set_line_style(width=1) g.translate(0.5, 0.5) colors = { "normal": self._style.get_color(gtk.StateFlags.NORMAL), "normal_bg": self._style.get_background_color(gtk.StateFlags.NORMAL), "selected": self._style.get_color(gtk.StateFlags.SELECTED), "selected_bg": self._style.get_background_color(gtk.StateFlags.SELECTED), } bottom = self.plot_area.y + self.plot_area.height for bar in self.fact_bars: bar.y = vertical * bar.category + 5 bar.height = vertical bar_start_time = bar.fact.start_time - self.view_time minutes = bar_start_time.seconds / 60 + bar_start_time.days * self.scope_hours * 60 bar.x = round(minutes / minute_pixel) + 0.5 bar.width = round((bar.fact.delta).seconds / 60 / minute_pixel) if self.chosen_selection.start_time and self.chosen_selection.width is None: # we have time but no pixels minutes = round((self.chosen_selection.start_time - self.view_time).seconds / 60 / minute_pixel) + 0.5 self.chosen_selection.x = minutes if self.chosen_selection.end_time: self.chosen_selection.width = round((self.chosen_selection.end_time - self.chosen_selection.start_time).seconds / 60 / minute_pixel) else: self.chosen_selection.width = 0 self.chosen_selection.height = self.chosen_selection.parent.height # use the oportunity to set proper colors too self.chosen_selection.fill = colors['selected_bg'] self.chosen_selection.duration_label.color = colors['selected'] #time scale g.set_color("#000") background = colors["normal_bg"] text = colors["normal"] tick_color = g.colors.contrast(background, 80) layout = g.create_layout(size = 10) for i in range(self.scope_hours * 60): time = (self.view_time + dt.timedelta(minutes=i)) g.set_color(tick_color) if time.minute == 0: g.move_to(round(i / minute_pixel), bottom - 15) g.line_to(round(i / minute_pixel), bottom) g.stroke() elif time.minute % 15 == 0: g.move_to(round(i / minute_pixel), bottom - 5) g.line_to(round(i / minute_pixel), bottom) g.stroke() if time.minute == 0 and time.hour % 4 == 0: if time.hour == 0: g.move_to(round(i / minute_pixel), self.plot_area.y) g.line_to(round(i / minute_pixel), bottom) label_minutes = time.strftime("%b %d") else: label_minutes = time.strftime("%H<small><sup>%M</sup></small>") g.set_color(text) layout.set_markup(label_minutes) g.move_to(round(i / minute_pixel) + 2, 0) pangocairo.show_layout(context, layout) #current time if self.view_time < dt.datetime.now() < self.view_time + dt.timedelta(hours = self.scope_hours): minutes = round((dt.datetime.now() - self.view_time).seconds / 60 / minute_pixel) g.rectangle(minutes, 0, self.width, self.height) g.fill(colors['normal_bg'], 0.7) g.move_to(minutes, self.plot_area.y) g.line_to(minutes, bottom) g.stroke("#f00", 0.4)
def week(view_date): # aligns start and end date to week start_date = view_date - dt.timedelta(view_date.weekday() + 1) start_date = start_date + dt.timedelta(locale_first_weekday()) end_date = start_date + dt.timedelta(6) return start_date, end_date
def month(view_date): # aligns start and end date to month start_date = view_date - dt.timedelta(view_date.day - 1) #set to beginning of month first_weekday, days_in_month = calendar.monthrange(view_date.year, view_date.month) end_date = start_date + dt.timedelta(days_in_month - 1) return start_date, end_date