From c2856aad23230ae2a75cf612250aab09b9b34109 Mon Sep 17 00:00:00 2001 From: Grzegorz Sobczyk Date: Thu, 16 Jul 2020 00:28:27 +0200 Subject: [PATCH 01/24] Add settings for external activities and exported flag in fact (still need to be finished, WIP) --- README.md | 5 + TODO.md | 8 + data/org.gnome.hamster.gschema.xml | 27 ++++ data/preferences.ui | 230 ++++++++++++++++++++++++++++- src/hamster/lib/dbus.py | 9 +- src/hamster/lib/fact.py | 8 +- src/hamster/preferences.py | 67 +++++++++ src/hamster/storage/db.py | 29 ++-- 8 files changed, 368 insertions(+), 15 deletions(-) create mode 100644 TODO.md diff --git a/README.md b/README.md index 28c5ff0c7..5a2995a59 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,8 @@ commands). Older versions are not supported. ```bash sudo apt install gettext intltool python3-gi python3-cairo python3-distutils python3-dbus python3-xdg libglib2.0-dev libglib2.0-bin gir1.2-gtk-3.0 gtk-update-icon-cache +# and for exporting issues +sudo apt install python-tz # and for documentation sudo apt install itstool yelp ``` @@ -79,6 +81,9 @@ sudo apt install itstool yelp Leap-15.0 and Leap-15.1: ```bash sudo zypper install intltool python3-pyxdg python3-cairo python3-gobject-Gdk +# and for exporting issues +sudo zypper install python-tz +# and for documentation sudo zypper install itstool yelp ``` diff --git a/TODO.md b/TODO.md new file mode 100644 index 000000000..dce6894ed --- /dev/null +++ b/TODO.md @@ -0,0 +1,8 @@ +# Todo list + +- [x] settings for external activities +- [ ] get external activities from jira +- [ ] cinnamon applet update +- [ ] export flag on overview +- [ ] export to jira menu item +- [ ] exporting to jira diff --git a/data/org.gnome.hamster.gschema.xml b/data/org.gnome.hamster.gschema.xml index dce4c1611..5d58820a5 100644 --- a/data/org.gnome.hamster.gschema.xml +++ b/data/org.gnome.hamster.gschema.xml @@ -7,6 +7,33 @@ The folder the last report was saved to + + "" + Activities source + + The source of activities + + + + "https://jira.unity.pl" + Jira URL + + + "" + Jira user + + + "" + Jira password + + + "resolution = Unresolved AND assignee = currentUser()" + Jira query + + + "customfield_10000" + Jira category field + 330 diff --git a/data/preferences.ui b/data/preferences.ui index 37c81ecb3..2c337d296 100644 --- a/data/preferences.ui +++ b/data/preferences.ui @@ -75,9 +75,237 @@ False True - 2 + 0 + + + True + False + + + True + False + Use following external activities list if available: + + + False + True + 4 + 0 + + + + + True + False + + + + + + True + True + 1 + + + + + True + True + 1 + + + + + True + False + + + True + False + JIRA url: + + + False + True + 4 + 0 + + + + + True + False + + + + + + True + True + 1 + + + + + True + True + 8 + + + + + True + False + + + True + False + JIRA user: + + + False + True + 4 + 0 + + + + + True + False + + + + + + True + True + 1 + + + + + True + True + 9 + + + + + True + False + + + True + False + JIRA password: + + + False + True + 4 + 0 + + + + + True + False + + + + + + True + True + 1 + + + + + True + True + 10 + + + + + True + False + + + True + False + JIRA query: + + + False + True + 4 + 0 + + + + + True + False + + + + + + True + True + 1 + + + + + True + True + 11 + + + + + True + False + + + True + False + JIRA category field: + + + False + True + 4 + 0 + + + + + True + False + + + + + + True + True + 1 + + + + + True + True + 12 + + diff --git a/src/hamster/lib/dbus.py b/src/hamster/lib/dbus.py index f282e4fdc..62ca27bc9 100644 --- a/src/hamster/lib/dbus.py +++ b/src/hamster/lib/dbus.py @@ -45,7 +45,7 @@ def from_dbus_fact_json(dbus_fact): def to_dbus_fact_json(fact): """Convert Fact to D-Bus JSON (str).""" d = {} - keys = ('activity', 'category', 'description', 'tags', 'id', 'activity_id') + keys = ('activity', 'category', 'description', 'tags', 'id', 'activity_id', 'exported') for key in keys: d[key] = getattr(fact, key) # isoformat(timespec="minutes") appears only in python3.6, nevermind @@ -85,8 +85,9 @@ def to_dbus_range(range): as List of fact tags i date i delta + b exported """ -fact_signature = '(iiissisasii)' +fact_signature = '(iiissisasiib)' def from_dbus_fact(dbus_fact): @@ -101,6 +102,7 @@ def from_dbus_fact(dbus_fact): activity_id=dbus_fact[5], category=dbus_fact[6], tags=dbus_fact[7], + exported=dbus_fact[10], id=dbus_fact[0] ) @@ -120,4 +122,5 @@ def to_dbus_fact(fact): fact.category or '', dbus.Array(fact.tags, signature = 's'), to_dbus_date(fact.date), - fact.delta.days * 24 * 60 * 60 + fact.delta.seconds) + fact.delta.days * 24 * 60 * 60 + fact.delta.seconds, + fact.exported), diff --git a/src/hamster/lib/fact.py b/src/hamster/lib/fact.py index d4d67fe1c..eae4a10b3 100644 --- a/src/hamster/lib/fact.py +++ b/src/hamster/lib/fact.py @@ -24,7 +24,7 @@ class FactError(Exception): class Fact(object): def __init__(self, activity="", category=None, description=None, tags=None, range=None, start=None, end=None, start_time=None, end_time=None, - id=None, activity_id=None): + id=None, activity_id=None, exported=False): """Homogeneous chunk of activity. The category, description and tags must be passed explicitly. @@ -40,6 +40,7 @@ def __init__(self, activity="", category=None, description=None, tags=None, Mutually exclusive with `range`. start_time (dt.datetime): Deprecated. Same as start. end_time (dt.datetime): Deprecated. Same as end. + exported (bool): exported to external system flag id (int): id in the database. Should be used with extreme caution, knowing exactly why. @@ -66,6 +67,7 @@ def __init__(self, activity="", category=None, description=None, tags=None, self.range = dt.Range(start, end) self.id = id self.activity_id = activity_id + self.exported = exported # TODO: might need some cleanup def as_dict(self): @@ -79,7 +81,8 @@ def as_dict(self): 'date': calendar.timegm(date.timetuple()) if date else "", 'start_time': self.range.start if isinstance(self.range.start, str) else calendar.timegm(self.range.start.timetuple()), 'end_time': self.range.end if isinstance(self.range.end, str) else calendar.timegm(self.range.end.timetuple()) if self.range.end else "", - 'delta': self.delta.total_seconds() # ugly, but needed for report.py + 'delta': self.delta.total_seconds(), # ugly, but needed for report.py + 'exported': self.exported # needed for report.py } @property @@ -244,6 +247,7 @@ def __eq__(self, other): and self.range.end == other.range.end and self.range.start == other.range.start and self.tags == other.tags + and self.exported == other.exported ) def __repr__(self): diff --git a/src/hamster/preferences.py b/src/hamster/preferences.py index c41b1d35c..1a18e98db 100644 --- a/src/hamster/preferences.py +++ b/src/hamster/preferences.py @@ -79,6 +79,41 @@ class PreferencesEditor(Controller): def __init__(self): Controller.__init__(self, ui_file="preferences.ui") + # activities source + self.activities_sources = [("", _("None")), + # ("evo", "Evolution"), + # ("gtg", "Getting Things Gnome"), + # ("rt", "Request Tracker"), + # ("redmine", "Redmine"), + ("jira", "JIRA")] + # gtk_combo_box_text_new + self.external_combo = gtk.ComboBoxText() + self.external_combo.set_entry_text_column(0) + for code, label in self.activities_sources: + self.external_combo.append_text(label) + self.external_combo.connect("changed", self.on_external_combo_changed) + self.get_widget("external_activities_pick").add(self.external_combo) + # JIRA prefs + self.jira_url = gtk.Entry() + self.jira_url.connect("changed", self.on_jira_url_changed) + self.get_widget('jira_url').add(self.jira_url) + + self.jira_user = gtk.Entry() + self.jira_user.connect("changed", self.on_jira_user_changed) + self.get_widget('jira_user').add(self.jira_user) + + self.jira_pass = gtk.Entry() + self.jira_pass.set_visibility(False) + self.jira_pass.connect("changed", self.on_jira_pass_changed) + self.get_widget('jira_pass').add(self.jira_pass) + + self.jira_query = gtk.Entry() + self.jira_query.connect("changed", self.on_jira_query_changed) + self.get_widget('jira_query').add(self.jira_query) + + self.jira_category_field = gtk.Entry() + self.jira_category_field.connect("changed", self.on_jira_category_field_changed) + self.get_widget('jira_category_field').add(self.jira_category_field) # create and fill activity tree self.activity_tree = self.get_widget('activity_list') @@ -106,6 +141,7 @@ def __init__(self): (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) @@ -136,6 +172,7 @@ def __init__(self): 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 @@ -164,16 +201,46 @@ def __init__(self): self.show() + def show(self): self.get_widget("notebook1").set_current_page(0) self.window.show_all() + def on_jira_url_changed(self, entry): + conf.set('jira-url', self.jira_url.get_text()) + + def on_jira_user_changed(self, entry): + conf.set('jira-user', self.jira_user.get_text()) + + def on_jira_pass_changed(self, entry): + conf.set('jira-pass', self.jira_pass.get_text()) + + def on_jira_query_changed(self, entry): + conf.set('jira-query', self.jira_query.get_text()) + + def on_jira_category_field_changed(self, entry): + conf.set('jira-category-field', self.jira_category_field.get_text()) + + def on_external_combo_changed(self, combo): + conf.set("activities-source", self.activities_sources[combo.get_active()][0]) + def load_config(self, *args): self.day_start.time = conf.day_start self.tags = [tag["name"] for tag in runtime.storage.get_tags(only_autocomplete=True)] self.get_widget("autocomplete_tags").set_text(", ".join(self.tags)) + current_source = conf.get("activities-source") + for i, (code, label) in enumerate(self.activities_sources): + if code == current_source: + self.external_combo.set_active(i) + + self.jira_url.set_text(conf.get("jira-url")) + self.jira_user.set_text(conf.get("jira-user")) + self.jira_pass.set_text(conf.get("jira-pass")) + self.jira_query.set_text(conf.get("jira-query")) + self.jira_category_field.set_text(conf.get("jira-category-field")) + def on_autocomplete_tags_view_focus_out_event(self, view, event): buf = self.get_widget("autocomplete_tags") updated_tags = buf.get_text(buf.get_start_iter(), buf.get_end_iter(), 0) diff --git a/src/hamster/storage/db.py b/src/hamster/storage/db.py index 468a95f9e..92d3ca6ff 100644 --- a/src/hamster/storage/db.py +++ b/src/hamster/storage/db.py @@ -400,6 +400,7 @@ def _dbfact_to_libfact(self, db_fact): start_time=db_fact["start_time"], end_time=db_fact["end_time"], id=db_fact["id"], + exporetd=db_fact["exported"], activity_id=db_fact["activity_id"]) def __get_fact(self, id): @@ -410,7 +411,8 @@ def __get_fact(self, id): a.description as description, b.name AS name, b.id as activity_id, coalesce(c.name, ?) as category, coalesce(c.id, -1) as category_id, - e.name as tag + e.name as tag, + a.exported as exported FROM facts a LEFT JOIN activities b ON a.activity_id = b.id LEFT JOIN categories c ON b.category_id = c.id @@ -502,6 +504,7 @@ def __solve_overlaps(self, start_time, end_time): """ if end_time is None or start_time is None: return + #TODO gso: end_time and start_time round # possible combinations and the OR clauses that catch them # (the side of the number marks if it catches the end or start time) @@ -523,16 +526,20 @@ def __solve_overlaps(self, start_time, end_time): start_time, end_time)) for fact in conflicts: + if fact["start_time"] is None: + continue + # fact is a sqlite.Row, indexable by column name + # handle case with not finished activities fact_end_time = fact["end_time"] or dt.datetime.now() # won't eliminate as it is better to have overlapping entries than loosing data - if start_time < fact["start_time"] and end_time > fact_end_time: + if start_time < fact["start_time"] and end_time >= fact_end_time: continue # split - truncate until beginning of new entry and create new activity for end if fact["start_time"] < start_time < fact_end_time and \ - fact["start_time"] < end_time < fact_end_time: + fact["start_time"] < end_time <= fact_end_time: logger.info("splitting %s" % fact["name"]) # truncate until beginning of the new entry @@ -541,6 +548,8 @@ def __solve_overlaps(self, start_time, end_time): WHERE id = ?""", (start_time, fact["id"])) fact_name = fact["name"] + # TODO gso: move changes from master + # create new fact for the end new_fact = Fact(activity=fact["name"], category=fact["category"], @@ -558,13 +567,13 @@ def __solve_overlaps(self, start_time, end_time): self.execute(tag_update, (new_fact_id, fact["id"])) #clone tags # overlap start - elif start_time < fact["start_time"] < end_time: + elif start_time <= fact["start_time"] <= end_time: logger.info("Overlapping start of %s" % fact["name"]) self.execute("UPDATE facts SET start_time=? WHERE id=?", (end_time, fact["id"])) # overlap end - elif start_time < fact_end_time < end_time: + elif start_time < fact_end_time <= end_time: logger.info("Overlapping end of %s" % fact["name"]) self.execute("UPDATE facts SET end_time=? WHERE id=?", (start_time, fact["id"])) @@ -667,10 +676,10 @@ def __add_fact(self, fact, temporary=False): # finally add the new entry insert = """ - INSERT INTO facts (activity_id, start_time, end_time, description) - VALUES (?, ?, ?, ?) + INSERT INTO facts (activity_id, start_time, end_time, description, exported) + VALUES (?, ?, ?, ?, ?) """ - self.execute(insert, (activity_id, start_time, end_time, fact.description)) + self.execute(insert, (activity_id, start_time, end_time, fact.description, fact.exported)) fact_id = self.__last_insert_rowid() @@ -703,7 +712,8 @@ def __get_facts(self, range, search_terms=""): a.description as description, b.name AS name, b.id as activity_id, coalesce(c.name, ?) as category, - e.name as tag + e.name as tag, + a.exported as exported FROM facts a LEFT JOIN activities b ON a.activity_id = b.id LEFT JOIN categories c ON b.category_id = c.id @@ -722,6 +732,7 @@ def __get_facts(self, range, search_terms=""): search_terms = search_terms[4:] search_terms = search_terms.replace('\\', '\\\\').replace('%', '\\%').replace('_', '\\_').replace("'", "''") + # TODO gso: add NOT operator query += """ AND a.id %s IN (SELECT id FROM fact_index WHERE fact_index MATCH '%s')""" % ('NOT' if reverse_search_terms else '', From 42c541d88d238f3a45f26bd5e233380c40fdf752 Mon Sep 17 00:00:00 2001 From: Grzegorz Sobczyk Date: Sun, 19 Jul 2020 23:02:10 +0200 Subject: [PATCH 02/24] Update .gitignore (pycharm files) --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 5f714f959..55187aaab 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,7 @@ hamster-time-tracker-*.tar.gz .lock-waf* build *.deb +.project +.pydevproject +venv/ +.idea/ From 6893ea5ffb11afb571ccd1050b9027cbe0bebd49 Mon Sep 17 00:00:00 2001 From: Grzegorz Sobczyk Date: Mon, 20 Jul 2020 01:20:14 +0200 Subject: [PATCH 03/24] Get external activities from Jira --- README.md | 4 + TODO.md | 7 +- requirements.txt | 2 + src/hamster-service.py | 5 + src/hamster/external/__init__.py | 0 src/hamster/external/external.py | 191 +++++++++++++++++++++++++++++++ src/hamster/lib/cache.py | 44 +++++++ src/hamster/lib/dbus.py | 2 +- src/hamster/storage/db.py | 32 +++++- src/hamster/storage/storage.py | 3 + 10 files changed, 282 insertions(+), 8 deletions(-) create mode 100644 requirements.txt create mode 100644 src/hamster/external/__init__.py create mode 100644 src/hamster/external/external.py create mode 100644 src/hamster/lib/cache.py diff --git a/README.md b/README.md index 5a2995a59..241f5e4a4 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,8 @@ sudo apt install gettext intltool python3-gi python3-cairo python3-distutils pyt sudo apt install python-tz # and for documentation sudo apt install itstool yelp +# and for jira integration +sudo apt install python3-jira python3-urllib3 ``` ##### openSUSE @@ -85,6 +87,8 @@ sudo zypper install intltool python3-pyxdg python3-cairo python3-gobject-Gdk sudo zypper install python-tz # and for documentation sudo zypper install itstool yelp +# and for jira integration +sudo zypper install python3-jira python3-urllib3 ``` ##### RPM-based diff --git a/TODO.md b/TODO.md index dce6894ed..6257d855f 100644 --- a/TODO.md +++ b/TODO.md @@ -1,8 +1,11 @@ # Todo list - [x] settings for external activities -- [ ] get external activities from jira -- [ ] cinnamon applet update +- [x] get external activities from jira +- [x] cinnamon applet update +- [ ] add suggestions to hamster from external source - [ ] export flag on overview - [ ] export to jira menu item - [ ] exporting to jira +- [ ] inserting fact between start and end time of another fact +- [ ] compare all files with master diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..339cf6f64 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +urllib3 +jira diff --git a/src/hamster-service.py b/src/hamster-service.py index 4b1e43a71..2cb530727 100644 --- a/src/hamster-service.py +++ b/src/hamster-service.py @@ -403,6 +403,11 @@ def GetActivities(self, search = ""): return [(row['name'], row['category'] or '') for row in self.get_activities(search)] + @dbus.service.method("org.gnome.Hamster", in_signature='s', out_signature='a(ss)') + def GetExtActivities(self, search = ""): + return [(row['name'], row['category'] or '') for row in self.get_ext_activities(search)] + + @dbus.service.method("org.gnome.Hamster", in_signature='ii', out_signature = 'b') def ChangeCategory(self, id, category_id): return self.change_category(id, category_id) diff --git a/src/hamster/external/__init__.py b/src/hamster/external/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/hamster/external/external.py b/src/hamster/external/external.py new file mode 100644 index 000000000..94359d4d5 --- /dev/null +++ b/src/hamster/external/external.py @@ -0,0 +1,191 @@ +# - coding: utf-8 - + +# Copyright (C) 2007-2009, 2012, 2014 Toms Bauģis +# Copyright (C) 2007 Patryk Zawadzki + +# This file is part of Project Hamster. + +# Project Hamster is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# Project Hamster is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with Project Hamster. If not, see . + +import logging + +logger = logging.getLogger(__name__) # noqa: E402 + +from hamster.lib.cache import cache +from gi.repository import Gtk as gtk +import re +import urllib3 + +try: + from jira.client import JIRA +except ImportError: + JIRA = None + +SOURCE_NONE = "" +SOURCE_JIRA = 'jira' +JIRA_ISSUE_NAME_REGEX = "^(\w+-\d+): " +ERROR_ADDITIONAL_MESSAGE = '\n\nCheck settings and reopen main window.' +MIN_QUERY_LENGTH = 3 +CURRENT_USER_ACTIVITIES_LIMIT = 5 + + +class ExternalSource(object): + def __init__(self, conf): + logger.debug('external init') + # gobject.GObject.__init__(self) + self.source = conf.get("activities-source") + # self.__gtg_connection = None + self.jira = None + self.jira_projects = None + self.jira_issue_types = None + self.jira_query = None + self.__http = None + + try: + self.__connect(conf) + except Exception as e: + error_msg = self.source + ' connection failed: ' + str(e) + self.on_error(error_msg + ERROR_ADDITIONAL_MESSAGE) + logger.warning(error_msg) + self.source = SOURCE_NONE + + def __connect(self, conf): + if JIRA and self.source == SOURCE_JIRA: + self.__http = urllib3.PoolManager() + self.__connect_to_jira(conf) + + def __connect_to_jira(self, conf): + self.jira_url = conf.get("jira-url") + self.jira_user = conf.get("jira-user") + self.jira_pass = conf.get("jira-pass") + self.jira_query = conf.get("jira-query") + self.jira_category = conf.get("jira-category-field") + self.jira_fields = ','.join(['summary', self.jira_category, 'issuetype']) + logger.info("user: %s, pass: *****" % self.jira_user) + if self.jira_url and self.jira_user and self.jira_pass and self.__is_connected(self.jira_url): + options = {'server': self.jira_url} + self.jira = JIRA(options, basic_auth=(self.jira_user, self.jira_pass), validate=True) + self.jira_projects = self.__get_jira_projects() + self.jira_issue_types = self.__get_jira_issue_types() + else: + self.on_error("Invalid Jira credentials") + self.source = SOURCE_NONE + + def get_activities(self, query=None): + if not self.source or not query: + return [] + # elif self.source == SOURCE_EVOLUTION: + # return [activity for activity in get_eds_tasks() + # if query is None or activity['name'].startswith(query)] + elif self.source == SOURCE_JIRA: + activities = self.__extract_from_jira(query, self.jira_query) + direct_issue = None + if query and re.match("^[a-zA-Z][a-zA-Z0-9]*-[0-9]+$", query): + if self.is_issue_from_existing_jira_project(query): + issue = self.jira.issue(query.upper()) + if issue: + direct_issue = self.__extract_activity_from_jira_issue(issue) + if direct_issue and direct_issue not in activities: + activities.append(direct_issue) + if len(activities) <= CURRENT_USER_ACTIVITIES_LIMIT and not direct_issue and len(query) >= MIN_QUERY_LENGTH: + li = query.split(' ') + fragments = filter(len, [self.__generate_fragment_jira_query(word) for word in li]) + jira_query = " AND ".join( + fragments) + " AND resolution = Unresolved order by priority desc, updated desc" + logging.warn(jira_query) + third_activities = self.__extract_from_jira('', jira_query) + if activities and third_activities: + activities.append({"name": "---------------------", "category": "other open"}) + activities.extend(third_activities) + return activities + + def __generate_fragment_jira_query(self, word): + if word.upper() in self.jira_projects: + return "project = " + word.upper() + elif word.lower() in self.jira_issue_types: + return "issuetype = " + word.lower() + elif word: + return "(assignee = '%s' OR summary ~ '%s*')" % (word, word) + else: + return "" + + def get_ticket_category(self, activity_id): + """get activity category depends on source""" + if not self.source: + return "" + + if self.source == SOURCE_JIRA: + # try: + issue = self.jira.issue(activity_id) + return self.__extract_activity_from_jira_issue(issue) + else: + return "" + + def __extract_activity_from_jira_issue(self, issue): + activity = {} + issue_id = issue.key + activity['name'] = str(issue_id) + ': ' + issue.fields.summary.replace(",", " ") + activity['rt_id'] = issue_id + if hasattr(issue.fields, self.jira_category): + activity['category'] = str(getattr(issue.fields, self.jira_category)) + else: + activity['category'] = "" + if not activity['category'] or activity['category'] == "None": + try: + activity['category'] = "%s/%s (%s)" % ( + getattr(issue.fields, 'project').key, getattr(issue.fields, 'issuetype').name, + getattr(issue.fields, 'project').name) + except Exception as e: + logger.warn(str(e)) + return activity + + def __extract_from_jira(self, query='', jira_query=None): + activities = [] + try: + results = self.__search_jira_issues(jira_query) + for issue in results: + activity = self.__extract_activity_from_jira_issue(issue) + if query is None or all(item in activity['name'].lower() for item in query.lower().split(' ')): + activities.append(activity) + except Exception as e: + logger.warn(e) + return activities + + def __get_jira_projects(self): + return [project.key for project in self.jira.projects()] + + def __get_jira_issue_types(self): + return [issuetype.name.lower() for issuetype in self.jira.issue_types()] + + @cache(seconds=30) + def __search_jira_issues(self, jira_query=None): + return self.jira.search_issues(jira_query, fields=self.jira_fields, maxResults=100) + + def on_error(self, msg): + md = gtk.MessageDialog(None, + 0, gtk.MessageType.ERROR, + gtk.ButtonsType.CLOSE, msg) + md.run() + md.destroy() + + # https://stackoverflow.com/questions/3764291/checking-network-connection + def __is_connected(self, url): + try: + self.__http.request('GET', url, timeout=1) + return True + except urllib3.HTTPError as err: + return False + + def is_issue_from_existing_jira_project(self, issue): + return issue.split('-', 1)[0].upper() in self.jira_projects diff --git a/src/hamster/lib/cache.py b/src/hamster/lib/cache.py new file mode 100644 index 000000000..751e1e327 --- /dev/null +++ b/src/hamster/lib/cache.py @@ -0,0 +1,44 @@ +# - coding: utf-8 - + +# Copyright (C) 2008-2010 Toms Bauģis + +# This file is part of Project Hamster. + +# Project Hamster is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# Project Hamster is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with Project Hamster. If not, see . + +from datetime import datetime, timedelta +import functools + + +def cache(seconds: int, maxsize: int = 128, typed: bool = False): + """ + copied from https://gist.github.com/Morreski/c1d08a3afa4040815eafd3891e16b945 + """ + + def wrapper_cache(func): + func = functools.lru_cache(maxsize=maxsize, typed=typed)(func) + func.delta = timedelta(seconds=seconds) + func.expiration = datetime.utcnow() + func.delta + + @functools.wraps(func) + def wrapped_func(*args, **kwargs): + if datetime.utcnow() >= func.expiration: + func.cache_clear() + func.expiration = datetime.utcnow() + func.delta + + return func(*args, **kwargs) + + return wrapped_func + + return wrapper_cache diff --git a/src/hamster/lib/dbus.py b/src/hamster/lib/dbus.py index 62ca27bc9..36779784f 100644 --- a/src/hamster/lib/dbus.py +++ b/src/hamster/lib/dbus.py @@ -123,4 +123,4 @@ def to_dbus_fact(fact): dbus.Array(fact.tags, signature = 's'), to_dbus_date(fact.date), fact.delta.days * 24 * 60 * 60 + fact.delta.seconds, - fact.exported), + fact.exported) diff --git a/src/hamster/storage/db.py b/src/hamster/storage/db.py index 92d3ca6ff..346c22dc9 100644 --- a/src/hamster/storage/db.py +++ b/src/hamster/storage/db.py @@ -36,6 +36,7 @@ import hamster from hamster.lib import datetime as dt +from hamster.external.external import ExternalSource from hamster.lib.configuration import conf from hamster.lib.fact import Fact from hamster.storage import storage @@ -47,6 +48,8 @@ class Storage(storage.Storage): con = None # Connection will be created on demand + external = None + external_need_update = False def __init__(self, unsorted_localized="Unsorted", database_dir=None): """Database storage. @@ -101,6 +104,8 @@ def on_db_file_change(monitor, gio_file, event_uri, event): self.run_fixtures() + self.external = ExternalSource(conf) + def __init_db_file(self, database_dir): if not database_dir: try: @@ -400,7 +405,7 @@ def _dbfact_to_libfact(self, db_fact): start_time=db_fact["start_time"], end_time=db_fact["end_time"], id=db_fact["id"], - exporetd=db_fact["exported"], + exported=db_fact["exported"], activity_id=db_fact["activity_id"]) def __get_fact(self, id): @@ -412,7 +417,7 @@ def __get_fact(self, id): b.name AS name, b.id as activity_id, coalesce(c.name, ?) as category, coalesce(c.id, -1) as category_id, e.name as tag, - a.exported as exported + a.exported AS exported FROM facts a LEFT JOIN activities b ON a.activity_id = b.id LEFT JOIN categories c ON b.category_id = c.id @@ -443,7 +448,7 @@ def __group_tags(self, facts): # we need dict so we can modify it (sqlite.Row is read only) # in python 2.5, sqlite does not have keys() yet, so we hardcode them (yay!) keys = ["id", "start_time", "end_time", "description", "name", - "activity_id", "category", "tag"] + "activity_id", "category", "tag", "exported"] grouped_fact = dict([(key, grouped_fact[key]) for key in keys]) grouped_fact["tags"] = [ft["tag"] for ft in fact_tags if ft["tag"]] @@ -772,6 +777,9 @@ def __get_category_activities(self, category_id): return self.fetchall(query, (category_id, )) + def __get_ext_activities(self, search): + return self.get_external().get_activities(search) + def __get_activities(self, search): """returns list of activities for autocomplete, activity names converted to lowercase""" @@ -873,7 +881,8 @@ def __check_index(self, start_date, end_date): a.description as description, b.name AS name, b.id as activity_id, coalesce(c.name, ?) as category, - e.name as tag + e.name as tag, + a.exported AS exported FROM facts a LEFT JOIN activities b ON a.activity_id = b.id LEFT JOIN categories c ON b.category_id = c.id @@ -981,7 +990,7 @@ def run_fixtures(self): """upgrade DB to hamster version""" version = self.fetchone("SELECT version FROM version")["version"] - current_version = 9 + current_version = 10 if version < 8: # working around sqlite's utf-f case sensitivity (bug 624438) @@ -1004,6 +1013,10 @@ def run_fixtures(self): # adding full text search self.execute("""CREATE VIRTUAL TABLE fact_index USING fts3(id, name, category, description, tag)""") + if version < 10: + # adding exported + self.execute("""ALTER TABLE facts ADD COLUMN exported bool default false""") + self.execute("""UPDATE facts set exported=1""") # at the happy end, update version number @@ -1014,6 +1027,15 @@ def run_fixtures(self): self.end_transaction() + def get_external(self): + if self.external_need_update: + self.refresh_external(conf) + return self.external + + def refresh_external(self, conf): + self.external = ExternalSource(conf) + self.external_need_update = False + # datetime/sql conversions diff --git a/src/hamster/storage/storage.py b/src/hamster/storage/storage.py index c16796241..28a821787 100644 --- a/src/hamster/storage/storage.py +++ b/src/hamster/storage/storage.py @@ -210,6 +210,9 @@ def get_category_activities(self, category_id = -1): def get_activities(self, search = ""): return self.__get_activities(search) + def get_ext_activities(self, search = ""): + return self.__get_ext_activities(search) + def change_category(self, id, category_id): changed = self.__change_category(id, category_id) if changed: From 4c6fd6eb371987f104221ceb2904e9d2b9ff2f05 Mon Sep 17 00:00:00 2001 From: Grzegorz Sobczyk Date: Wed, 22 Jul 2020 09:38:39 +0200 Subject: [PATCH 04/24] Add export feature from overview window - exported flag into overview window - added export feature from overview window - exporting worklogs to jira (only finished activities) - added few shortcuts --- NEWS.md | 7 ++ TODO.md | 10 +- data/edit_activity.ui | 81 ++++++++++++---- src/hamster-service.py | 19 +++- src/hamster-windows-service.py | 4 + src/hamster/client.py | 7 ++ src/hamster/edit_activity.py | 8 ++ src/hamster/external/external.py | 49 +++++++++- src/hamster/lib/configuration.py | 13 +++ src/hamster/lib/fact.py | 5 +- src/hamster/lib/parsing.py | 5 +- src/hamster/overview.py | 162 +++++++++++++++++++++++-------- src/hamster/preferences.py | 1 + src/hamster/storage/db.py | 10 +- src/hamster/storage/storage.py | 4 +- src/hamster/widgets/facttree.py | 18 +++- tests/test_stuff.py | 15 +++ 17 files changed, 345 insertions(+), 73 deletions(-) diff --git a/NEWS.md b/NEWS.md index 83509b0b4..7253fa6d4 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,10 @@ +## Changes in 3.1.0 + +* Added new shortcuts: + - Ctrl-=: clone or fallback to new if none selected. + - e: edit selected activity + - x: toggle export flag + ## Changes in 3.0.2 * Switch from deprecated xml2po to itstool for translating help files diff --git a/TODO.md b/TODO.md index 6257d855f..625dd385a 100644 --- a/TODO.md +++ b/TODO.md @@ -3,9 +3,13 @@ - [x] settings for external activities - [x] get external activities from jira - [x] cinnamon applet update +- [x] export flag on overview +- [x] exporting window +- [x] exporting to jira + - [x] only finished activities +- [-] exporter invoked from command line (today facts) +- [-] export to jira item in menu - [ ] add suggestions to hamster from external source -- [ ] export flag on overview -- [ ] export to jira menu item -- [ ] exporting to jira - [ ] inserting fact between start and end time of another fact - [ ] compare all files with master +- [x] integrate exporting with overview window diff --git a/data/edit_activity.ui b/data/edit_activity.ui index f3e378bf7..4d16c534a 100644 --- a/data/edit_activity.ui +++ b/data/edit_activity.ui @@ -1,5 +1,5 @@ - + @@ -18,6 +18,9 @@ True + + + True @@ -431,29 +434,71 @@ - + True - False - vertical + True - + True False - start - tags - - - - + vertical + + + True + False + start + tags + + + + + + + False + False + 0 + + + + + - False - False - 0 + False + True - + + True + False + vertical + 3 + + + True + False + start + exported + + + + + + + False + False + 0 + + + + + + + + False + True + @@ -531,10 +576,10 @@ 6 + + + - - - diff --git a/src/hamster-service.py b/src/hamster-service.py index 2cb530727..280d077a0 100644 --- a/src/hamster-service.py +++ b/src/hamster-service.py @@ -299,6 +299,23 @@ def GetFacts(self, start_date, end_date, search_terms): s search_terms: Bleh. If starts with "not ", the search terms will be reversed Returns an array of D-Bus fact structures. + Legacy. To be superceded by GetFactsJSON at some point. + """ + self.GetFactsLimited(start_date, end_date, search_terms, 0, True) + + @dbus.service.method("org.gnome.Hamster", + in_signature='uusib', + out_signature='a{}'.format(fact_signature)) + def GetFactsLimited(self, start_date, end_date, search_terms, limit, asc_by_date): + """Gets facts between the day of start_date and the day of end_date. + Parameters: + i start_date: Seconds since epoch (timestamp). Use 0 for today + i end_date: Seconds since epoch (timestamp). Use 0 for today + s search_terms: Bleh. If starts with "not ", the search terms will be reversed + i limit: 10 + b asc_by_date: True + Returns an array of D-Bus fact structures. + Legacy. To be superceded by GetFactsJSON at some point. """ #TODO: Assert start > end ? @@ -310,7 +327,7 @@ def GetFacts(self, start_date, end_date, search_terms): if end_date: end = dt.datetime.utcfromtimestamp(end_date).date() - return [to_dbus_fact(fact) for fact in self.get_facts(start, end, search_terms)] + return [to_dbus_fact(fact) for fact in self.get_facts(start, end, search_terms, limit, asc_by_date)] @dbus.service.method("org.gnome.Hamster", diff --git a/src/hamster-windows-service.py b/src/hamster-windows-service.py index 37bcad907..0f3f9d1a0 100644 --- a/src/hamster-windows-service.py +++ b/src/hamster-windows-service.py @@ -76,6 +76,10 @@ def about(self): def preferences(self): self._open_window("prefs") + @dbus.service.method("org.gnome.Hamster.WindowServer") + def exporter(self): + self._open_window("exporter") + if __name__ == '__main__': from hamster.lib import i18n diff --git a/src/hamster/client.py b/src/hamster/client.py index 736d23249..93ad3ab1a 100644 --- a/src/hamster/client.py +++ b/src/hamster/client.py @@ -162,6 +162,13 @@ def get_activities(self, search = ""): """ return self._to_dict(('name', 'category'), self.conn.GetActivities(search)) + def get_ext_activities(self, search = ""): + """returns list of activities name matching search criteria. + results are sorted by most recent usage. + search is case insensitive + """ + return self._to_dict(('name', 'category'), self.conn.GetExtActivities(search)) + def get_categories(self): """returns list of categories""" return self._to_dict(('id', 'name'), self.conn.GetCategories()) diff --git a/src/hamster/edit_activity.py b/src/hamster/edit_activity.py index 59c2d6f43..4a3c40287 100644 --- a/src/hamster/edit_activity.py +++ b/src/hamster/edit_activity.py @@ -82,6 +82,9 @@ def __init__(self, action, fact_id=None): self.tags_entry = widgets.TagsEntry() self.get_widget("tags box").add(self.tags_entry) + self.exported_checkbox = gtk.CheckButton(label=_("do not export")) + self.get_widget("exported box").add(self.exported_checkbox) + self.save_button = self.get_widget("save_button") # this will set self.master_is_cmdline @@ -123,6 +126,7 @@ def __init__(self, action, fact_id=None): self.activity_entry.connect("changed", self.on_activity_changed) self.category_entry.connect("changed", self.on_category_changed) self.tags_entry.connect("changed", self.on_tags_changed) + self.exported_checkbox.connect("toggled", self.on_exported_toggled) self._gui.connect_signals(self) self.validate_fields() @@ -283,6 +287,9 @@ def on_tags_changed(self, widget): self.fact.tags = self.tags_entry.get_tags() self.update_cmdline() + def on_exported_toggled(self, widget): + self.fact.exported = self.exported_checkbox.get_active() + def present(self): self.window.present() @@ -308,6 +315,7 @@ def update_fields(self): self.category_entry.set_text(self.fact.category) self.description_buffer.set_text(self.fact.description) self.tags_entry.set_tags(self.fact.tags) + self.exported_checkbox.set_active(self.fact.exported) self.validate_fields() def update_status(self, status, markup): diff --git a/src/hamster/external/external.py b/src/hamster/external/external.py index 94359d4d5..eca5b79fa 100644 --- a/src/hamster/external/external.py +++ b/src/hamster/external/external.py @@ -19,6 +19,9 @@ # along with Project Hamster. If not, see . import logging +import time + +from hamster.lib import Fact, stuff logger = logging.getLogger(__name__) # noqa: E402 @@ -34,7 +37,7 @@ SOURCE_NONE = "" SOURCE_JIRA = 'jira' -JIRA_ISSUE_NAME_REGEX = "^(\w+-\d+): " +JIRA_ISSUE_NAME_REGEX = "^(\w+-\d+):? " ERROR_ADDITIONAL_MESSAGE = '\n\nCheck settings and reopen main window.' MIN_QUERY_LENGTH = 3 CURRENT_USER_ACTIVITIES_LIMIT = 5 @@ -46,7 +49,7 @@ def __init__(self, conf): # gobject.GObject.__init__(self) self.source = conf.get("activities-source") # self.__gtg_connection = None - self.jira = None + self.jira: JIRA = None self.jira_projects = None self.jira_issue_types = None self.jira_query = None @@ -189,3 +192,45 @@ def __is_connected(self, url): def is_issue_from_existing_jira_project(self, issue): return issue.split('-', 1)[0].upper() in self.jira_projects + + def __add_jira_worklog(self, issue_id, text, start_time, time_worked): + """ + :type start_time: date + :param time_worked: int time spent in minutes + """ + logger.info(_("updating issue #%s: %s min, comment: \n%s") % (issue_id, time_worked, text)) + self.jira.add_worklog(issue=issue_id, comment=text, started=start_time, timeSpent="%sm" % time_worked) + + def get_text(self, fact: Fact): + text = "" + if fact.description: + text += "%s\n" % (fact.description) + text += "%s, %s-%s" % (fact.date, fact.range.start.strftime("%H:%M"), fact.range.end.strftime("%H:%M")) + if fact.tags: + text += " ("+", ".join(fact.tags)+")" + return text + + def export(self, fact: Fact) -> bool: + """ + :return: bool fact was exported + """ + logger.info("Exporting %s" % fact.activity) + if not fact.range.end: + logger.info("Skipping fact without end date") + return False + if self.source == SOURCE_JIRA: + jira_match = re.match(JIRA_ISSUE_NAME_REGEX, fact.activity) + if jira_match: + issue_id = jira_match.group(1) + comment = self.get_text(fact) + time_worked = stuff.duration_minutes(fact.delta) + try: + self.__add_jira_worklog(issue_id, comment, fact.range.start, int(time_worked)) + return True + except Exception as e: + logger.error(e) + else: + logger.warning("skipping fact %s - unknown issue" % fact.activity) + else: + logger.warning("invalid source, don't know where export to") + return False diff --git a/src/hamster/lib/configuration.py b/src/hamster/lib/configuration.py index 24b42c668..c12915c3b 100644 --- a/src/hamster/lib/configuration.py +++ b/src/hamster/lib/configuration.py @@ -22,6 +22,9 @@ """ import logging + +from hamster.external.external import ExternalSource + logger = logging.getLogger(__name__) # noqa: E402 import os @@ -109,6 +112,8 @@ class RuntimeStore(Singleton): data_dir = "" home_data_dir = "" storage = None + external = None + external_need_update = True def __init__(self): self.version = hamster.__version__ @@ -124,6 +129,14 @@ def __init__(self): self.storage = Storage() self.home_data_dir = os.path.realpath(os.path.join(xdg_data_home, "hamster")) + def get_external(self) -> ExternalSource: + if self.external_need_update: + self.refresh_external(conf) + return self.external + + def refresh_external(self, conf): + self.external = ExternalSource(conf) + self.external_need_update = False runtime = RuntimeStore() diff --git a/src/hamster/lib/fact.py b/src/hamster/lib/fact.py index eae4a10b3..ed96c4199 100644 --- a/src/hamster/lib/fact.py +++ b/src/hamster/lib/fact.py @@ -220,11 +220,12 @@ def serialized(self, range_pos="head", default_day=None): explicit_none=need_explicit) # no need for space if name or datetime is missing space = " " if name and datetime else "" + exported_marker = "[x] " if self.exported else "" assert range_pos in ("head", "tail") if range_pos == "head": - return "{}{}{}".format(datetime, space, name) + return "{}{}{}{}".format(exported_marker, datetime, space, name) else: - return "{}{}{}".format(name, space, datetime) + return "{}{}{}{}".format(exported_marker, name, space, datetime) def _set(self, **kwds): """Modify attributes. diff --git a/src/hamster/lib/parsing.py b/src/hamster/lib/parsing.py index 527c574c9..edec51500 100644 --- a/src/hamster/lib/parsing.py +++ b/src/hamster/lib/parsing.py @@ -37,7 +37,7 @@ def parse_fact(text, range_pos="head", default_day=None, ref="now"): Returns found fields as a dict. Tentative syntax (not accurate): - start [- end_time] activity[@category][,, description][,,]{ #tag} + [[x] ]start [- end_time] activity[@category][,, description][,,]{ #tag} According to the legacy tests, # were allowed in the description """ @@ -47,6 +47,9 @@ def parse_fact(text, range_pos="head", default_day=None, ref="now"): if not text: return res + res["exported"] = text.startswith("[x]") + text = text.replace("[x]", "", 1).strip() + # datetimes # force at least a space to avoid matching 10.00@cat (start, end), remaining_text = dt.Range.parse(text, position=range_pos, diff --git a/src/hamster/overview.py b/src/hamster/overview.py index b83fce159..2200d7ed3 100644 --- a/src/hamster/overview.py +++ b/src/hamster/overview.py @@ -17,35 +17,33 @@ # You should have received a copy of the GNU General Public License # along with Project Hamster. If not, see . +import logging + +logger = logging.getLogger(__name__) # noqa: E402 + import sys -import bisect -import itertools +import threading import webbrowser - from collections import defaultdict from math import ceil +import cairo from gi.repository import GLib as glib -from gi.repository import Gtk as gtk -from gi.repository import Gdk as gdk from gi.repository import GObject as gobject -from gi.repository import PangoCairo as pangocairo +from gi.repository import Gdk as gdk +from gi.repository import Gtk as gtk from gi.repository import Pango as pango -import cairo +from gi.repository import PangoCairo as pangocairo import hamster.client +from hamster import reports +from hamster import widgets from hamster.lib import datetime as dt from hamster.lib import graphics from hamster.lib import layout -from hamster import reports from hamster.lib import stuff -from hamster import widgets - -from hamster.lib.configuration import Controller - - +from hamster.lib.configuration import Controller, runtime from hamster.lib.pytweener import Easing - from hamster.widgets.dates import RangePick from hamster.widgets.facttree import FactTree @@ -91,10 +89,9 @@ def __init__(self): self.add_activity_button.set_tooltip_markup(_("Add activity (Ctrl-+)")) self.pack_end(self.add_activity_button) - self.system_menu = gtk.Menu() self.system_button.set_popup(self.system_menu) - self.menu_export = gtk.MenuItem(label=_("Export...")) + self.menu_export = gtk.MenuItem(label=_("Export to file...")) self.system_menu.append(self.menu_export) self.menu_prefs = gtk.MenuItem(label=_("Tracking Settings")) self.system_menu.append(self.menu_prefs) @@ -102,7 +99,6 @@ def __init__(self): self.system_menu.append(self.menu_help) self.system_menu.show_all() - self.time_back.connect("clicked", self.on_time_back_click) self.time_forth.connect("clicked", self.on_time_forth_click) self.connect("button-press-event", self.on_button_press) @@ -141,7 +137,6 @@ def __init__(self, width=0, height=0, vertical=None, **kwargs): self._seen_keys = [] - def set_items(self, items): """expects a list of key, value to work with""" res = [] @@ -150,7 +145,6 @@ def set_items(self, items): res.append((key, val, val * 1.0 / max_value)) self._items = res - def _take_color(self, key): if key in self._seen_keys: index = self._seen_keys.index(key) @@ -159,7 +153,6 @@ def _take_color(self, key): index = len(self._seen_keys) - 1 return self.colors[index % len(self.colors)] - def on_render(self, sprite): if not self._items: self.graphics.clear() @@ -177,6 +170,7 @@ def on_render(self, sprite): class Label(object): """a much cheaper label that would be suitable for cellrenderer""" + def __init__(self, x=0, y=0, color=None, use_markup=False): self.x = x self.y = y @@ -212,7 +206,7 @@ def __init__(self, **kwargs): 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 + 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) @@ -262,6 +256,96 @@ def _draw(self, context, opacity, matrix): g.restore_context() +class Exporter(gtk.Box): + def __init__(self, storage: hamster.client.Storage): + gtk.Box.__init__(self, orientation=gtk.Orientation.HORIZONTAL, spacing=8) + self.set_border_width(12) + + self.progressbar = gtk.ProgressBar() + self.progressbar.set_show_text(True) + self.pack_start(self.progressbar, True, True, 0) + + self.start_button = gtk.Button.new_with_label(_("📤 Start export")) + self.start_button.connect("clicked", self.on_start_button_clicked) + self.pack_start(self.start_button, False, False, 1) + + self.connect("destroy", self.on_destroy_event) + + self.storage = storage + self.export_thread: ExportThread = None + self.facts = [] + + def _init_labels(self): + if not self.export_thread: + self.progressbar.set_text(_("Waiting for action (%s activities to export)") % len(self.facts)) + self.progressbar.set_fraction(0) + self.start_button.set_label(_("📤 Start export")) + self.start_button.set_sensitive(True) + + def set_facts(self, facts): + self.facts = list(filter(lambda f: not f.exported, facts)) + self._init_labels() + + def on_start_button_clicked(self, button): + if not self.export_thread: + self.export_thread = ExportThread(self.facts, self._update_progressbar, self._finish_export, self.storage) + self.start_button.set_sensitive(False) + self.start_button.set_label(_("Exporting...")) + # start the thread + self.export_thread.start() + + def _finish_export(self, interrupted): + if interrupted: + self.progressbar.set_text(_("Interrupted")) + else: + self.progressbar.set_fraction(1.0) + self.progressbar.set_text(_("Done")) + self.start_button.set_label(_("Done")) + self.start_button.set_sensitive(False) + self.export_thread = None + + def _update_progressbar(self, fraction, label): + self.progressbar.set_fraction(fraction) + self.progressbar.set_text(label) + # self.done_button.set_label(_("Stop")) + + def on_destroy_event(self, event): + if self.export_thread: + self.export_thread.shutdown() + +class ExportThread(threading.Thread): + def __init__(self, facts, callback, finish_callback, storage: hamster.client.Storage): + threading.Thread.__init__(self) + self.storage = storage + self.facts = facts + self.callback = callback + self.finish_callback = finish_callback + self.steps = (len(self.facts) + 1) + self.interrupt = False + + def run(self): + glib.idle_add(self.callback, 0.0, _("Connecting to external source...")) + external = runtime.get_external() + for idx, fact in enumerate(self.facts): + if self.interrupt: + logger.info("Interrupting export thread") + break + fraction = float(idx + 1) / self.steps + label = _("Exporting: %s - %s") % (fact.activity, fact.delta) + glib.idle_add(self.callback, fraction, label) + exported = external.export(fact) + if exported: + # TODO mark as exported + fact.exported = True + self.storage.update_fact(fact.id, fact, False) + pass + # TODO external.report(fact) + glib.idle_add(self.finish_callback, self.interrupt) + + def shutdown(self): + logger.info("Trying to shutdown") + self.interrupt = True + class Totals(graphics.Scene): def __init__(self): @@ -301,9 +385,6 @@ def __init__(self): main.add_child(self.activities_chart, self.categories_chart, self.tag_chart) - - - # for use in animation self.height_proxy = graphics.Sprite(x=0) self.height_proxy.height = 70 @@ -315,7 +396,6 @@ def __init__(self): self.connect("state-flags-changed", self.on_state_flags_changed) self.connect("style-updated", self.on_style_changed) - def set_facts(self, facts): totals = defaultdict(lambda: defaultdict(dt.timedelta)) for fact in facts: @@ -325,7 +405,6 @@ def set_facts(self, facts): for tag in fact.tags: totals["tag"][tag] += fact.delta - for key, group in totals.items(): totals[key] = sorted(group.items(), key=lambda x: x[1], reverse=True) self.totals = totals @@ -339,9 +418,9 @@ def set_facts(self, facts): grand_total = sum(delta.total_seconds() / 60 for __, delta in totals['activity']) self.category_totals.markup = "Total: %s; " % stuff.format_duration(grand_total) - self.category_totals.markup += ", ".join("%s: %s" % (stuff.escape_pango(cat), stuff.format_duration(hours)) for cat, hours in totals['category']) - - + self.category_totals.markup += ", ".join( + "%s: %s" % (stuff.escape_pango(cat), stuff.format_duration(hours)) for cat, hours in + totals['category']) def on_click(self, scene, sprite, event): self.collapsed = not self.collapsed @@ -367,7 +446,6 @@ def delayed_leave(sprite): on_complete=delayed_leave, on_update=lambda sprite: sprite.redraw()) - def on_mouse_leave(self, scene, event): if not self.collapsed: return @@ -387,6 +465,7 @@ def on_style_changed(self, _): def change_height(self, new_height): self.stop_animation(self.height_proxy) + def on_update_dummy(sprite): self.set_size_request(200, sprite.height) @@ -417,7 +496,7 @@ def __init__(self): self.window.set_position(gtk.WindowPosition.CENTER) self.window.set_default_icon_name("org.gnome.Hamster.GUI") - self.window.set_default_size(700, 500) + self.window.set_default_size(1000, 700) self.storage = hamster.client.Storage() self.storage.connect("facts-changed", self.on_facts_changed) @@ -431,7 +510,6 @@ def __init__(self): self.report_chooser = None - self.search_box = gtk.Revealer() space = gtk.Box(border_width=5) @@ -445,16 +523,19 @@ def __init__(self): space.pack_start(self.filter_entry, True, True, 0) main.pack_start(self.search_box, False, True, 0) - window = gtk.ScrolledWindow() window.set_policy(gtk.PolicyType.NEVER, gtk.PolicyType.AUTOMATIC) self.fact_tree = FactTree() self.fact_tree.connect("on-activate-row", self.on_row_activated) self.fact_tree.connect("on-delete-called", self.on_row_delete_called) + self.fact_tree.connect("on-toggle-exported-row", self.on_row_toggle_exported_called) window.add(self.fact_tree) main.pack_start(window, True, True, 1) + self.exporter = Exporter(self.storage) + main.pack_start(self.exporter, False, True, 1) + self.totals = Totals() main.pack_start(self.totals, False, True, 1) @@ -470,7 +551,6 @@ def __init__(self): self.header_bar.menu_export.connect("activate", self.on_export_clicked) self.header_bar.menu_help.connect("activate", self.on_help_clicked) - self.window.connect("key-press-event", self.on_key_press) self.facts = [] @@ -480,7 +560,6 @@ def __init__(self): gobject.timeout_add_seconds(60, self.on_timeout) self.window.show_all() - def on_key_press(self, window, event): if self.filter_entry.has_focus(): if event.keyval == gdk.KEY_Escape: @@ -503,7 +582,7 @@ def on_key_press(self, window, event): if self.fact_tree.has_focus() or self.totals.has_focus(): if event.keyval == gdk.KEY_Tab: - pass # TODO - deal with tab as our scenes eat up navigation + pass # TODO - deal with tab as our scenes eat up navigation if event.state & gdk.ModifierType.CONTROL_MASK: # the ctrl+things @@ -516,7 +595,7 @@ def on_key_press(self, window, event): self.start_new_fact(clone_selected=True, fallback=False) elif event.keyval == gdk.KEY_space: self.storage.stop_tracking() - elif event.keyval in (gdk.KEY_KP_Add, gdk.KEY_plus): + elif event.keyval in (gdk.KEY_KP_Add, gdk.KEY_plus, gdk.KEY_equal): # same as pressing the + icon self.start_new_fact(clone_selected=True, fallback=True) @@ -527,10 +606,11 @@ def find_facts(self): start, end = self.header_bar.range_pick.get_range() search_active = self.header_bar.search_button.get_active() search = "" if not search_active else self.filter_entry.get_text() - search = "%s*" % search if search else "" # search anywhere + search = "%s*" % search if search else "" # search anywhere self.facts = self.storage.get_facts(start, end, search_terms=search) self.fact_tree.set_facts(self.facts) self.totals.set_facts(self.facts) + self.exporter.set_facts(self.facts) self.header_bar.stop_button.set_sensitive( self.facts and not self.facts[-1].end_time) @@ -566,6 +646,10 @@ def on_row_delete_called(self, tree, fact): self.storage.remove_fact(fact.id) self.find_facts() + def on_row_toggle_exported_called(self, tree, fact): + fact.exported = not fact.exported + self.storage.update_fact(fact.id, fact) + def on_search_toggled(self, button): active = button.get_active() self.search_box.set_reveal_child(active) @@ -613,7 +697,7 @@ def on_report_chosen(widget, format, path): try: gtk.show_uri(None, "file://%s" % path, gdk.CURRENT_TIME) except: - pass # bug 626656 - no use in capturing this one i think + pass # bug 626656 - no use in capturing this one i think def on_report_chooser_closed(widget): self.report_chooser = None diff --git a/src/hamster/preferences.py b/src/hamster/preferences.py index 1a18e98db..be76b8eaa 100644 --- a/src/hamster/preferences.py +++ b/src/hamster/preferences.py @@ -574,5 +574,6 @@ def on_day_start_changed(self, widget): day_start = day_start.hour * 60 + day_start.minute conf.set("day-start-minutes", day_start) + def on_close_button_clicked(self, button): self.close_window() diff --git a/src/hamster/storage/db.py b/src/hamster/storage/db.py index 346c22dc9..6a7ef7bb1 100644 --- a/src/hamster/storage/db.py +++ b/src/hamster/storage/db.py @@ -704,7 +704,7 @@ def __last_insert_rowid(self): def __get_todays_facts(self): return self.__get_facts(dt.Range.today()) - def __get_facts(self, range, search_terms=""): + def __get_facts(self, range, search_terms="", limit = 0, asc_by_date = True): datetime_from = range.start datetime_to = range.end @@ -743,7 +743,13 @@ def __get_facts(self, range, search_terms=""): WHERE fact_index MATCH '%s')""" % ('NOT' if reverse_search_terms else '', search_terms) - query += " ORDER BY a.start_time, e.name" + if asc_by_date: + query += " ORDER BY a.start_time, e.name" + else: + query += " ORDER BY a.start_time desc, e.name" + + if limit and limit > 0: + query += " LIMIT " + str(limit) fact_rows = self.fetchall(query, (self._unsorted_localized, datetime_from, diff --git a/src/hamster/storage/storage.py b/src/hamster/storage/storage.py index 28a821787..e72f02002 100644 --- a/src/hamster/storage/storage.py +++ b/src/hamster/storage/storage.py @@ -156,9 +156,9 @@ def remove_fact(self, fact_id): self.end_transaction() - def get_facts(self, start, end=None, search_terms=""): + def get_facts(self, start, end=None, search_terms="", limit=0, asc_by_date=True): range = dt.Range.from_start_end(start, end) - return self.__get_facts(range, search_terms) + return self.__get_facts(range, search_terms, limit, asc_by_date) def get_todays_facts(self): diff --git a/src/hamster/widgets/facttree.py b/src/hamster/widgets/facttree.py index fdbb2b9ee..fa1ffa64e 100644 --- a/src/hamster/widgets/facttree.py +++ b/src/hamster/widgets/facttree.py @@ -127,8 +127,9 @@ def set_text(self, text): class FactRow(object): def __init__(self): - self.time_label = Label() - self.activity_label = Label(x=100) + self.to_export = Label() + self.time_label = Label(x=30) + self.activity_label = Label(x=130) self.category_label = Label() self.description_label = Label() @@ -175,6 +176,8 @@ def set_fact(self, fact): time_label += fact.end_time.strftime(" %H:%M") self.time_label.set_text(time_label) + self.to_export.set_text("🔸" if fact.exported else ("📤️" if fact.range.end else "⏳")) + self.activity_label.set_text(stuff.escape_pango(fact.activity)) category_text = " - {}".format(stuff.escape_pango(fact.category)) if fact.category else "" @@ -231,6 +234,7 @@ def show(self, g, colors, fact=None, is_selected=False): # Do not show the start/end time for Totals if not isinstance(self.fact, TotalFact): self.time_label.show(g) + self.to_export.show(g) self.activity_label.show(g, self.activity_label.get_text() if not isinstance(self.fact, TotalFact) else "{}".format(self.activity_label.get_text())) if self.fact.category: @@ -289,6 +293,7 @@ class FactTree(graphics.Scene, gtk.Scrollable): # enter or double-click, passes in current day and fact 'on-activate-row': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT)), 'on-delete-called': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)), + 'on-toggle-exported-row': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)), } hadjustment = gobject.property(type=gtk.Adjustment, default=None) @@ -360,6 +365,9 @@ def on_mouse_down(self, scene, event): def activate_row(self, day, fact): self.emit("on-activate-row", day, fact) + def toggle_exported_row(self, day, fact): + self.emit("on-toggle-exported-row", fact) + def delete_row(self, fact): self.emit("on-delete-called", fact) @@ -404,7 +412,11 @@ def on_key_press(self, scene, event): self.y -= self.height * 0.8 self.on_scroll() - elif event.keyval == gdk.KEY_Return: + elif event.keyval == gdk.KEY_x: + if self.current_fact: + self.toggle_exported_row(self.hover_day, self.current_fact) + + elif event.keyval in (gdk.KEY_Return, gdk.KEY_e): if self.current_fact: self.activate_row(self.hover_day, self.current_fact) diff --git a/tests/test_stuff.py b/tests/test_stuff.py index 218c07b71..0484518e1 100644 --- a/tests/test_stuff.py +++ b/tests/test_stuff.py @@ -83,6 +83,21 @@ def test_category(self): assert activity.end_time is None assert not activity.description + def test_exported(self): + # plain activity name + activity = Fact.parse("[x] just a simple case") + self.assertEqual(activity.activity, "just a simple case") + self.assertEqual(activity.exported, True) + assert activity.start_time is None + assert activity.end_time is None + assert not activity.description + + def test_serialized_with_exported(self): + # plain activity name + exported = Fact("activity", exported=True) + not_exported = Fact("activity", exported=False) + self.assertNotEqual(exported, not_exported) + def test_description(self): # plain activity name activity = Fact.parse("case,, with added descriptiön") From 90d18f5ac9e67a84155447911940c107f1493079 Mon Sep 17 00:00:00 2001 From: Grzegorz Sobczyk Date: Thu, 23 Jul 2020 15:54:43 +0200 Subject: [PATCH 05/24] Mark cloned activitity as not exported --- .github/workflows/testing.yml | 2 + TODO.md | 3 +- src/hamster/edit_activity.py | 7 +- src/hamster/external/external.py | 115 ++++++++++++++----------------- src/hamster/overview.py | 2 +- 5 files changed, 60 insertions(+), 69 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 9ba7de37e..d3c04041e 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -34,6 +34,8 @@ jobs: PACKAGES="$PACKAGES python${{matrix.python-version}}" # Normal dependencies PACKAGES="$PACKAGES gettext intltool python3-gi python3-cairo python3-dbus python3-xdg libglib2.0-dev libglib2.0-bin gir1.2-gtk-3.0" + # Jira dependencies (optional, python3-jira doesn't exists in ubuntu 16) + # PACKAGES="$PACKAGES python3-jira python3-urllib3" # The gtk-update-icon-cache used to live in libgtk2.0-bin, # but was moved to its own package. Similar for distutils # (included by default in python stdlib). diff --git a/TODO.md b/TODO.md index 625dd385a..256a714ed 100644 --- a/TODO.md +++ b/TODO.md @@ -10,6 +10,5 @@ - [-] exporter invoked from command line (today facts) - [-] export to jira item in menu - [ ] add suggestions to hamster from external source -- [ ] inserting fact between start and end time of another fact -- [ ] compare all files with master +- [ ] compare all files with `python-2.x` branch and move forgotten changes - [x] integrate exporting with overview window diff --git a/src/hamster/edit_activity.py b/src/hamster/edit_activity.py index 4a3c40287..ff7806297 100644 --- a/src/hamster/edit_activity.py +++ b/src/hamster/edit_activity.py @@ -98,7 +98,8 @@ def __init__(self, action, fact_id=None): elif action == "clone": base_fact = runtime.storage.get_fact(fact_id) self.fact = base_fact.copy(start_time=dt.datetime.now(), - end_time=None) + end_time=None, + exported=False) else: self.fact = Fact(start_time=dt.datetime.now()) @@ -288,7 +289,9 @@ def on_tags_changed(self, widget): self.update_cmdline() def on_exported_toggled(self, widget): - self.fact.exported = self.exported_checkbox.get_active() + if not self.master_is_cmdline: + self.fact.exported = self.exported_checkbox.get_active() + self.update_cmdline() def present(self): self.window.present() diff --git a/src/hamster/external/external.py b/src/hamster/external/external.py index eca5b79fa..e39349c42 100644 --- a/src/hamster/external/external.py +++ b/src/hamster/external/external.py @@ -49,7 +49,7 @@ def __init__(self, conf): # gobject.GObject.__init__(self) self.source = conf.get("activities-source") # self.__gtg_connection = None - self.jira: JIRA = None + self.jira = None self.jira_projects = None self.jira_issue_types = None self.jira_query = None @@ -79,35 +79,34 @@ def __connect_to_jira(self, conf): if self.jira_url and self.jira_user and self.jira_pass and self.__is_connected(self.jira_url): options = {'server': self.jira_url} self.jira = JIRA(options, basic_auth=(self.jira_user, self.jira_pass), validate=True) - self.jira_projects = self.__get_jira_projects() - self.jira_issue_types = self.__get_jira_issue_types() + self.jira_projects = self.__jira_get_projects() + self.jira_issue_types = self.__jira_get_issue_types() else: self.on_error("Invalid Jira credentials") self.source = SOURCE_NONE def get_activities(self, query=None): + query = query.strip() if not self.source or not query: return [] - # elif self.source == SOURCE_EVOLUTION: - # return [activity for activity in get_eds_tasks() - # if query is None or activity['name'].startswith(query)] elif self.source == SOURCE_JIRA: - activities = self.__extract_from_jira(query, self.jira_query) + activities = self.__jira_get_activities(query, self.jira_query) direct_issue = None if query and re.match("^[a-zA-Z][a-zA-Z0-9]*-[0-9]+$", query): - if self.is_issue_from_existing_jira_project(query): + if self.__jira_is_issue_from_existing_project(query): issue = self.jira.issue(query.upper()) if issue: - direct_issue = self.__extract_activity_from_jira_issue(issue) - if direct_issue and direct_issue not in activities: - activities.append(direct_issue) + direct_issue = self.__jira_extract_activity_from_issue(issue) + if direct_issue not in activities: + activities.append(direct_issue) if len(activities) <= CURRENT_USER_ACTIVITIES_LIMIT and not direct_issue and len(query) >= MIN_QUERY_LENGTH: - li = query.split(' ') - fragments = filter(len, [self.__generate_fragment_jira_query(word) for word in li]) + words = query.split(' ') + # filter empty elements + fragments = filter(len, [self.__generate_fragment_jira_query(word) for word in words]) jira_query = " AND ".join( fragments) + " AND resolution = Unresolved order by priority desc, updated desc" - logging.warn(jira_query) - third_activities = self.__extract_from_jira('', jira_query) + logging.info(jira_query) + third_activities = self.__jira_get_activities('', jira_query) if activities and third_activities: activities.append({"name": "---------------------", "category": "other open"}) activities.extend(third_activities) @@ -123,23 +122,22 @@ def __generate_fragment_jira_query(self, word): else: return "" - def get_ticket_category(self, activity_id): - """get activity category depends on source""" - if not self.source: - return "" - - if self.source == SOURCE_JIRA: - # try: - issue = self.jira.issue(activity_id) - return self.__extract_activity_from_jira_issue(issue) - else: - return "" + def __jira_get_activities(self, query='', jira_query=None): + activities = [] + try: + results = self.__jira_search_issues(jira_query) + for issue in results: + activity = self.__jira_extract_activity_from_issue(issue) + if query is None or all(item in activity['name'].lower() for item in query.lower().split(' ')): + activities.append(activity) + except Exception as e: + logger.warn(e) + return activities - def __extract_activity_from_jira_issue(self, issue): + def __jira_extract_activity_from_issue(self, issue): activity = {} issue_id = issue.key activity['name'] = str(issue_id) + ': ' + issue.fields.summary.replace(",", " ") - activity['rt_id'] = issue_id if hasattr(issue.fields, self.jira_category): activity['category'] = str(getattr(issue.fields, self.jira_category)) else: @@ -150,29 +148,17 @@ def __extract_activity_from_jira_issue(self, issue): getattr(issue.fields, 'project').key, getattr(issue.fields, 'issuetype').name, getattr(issue.fields, 'project').name) except Exception as e: - logger.warn(str(e)) + logger.warning(e) return activity - def __extract_from_jira(self, query='', jira_query=None): - activities = [] - try: - results = self.__search_jira_issues(jira_query) - for issue in results: - activity = self.__extract_activity_from_jira_issue(issue) - if query is None or all(item in activity['name'].lower() for item in query.lower().split(' ')): - activities.append(activity) - except Exception as e: - logger.warn(e) - return activities - - def __get_jira_projects(self): + def __jira_get_projects(self): return [project.key for project in self.jira.projects()] - def __get_jira_issue_types(self): + def __jira_get_issue_types(self): return [issuetype.name.lower() for issuetype in self.jira.issue_types()] @cache(seconds=30) - def __search_jira_issues(self, jira_query=None): + def __jira_search_issues(self, jira_query=None): return self.jira.search_issues(jira_query, fields=self.jira_fields, maxResults=100) def on_error(self, msg): @@ -190,26 +176,9 @@ def __is_connected(self, url): except urllib3.HTTPError as err: return False - def is_issue_from_existing_jira_project(self, issue): + def __jira_is_issue_from_existing_project(self, issue): return issue.split('-', 1)[0].upper() in self.jira_projects - def __add_jira_worklog(self, issue_id, text, start_time, time_worked): - """ - :type start_time: date - :param time_worked: int time spent in minutes - """ - logger.info(_("updating issue #%s: %s min, comment: \n%s") % (issue_id, time_worked, text)) - self.jira.add_worklog(issue=issue_id, comment=text, started=start_time, timeSpent="%sm" % time_worked) - - def get_text(self, fact: Fact): - text = "" - if fact.description: - text += "%s\n" % (fact.description) - text += "%s, %s-%s" % (fact.date, fact.range.start.strftime("%H:%M"), fact.range.end.strftime("%H:%M")) - if fact.tags: - text += " ("+", ".join(fact.tags)+")" - return text - def export(self, fact: Fact) -> bool: """ :return: bool fact was exported @@ -222,10 +191,10 @@ def export(self, fact: Fact) -> bool: jira_match = re.match(JIRA_ISSUE_NAME_REGEX, fact.activity) if jira_match: issue_id = jira_match.group(1) - comment = self.get_text(fact) + comment = self.__get_comment_to_export(fact) time_worked = stuff.duration_minutes(fact.delta) try: - self.__add_jira_worklog(issue_id, comment, fact.range.start, int(time_worked)) + self.__jira_add_worklog(issue_id, comment, fact.range.start, int(time_worked)) return True except Exception as e: logger.error(e) @@ -234,3 +203,21 @@ def export(self, fact: Fact) -> bool: else: logger.warning("invalid source, don't know where export to") return False + + def __get_comment_to_export(self, fact: Fact): + text = "" + if fact.description: + text += "%s\n" % (fact.description) + text += "%s, %s-%s" % (fact.date, fact.range.start.strftime("%H:%M"), fact.range.end.strftime("%H:%M")) + if fact.tags: + text += " (" + ", ".join(fact.tags) + ")" + return text + + def __jira_add_worklog(self, issue_id, text, start_time, time_worked): + """ + :type start_time: date + :param time_worked: int time spent in minutes + """ + logger.info(_("updating issue #%s: %s min, comment: \n%s") % (issue_id, time_worked, text)) + self.jira.add_worklog(issue=issue_id, comment=text, started=start_time, timeSpent="%sm" % time_worked) + diff --git a/src/hamster/overview.py b/src/hamster/overview.py index 2200d7ed3..f96de4e3c 100644 --- a/src/hamster/overview.py +++ b/src/hamster/overview.py @@ -272,7 +272,7 @@ def __init__(self, storage: hamster.client.Storage): self.connect("destroy", self.on_destroy_event) self.storage = storage - self.export_thread: ExportThread = None + self.export_thread = None self.facts = [] def _init_labels(self): From c598b4ae478247e9e5e3cafb30f9323e3a1f85c4 Mon Sep 17 00:00:00 2001 From: Grzegorz Sobczyk Date: Sun, 26 Jul 2020 20:51:11 +0200 Subject: [PATCH 06/24] Export activities to external source via dbus --- NEWS.md | 2 ++ TODO.md | 2 ++ src/hamster-cli.py | 6 +++--- src/hamster-service.py | 9 +++++++-- src/hamster/client.py | 12 +++++++++--- src/hamster/external/external.py | 19 +++++++++---------- src/hamster/overview.py | 8 ++++---- src/hamster/reports.py | 12 ++++++++++++ src/hamster/storage/db.py | 4 ++++ src/hamster/storage/storage.py | 3 +++ 10 files changed, 55 insertions(+), 22 deletions(-) diff --git a/NEWS.md b/NEWS.md index 7253fa6d4..e48d83229 100644 --- a/NEWS.md +++ b/NEWS.md @@ -4,6 +4,8 @@ - Ctrl-=: clone or fallback to new if none selected. - e: edit selected activity - x: toggle export flag +* Gathering activities from external source (right now only Jira and only via dbus) +* Export activities as worklogs to jira ## Changes in 3.0.2 diff --git a/TODO.md b/TODO.md index 256a714ed..2bbd9bd50 100644 --- a/TODO.md +++ b/TODO.md @@ -12,3 +12,5 @@ - [ ] add suggestions to hamster from external source - [ ] compare all files with `python-2.x` branch and move forgotten changes - [x] integrate exporting with overview window +- [x] exporting activities via DBUS +- [ ] zsh completion diff --git a/src/hamster-cli.py b/src/hamster-cli.py index 9c6786787..4cf078678 100644 --- a/src/hamster-cli.py +++ b/src/hamster-cli.py @@ -235,9 +235,9 @@ def assist(self, *args): assist_command = args[0] if args else "" if assist_command == "start": - hamster_client._activities(sys.argv[-1]) + hamster_client._activities(" ".join(args[1:])) elif assist_command == "export": - formats = "html tsv xml ical".split() + formats = "html tsv xml ical hamster".split() chosen = sys.argv[-1] formats = [f for f in formats if not chosen or f.startswith(chosen)] print("\n".join(formats)) @@ -422,7 +422,7 @@ def version(self): * list [start-date [end-date]]: List activities * search [terms] [start-date [end-date]]: List activities matching a search term - * export [html|tsv|ical|xml] [start-date [end-date]]: Export activities with + * export [html|tsv|ical|xml|hamster] [start-date [end-date]]: Export activities with the specified format * current: Print current activity * activities: List all the activities names, one per line. diff --git a/src/hamster-service.py b/src/hamster-service.py index 280d077a0..3c229bbfa 100644 --- a/src/hamster-service.py +++ b/src/hamster-service.py @@ -416,15 +416,20 @@ def GetCategoryActivities(self, category_id): @dbus.service.method("org.gnome.Hamster", in_signature='s', out_signature='a(ss)') - def GetActivities(self, search = ""): + def GetActivities(self, search=""): return [(row['name'], row['category'] or '') for row in self.get_activities(search)] @dbus.service.method("org.gnome.Hamster", in_signature='s', out_signature='a(ss)') - def GetExtActivities(self, search = ""): + def GetExtActivities(self, search=""): return [(row['name'], row['category'] or '') for row in self.get_ext_activities(search)] + @dbus.service.method("org.gnome.Hamster", in_signature='i', out_signature='b') + def ExportFact(self, fact_id): + return self.export_fact(fact_id) + + @dbus.service.method("org.gnome.Hamster", in_signature='ii', out_signature = 'b') def ChangeCategory(self, id, category_id): return self.change_category(id, category_id) diff --git a/src/hamster/client.py b/src/hamster/client.py index 93ad3ab1a..5fa2ad8f2 100644 --- a/src/hamster/client.py +++ b/src/hamster/client.py @@ -115,7 +115,7 @@ def conn(self): see also: https://github.com/projecthamster/hamster#kill-hamster-daemons """.format(server_version, client_version) - ) + ) ) return self._connection @@ -155,20 +155,26 @@ def get_facts(self, start, end=None, search_terms=""): return [from_dbus_fact_json(fact) for fact in self.conn.GetFactsJSON(dbus_range, search_terms)] - def get_activities(self, search = ""): + def get_activities(self, search=""): """returns list of activities name matching search criteria. results are sorted by most recent usage. search is case insensitive """ return self._to_dict(('name', 'category'), self.conn.GetActivities(search)) - def get_ext_activities(self, search = ""): + def get_ext_activities(self, search=""): """returns list of activities name matching search criteria. results are sorted by most recent usage. search is case insensitive """ return self._to_dict(('name', 'category'), self.conn.GetExtActivities(search)) + def export_fact(self, fact_id): + """export facts to external source. + :returns true if fact was exported + """ + return self.conn.ExportFact(fact_id) + def get_categories(self): """returns list of categories""" return self._to_dict(('id', 'name'), self.conn.GetCategories()) diff --git a/src/hamster/external/external.py b/src/hamster/external/external.py index e39349c42..1620d17cc 100644 --- a/src/hamster/external/external.py +++ b/src/hamster/external/external.py @@ -37,7 +37,7 @@ SOURCE_NONE = "" SOURCE_JIRA = 'jira' -JIRA_ISSUE_NAME_REGEX = "^(\w+-\d+):? " +JIRA_ISSUE_NAME_REGEX = "^([a-zA-Z][a-zA-Z0-9]*-[0-9]+)" ERROR_ADDITIONAL_MESSAGE = '\n\nCheck settings and reopen main window.' MIN_QUERY_LENGTH = 3 CURRENT_USER_ACTIVITIES_LIMIT = 5 @@ -74,7 +74,7 @@ def __connect_to_jira(self, conf): self.jira_pass = conf.get("jira-pass") self.jira_query = conf.get("jira-query") self.jira_category = conf.get("jira-category-field") - self.jira_fields = ','.join(['summary', self.jira_category, 'issuetype']) + self.jira_fields = ','.join(['summary', self.jira_category, 'issuetype', 'assignee', 'project']) logger.info("user: %s, pass: *****" % self.jira_user) if self.jira_url and self.jira_user and self.jira_pass and self.__is_connected(self.jira_url): options = {'server': self.jira_url} @@ -92,9 +92,9 @@ def get_activities(self, query=None): elif self.source == SOURCE_JIRA: activities = self.__jira_get_activities(query, self.jira_query) direct_issue = None - if query and re.match("^[a-zA-Z][a-zA-Z0-9]*-[0-9]+$", query): + if query and re.match(JIRA_ISSUE_NAME_REGEX, query): if self.__jira_is_issue_from_existing_project(query): - issue = self.jira.issue(query.upper()) + issue = self.jira.issue(query.upper(), fields=self.jira_fields) if issue: direct_issue = self.__jira_extract_activity_from_issue(issue) if direct_issue not in activities: @@ -137,16 +137,15 @@ def __jira_get_activities(self, query='', jira_query=None): def __jira_extract_activity_from_issue(self, issue): activity = {} issue_id = issue.key - activity['name'] = str(issue_id) + ': ' + issue.fields.summary.replace(",", " ") - if hasattr(issue.fields, self.jira_category): - activity['category'] = str(getattr(issue.fields, self.jira_category)) + fields = issue.fields + activity['name'] = str(issue_id) + ': ' + fields.summary.replace(",", " ") + (" 👨‍💼" + fields.assignee.name if fields.assignee else "") + if hasattr(fields, self.jira_category): + activity['category'] = str(getattr(fields, self.jira_category)) else: activity['category'] = "" if not activity['category'] or activity['category'] == "None": try: - activity['category'] = "%s/%s (%s)" % ( - getattr(issue.fields, 'project').key, getattr(issue.fields, 'issuetype').name, - getattr(issue.fields, 'project').name) + activity['category'] = "%s/%s (%s)" % (fields.project.key, fields.issuetype.name, fields.project.name) except Exception as e: logger.warning(e) return activity diff --git a/src/hamster/overview.py b/src/hamster/overview.py index f96de4e3c..c6c1bbabb 100644 --- a/src/hamster/overview.py +++ b/src/hamster/overview.py @@ -325,7 +325,6 @@ def __init__(self, facts, callback, finish_callback, storage: hamster.client.Sto def run(self): glib.idle_add(self.callback, 0.0, _("Connecting to external source...")) - external = runtime.get_external() for idx, fact in enumerate(self.facts): if self.interrupt: logger.info("Interrupting export thread") @@ -333,13 +332,14 @@ def run(self): fraction = float(idx + 1) / self.steps label = _("Exporting: %s - %s") % (fact.activity, fact.delta) glib.idle_add(self.callback, fraction, label) - exported = external.export(fact) + exported = self.storage.export_fact(fact.id) if exported: - # TODO mark as exported fact.exported = True self.storage.update_fact(fact.id, fact, False) pass - # TODO external.report(fact) + else: + logger.info("Fact not exported: %s" % fact.activity) + glib.idle_add(self.finish_callback, self.interrupt) def shutdown(self): diff --git a/src/hamster/reports.py b/src/hamster/reports.py index 69a4a258d..c3947a089 100644 --- a/src/hamster/reports.py +++ b/src/hamster/reports.py @@ -55,6 +55,8 @@ def simple(facts, start_date, end_date, format, path = None): writer = XMLWriter(report_path) elif format == "ical": writer = ICalWriter(report_path) + elif format == "hamster": + writer = HamsterWriter(report_path) else: #default to HTML writer = HTMLWriter(report_path, start_date, end_date) @@ -156,6 +158,16 @@ def _write_fact(self, fact): def _finish(self, facts): pass +class HamsterWriter(ReportWriter): + def __init__(self, path): + ReportWriter.__init__(self, path) + + def _write_fact(self, fact): + self.file.write(fact.serialized() + "\n") + + def _finish(self, facts): + pass + class XMLWriter(ReportWriter): def __init__(self, path): ReportWriter.__init__(self, path) diff --git a/src/hamster/storage/db.py b/src/hamster/storage/db.py index 6a7ef7bb1..18ac83e3f 100644 --- a/src/hamster/storage/db.py +++ b/src/hamster/storage/db.py @@ -786,6 +786,10 @@ def __get_category_activities(self, category_id): def __get_ext_activities(self, search): return self.get_external().get_activities(search) + def __export_fact(self, fact_id) -> bool: + fact = self.get_fact(fact_id) + return self.get_external().export(fact) + def __get_activities(self, search): """returns list of activities for autocomplete, activity names converted to lowercase""" diff --git a/src/hamster/storage/storage.py b/src/hamster/storage/storage.py index e72f02002..5d8441c67 100644 --- a/src/hamster/storage/storage.py +++ b/src/hamster/storage/storage.py @@ -213,6 +213,9 @@ def get_activities(self, search = ""): def get_ext_activities(self, search = ""): return self.__get_ext_activities(search) + def export_fact(self, fact_id): + return self.__export_fact(fact_id) + def change_category(self, id, category_id): changed = self.__change_category(id, category_id) if changed: From 8a8a32195206cd43ec4e8f66b19789f28ac78fa1 Mon Sep 17 00:00:00 2001 From: Grzegorz Sobczyk Date: Mon, 27 Jul 2020 16:47:35 +0200 Subject: [PATCH 07/24] Change external activity name format --- src/hamster/external/external.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/hamster/external/external.py b/src/hamster/external/external.py index 1620d17cc..f8c546531 100644 --- a/src/hamster/external/external.py +++ b/src/hamster/external/external.py @@ -74,7 +74,7 @@ def __connect_to_jira(self, conf): self.jira_pass = conf.get("jira-pass") self.jira_query = conf.get("jira-query") self.jira_category = conf.get("jira-category-field") - self.jira_fields = ','.join(['summary', self.jira_category, 'issuetype', 'assignee', 'project']) + self.jira_fields = ','.join(['summary', self.jira_category, 'issuetype', 'assignee', 'project', 'status']) logger.info("user: %s, pass: *****" % self.jira_user) if self.jira_url and self.jira_user and self.jira_pass and self.__is_connected(self.jira_url): options = {'server': self.jira_url} @@ -138,7 +138,10 @@ def __jira_extract_activity_from_issue(self, issue): activity = {} issue_id = issue.key fields = issue.fields - activity['name'] = str(issue_id) + ': ' + fields.summary.replace(",", " ") + (" 👨‍💼" + fields.assignee.name if fields.assignee else "") + activity['name'] = str(issue_id) \ + + ': ' + fields.summary.replace(",", " ") \ + + " (%s)" % fields.status.name \ + + (" 👨‍💼" + fields.assignee.name if fields.assignee else "") if hasattr(fields, self.jira_category): activity['category'] = str(getattr(fields, self.jira_category)) else: @@ -219,4 +222,3 @@ def __jira_add_worklog(self, issue_id, text, start_time, time_worked): """ logger.info(_("updating issue #%s: %s min, comment: \n%s") % (issue_id, time_worked, text)) self.jira.add_worklog(issue=issue_id, comment=text, started=start_time, timeSpent="%sm" % time_worked) - From 46e00cdd33afac54b55d05bfc4b16d383896de08 Mon Sep 17 00:00:00 2001 From: Grzegorz Sobczyk Date: Mon, 27 Jul 2020 18:07:40 +0200 Subject: [PATCH 08/24] Remove local TODO list for external activities --- TODO.md | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 TODO.md diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 2bbd9bd50..000000000 --- a/TODO.md +++ /dev/null @@ -1,16 +0,0 @@ -# Todo list - -- [x] settings for external activities -- [x] get external activities from jira -- [x] cinnamon applet update -- [x] export flag on overview -- [x] exporting window -- [x] exporting to jira - - [x] only finished activities -- [-] exporter invoked from command line (today facts) -- [-] export to jira item in menu -- [ ] add suggestions to hamster from external source -- [ ] compare all files with `python-2.x` branch and move forgotten changes -- [x] integrate exporting with overview window -- [x] exporting activities via DBUS -- [ ] zsh completion From ffd2495c41c13d5e3ba9641d6aea3d33f2029205 Mon Sep 17 00:00:00 2001 From: Grzegorz Sobczyk Date: Mon, 3 Aug 2020 22:48:12 +0200 Subject: [PATCH 09/24] Better error messages for external activities Show error message when jira module isn't installed and external source is active --- src/hamster/external/external.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/hamster/external/external.py b/src/hamster/external/external.py index f8c546531..38f313912 100644 --- a/src/hamster/external/external.py +++ b/src/hamster/external/external.py @@ -27,6 +27,7 @@ from hamster.lib.cache import cache from gi.repository import Gtk as gtk +from gi.repository import GLib as glib import re import urllib3 @@ -59,14 +60,20 @@ def __init__(self, conf): self.__connect(conf) except Exception as e: error_msg = self.source + ' connection failed: ' + str(e) - self.on_error(error_msg + ERROR_ADDITIONAL_MESSAGE) - logger.warning(error_msg) self.source = SOURCE_NONE + self.__on_error(error_msg + ERROR_ADDITIONAL_MESSAGE) + logger.warning(error_msg) def __connect(self, conf): - if JIRA and self.source == SOURCE_JIRA: - self.__http = urllib3.PoolManager() - self.__connect_to_jira(conf) + if self.source == SOURCE_JIRA: + if JIRA: + self.__http = urllib3.PoolManager() + self.__connect_to_jira(conf) + else: + self.source = SOURCE_NONE + self.__on_error(_("Is Jira module installed (see README)? " + "Didn't found it! " + "External activities feature will be disabled.")) def __connect_to_jira(self, conf): self.jira_url = conf.get("jira-url") @@ -82,8 +89,8 @@ def __connect_to_jira(self, conf): self.jira_projects = self.__jira_get_projects() self.jira_issue_types = self.__jira_get_issue_types() else: - self.on_error("Invalid Jira credentials") self.source = SOURCE_NONE + self.__on_error("Invalid Jira credentials") def get_activities(self, query=None): query = query.strip() @@ -163,7 +170,10 @@ def __jira_get_issue_types(self): def __jira_search_issues(self, jira_query=None): return self.jira.search_issues(jira_query, fields=self.jira_fields, maxResults=100) - def on_error(self, msg): + def __on_error(self, msg): + glib.idle_add(self.__on_error_dialog, msg) + + def __on_error_dialog(self, msg): md = gtk.MessageDialog(None, 0, gtk.MessageType.ERROR, gtk.ButtonsType.CLOSE, msg) @@ -189,6 +199,9 @@ def export(self, fact: Fact) -> bool: if not fact.range.end: logger.info("Skipping fact without end date") return False + if fact.exported: + logger.info("Skipping exported fact") + return False if self.source == SOURCE_JIRA: jira_match = re.match(JIRA_ISSUE_NAME_REGEX, fact.activity) if jira_match: From 0668b1e4287babe1cc4d9c77ba4d603de66a1cd8 Mon Sep 17 00:00:00 2001 From: Grzegorz Sobczyk Date: Mon, 3 Aug 2020 22:48:42 +0200 Subject: [PATCH 10/24] Changed export label on edit activity .. to be more descriptive --- data/edit_activity.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/edit_activity.ui b/data/edit_activity.ui index 4d16c534a..ec1c5a129 100644 --- a/data/edit_activity.ui +++ b/data/edit_activity.ui @@ -479,7 +479,7 @@ True False start - exported + exported to external source From 90e8cf65895a8233ac4198f5a310f40b0ac565b6 Mon Sep 17 00:00:00 2001 From: Grzegorz Sobczyk Date: Mon, 3 Aug 2020 22:51:44 +0200 Subject: [PATCH 11/24] Add external format to exporter (CLI) Added possibility to export activities to external source using command line --- NEWS.md | 1 + src/hamster-cli.py | 4 ++-- src/hamster/reports.py | 21 +++++++++++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/NEWS.md b/NEWS.md index e48d83229..998f2da25 100644 --- a/NEWS.md +++ b/NEWS.md @@ -6,6 +6,7 @@ - x: toggle export flag * Gathering activities from external source (right now only Jira and only via dbus) * Export activities as worklogs to jira +* Export activities to external source from command line (`hamster export external` command) ## Changes in 3.0.2 diff --git a/src/hamster-cli.py b/src/hamster-cli.py index 4cf078678..f0fde0418 100644 --- a/src/hamster-cli.py +++ b/src/hamster-cli.py @@ -237,7 +237,7 @@ def assist(self, *args): if assist_command == "start": hamster_client._activities(" ".join(args[1:])) elif assist_command == "export": - formats = "html tsv xml ical hamster".split() + formats = "html tsv xml ical hamster external".split() chosen = sys.argv[-1] formats = [f for f in formats if not chosen or f.startswith(chosen)] print("\n".join(formats)) @@ -422,7 +422,7 @@ def version(self): * list [start-date [end-date]]: List activities * search [terms] [start-date [end-date]]: List activities matching a search term - * export [html|tsv|ical|xml|hamster] [start-date [end-date]]: Export activities with + * export [html|tsv|ical|xml|hamster|external] [start-date [end-date]]: Export activities with the specified format * current: Print current activity * activities: List all the activities names, one per line. diff --git a/src/hamster/reports.py b/src/hamster/reports.py index c3947a089..3cb048ada 100644 --- a/src/hamster/reports.py +++ b/src/hamster/reports.py @@ -29,6 +29,7 @@ from string import Template from textwrap import dedent +from hamster import client from hamster.lib import datetime as dt from hamster.lib.configuration import runtime from hamster.lib import stuff @@ -57,6 +58,8 @@ def simple(facts, start_date, end_date, format, path = None): writer = ICalWriter(report_path) elif format == "hamster": writer = HamsterWriter(report_path) + elif format == "external": + writer = ExternalWriter(report_path) else: #default to HTML writer = HTMLWriter(report_path, start_date, end_date) @@ -168,6 +171,24 @@ def _write_fact(self, fact): def _finish(self, facts): pass +class ExternalWriter(ReportWriter): + def __init__(self, path): + ReportWriter.__init__(self, path) + self.storage = client.Storage() + + def _write_fact(self, fact): + exported = self.storage.export_fact(fact.id) + if exported: + self.file.write(_("Exported: %s - %s") % (fact.activity, fact.delta) + "\n") + fact.exported = True + self.storage.update_fact(fact.id, fact, False) + pass + else: + self.file.write(_("Fact not exported: %s" % fact.activity) + "\n") + + def _finish(self, facts): + pass + class XMLWriter(ReportWriter): def __init__(self, path): ReportWriter.__init__(self, path) From fea02f6a9f43c48fdb6fe781306baf867188472c Mon Sep 17 00:00:00 2001 From: Grzegorz Sobczyk Date: Mon, 3 Aug 2020 23:59:37 +0200 Subject: [PATCH 12/24] Add polish translations --- .gitignore | 1 + po/pl.po | 1314 ++++++++++++++++++++-------------------- src/hamster/reports.py | 2 +- 3 files changed, 644 insertions(+), 673 deletions(-) diff --git a/.gitignore b/.gitignore index 55187aaab..29c758b0f 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ build .pydevproject venv/ .idea/ +*.iml \ No newline at end of file diff --git a/po/pl.po b/po/pl.po index 1212a42eb..796e98fe9 100644 --- a/po/pl.po +++ b/po/pl.po @@ -8,12 +8,11 @@ # Łukasz Jernaś , 2009. # Piotr Drąg , 2010-2012. # Aviary.pl , 2008-2012. -#: ../src/hamster-cli:342 msgid "" msgstr "" "Project-Id-Version: hamster-time-tracker\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2012-12-02 19:21+0100\n" +"POT-Creation-Date: 2020-08-03 23:34+0200\n" "PO-Revision-Date: 2012-09-04 01:58+0200\n" "Last-Translator: Piotr Drąg \n" "Language-Team: Polish \n" @@ -26,447 +25,149 @@ msgstr "" "X-Poedit-Language: Polish\n" "X-Poedit-Country: Poland\n" -#: ../data/edit_activity.ui.h:1 ../data/today.ui.h:15 -msgid "Add Earlier Activity" -msgstr "Dodaj wcześniejszą czynność" +#: ../data/org.gnome.hamster.gschema.xml.h:1 +msgid "The folder the last report was saved to" +msgstr "Folder do którego zapisano raport to" -#: ../data/edit_activity.ui.h:2 ../data/range_pick.ui.h:5 -msgid "to" -msgstr "do" +#: ../data/org.gnome.hamster.gschema.xml.h:2 +msgid "Activities source" +msgstr "Źródło czynności" -#: ../data/edit_activity.ui.h:3 -msgid "in progress" -msgstr "w trakcie" +#: ../data/org.gnome.hamster.gschema.xml.h:3 +msgid "The source of activities" +msgstr "Źródło czynności" -#: ../data/edit_activity.ui.h:4 -msgid "Description:" -msgstr "Opis:" +#: ../data/org.gnome.hamster.gschema.xml.h:4 +msgid "Jira URL" +msgstr "Jira URL" -#: ../data/edit_activity.ui.h:5 -msgid "Time:" -msgstr "Czas:" +#: ../data/org.gnome.hamster.gschema.xml.h:5 +msgid "Jira user" +msgstr "Jira, użytkownik" -#: ../data/edit_activity.ui.h:6 -msgid "Activity:" -msgstr "Czynność:" +#: ../data/org.gnome.hamster.gschema.xml.h:6 +msgid "Jira password" +msgstr "Jira, hasło" -#: ../data/edit_activity.ui.h:7 -msgid "Tags:" -msgstr "Etykiety:" +#: ../data/org.gnome.hamster.gschema.xml.h:7 +msgid "Jira query" +msgstr "Jira, zapytanie o czynności" -#: ../data/hamster.schemas.in.h:1 -msgid "Stop tracking on idle" -msgstr "Zatrzymanie czasu podczas bezczynności" +#: ../data/org.gnome.hamster.gschema.xml.h:8 +msgid "Jira category field" +msgstr "Jira, pole kategorii" -#: ../data/hamster.schemas.in.h:2 -msgid "Stop tracking current activity when computer becomes idle" -msgstr "" -"Zatrzymanie śledzenia bieżącej czynności podczas bezczynności komputera" - -#: ../data/hamster.schemas.in.h:3 ../data/preferences.ui.h:2 -msgid "Stop tracking on shutdown" -msgstr "Zatrzymanie śledzenia przy wyłączeniu komputera" - -#: ../data/hamster.schemas.in.h:4 -msgid "Stop tracking current activity on shutdown" -msgstr "Zatrzymanie śledzenia bieżącej czynności przy wyłączeniu komputera" - -#: ../data/hamster.schemas.in.h:5 -msgid "Remind of current task every x minutes" -msgstr "Przypominanie o bieżącej czynności co x minut" - -#: ../data/hamster.schemas.in.h:6 -msgid "" -"Remind of current task every specified amount of minutes. Set to 0 or " -"greater than 120 to disable reminder." -msgstr "" -"Przypominanie o bieżącej czynności co określoną liczbę minut. Ustawienie " -"wartości na 0 lub więcej niż 120, wyłącza przypominanie." - -#: ../data/hamster.schemas.in.h:7 ../data/preferences.ui.h:4 -msgid "Also remind when no activity is set" -msgstr "Przypominanie również, gdy nie ustawiono żadnej czynności" - -#: ../data/hamster.schemas.in.h:8 -msgid "" -"Also remind every notify_interval minutes if no activity has been started." -msgstr "" -"Przypominanie co notify_interval minut nawet, jeśli nie uruchomiono żadnej " -"czynności." - -#: ../data/hamster.schemas.in.h:9 +#: ../data/org.gnome.hamster.gschema.xml.h:9 msgid "At what time does the day start (defaults to 5:30AM)" msgstr "Kiedy powinien rozpoczynać się dzień (domyślnie o 5:30)" -#: ../data/hamster.schemas.in.h:10 -msgid "" -"Activities will be counted as to belong to yesterday if the current time is " -"less than the specified day start; and today, if it is over the time. " -"Activities that span two days, will tip over to the side where the largest " -"part of the activity is." -msgstr "" -"Czynności będą liczone jako należące do dnia poprzedniego, jeśli bieżący " -"czas jest wcześniejszy niż określony czas rozpoczęcia danego dnia, a jako " -"należące do dnia dzisiejszego, jeśli ten czas minął. Czynności rozdzielone " -"na dwa dni będą przesuwane na ten dzień, w którym odbyło się większość czasu " -"czynności." - -#: ../data/hamster.schemas.in.h:11 -msgid "Should workspace switch trigger activity switch" -msgstr "" -"Określa, czy przełączenie obszaru roboczego wywołuje przełączenie czynności" - -#: ../data/hamster.schemas.in.h:12 +#: ../data/org.gnome.hamster.gschema.xml.h:10 msgid "" -"List of enabled tracking methods. \"name\" will enable switching activities " -"by name defined in workspace_mapping. \"memory\" will enable switching to " -"the last activity when returning to a previous workspace." +"The hamster day of an activity is the civil date of start time, provided " +"start time is after day-start. On the contrary, if start time is earlier " +"than day-start, then the activity belongs to the previous hamster day." msgstr "" -"Lista włączonych metod śledzenia. Wartość \"name\" włączy przełączanie " -"czynności według nazwy określonej w kluczu \"workspace_mapping\". Wartość " -"\"memory\" włączy przełączanie na ostatnią czynność podczas powracania na " -"poprzedni obszar roboczy." - -#: ../data/hamster.schemas.in.h:13 -msgid "Switch activity on workspace change" -msgstr "Przełączenie czynności po zmianie obszaru roboczego" - -#: ../data/hamster.schemas.in.h:14 -msgid "" -"If switching by name is enabled, this list sets the activity names that " -"should be switched to, workspaces represented by the index of item." -msgstr "" -"Jeśli włączone jest przełączanie według nazwy, to ta lista ustawia nazwy " -"czynności, na które można się przełączyć, z obszarem roboczym prezentowanym " -"przez indeks elementów." - -#: ../data/hamster.schemas.in.h:15 -msgid "Show / hide Time Tracker Window" -msgstr "Wyświetlanie/ukrywanie okna zarządzania czasem" - -#: ../data/hamster.schemas.in.h:16 -msgid "Keyboard shortcut for showing / hiding the Time Tracker window." -msgstr "Skrót klawiszowy do wyświetlenia/ukrywania okna zarządzania czasem." - -#: ../data/hamster.schemas.in.h:17 -msgid "Toggle hamster application window action" -msgstr "Przełączenie czynności okna programu Hamster" - -#: ../data/hamster.schemas.in.h:18 -msgid "Command for toggling visibility of the hamster application window." -msgstr "Polecenie do przełącznika widoczności okna programu Hamster." - -#: ../data/hamster.schemas.in.h:19 -msgid "Toggle hamster application window" -msgstr "Przełączenie okna programu Hamster" - -#: ../data/hamster.schemas.in.h:20 -msgid "Toggle visibility of the hamster application window." -msgstr "Przełączenie widoczności okna programu Hamster." - -#: ../data/hamster.desktop.in.in.h:1 -#: ../data/hamster-windows-service.desktop.in.in.h:1 ../data/today.ui.h:1 -#: ../src/hamster-cli:133 ../src/hamster/about.py:39 -#: ../src/hamster/about.py:40 ../src/hamster/today.py:63 -msgid "Time Tracker" -msgstr "Zarządzanie czasem" - -#: ../data/hamster.desktop.in.in.h:2 -#: ../data/hamster-windows-service.desktop.in.in.h:2 -msgid "Project Hamster - track your time" -msgstr "Projekt Hamster - zarządzanie czasem" - -#: ../data/hamster-time-tracker-overview.desktop.in.in.h:1 -msgid "Time Tracking Overview" -msgstr "Okno podglądu zarządzania czasem" - -#: ../data/hamster-time-tracker-overview.desktop.in.in.h:2 -msgid "The overview window of hamster time tracker" -msgstr "Okno podglądu programu do zarządzania czasem Hamster" - -#: ../data/overview_totals.ui.h:1 -msgid "Show Statistics" -msgstr "Wyświetl statystyki" - -#: ../data/overview_totals.ui.h:2 -msgid "Categories" -msgstr "Kategorie" - -#: ../data/overview_totals.ui.h:3 ../data/overview.ui.h:9 -msgid "Activities" -msgstr "Czynności" - -#: ../data/overview_totals.ui.h:4 ../src/hamster-cli:278 -#: ../src/hamster/reports.py:319 ../src/hamster/today.py:150 -msgid "Tags" -msgstr "Etykiety" - -#: ../data/overview_totals.ui.h:5 -msgid "No data for this interval" -msgstr "Brak danych dla tego przedziału czasowego" - -#: ../data/overview.ui.h:1 -msgid "Save report..." -msgstr "Zapisz sprawozdanie..." - -#: ../data/overview.ui.h:2 -msgid "Day" -msgstr "Dzień" +"Godzina oznaczająca start dnia pracy. Jeśli start czynności jest przed " +"wyznaczoną godziną, to czynność jest liczona do dnia poprzedniego" -#: ../data/overview.ui.h:3 -msgid "Week" -msgstr "Tydzień" - -#: ../data/overview.ui.h:4 -msgid "Month" -msgstr "Miesiąc" - -#: ../data/overview.ui.h:5 -msgid "Overview — Hamster" -msgstr "Przegląd — Hamster" - -#: ../data/overview.ui.h:6 -msgid "_Overview" -msgstr "_Przegląd" +#: ../src/hamster-cli.py:325 +msgid "No activity" +msgstr "Brak czynności" -#: ../data/overview.ui.h:7 ../src/hamster-cli:276 -#: ../src/hamster/preferences.py:212 ../src/hamster/reports.py:317 -#: ../src/hamster/today.py:144 +#: ../src/hamster-cli.py:347 ../src/hamster/reports.py:332 msgid "Activity" msgstr "Czynność" -#: ../data/overview.ui.h:8 -msgid "_View" -msgstr "_Widok" - -#: ../data/overview.ui.h:10 ../src/hamster/reports.py:308 -msgid "Totals" -msgstr "Ogółem" - -#: ../data/overview.ui.h:11 -msgid "Remove" -msgstr "Usuń" - -#: ../data/overview.ui.h:12 -msgid "Add new" -msgstr "Dodaj nową" - -#: ../data/overview.ui.h:13 -msgid "Edit" -msgstr "Zmodyfikuj" - -#: ../data/preferences.ui.h:1 -msgid "Time Tracker Preferences" -msgstr "Preferencje programu zarządzania czasem" - -#: ../data/preferences.ui.h:3 -msgid "Stop tracking when computer becomes idle" -msgstr "Zatrzymanie śledzenia podczas bezczynności komputera" - -#: ../data/preferences.ui.h:5 -msgid "Remind of current activity every:" -msgstr "Przypominanie o bieżącej czynności co:" - -#: ../data/preferences.ui.h:6 -msgid "New day starts at" -msgstr "Nowy dzień rozpoczyna się o" - -#: ../data/preferences.ui.h:7 -msgid "Use following todo list if available:" -msgstr "Użycie następującej listy czynności do wykonania, jeśli jest dostępna:" - -#: ../data/preferences.ui.h:8 -msgid "Integration" -msgstr "Integracja" - -#: ../data/preferences.ui.h:9 -msgid "Tracking" -msgstr "Śledzenie" - -#: ../data/preferences.ui.h:10 -msgid "_Categories" -msgstr "_Kategorie" - -#: ../data/preferences.ui.h:11 -msgid "Category list" -msgstr "Lista kategorii" - -#: ../data/preferences.ui.h:12 -msgid "Add category" -msgstr "Dodaje kategorię" - -#: ../data/preferences.ui.h:13 -msgid "Remove category" -msgstr "Usuwa kategorię" - -#: ../data/preferences.ui.h:14 -msgid "Edit category" -msgstr "Modyfikuje kategorię" - -#: ../data/preferences.ui.h:15 -msgid "_Activities" -msgstr "_Czynności" - -#: ../data/preferences.ui.h:16 -msgid "Activity list" -msgstr "Lista czynności" - -#: ../data/preferences.ui.h:17 -msgid "Add activity" -msgstr "Dodaje czynność" - -#: ../data/preferences.ui.h:18 -msgid "Remove activity" -msgstr "Usuwa czynność" - -#: ../data/preferences.ui.h:19 -msgid "Edit activity" -msgstr "Modyfikuje czynność" - -#: ../data/preferences.ui.h:20 -msgid "Tags that should appear in autocomplete" -msgstr "" -"Etykiety, które powinny pojawiać się podczas automatycznego uzupełniania" - -#: ../data/preferences.ui.h:21 -msgid "Categories and Tags" -msgstr "Kategorie i etykiety" - -#: ../data/preferences.ui.h:22 -msgid "Resume the last activity when returning to a workspace" -msgstr "Wznawia ostatnią czynność po powrocie do obszaru roboczego" - -#: ../data/preferences.ui.h:23 -msgid "Start new activity when switching workspaces:" -msgstr "Rozpoczyna nową czynność podczas przełączania obszarów roboczych:" - -#: ../data/preferences.ui.h:24 -msgid "Workspaces" -msgstr "Obszary robocze" - -#: ../data/range_pick.ui.h:1 -msgid "Day:" -msgstr "Dzień:" - -#: ../data/range_pick.ui.h:2 -msgid "Week:" -msgstr "Tydzień:" - -#: ../data/range_pick.ui.h:3 -msgid "Month:" -msgstr "Miesiąc:" - -#: ../data/range_pick.ui.h:4 -msgid "Range:" -msgstr "Zakres:" - -#: ../data/range_pick.ui.h:6 -msgid "Apply" -msgstr "Zastosuj" - -#: ../data/today.ui.h:2 -msgid "_Tracking" -msgstr "Śl_edzenie" - -#: ../data/today.ui.h:3 -msgid "Add earlier activity" -msgstr "Dodaj wcześniejszą czynność" - -#: ../data/today.ui.h:4 -msgid "Overview" -msgstr "Przegląd" - -#: ../data/today.ui.h:5 -msgid "Statistics" -msgstr "Statystyki" - -#: ../data/today.ui.h:6 -msgid "_Edit" -msgstr "_Edycja" - -#: ../data/today.ui.h:7 -msgid "_Help" -msgstr "Pomo_c" - -#: ../data/today.ui.h:8 -msgid "Contents" -msgstr "Spis treści" - -#: ../data/today.ui.h:9 -msgid "Sto_p tracking" -msgstr "_Zatrzymaj śledzenie" - -#: ../data/today.ui.h:10 -msgid "S_witch" -msgstr "_Przełącz" - -#: ../data/today.ui.h:11 -msgid "Start _Tracking" -msgstr "_Rozpocznij śledzenie" - -#: ../data/today.ui.h:12 -msgid "Start new activity" -msgstr "Rozpocznij nową czynność" - -#: ../data/today.ui.h:13 -msgid "Today" -msgstr "Dzisiaj" - -#: ../data/today.ui.h:14 -msgid "totals" -msgstr "ogółem" - -#: ../data/today.ui.h:16 -msgid "Show Overview" -msgstr "Wyświetl podgląd" - -#: ../src/hamster-cli:254 ../src/hamster/today.py:289 -msgid "No activity" -msgstr "Brak czynności" - -#: ../src/hamster-cli:277 ../src/hamster/preferences.py:155 -#: ../src/hamster/reports.py:318 +#: ../src/hamster-cli.py:348 ../src/hamster/preferences.py:150 +#: ../src/hamster/reports.py:333 msgid "Category" msgstr "Kategoria" -#: ../src/hamster-cli:279 ../src/hamster/reports.py:323 +#: ../src/hamster-cli.py:349 ../src/hamster/reports.py:334 +msgid "Tags" +msgstr "Etykiety" + +#: ../src/hamster-cli.py:350 ../src/hamster/reports.py:338 msgid "Description" msgstr "Opis" -#: ../src/hamster-cli:280 ../src/hamster/reports.py:320 +#: ../src/hamster-cli.py:351 ../src/hamster/reports.py:335 msgid "Start" msgstr "Rozpoczęcie" -#: ../src/hamster-cli:281 ../src/hamster/reports.py:321 +#: ../src/hamster-cli.py:352 ../src/hamster/reports.py:336 msgid "End" msgstr "Zakończenie" -#: ../src/hamster-cli:282 ../src/hamster/reports.py:322 +#: ../src/hamster-cli.py:353 ../src/hamster/reports.py:337 msgid "Duration" msgstr "Czas trwania" -#: ../src/hamster-cli:308 -#, fuzzy -msgid "Uncategorized" -msgstr "kategorie" +#: ../src/hamster-cli.py:379 ../src/hamster/preferences.py:52 +#: ../src/hamster/reports.py:82 ../src/hamster/reports.py:114 +#: ../src/hamster/reports.py:273 ../src/hamster/widgets/activityentry.py:617 +msgid "Unsorted" +msgstr "Bez kategorii" -#: ../src/hamster/about.py:42 +#: ../src/hamster-cli.py:418 +msgid "" +"\n" +"Actions:\n" +" * add [activity [start-time [end-time]]]: Add an activity\n" +" * stop: Stop tracking current activity.\n" +" * list [start-date [end-date]]: List activities\n" +" * search [terms] [start-date [end-date]]: List activities matching a " +"search\n" +" term\n" +" * export [html|tsv|ical|xml|hamster|external] [start-date [end-date]]: " +"Export activities with\n" +" the specified format\n" +" * current: Print current activity\n" +" * activities: List all the activities names, one per line.\n" +" * categories: List all the categories names, one per line.\n" +"\n" +" * overview / preferences / add / about: launch specific window\n" +"\n" +" * version: Show the Hamster version\n" +"\n" +"Time formats:\n" +" * 'YYYY-MM-DD hh:mm': If start-date is missing, it will default to " +"today.\n" +" If end-date is missing, it will default to start-date.\n" +" * '-minutes': Relative time in minutes from the current date and time.\n" +"Note:\n" +" * For list/search/export a \"hamster day\" starts at the time set in " +"the\n" +" preferences (default 05:00) and ends one minute earlier the next day.\n" +" Activities are reported for each \"hamster day\" in the interval.\n" +"\n" +"Example usage:\n" +" hamster start bananas -20\n" +" start activity 'bananas' with start time 20 minutes ago\n" +"\n" +" hamster search pancakes 2012-08-01 2012-08-30\n" +" look for an activity matching terms 'pancakes` between 1st and 30st\n" +" August 2012. Will check against activity, category, description and " +"tags\n" +msgstr "" + +#: ../src/hamster/about.py:33 msgid "Project Hamster — track your time" msgstr "Projekt Hamster — zarządzanie czasem" -#: ../src/hamster/about.py:43 +#: ../src/hamster/about.py:34 msgid "Copyright © 2007–2010 Toms Bauģis and others" msgstr "Copyright © 2007–2010 Toms Bauģis i inni" -#: ../src/hamster/about.py:45 +#: ../src/hamster/about.py:36 msgid "Project Hamster Website" msgstr "Witryna programu Hamster" -#: ../src/hamster/about.py:46 +#: ../src/hamster/about.py:37 msgid "About Time Tracker" msgstr "O programie do zarządzania czasem" -#: ../src/hamster/about.py:56 +#: ../src/hamster/about.py:47 msgid "translator-credits" msgstr "" "Tomasz Dominikowski , 2008-2009\n" @@ -474,185 +175,151 @@ msgstr "" "Piotr Drąg , 2010-2012\n" "Aviary.pl , 2008-2012" -#: ../src/hamster/db.py:288 ../src/hamster/db.py:298 ../src/hamster/db.py:354 -#: ../src/hamster/db.py:658 ../src/hamster/db.py:845 -#: ../src/hamster/edit_activity.py:59 ../src/hamster/preferences.py:58 -#: ../src/hamster/reports.py:88 ../src/hamster/reports.py:127 -#: ../src/hamster/reports.py:256 ../src/hamster/today.py:275 -msgid "Unsorted" -msgstr "Bez sortowania" +#: ../src/hamster/edit_activity.py:85 +msgid "do not export" +msgstr "Nie eksportuj" -#. defaults -#: ../src/hamster/db.py:937 -msgid "Work" -msgstr "Praca" +#: ../src/hamster/edit_activity.py:93 +msgid "Update activity" +msgstr "Aktualizowanie czynności" -#: ../src/hamster/db.py:938 -msgid "Reading news" -msgstr "Czytanie wiadomości" +#: ../src/hamster/edit_activity.py:93 +msgid "Add activity" +msgstr "Dodaje czynność" -#: ../src/hamster/db.py:939 -msgid "Checking stocks" -msgstr "Sprawdzanie notowań giełdowych" +#: ../src/hamster/overview.py:71 +msgid "Menu" +msgstr "Menu" -#: ../src/hamster/db.py:940 -msgid "Super secret project X" -msgstr "Supertajny projekt X" +#: ../src/hamster/overview.py:77 +msgid "Filter activities" +msgstr "Filtruj czynności" -#: ../src/hamster/db.py:941 -msgid "World domination" -msgstr "Dominacja nad światem" +#: ../src/hamster/overview.py:83 +msgid "Stop tracking (Ctrl-SPACE)" +msgstr "_Zatrzymaj śledzenie (Ctrl-SPACE)" -#: ../src/hamster/db.py:943 -msgid "Day-to-day" -msgstr "Dzień za dniem" +#: ../src/hamster/overview.py:89 +msgid "Add activity (Ctrl-+)" +msgstr "Dodaj czynność (Ctrl-+)" -#: ../src/hamster/db.py:944 -msgid "Lunch" -msgstr "Obiad" +#: ../src/hamster/overview.py:94 +msgid "Export to file..." +msgstr "Eksportuj do pliku" -#: ../src/hamster/db.py:945 -msgid "Watering flowers" -msgstr "Podlewanie kwiatów" +#: ../src/hamster/overview.py:96 +msgid "Tracking Settings" +msgstr "Ustawienia śledzenia" -#: ../src/hamster/db.py:946 -msgid "Doing handstands" -msgstr "Stanie na rękach" +#: ../src/hamster/overview.py:98 +msgid "Help" +msgstr "Pomoc" -#: ../src/hamster/edit_activity.py:75 -msgid "Update activity" -msgstr "Aktualizowanie czynności" +#: ../src/hamster/overview.py:268 ../src/hamster/overview.py:282 +msgid "📤 Start export" +msgstr "📤 Zacznij export" -#. duration in round hours -#: ../src/hamster/lib/stuff.py:57 +#: ../src/hamster/overview.py:280 #, python-format -msgid "%dh" -msgstr "%dh" +msgid "Waiting for action (%s activities to export)" +msgstr "Oczekiwanie na akcję (%s czynności do eksportu)" -#. duration less than hour -#: ../src/hamster/lib/stuff.py:60 -#, python-format -msgid "%dmin" -msgstr "%dmin" +#: ../src/hamster/overview.py:293 +msgid "Exporting..." +msgstr "Eksportowanie..." -#. x hours, y minutes -#: ../src/hamster/lib/stuff.py:63 -#, python-format -msgid "%dh %dmin" -msgstr "%dh %dmin" +#: ../src/hamster/overview.py:299 +msgid "Interrupted" +msgstr "Przerwano" -#. label of date range if looking on single day -#. date format for overview label when only single day is visible -#. Using python datetime formatting syntax. See: -#. http://docs.python.org/library/time.html#time.strftime -#: ../src/hamster/lib/stuff.py:80 -msgid "%B %d, %Y" -msgstr "%d %B %Y" - -#. label of date range if start and end years don't match -#. letter after prefixes (start_, end_) is the one of -#. standard python date formatting ones- you can use all of them -#. see http://docs.python.org/library/time.html#time.strftime -#: ../src/hamster/lib/stuff.py:86 -#, python-format -msgid "%(start_B)s %(start_d)s, %(start_Y)s – %(end_B)s %(end_d)s, %(end_Y)s" -msgstr "%(start_d)s %(start_B)s, %(start_Y)s – %(end_d)s %(end_B)s, %(end_Y)s" - -#. label of date range if start and end month do not match -#. letter after prefixes (start_, end_) is the one of -#. standard python date formatting ones- you can use all of them -#. see http://docs.python.org/library/time.html#time.strftime -#: ../src/hamster/lib/stuff.py:92 -#, python-format -msgid "%(start_B)s %(start_d)s – %(end_B)s %(end_d)s, %(end_Y)s" -msgstr "%(start_d)s %(start_B)s – %(end_d)s %(end_B)s, %(end_Y)s" - -#. label of date range for interval in same month -#. letter after prefixes (start_, end_) is the one of -#. standard python date formatting ones- you can use all of them -#. see http://docs.python.org/library/time.html#time.strftime -#: ../src/hamster/lib/stuff.py:98 -#, python-format -msgid "%(start_B)s %(start_d)s – %(end_d)s, %(end_Y)s" -msgstr "%(start_d)s %(start_B)s – %(end_d)s, %(end_Y)s" +#: ../src/hamster/overview.py:302 ../src/hamster/overview.py:303 +msgid "Done" +msgstr "Koniec" -#: ../src/hamster/overview_activities.py:88 -msgctxt "overview list" -msgid "%A, %b %d" -msgstr "%A, %d %b" +#: ../src/hamster/overview.py:327 +msgid "Connecting to external source..." +msgstr "Łączenie do zewnętrznego źródła..." -#: ../src/hamster/overview_totals.py:161 +#: ../src/hamster/overview.py:333 #, python-format -msgid "%s hours tracked total" -msgstr "Ogółem prześledzono %s godzin" +msgid "Exporting: %s - %s" +msgstr "Eksportowanie: %s - %s" + +#: ../src/hamster/overview.py:367 +msgid "Click to see stats" +msgstr "Kliknij aby zobaczyć statystyki" + +#: ../src/hamster/overview.py:673 +msgid "Failed to open {}" +msgstr "Nie udało się otworzyć {}" + +#: ../src/hamster/overview.py:674 +msgid "Error: \"{}\" - is a help browser installed on this computer?" +msgstr "Błąd: \"{}\" - czy zainstalowana została przeglądarka pomocy?" -#. Translators: 'None' refers here to the Todo list choice in Hamster preferences (Tracking tab) -#: ../src/hamster/preferences.py:113 +#. activities source +#: ../src/hamster/preferences.py:83 msgid "None" msgstr "Brak" -#: ../src/hamster/preferences.py:130 ../src/hamster/preferences.py:208 +#: ../src/hamster/preferences.py:125 msgid "Name" msgstr "Nazwa" -#: ../src/hamster/preferences.py:664 +#: ../src/hamster/preferences.py:542 msgid "New category" msgstr "Nowa kategoria" -#: ../src/hamster/preferences.py:677 +#: ../src/hamster/preferences.py:556 msgid "New activity" msgstr "Nowa czynność" -#. notify interval slider value label -#: ../src/hamster/preferences.py:738 -#, python-format -msgid "%(interval_minutes)d minute" -msgid_plural "%(interval_minutes)d minutes" -msgstr[0] "%(interval_minutes)d minuta" -msgstr[1] "%(interval_minutes)d minuty" -msgstr[2] "%(interval_minutes)d minut" - -#. notify interval slider value label -#: ../src/hamster/preferences.py:743 -msgid "Never" -msgstr "Nigdy" - #. column title in the TSV export format -#: ../src/hamster/reports.py:148 +#: ../src/hamster/reports.py:138 msgid "activity" msgstr "czynność" #. column title in the TSV export format -#: ../src/hamster/reports.py:150 +#: ../src/hamster/reports.py:140 msgid "start time" msgstr "początek" #. column title in the TSV export format -#: ../src/hamster/reports.py:152 +#: ../src/hamster/reports.py:142 msgid "end time" msgstr "koniec" #. column title in the TSV export format -#: ../src/hamster/reports.py:154 +#: ../src/hamster/reports.py:144 msgid "duration minutes" msgstr "czas trwania w minutach" #. column title in the TSV export format -#: ../src/hamster/reports.py:156 +#: ../src/hamster/reports.py:146 msgid "category" msgstr "kategoria" #. column title in the TSV export format -#: ../src/hamster/reports.py:158 +#: ../src/hamster/reports.py:148 msgid "description" msgstr "opis" #. column title in the TSV export format -#: ../src/hamster/reports.py:160 ../src/hamster/reports.py:312 +#: ../src/hamster/reports.py:150 ../src/hamster/reports.py:327 msgid "tags" msgstr "etykiety" -#: ../src/hamster/reports.py:207 +#: ../src/hamster/reports.py:182 +#, python-format +msgid "Exported: %s - %s" +msgstr "Wyeksportowano: %s - %s" + +#: ../src/hamster/reports.py:187 +#, python-format +msgid "Fact not exported: %s" +msgstr "Czynność nie została wyeksportowana: %s" + +#: ../src/hamster/reports.py:224 #, python-format msgid "" "Activity report for %(start_B)s %(start_d)s, %(start_Y)s – %(end_B)s " @@ -661,19 +328,19 @@ msgstr "" "Raport czynności dla %(start_B)s %(start_d)s, %(start_Y)s – %(end_B)s " "%(end_d)s, %(end_Y)s" -#: ../src/hamster/reports.py:209 +#: ../src/hamster/reports.py:226 #, python-format msgid "" "Activity report for %(start_B)s %(start_d)s – %(end_B)s %(end_d)s, %(end_Y)s" msgstr "" "Raport czynności dla %(start_B)s %(start_d)s – %(end_B)s %(end_d)s, %(end_Y)s" -#: ../src/hamster/reports.py:211 +#: ../src/hamster/reports.py:228 #, python-format msgid "Activity report for %(start_B)s %(start_d)s, %(start_Y)s" msgstr "Raport czynności dla %(start_B)s %(start_d)s, %(start_Y)s" -#: ../src/hamster/reports.py:213 +#: ../src/hamster/reports.py:230 #, python-format msgid "Activity report for %(start_B)s %(start_d)s – %(end_d)s, %(end_Y)s" msgstr "Raport czynności dla %(start_B)s %(start_d)s – %(end_d)s, %(end_Y)s" @@ -681,209 +348,512 @@ msgstr "Raport czynności dla %(start_B)s %(start_d)s – %(end_d)s, %(end_Y)s" #. date column format for each row in HTML report #. Using python datetime formatting syntax. See: #. http://docs.python.org/library/time.html#time.strftime -#: ../src/hamster/reports.py:265 ../src/hamster/reports.py:297 +#: ../src/hamster/reports.py:282 ../src/hamster/reports.py:314 msgctxt "html report" msgid "%b %d, %Y" msgstr "%d %b %Y" -#. grand_total = _("%s hours") % ("%.1f" % (total_duration.seconds / 60.0 / 60 + total_duration.days * 24)), -#: ../src/hamster/reports.py:306 +#: ../src/hamster/reports.py:321 msgid "Totals by Day" msgstr "Ogółem według dni" -#: ../src/hamster/reports.py:307 +#: ../src/hamster/reports.py:322 msgid "Activity Log" msgstr "Dziennik czynności" -#: ../src/hamster/reports.py:310 +#: ../src/hamster/reports.py:323 +msgid "Totals" +msgstr "Ogółem" + +#: ../src/hamster/reports.py:325 msgid "activities" msgstr "czynności" -#: ../src/hamster/reports.py:311 +#: ../src/hamster/reports.py:326 msgid "categories" msgstr "kategorie" -#: ../src/hamster/reports.py:314 +#: ../src/hamster/reports.py:329 msgid "Distinguish:" msgstr "Rozróżnienie:" -#: ../src/hamster/reports.py:316 +#: ../src/hamster/reports.py:331 msgid "Date" msgstr "Data" -#: ../src/hamster/reports.py:326 +#: ../src/hamster/reports.py:341 msgid "Show template" msgstr "Wyświetlanie szablonu" -#: ../src/hamster/reports.py:327 +#: ../src/hamster/reports.py:342 #, python-format msgid "You can override it by storing your version in %(home_folder)s" msgstr "Można go zastąpić przez umieszczenie swojej wersji w %(home_folder)s" -#: ../src/hamster/stats.py:147 -msgctxt "years" -msgid "All" -msgstr "Wszystkie" +#: ../src/hamster/widgets/reportchooserdialog.py:37 +msgid "Save Report — Time Tracker" +msgstr "Zapisywanie sprawozdania — zarządzanie czasem" -#: ../src/hamster/stats.py:177 -msgid "" -"There is no data to generate statistics yet.\n" -"A week of usage would be nice!" -msgstr "" -"Brak wystarczającej ilości danych do wygenerowania statystyk.\n" -"Potrzeba przynajmniej tygodnia danych." +#: ../src/hamster/widgets/reportchooserdialog.py:55 +msgid "HTML Report" +msgstr "Raport w formacie HTML" -#: ../src/hamster/stats.py:180 -msgid "Collecting data — check back after a week has passed!" -msgstr "" -"Zbieranie danych — proszę zajrzeć tutaj ponownie po upływie jednego tygodnia." +#: ../src/hamster/widgets/reportchooserdialog.py:63 +msgid "Tab-Separated Values (TSV)" +msgstr "Wartości oddzielone znakami tabulatora (TSV)" -#. date format for the first record if the year has not been selected -#. Using python datetime formatting syntax. See: -#. http://docs.python.org/library/time.html#time.strftime -#: ../src/hamster/stats.py:331 -msgctxt "first record" -msgid "%b %d, %Y" -msgstr "%d %b %Y" +#: ../src/hamster/widgets/reportchooserdialog.py:71 +msgid "XML" +msgstr "XML" -#. date of first record when year has been selected -#. Using python datetime formatting syntax. See: -#. http://docs.python.org/library/time.html#time.strftime -#: ../src/hamster/stats.py:336 -msgctxt "first record" -msgid "%b %d" -msgstr "%d %b" +#: ../src/hamster/widgets/reportchooserdialog.py:78 +msgid "iCal" +msgstr "iCal" -#: ../src/hamster/stats.py:338 -#, python-format -msgid "First activity was recorded on %s." -msgstr "Data zarejestrowania pierwszej czynności: %s." +#. title in the report file name +#: ../src/hamster/widgets/reportchooserdialog.py:95 +msgid "Time track" +msgstr "Zarządzanie czasem" -#: ../src/hamster/stats.py:347 ../src/hamster/stats.py:351 -#, python-format -msgid "%(num)s year" -msgid_plural "%(num)s years" -msgstr[0] "%(num)s rok" -msgstr[1] "%(num)s lata" -msgstr[2] "%(num)s lat" - -#. FIXME: difficult string to properly pluralize -#: ../src/hamster/stats.py:356 -#, python-format -msgid "" -"Time tracked so far is %(human_days)s human days (%(human_years)s) or " -"%(working_days)s working days (%(working_years)s)." -msgstr "" -"Do chwili obecnej prześledzono czynności o łącznym czasie trwania " -"%(human_days)s dni (%(human_years)s) lub %(working_days)s dni roboczych " -"(%(working_years)s)." +#~ msgid "Add Earlier Activity" +#~ msgstr "Dodaj wcześniejszą czynność" + +#~ msgid "to" +#~ msgstr "do" -#. How the date of the longest activity should be displayed in statistics -#. Using python datetime formatting syntax. See: -#. http://docs.python.org/library/time.html#time.strftime -#: ../src/hamster/stats.py:374 -msgctxt "date of the longest activity" -msgid "%b %d, %Y" -msgstr "%d %b %Y" +#~ msgid "in progress" +#~ msgstr "w trakcie" -#: ../src/hamster/stats.py:379 -#, python-format -msgid "Longest continuous work happened on %(date)s and was %(hours)s hour." -msgid_plural "" -"Longest continuous work happened on %(date)s and was %(hours)s hours." -msgstr[0] "" -"Najdłuższa czynność została odnotowana dnia %(date)s i trwała %(hours)s " -"godzinę." -msgstr[1] "" -"Najdłuższa czynność została odnotowana dnia %(date)s i trwała %(hours)s " -"godziny." -msgstr[2] "" -"Najdłuższa czynność została odnotowana dnia %(date)s i trwała %(hours)s " -"godzin." - -#. total records (in selected scope) -#: ../src/hamster/stats.py:387 -#, python-format -msgid "There is %s record." -msgid_plural "There are %s records." -msgstr[0] "Odnaleziono %s zapis." -msgstr[1] "Odnaleziono %s zapisy." -msgstr[2] "Odnaleziono %s zapisów." +#~ msgid "Description:" +#~ msgstr "Opis:" -#: ../src/hamster/stats.py:407 -msgid "Hamster would like to observe you some more!" -msgstr "Program Hamster musi zebrać więcej danych." +#~ msgid "Time:" +#~ msgstr "Czas:" -#: ../src/hamster/stats.py:409 -#, python-format -msgid "" -"With %s percent of all activities starting before 9am, you seem to be an " -"early bird." -msgstr "" -"Jako że %s procent wszystkich czynności rozpoczyna się przed 9 rano, więc " -"użytkownik wydaje się być rannym ptaszkiem." +#~ msgid "Activity:" +#~ msgstr "Czynność:" -#: ../src/hamster/stats.py:412 -#, python-format -msgid "" -"With %s percent of all activities starting after 11pm, you seem to be a " -"night owl." -msgstr "" -"Jako że %s procent wszystkich czynności zaczyna się po 11 popołudniu, więc " -"użytkownik wydaje się być nocnym markiem." +#~ msgid "Tags:" +#~ msgstr "Etykiety:" -#: ../src/hamster/stats.py:415 -#, python-format -msgid "" -"With %s percent of all activities being shorter than 15 minutes, you seem to " -"be a busy bee." -msgstr "" -"Jako że %s procent wszystkich czynności jest krótsza niż 15 minut, więc " -"użytkownik wydaje się być pracowitą pszczółką." +#~ msgid "Stop tracking on idle" +#~ msgstr "Zatrzymanie czasu podczas bezczynności" -#: ../src/hamster/today.py:243 -msgid "No records today" -msgstr "Brak zapisów na dzisiaj" +#~ msgid "Stop tracking current activity when computer becomes idle" +#~ msgstr "" +#~ "Zatrzymanie śledzenia bieżącej czynności podczas bezczynności komputera" -#: ../src/hamster/today.py:250 -#, python-format -msgid "%(category)s: %(duration)s" -msgstr "%(category)s: %(duration)s" +#~ msgid "Stop tracking on shutdown" +#~ msgstr "Zatrzymanie śledzenia przy wyłączeniu komputera" -#. duration in main drop-down per category in hours -#: ../src/hamster/today.py:253 -#, python-format -msgid "%sh" -msgstr "%sh" +#~ msgid "Stop tracking current activity on shutdown" +#~ msgstr "Zatrzymanie śledzenia bieżącej czynności przy wyłączeniu komputera" -#: ../src/hamster/today.py:280 -msgid "Just started" -msgstr "Dopiero rozpoczęto" +#~ msgid "Remind of current task every x minutes" +#~ msgstr "Przypominanie o bieżącej czynności co x minut" -#: ../src/hamster/widgets/reportchooserdialog.py:39 -msgid "Save Report — Time Tracker" -msgstr "Zapisywanie sprawozdania — zarządzanie czasem" +#~ msgid "" +#~ "Remind of current task every specified amount of minutes. Set to 0 or " +#~ "greater than 120 to disable reminder." +#~ msgstr "" +#~ "Przypominanie o bieżącej czynności co określoną liczbę minut. Ustawienie " +#~ "wartości na 0 lub więcej niż 120, wyłącza przypominanie." -#: ../src/hamster/widgets/reportchooserdialog.py:57 -msgid "HTML Report" -msgstr "Raport w formacie HTML" +#~ msgid "Also remind when no activity is set" +#~ msgstr "Przypominanie również, gdy nie ustawiono żadnej czynności" -#: ../src/hamster/widgets/reportchooserdialog.py:65 -msgid "Tab-Separated Values (TSV)" -msgstr "Wartości oddzielone znakami tabulatora (TSV)" +#~ msgid "" +#~ "Also remind every notify_interval minutes if no activity has been started." +#~ msgstr "" +#~ "Przypominanie co notify_interval minut nawet, jeśli nie uruchomiono " +#~ "żadnej czynności." -#: ../src/hamster/widgets/reportchooserdialog.py:73 -msgid "XML" -msgstr "XML" +#~ msgid "" +#~ "Activities will be counted as to belong to yesterday if the current time " +#~ "is less than the specified day start; and today, if it is over the time. " +#~ "Activities that span two days, will tip over to the side where the " +#~ "largest part of the activity is." +#~ msgstr "" +#~ "Czynności będą liczone jako należące do dnia poprzedniego, jeśli bieżący " +#~ "czas jest wcześniejszy niż określony czas rozpoczęcia danego dnia, a jako " +#~ "należące do dnia dzisiejszego, jeśli ten czas minął. Czynności " +#~ "rozdzielone na dwa dni będą przesuwane na ten dzień, w którym odbyło się " +#~ "większość czasu czynności." -#: ../src/hamster/widgets/reportchooserdialog.py:80 -msgid "iCal" -msgstr "iCal" +#~ msgid "Should workspace switch trigger activity switch" +#~ msgstr "" +#~ "Określa, czy przełączenie obszaru roboczego wywołuje przełączenie " +#~ "czynności" -#. title in the report file name -#: ../src/hamster/widgets/reportchooserdialog.py:97 -msgid "Time track" -msgstr "Zarządzanie czasem" +#~ msgid "" +#~ "List of enabled tracking methods. \"name\" will enable switching " +#~ "activities by name defined in workspace_mapping. \"memory\" will enable " +#~ "switching to the last activity when returning to a previous workspace." +#~ msgstr "" +#~ "Lista włączonych metod śledzenia. Wartość \"name\" włączy przełączanie " +#~ "czynności według nazwy określonej w kluczu \"workspace_mapping\". Wartość " +#~ "\"memory\" włączy przełączanie na ostatnią czynność podczas powracania na " +#~ "poprzedni obszar roboczy." + +#~ msgid "Switch activity on workspace change" +#~ msgstr "Przełączenie czynności po zmianie obszaru roboczego" + +#~ msgid "" +#~ "If switching by name is enabled, this list sets the activity names that " +#~ "should be switched to, workspaces represented by the index of item." +#~ msgstr "" +#~ "Jeśli włączone jest przełączanie według nazwy, to ta lista ustawia nazwy " +#~ "czynności, na które można się przełączyć, z obszarem roboczym " +#~ "prezentowanym przez indeks elementów." + +#~ msgid "Show / hide Time Tracker Window" +#~ msgstr "Wyświetlanie/ukrywanie okna zarządzania czasem" + +#~ msgid "Keyboard shortcut for showing / hiding the Time Tracker window." +#~ msgstr "Skrót klawiszowy do wyświetlenia/ukrywania okna zarządzania czasem." + +#~ msgid "Toggle hamster application window action" +#~ msgstr "Przełączenie czynności okna programu Hamster" + +#~ msgid "Command for toggling visibility of the hamster application window." +#~ msgstr "Polecenie do przełącznika widoczności okna programu Hamster." + +#~ msgid "Toggle hamster application window" +#~ msgstr "Przełączenie okna programu Hamster" + +#~ msgid "Toggle visibility of the hamster application window." +#~ msgstr "Przełączenie widoczności okna programu Hamster." + +#~ msgid "Time Tracker" +#~ msgstr "Zarządzanie czasem" + +#~ msgid "Project Hamster - track your time" +#~ msgstr "Projekt Hamster - zarządzanie czasem" + +#~ msgid "Time Tracking Overview" +#~ msgstr "Okno podglądu zarządzania czasem" + +#~ msgid "The overview window of hamster time tracker" +#~ msgstr "Okno podglądu programu do zarządzania czasem Hamster" + +#~ msgid "Show Statistics" +#~ msgstr "Wyświetl statystyki" + +#~ msgid "Categories" +#~ msgstr "Kategorie" + +#~ msgid "No data for this interval" +#~ msgstr "Brak danych dla tego przedziału czasowego" + +#~ msgid "Save report..." +#~ msgstr "Zapisz sprawozdanie..." + +#~ msgid "Day" +#~ msgstr "Dzień" + +#~ msgid "Week" +#~ msgstr "Tydzień" + +#~ msgid "Month" +#~ msgstr "Miesiąc" + +#~ msgid "Overview — Hamster" +#~ msgstr "Przegląd — Hamster" + +#~ msgid "_Overview" +#~ msgstr "_Przegląd" + +#~ msgid "_View" +#~ msgstr "_Widok" + +#~ msgid "Remove" +#~ msgstr "Usuń" + +#~ msgid "Add new" +#~ msgstr "Dodaj nową" + +#~ msgid "Edit" +#~ msgstr "Zmodyfikuj" + +#~ msgid "Time Tracker Preferences" +#~ msgstr "Preferencje programu zarządzania czasem" + +#~ msgid "Stop tracking when computer becomes idle" +#~ msgstr "Zatrzymanie śledzenia podczas bezczynności komputera" + +#~ msgid "Remind of current activity every:" +#~ msgstr "Przypominanie o bieżącej czynności co:" + +#~ msgid "New day starts at" +#~ msgstr "Nowy dzień rozpoczyna się o" + +#~ msgid "Use following todo list if available:" +#~ msgstr "" +#~ "Użycie następującej listy czynności do wykonania, jeśli jest dostępna:" + +#~ msgid "Integration" +#~ msgstr "Integracja" + +#~ msgid "_Categories" +#~ msgstr "_Kategorie" + +#~ msgid "Category list" +#~ msgstr "Lista kategorii" + +#~ msgid "Add category" +#~ msgstr "Dodaje kategorię" + +#~ msgid "Remove category" +#~ msgstr "Usuwa kategorię" + +#~ msgid "_Activities" +#~ msgstr "_Czynności" + +#~ msgid "Activity list" +#~ msgstr "Lista czynności" + +#~ msgid "Remove activity" +#~ msgstr "Usuwa czynność" + +#~ msgid "Edit activity" +#~ msgstr "Modyfikuje czynność" + +#~ msgid "Tags that should appear in autocomplete" +#~ msgstr "" +#~ "Etykiety, które powinny pojawiać się podczas automatycznego uzupełniania" + +#~ msgid "Categories and Tags" +#~ msgstr "Kategorie i etykiety" + +#~ msgid "Resume the last activity when returning to a workspace" +#~ msgstr "Wznawia ostatnią czynność po powrocie do obszaru roboczego" + +#~ msgid "Start new activity when switching workspaces:" +#~ msgstr "Rozpoczyna nową czynność podczas przełączania obszarów roboczych:" + +#~ msgid "Workspaces" +#~ msgstr "Obszary robocze" + +#~ msgid "Day:" +#~ msgstr "Dzień:" + +#~ msgid "Week:" +#~ msgstr "Tydzień:" + +#~ msgid "Month:" +#~ msgstr "Miesiąc:" + +#~ msgid "Range:" +#~ msgstr "Zakres:" + +#~ msgid "Apply" +#~ msgstr "Zastosuj" + +#~ msgid "_Tracking" +#~ msgstr "Śl_edzenie" + +#~ msgid "Add earlier activity" +#~ msgstr "Dodaj wcześniejszą czynność" + +#~ msgid "Overview" +#~ msgstr "Przegląd" + +#~ msgid "Statistics" +#~ msgstr "Statystyki" + +#~ msgid "_Edit" +#~ msgstr "_Edycja" + +#~ msgid "Contents" +#~ msgstr "Spis treści" + +#~ msgid "S_witch" +#~ msgstr "_Przełącz" + +#~ msgid "Start _Tracking" +#~ msgstr "_Rozpocznij śledzenie" + +#~ msgid "Start new activity" +#~ msgstr "Rozpocznij nową czynność" + +#~ msgid "Today" +#~ msgstr "Dzisiaj" + +#~ msgid "totals" +#~ msgstr "ogółem" + +#~ msgid "Show Overview" +#~ msgstr "Wyświetl podgląd" + +#, fuzzy +#~ msgid "Uncategorized" +#~ msgstr "kategorie" + +#~ msgid "Work" +#~ msgstr "Praca" + +#~ msgid "Reading news" +#~ msgstr "Czytanie wiadomości" + +#~ msgid "Checking stocks" +#~ msgstr "Sprawdzanie notowań giełdowych" + +#~ msgid "Super secret project X" +#~ msgstr "Supertajny projekt X" + +#~ msgid "World domination" +#~ msgstr "Dominacja nad światem" + +#~ msgid "Day-to-day" +#~ msgstr "Dzień za dniem" + +#~ msgid "Lunch" +#~ msgstr "Obiad" + +#~ msgid "Watering flowers" +#~ msgstr "Podlewanie kwiatów" + +#~ msgid "Doing handstands" +#~ msgstr "Stanie na rękach" + +#~ msgid "%dh" +#~ msgstr "%dh" + +#~ msgid "%dmin" +#~ msgstr "%dmin" + +#~ msgid "%dh %dmin" +#~ msgstr "%dh %dmin" + +#~ msgid "%B %d, %Y" +#~ msgstr "%d %B %Y" + +#~ msgid "" +#~ "%(start_B)s %(start_d)s, %(start_Y)s – %(end_B)s %(end_d)s, %(end_Y)s" +#~ msgstr "" +#~ "%(start_d)s %(start_B)s, %(start_Y)s – %(end_d)s %(end_B)s, %(end_Y)s" + +#~ msgid "%(start_B)s %(start_d)s – %(end_B)s %(end_d)s, %(end_Y)s" +#~ msgstr "%(start_d)s %(start_B)s – %(end_d)s %(end_B)s, %(end_Y)s" + +#~ msgid "%(start_B)s %(start_d)s – %(end_d)s, %(end_Y)s" +#~ msgstr "%(start_d)s %(start_B)s – %(end_d)s, %(end_Y)s" + +#~ msgctxt "overview list" +#~ msgid "%A, %b %d" +#~ msgstr "%A, %d %b" + +#~ msgid "%s hours tracked total" +#~ msgstr "Ogółem prześledzono %s godzin" + +#~ msgid "%(interval_minutes)d minute" +#~ msgid_plural "%(interval_minutes)d minutes" +#~ msgstr[0] "%(interval_minutes)d minuta" +#~ msgstr[1] "%(interval_minutes)d minuty" +#~ msgstr[2] "%(interval_minutes)d minut" + +#~ msgid "Never" +#~ msgstr "Nigdy" + +#~ msgctxt "years" +#~ msgid "All" +#~ msgstr "Wszystkie" + +#~ msgid "" +#~ "There is no data to generate statistics yet.\n" +#~ "A week of usage would be nice!" +#~ msgstr "" +#~ "Brak wystarczającej ilości danych do wygenerowania statystyk.\n" +#~ "Potrzeba przynajmniej tygodnia danych." + +#~ msgid "Collecting data — check back after a week has passed!" +#~ msgstr "" +#~ "Zbieranie danych — proszę zajrzeć tutaj ponownie po upływie jednego " +#~ "tygodnia." + +#~ msgctxt "first record" +#~ msgid "%b %d, %Y" +#~ msgstr "%d %b %Y" + +#~ msgctxt "first record" +#~ msgid "%b %d" +#~ msgstr "%d %b" + +#~ msgid "First activity was recorded on %s." +#~ msgstr "Data zarejestrowania pierwszej czynności: %s." + +#~ msgid "%(num)s year" +#~ msgid_plural "%(num)s years" +#~ msgstr[0] "%(num)s rok" +#~ msgstr[1] "%(num)s lata" +#~ msgstr[2] "%(num)s lat" + +#~ msgid "" +#~ "Time tracked so far is %(human_days)s human days (%(human_years)s) or " +#~ "%(working_days)s working days (%(working_years)s)." +#~ msgstr "" +#~ "Do chwili obecnej prześledzono czynności o łącznym czasie trwania " +#~ "%(human_days)s dni (%(human_years)s) lub %(working_days)s dni roboczych " +#~ "(%(working_years)s)." + +#~ msgctxt "date of the longest activity" +#~ msgid "%b %d, %Y" +#~ msgstr "%d %b %Y" + +#~ msgid "Longest continuous work happened on %(date)s and was %(hours)s hour." +#~ msgid_plural "" +#~ "Longest continuous work happened on %(date)s and was %(hours)s hours." +#~ msgstr[0] "" +#~ "Najdłuższa czynność została odnotowana dnia %(date)s i trwała %(hours)s " +#~ "godzinę." +#~ msgstr[1] "" +#~ "Najdłuższa czynność została odnotowana dnia %(date)s i trwała %(hours)s " +#~ "godziny." +#~ msgstr[2] "" +#~ "Najdłuższa czynność została odnotowana dnia %(date)s i trwała %(hours)s " +#~ "godzin." + +#~ msgid "There is %s record." +#~ msgid_plural "There are %s records." +#~ msgstr[0] "Odnaleziono %s zapis." +#~ msgstr[1] "Odnaleziono %s zapisy." +#~ msgstr[2] "Odnaleziono %s zapisów." + +#~ msgid "Hamster would like to observe you some more!" +#~ msgstr "Program Hamster musi zebrać więcej danych." + +#~ msgid "" +#~ "With %s percent of all activities starting before 9am, you seem to be an " +#~ "early bird." +#~ msgstr "" +#~ "Jako że %s procent wszystkich czynności rozpoczyna się przed 9 rano, więc " +#~ "użytkownik wydaje się być rannym ptaszkiem." + +#~ msgid "" +#~ "With %s percent of all activities starting after 11pm, you seem to be a " +#~ "night owl." +#~ msgstr "" +#~ "Jako że %s procent wszystkich czynności zaczyna się po 11 popołudniu, " +#~ "więc użytkownik wydaje się być nocnym markiem." + +#~ msgid "" +#~ "With %s percent of all activities being shorter than 15 minutes, you seem " +#~ "to be a busy bee." +#~ msgstr "" +#~ "Jako że %s procent wszystkich czynności jest krótsza niż 15 minut, więc " +#~ "użytkownik wydaje się być pracowitą pszczółką." + +#~ msgid "No records today" +#~ msgstr "Brak zapisów na dzisiaj" + +#~ msgid "%(category)s: %(duration)s" +#~ msgstr "%(category)s: %(duration)s" + +#~ msgid "%sh" +#~ msgstr "%sh" + +#~ msgid "Just started" +#~ msgstr "Dopiero rozpoczęto" #~ msgid "Show activities window" #~ msgstr "Wyświetlanie okna czynności" diff --git a/src/hamster/reports.py b/src/hamster/reports.py index 3cb048ada..99659172c 100644 --- a/src/hamster/reports.py +++ b/src/hamster/reports.py @@ -184,7 +184,7 @@ def _write_fact(self, fact): self.storage.update_fact(fact.id, fact, False) pass else: - self.file.write(_("Fact not exported: %s" % fact.activity) + "\n") + self.file.write(_("Activity not exported: %s" % fact.activity) + "\n") def _finish(self, facts): pass From 22faf2566bd758ebf22b51aefac5d3565a89398b Mon Sep 17 00:00:00 2001 From: Grzegorz Sobczyk Date: Sat, 29 Aug 2020 18:32:19 +0200 Subject: [PATCH 13/24] Added more logging info when exporting activities --- src/hamster/external/external.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/hamster/external/external.py b/src/hamster/external/external.py index 38f313912..e9a92a835 100644 --- a/src/hamster/external/external.py +++ b/src/hamster/external/external.py @@ -138,7 +138,7 @@ def __jira_get_activities(self, query='', jira_query=None): if query is None or all(item in activity['name'].lower() for item in query.lower().split(' ')): activities.append(activity) except Exception as e: - logger.warn(e) + logger.warning(e) return activities def __jira_extract_activity_from_issue(self, issue): @@ -185,7 +185,8 @@ def __is_connected(self, url): try: self.__http.request('GET', url, timeout=1) return True - except urllib3.HTTPError as err: + except Exception as err: + logger.info(err) return False def __jira_is_issue_from_existing_project(self, issue): From 1c4a968e75a5544af98e472da4a6997d0f75d720 Mon Sep 17 00:00:00 2001 From: Grzegorz Sobczyk Date: Fri, 4 Sep 2020 09:16:13 +0200 Subject: [PATCH 14/24] README actualization despite problems with jira on Ubuntu 18.4 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 241f5e4a4..5ff75e792 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ sudo apt install gettext intltool python3-gi python3-cairo python3-distutils pyt sudo apt install python-tz # and for documentation sudo apt install itstool yelp -# and for jira integration +# and for jira integration (should be python3-jira>=2.0.0, if not - use pip) sudo apt install python3-jira python3-urllib3 ``` @@ -87,7 +87,7 @@ sudo zypper install intltool python3-pyxdg python3-cairo python3-gobject-Gdk sudo zypper install python-tz # and for documentation sudo zypper install itstool yelp -# and for jira integration +# and for jira integration (should be python3-jira>=2.0.0, if not - use pip) sudo zypper install python3-jira python3-urllib3 ``` From e9813b623704d9b5297450e7af1811282f69196b Mon Sep 17 00:00:00 2001 From: Grzegorz Sobczyk Date: Thu, 1 Oct 2020 00:24:10 +0200 Subject: [PATCH 15/24] - ignore case suggest - fix errors from pango when activity contains & sign - bolding matching text with ignore case --- src/hamster/external/external.py | 8 ++++---- src/hamster/widgets/activityentry.py | 23 +++++++++++++---------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/hamster/external/external.py b/src/hamster/external/external.py index e9a92a835..60546fdd2 100644 --- a/src/hamster/external/external.py +++ b/src/hamster/external/external.py @@ -94,7 +94,7 @@ def __connect_to_jira(self, conf): def get_activities(self, query=None): query = query.strip() - if not self.source or not query: + if not self.source: return [] elif self.source == SOURCE_JIRA: activities = self.__jira_get_activities(query, self.jira_query) @@ -113,10 +113,10 @@ def get_activities(self, query=None): jira_query = " AND ".join( fragments) + " AND resolution = Unresolved order by priority desc, updated desc" logging.info(jira_query) - third_activities = self.__jira_get_activities('', jira_query) - if activities and third_activities: + default_jira_activities = self.__jira_get_activities('', jira_query) + if activities and default_jira_activities: activities.append({"name": "---------------------", "category": "other open"}) - activities.extend(third_activities) + activities.extend(default_jira_activities) return activities def __generate_fragment_jira_query(self, word): diff --git a/src/hamster/widgets/activityentry.py b/src/hamster/widgets/activityentry.py index 97a3ac02e..8ac67c971 100644 --- a/src/hamster/widgets/activityentry.py +++ b/src/hamster/widgets/activityentry.py @@ -24,6 +24,7 @@ import cairo import re +from xml.sax.saxutils import escape from gi.repository import Gdk as gdk from gi.repository import Gtk as gtk from gi.repository import GObject as gobject @@ -51,7 +52,7 @@ def extract_search(text): search += "@%s" % fact.category if fact.tags: search += " #%s" % (" #".join(fact.tags)) - return search + return search.lower() class DataRow(object): """want to split out visible label, description, activity data @@ -227,6 +228,8 @@ def __init__(self, updating=True, **kwargs): box.add(self.complete_tree) self.storage = client.Storage() + self.todays_facts = None + self.local_suggestions = None self.load_suggestions() self.ignore_stroke = False @@ -321,7 +324,7 @@ def load_suggestions(self): suggestions[label] += 0 # list of (label, score), higher scores first - self.suggestions = sorted(suggestions.items(), key=lambda x: x[1], reverse=True) + self.local_suggestions = sorted(suggestions.items(), key=lambda x: x[1], reverse=True) def complete_first(self): text = self.get_text() @@ -336,11 +339,9 @@ def complete_first(self): return text, None - def update_entry(self, text): self.set_text(text or "") - def update_suggestions(self, text=""): """ * from previous activity | set time | minutes ago | start now @@ -377,17 +378,16 @@ def update_suggestions(self, text=""): looking_for = fields[fields.index(field)+1] break - fragments = [f for f in re.split("[\s|#]", text)] current_fragment = fragments[-1] if fragments else "" - search = extract_search(text) matches = [] - for match, score in self.suggestions: - if search in match: - if match.startswith(search): + suggestions = self.local_suggestions + for match, score in suggestions: + if search in match.lower(): + if match.lower().startswith(search): score += 10**8 # boost beginnings matches.append((match, score)) @@ -399,7 +399,7 @@ def update_suggestions(self, text=""): if fact.end_time: label += fact.end_time.strftime("-%H:%M") - markup_label = label + " " + (stuff.escape_pango(match).replace(search, "%s" % search) if search else match) + markup_label = label + " " + (self.__bold_search(match, search) if search else escape(match)) label += " " + match res.append(DataRow(markup_label, match, label)) @@ -451,6 +451,9 @@ def update_suggestions(self, text=""): self.complete_tree.set_rows(res) + def __bold_search(self, match, search): + pattern = re.compile("(%s)" % re.escape(search), re.IGNORECASE) + return re.sub(pattern, r"\1", escape(match)) def show_suggestions(self, text): if not self.get_window(): From a607108d3a54b085a3855bcb59ad1ef4a45c2577 Mon Sep 17 00:00:00 2001 From: Grzegorz Sobczyk Date: Thu, 1 Oct 2020 08:43:55 +0200 Subject: [PATCH 16/24] load external activities to suggest - (jira) it loads only activities specified by default jira query - loading occurs on popup show --- src/hamster/widgets/activityentry.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/hamster/widgets/activityentry.py b/src/hamster/widgets/activityentry.py index 8ac67c971..4ed2e9541 100644 --- a/src/hamster/widgets/activityentry.py +++ b/src/hamster/widgets/activityentry.py @@ -231,6 +231,8 @@ def __init__(self, updating=True, **kwargs): self.todays_facts = None self.local_suggestions = None self.load_suggestions() + self.ext_suggestions = None + self.load_ext_suggestions() self.ignore_stroke = False self.set_icon_from_icon_name(gtk.EntryIconPosition.SECONDARY, "go-down-symbolic") @@ -294,6 +296,16 @@ def on_tree_select_row(self, tree, row): self.update_entry(label) self.set_position(-1) + def load_ext_suggestions(self): + facts = self.storage.get_ext_activities() + self.ext_suggestions = [] + for fact in facts: + label = fact.get("name") + category = fact.get("category") + if category: + label += "@%s" % category + score = 10**10 + self.ext_suggestions.append((label, score)) def load_suggestions(self): self.todays_facts = self.storage.get_todays_facts() @@ -384,7 +396,7 @@ def update_suggestions(self, text=""): search = extract_search(text) matches = [] - suggestions = self.local_suggestions + suggestions = self.local_suggestions + self.ext_suggestions for match, score in suggestions: if search in match.lower(): if match.lower().startswith(search): From 6bcc9e80629e04d02d9c746ddf0d62822b7d02f1 Mon Sep 17 00:00:00 2001 From: Grzegorz Sobczyk Date: Thu, 1 Oct 2020 08:48:01 +0200 Subject: [PATCH 17/24] import urllib only when jira library is present --- src/hamster/external/external.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/hamster/external/external.py b/src/hamster/external/external.py index 60546fdd2..b214c59bf 100644 --- a/src/hamster/external/external.py +++ b/src/hamster/external/external.py @@ -29,12 +29,13 @@ from gi.repository import Gtk as gtk from gi.repository import GLib as glib import re -import urllib3 try: from jira.client import JIRA + import urllib3 except ImportError: JIRA = None + urllib3 = None SOURCE_NONE = "" SOURCE_JIRA = 'jira' From 67153aa8aabce75d4e8175ccc552cddd0b9a9ef6 Mon Sep 17 00:00:00 2001 From: Grzegorz Sobczyk Date: Thu, 1 Oct 2020 09:44:20 +0200 Subject: [PATCH 18/24] change default size of edit activity window (from 600px to 1000px width) --- src/hamster/edit_activity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hamster/edit_activity.py b/src/hamster/edit_activity.py index ff7806297..c66f6b3ff 100644 --- a/src/hamster/edit_activity.py +++ b/src/hamster/edit_activity.py @@ -49,7 +49,7 @@ def __init__(self, action, fact_id=None): self._gui = load_ui_file("edit_activity.ui") self.window = self.get_widget('custom_fact_window') - self.window.set_size_request(600, 200) + self.window.set_size_request(1000, 200) self.action = action From 028d698e78a31a691e072f133f3598e46afd1241 Mon Sep 17 00:00:00 2001 From: Grzegorz Sobczyk Date: Thu, 1 Oct 2020 09:56:41 +0200 Subject: [PATCH 19/24] change max suggestions to 10 (instead of 7) --- src/hamster/widgets/activityentry.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/hamster/widgets/activityentry.py b/src/hamster/widgets/activityentry.py index 4ed2e9541..0b2cd1a2a 100644 --- a/src/hamster/widgets/activityentry.py +++ b/src/hamster/widgets/activityentry.py @@ -18,6 +18,8 @@ # along with Project Hamster. If not, see . import logging + +MAX_USER_SUGGESTIONS = 10 logger = logging.getLogger(__name__) # noqa: E402 import bisect @@ -404,7 +406,7 @@ def update_suggestions(self, text=""): matches.append((match, score)) # need to limit these guys, sorry - matches = sorted(matches, key=lambda x: x[1], reverse=True)[:7] + matches = sorted(matches, key=lambda x: x[1], reverse=True)[:MAX_USER_SUGGESTIONS] for match, score in matches: label = (fact.start_time or now).strftime("%H:%M") From b6f9214f3518f93d142d99fc60be19c80442601a Mon Sep 17 00:00:00 2001 From: Grzegorz Sobczyk Date: Thu, 1 Oct 2020 09:57:15 +0200 Subject: [PATCH 20/24] remove dummy separator from external.py --- src/hamster/external/external.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/hamster/external/external.py b/src/hamster/external/external.py index b214c59bf..9f1ba7d34 100644 --- a/src/hamster/external/external.py +++ b/src/hamster/external/external.py @@ -115,8 +115,6 @@ def get_activities(self, query=None): fragments) + " AND resolution = Unresolved order by priority desc, updated desc" logging.info(jira_query) default_jira_activities = self.__jira_get_activities('', jira_query) - if activities and default_jira_activities: - activities.append({"name": "---------------------", "category": "other open"}) activities.extend(default_jira_activities) return activities From 25a90231dea7e3b22dbc8984c053ce0f3065664d Mon Sep 17 00:00:00 2001 From: Grzegorz Sobczyk Date: Thu, 1 Oct 2020 09:58:51 +0200 Subject: [PATCH 21/24] searching and higlihting suggestions for multiple words each of word must exists in suggested entry --- src/hamster/widgets/activityentry.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/hamster/widgets/activityentry.py b/src/hamster/widgets/activityentry.py index 0b2cd1a2a..0a919701f 100644 --- a/src/hamster/widgets/activityentry.py +++ b/src/hamster/widgets/activityentry.py @@ -400,7 +400,9 @@ def update_suggestions(self, text=""): matches = [] suggestions = self.local_suggestions + self.ext_suggestions for match, score in suggestions: - if search in match.lower(): + search_words = search.split(" ") + match_words = match.lower().split(" ") + if all(search_word in match_words for search_word in search_words): if match.lower().startswith(search): score += 10**8 # boost beginnings matches.append((match, score)) @@ -466,8 +468,12 @@ def update_suggestions(self, text=""): self.complete_tree.set_rows(res) def __bold_search(self, match, search): - pattern = re.compile("(%s)" % re.escape(search), re.IGNORECASE) - return re.sub(pattern, r"\1", escape(match)) + result = escape(match) + for word in search.split(" "): + pattern = re.compile("(%s)" % re.escape(word), re.IGNORECASE) + result = re.sub(pattern, r"\1", result) + + return result def show_suggestions(self, text): if not self.get_window(): From 4aed0fcdd3abe1eaad58d008f96c5a78deb5d30c Mon Sep 17 00:00:00 2001 From: Grzegorz Sobczyk Date: Thu, 1 Oct 2020 10:03:24 +0200 Subject: [PATCH 22/24] fill suggestions for external activities after 1sek (in background) --- src/hamster/widgets/activityentry.py | 36 ++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/src/hamster/widgets/activityentry.py b/src/hamster/widgets/activityentry.py index 0a919701f..e8fd386bc 100644 --- a/src/hamster/widgets/activityentry.py +++ b/src/hamster/widgets/activityentry.py @@ -56,6 +56,11 @@ def extract_search(text): search += " #%s" % (" #".join(fact.tags)) return search.lower() +def extract_search_without_tags_and_category(text): + fact = Fact.parse(text) + search = fact.activity + return search.lower() + class DataRow(object): """want to split out visible label, description, activity data and activity data with time (full_data)""" @@ -233,8 +238,10 @@ def __init__(self, updating=True, **kwargs): self.todays_facts = None self.local_suggestions = None self.load_suggestions() - self.ext_suggestions = None - self.load_ext_suggestions() + + self.ext_suggestions = [] + self.ext_suggestion_filler_timer = gobject.timeout_add(0, self.__refresh_ext_suggestions, "") + self.ignore_stroke = False self.set_icon_from_icon_name(gtk.EntryIconPosition.SECONDARY, "go-down-symbolic") @@ -244,8 +251,6 @@ def __init__(self, updating=True, **kwargs): self.connect("focus-out-event", self.on_focus_out) self.connect("icon-press", self.on_icon_press) - - def on_changed(self, entry): text = self.get_text() @@ -298,16 +303,25 @@ def on_tree_select_row(self, tree, row): self.update_entry(label) self.set_position(-1) - def load_ext_suggestions(self): - facts = self.storage.get_ext_activities() - self.ext_suggestions = [] + def __load_ext_suggestions_with_timer(self, query=""): + if self.ext_suggestion_filler_timer: + gobject.source_remove(self.ext_suggestion_filler_timer) + self.ext_suggestion_filler_timer = gobject.timeout_add(1000, self.__refresh_ext_suggestions, extract_search_without_tags_and_category(query)) + + def __refresh_ext_suggestions(self, query=""): + suggestions = [] + facts = self.storage.get_ext_activities(query) for fact in facts: label = fact.get("name") category = fact.get("category") if category: label += "@%s" % category score = 10**10 - self.ext_suggestions.append((label, score)) + suggestions.append((label, score)) + logger.debug("external suggestion refreshed for query: %s" % query) + self.ext_suggestions = suggestions + self.update_suggestions(self.get_text()) + self.ext_suggestion_filler_timer = None def load_suggestions(self): self.todays_facts = self.storage.get_todays_facts() @@ -398,7 +412,7 @@ def update_suggestions(self, text=""): search = extract_search(text) matches = [] - suggestions = self.local_suggestions + self.ext_suggestions + suggestions = self.local_suggestions for match, score in suggestions: search_words = search.split(" ") match_words = match.lower().split(" ") @@ -406,6 +420,8 @@ def update_suggestions(self, text=""): if match.lower().startswith(search): score += 10**8 # boost beginnings matches.append((match, score)) + for match, score in self.ext_suggestions: + matches.append((match, score)) # need to limit these guys, sorry matches = sorted(matches, key=lambda x: x[1], reverse=True)[:MAX_USER_SUGGESTIONS] @@ -484,7 +500,7 @@ def show_suggestions(self, text): x, y = entry_x + entry_alloc.x, entry_y + entry_alloc.y + entry_alloc.height self.popup.show_all() - + self.__load_ext_suggestions_with_timer(text) self.update_suggestions(text) tree_w, tree_h = self.complete_tree.get_size_request() From 03bb263013c2d3048eaffaa18a665407756ccc7d Mon Sep 17 00:00:00 2001 From: Grzegorz Sobczyk Date: Thu, 1 Oct 2020 11:12:23 +0200 Subject: [PATCH 23/24] - refreshing visible suggestions after storage returns them - fix window size --- src/hamster/edit_activity.py | 3 ++- src/hamster/widgets/activityentry.py | 25 ++++++++++++++++++------- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/hamster/edit_activity.py b/src/hamster/edit_activity.py index c66f6b3ff..ed7893a8a 100644 --- a/src/hamster/edit_activity.py +++ b/src/hamster/edit_activity.py @@ -49,7 +49,8 @@ def __init__(self, action, fact_id=None): self._gui = load_ui_file("edit_activity.ui") self.window = self.get_widget('custom_fact_window') - self.window.set_size_request(1000, 200) + self.window.set_size_request(600, 200) + self.window.set_default_size(1000, 200) self.action = action diff --git a/src/hamster/widgets/activityentry.py b/src/hamster/widgets/activityentry.py index e8fd386bc..6b126ade3 100644 --- a/src/hamster/widgets/activityentry.py +++ b/src/hamster/widgets/activityentry.py @@ -304,9 +304,11 @@ def on_tree_select_row(self, tree, row): self.set_position(-1) def __load_ext_suggestions_with_timer(self, query=""): + self.set_icon_from_icon_name(gtk.EntryIconPosition.PRIMARY, "emblem-synchronizing-symbolic") if self.ext_suggestion_filler_timer: gobject.source_remove(self.ext_suggestion_filler_timer) - self.ext_suggestion_filler_timer = gobject.timeout_add(1000, self.__refresh_ext_suggestions, extract_search_without_tags_and_category(query)) + self.ext_suggestion_filler_timer = gobject.timeout_add(500, self.__refresh_ext_suggestions, + extract_search_without_tags_and_category(query)) def __refresh_ext_suggestions(self, query=""): suggestions = [] @@ -321,6 +323,8 @@ def __refresh_ext_suggestions(self, query=""): logger.debug("external suggestion refreshed for query: %s" % query) self.ext_suggestions = suggestions self.update_suggestions(self.get_text()) + self.update_suggestions_popup() + self.set_icon_from_icon_name(gtk.EntryIconPosition.PRIMARY, None) self.ext_suggestion_filler_timer = None def load_suggestions(self): @@ -483,7 +487,8 @@ def update_suggestions(self, text=""): self.complete_tree.set_rows(res) - def __bold_search(self, match, search): + @staticmethod + def __bold_search(match, search): result = escape(match) for word in search.split(" "): pattern = re.compile("(%s)" % re.escape(word), re.IGNORECASE) @@ -495,18 +500,24 @@ def show_suggestions(self, text): if not self.get_window(): return - entry_alloc = self.get_allocation() - entry_x, entry_y = self.get_window().get_origin()[1:] - x, y = entry_x + entry_alloc.x, entry_y + entry_alloc.y + entry_alloc.height - self.popup.show_all() - self.__load_ext_suggestions_with_timer(text) self.update_suggestions(text) + self.update_suggestions_popup() + self.__load_ext_suggestions_with_timer(text) + def update_suggestions_popup(self): + if not self.get_window(): + return + + # self.popup.hide() + entry_alloc = self.get_allocation() + entry_x, entry_y = self.get_window().get_origin()[1:] + x, y = entry_x + entry_alloc.x, entry_y + entry_alloc.y + entry_alloc.height tree_w, tree_h = self.complete_tree.get_size_request() self.popup.move(x, y) self.popup.resize(entry_alloc.width, tree_h) + self.popup.queue_draw() self.popup.show_all() From a1c932aad9eec28e7e2e13cb9835885fe1b6d4f2 Mon Sep 17 00:00:00 2001 From: Grzegorz Sobczyk Date: Thu, 1 Oct 2020 14:36:26 +0200 Subject: [PATCH 24/24] replace hamster special characters in issue summary to valid ones --- src/hamster/external/external.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hamster/external/external.py b/src/hamster/external/external.py index 9f1ba7d34..207f1fb21 100644 --- a/src/hamster/external/external.py +++ b/src/hamster/external/external.py @@ -145,7 +145,7 @@ def __jira_extract_activity_from_issue(self, issue): issue_id = issue.key fields = issue.fields activity['name'] = str(issue_id) \ - + ': ' + fields.summary.replace(",", " ") \ + + ': ' + fields.summary.replace(",", " ").replace("#", "*").replace("@", " ") \ + " (%s)" % fields.status.name \ + (" 👨‍💼" + fields.assignee.name if fields.assignee else "") if hasattr(fields, self.jira_category):