def test_datetime_from_day_time(self): day = dt.date(2018, 8, 13) time = dt.time(23, 10) expected = dt.datetime(2018, 8, 13, 23, 10) # 2018-08-13 23:10 self.assertEqual(dt.datetime.from_day_time(day, time), expected) day = dt.date(2018, 8, 13) time = dt.time(0, 10) expected = dt.datetime(2018, 8, 14, 0, 10) # 2018-08-14 00:10 self.assertEqual(dt.datetime.from_day_time(day, time), expected)
def figure_time(self, str_time): if not str_time: return None isPM = "pm" in str_time # strip everything non-numeric and consider hours to be first number # and minutes - second number numbers = re.split("\D", str_time) numbers = [x for x in numbers if x!=""] hours, minutes = None, None if len(numbers) == 1 and len(numbers[0]) == 4: hours, minutes = int(numbers[0][:2]), int(numbers[0][2:]) else: if len(numbers) >= 1: hours = int(numbers[0]) if len(numbers) >= 2: minutes = int(numbers[1]) if isPM: # make sure we don't add if we're already at noon if hours != 12: hours += 12 if (hours is None or minutes is None) or hours > 24 or minutes > 60: return None # no can do return dt.time(hours, minutes)
def list(self, *times): """list facts within a date range""" (start_time, end_time), __ = dt.Range.parse(" ".join(times or [])) start_time = start_time or dt.datetime.combine(dt.date.today(), dt.time()) end_time = end_time or start_time.replace(hour=23, minute=59, second=59) self._list(start_time, end_time)
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 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_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 search(self, *args): """search for activities by name and optionally within a date range""" args = args or [] search = "" if args: search = args[0] (start_time, end_time), __ = dt.Range.parse(" ".join(args[1:])) start_time = start_time or dt.datetime.combine(dt.date.today(), dt.time()) end_time = end_time or start_time.replace(hour=23, minute=59, second=59) self._list(start_time, end_time, search)
def export(self, *args): args = args or [] export_format, start_time, end_time = "html", None, None if args: export_format = args[0] (start_time, end_time), __ = dt.Range.parse(" ".join(args[1:])) start_time = start_time or dt.datetime.combine(dt.date.today(), dt.time()) end_time = end_time or start_time.replace(hour=23, minute=59, second=59) facts = self.storage.get_facts(start_time, end_time) writer = reports.simple(facts, start_time.date(), end_time.date(), export_format)
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 figure_time(self, str_time): if not str_time: return None # strip everything non-numeric and consider hours to be first number # and minutes - second number numbers = re.split("\D", str_time) numbers = [x for x in numbers if x!=""] hours, minutes = None, None if len(numbers) == 1 and len(numbers[0]) == 4: hours, minutes = int(numbers[0][:2]), int(numbers[0][2:]) else: if len(numbers) >= 1: hours = int(numbers[0]) if len(numbers) >= 2: minutes = int(numbers[1]) if (hours is None or minutes is None) or hours > 24 or minutes > 60: return None # no can do return dt.time(hours, minutes)
def day_start(self): """Start of the hamster day.""" day_start_minutes = self.get("day-start-minutes") hours, minutes = divmod(day_start_minutes, 60) return dt.time(hours, minutes)
def __init__(self): Controller.__init__(self, ui_file="preferences.ui") # create and fill activity tree self.activity_tree = self.get_widget('activity_list') self.get_widget("activities_label").set_mnemonic_widget(self.activity_tree) self.activity_store = ActivityStore() self.external_listeners = [] self.activityColumn = gtk.TreeViewColumn(_("Name")) self.activityColumn.set_expand(True) self.activityCell = gtk.CellRendererText() self.external_listeners.extend([ (self.activityCell, self.activityCell.connect('edited', self.activity_name_edited_cb, self.activity_store)) ]) self.activityColumn.pack_start(self.activityCell, True) self.activityColumn.set_attributes(self.activityCell, text=1) self.activityColumn.set_sort_column_id(1) self.activity_tree.append_column(self.activityColumn) self.activity_tree.set_model(self.activity_store) self.selection = self.activity_tree.get_selection() self.external_listeners.extend([ (self.selection, self.selection.connect('changed', self.activity_changed, self.activity_store)) ]) # create and fill category tree self.category_tree = self.get_widget('category_list') self.get_widget("categories_label").set_mnemonic_widget(self.category_tree) self.category_store = CategoryStore() self.categoryColumn = gtk.TreeViewColumn(_("Category")) self.categoryColumn.set_expand(True) self.categoryCell = gtk.CellRendererText() self.external_listeners.extend([ (self.categoryCell, self.categoryCell.connect('edited', self.category_edited_cb, self.category_store)) ]) self.categoryColumn.pack_start(self.categoryCell, True) self.categoryColumn.set_attributes(self.categoryCell, text=1) self.categoryColumn.set_sort_column_id(1) self.categoryColumn.set_cell_data_func(self.categoryCell, self.unsorted_painter) self.category_tree.append_column(self.categoryColumn) self.category_store.load() self.category_tree.set_model(self.category_store) selection = self.category_tree.get_selection() self.external_listeners.extend([ (selection, selection.connect('changed', self.category_changed_cb, self.category_store)) ]) self.day_start = widgets.TimeInput(dt.time(5,30)) self.get_widget("day_start_placeholder").add(self.day_start) self.load_config() # Allow enable drag and drop of rows including row move self.activity_tree.enable_model_drag_source(gdk.ModifierType.BUTTON1_MASK, self.TARGETS, gdk.DragAction.DEFAULT| gdk.DragAction.MOVE) self.category_tree.enable_model_drag_dest(self.TARGETS, gdk.DragAction.MOVE) self.activity_tree.connect("drag_data_get", self.drag_data_get_data) self.category_tree.connect("drag_data_received", self.on_category_drop) #select first category selection = self.category_tree.get_selection() selection.select_path((0,)) self.prev_selected_activity = None self.prev_selected_category = None self.external_listeners.extend([ (self.day_start, self.day_start.connect("time-entered", self.on_day_start_changed)) ]) self.show()
def zero_hour(date): return dt.datetime.combine(date.date(), dt.time(0,0))
def test_roundtrips(self): for start_time in ( None, dt.time(12, 33), ): for end_time in ( None, dt.time(13, 34), ): for activity in ( "activity", "#123 with two #hash", "activity, with comma", "17.00 tea", ): for category in ( "", "category", ): for description in ( "", "description", "with #hash", "with, comma", "with @at", "multiline\ndescription", ): for tags in ( [], ["single"], ["with space"], ["two", "tags"], ["with @at"], ): start = dt.datetime.from_day_time( dt.hday.today(), start_time) if start_time else None end = dt.datetime.from_day_time( dt.hday.today(), end_time) if end_time else None if end and not start: # end without start is not parseable continue fact = Fact(start_time=start, end_time=end, activity=activity, category=category, description=description, tags=tags) for range_pos in ("head", "tail"): fact_str = fact.serialized( range_pos=range_pos) parsed = Fact.parse(fact_str, range_pos=range_pos) self.assertEqual(fact, parsed) self.assertEqual(parsed.range.start, fact.range.start) self.assertEqual(parsed.range.end, fact.range.end) self.assertEqual(parsed.activity, fact.activity) self.assertEqual(parsed.category, fact.category) self.assertEqual(parsed.description, fact.description) self.assertEqual(parsed.tags, fact.tags)
def on_enter_frame(self, scene, context): g = graphics.Graphics(context) rowcount, keys = len(self.keys), self.keys start_hour = 0 if self.start_time: start_hour = self.start_time end_hour = 24 * 60 if self.end_time: end_hour = self.end_time # push graph to the right, so it doesn't overlap legend_width = self.legend_width or self.longest_label(keys) self.graph_x = legend_width self.graph_x += 8 #add another 8 pixes of padding self.graph_width = self.width - self.graph_x # TODO - should handle the layout business in graphics self.layout = context.create_layout() default_font = "Sans Serif" #pango.FontDescription(self.get_style().font_desc.to_string()) default_font.set_size(8 * pango.SCALE) self.layout.set_font_description(default_font) #on the botttom leave some space for label self.layout.set_text("1234567890:") label_w, label_h = self.layout.get_pixel_size() self.graph_y, self.graph_height = 0, self.height - label_h - 4 if not self.data: #if we have nothing, let's go home return positions = {} y = 0 bar_width = min(self.graph_height / float(len(self.keys)), self.max_bar_width) for i, key in enumerate(self.keys): positions[key] = (y + self.graph_y, round(bar_width - 1)) y = y + round(bar_width) bar_width = min(self.max_bar_width, (self.graph_height - y) / float(max(1, len(self.keys) - i - 1))) max_bar_size = self.graph_width - 15 # now for the text - we want reduced contrast for relaxed visuals fg_color = "#666" #self.get_style().fg[gtk.StateType.NORMAL].to_string() label_color = self.colors.contrast(fg_color, 80) self.layout.set_alignment(pango.Alignment.RIGHT) self.layout.set_ellipsize(pango.ELLIPSIZE_END) # bars and labels self.layout.set_width(legend_width * pango.SCALE) factor = max_bar_size / float(end_hour - start_hour) # determine bar color bg_color = "#eee" #self.get_style().bg[gtk.StateType.NORMAL].to_string() base_color = self.colors.contrast(bg_color, 30) for i, label in enumerate(keys): g.set_color(label_color) self.layout.set_text(label) label_w, label_h = self.layout.get_pixel_size() context.move_to( 0, positions[label][0] + (positions[label][1] - label_h) / 2) context.show_layout(self.layout) if isinstance(self.data[i], list) == False: self.data[i] = [self.data[i]] for row in self.data[i]: bar_x = round((row[0] - start_hour) * factor) bar_size = round((row[1] - start_hour) * factor - bar_x) g.fill_area(round(self.graph_x + bar_x), positions[label][0], bar_size, positions[label][1], base_color) #white grid and scale values self.layout.set_width(-1) context.set_line_width(1) pace = ((end_hour - start_hour) / 3) / 60 * 60 last_position = positions[keys[-1]] grid_color = "#aaa" # self.get_style().bg[gtk.StateType.NORMAL].to_string() for i in range(start_hour + 60, end_hour, pace): x = round((i - start_hour) * factor) minutes = i % (24 * 60) self.layout.set_markup( dt.time(minutes / 60, minutes % 60).strftime("%-I<small><sup>%M</sup></small>")) label_w, label_h = self.layout.get_pixel_size() context.move_to(self.graph_x + x - label_w / 2, last_position[0] + last_position[1] + 4) g.set_color(label_color) context.show_layout(self.layout) g.set_color(grid_color) g.move_to(round(self.graph_x + x) + 0.5, self.graph_y) g.line_to( round(self.graph_x + x) + 0.5, last_position[0] + last_position[1]) context.stroke()