diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml
index 7381e1b6e..25b92384e 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/.gitignore b/.gitignore
index 5f714f959..29c758b0f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,3 +28,8 @@ hamster-time-tracker-*.tar.gz
.lock-waf*
build
*.deb
+.project
+.pydevproject
+venv/
+.idea/
+*.iml
\ No newline at end of file
diff --git a/NEWS.md b/NEWS.md
index 83509b0b4..998f2da25 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -1,3 +1,13 @@
+## 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
+* 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
* Switch from deprecated xml2po to itstool for translating help files
diff --git a/README.md b/README.md
index 13354bf2e..8d57b3052 100644
--- a/README.md
+++ b/README.md
@@ -97,8 +97,12 @@ 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
+# and for jira integration (should be python3-jira>=2.0.0, if not - use pip)
+sudo apt install python3-jira python3-urllib3
```
##### openSUSE
@@ -106,7 +110,12 @@ 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
+# and for jira integration (should be python3-jira>=2.0.0, if not - use pip)
+sudo zypper install python3-jira python3-urllib3
```
##### RPM-based
diff --git a/data/edit_activity.ui b/data/edit_activity.ui
index f3e378bf7..ec1c5a129 100644
--- a/data/edit_activity.ui
+++ b/data/edit_activity.ui
@@ -1,5 +1,5 @@
-
+
-
-
-
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/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/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-cli.py b/src/hamster-cli.py
index 9c6786787..f0fde0418 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 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] [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-service.py b/src/hamster-service.py
index 4b1e43a71..3c229bbfa 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",
@@ -399,10 +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=""):
+ 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-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..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,13 +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=""):
+ """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/edit_activity.py b/src/hamster/edit_activity.py
index 59c2d6f43..ed7893a8a 100644
--- a/src/hamster/edit_activity.py
+++ b/src/hamster/edit_activity.py
@@ -50,6 +50,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_default_size(1000, 200)
self.action = action
@@ -82,6 +83,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
@@ -95,7 +99,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())
@@ -123,6 +128,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 +289,11 @@ def on_tags_changed(self, widget):
self.fact.tags = self.tags_entry.get_tags()
self.update_cmdline()
+ def on_exported_toggled(self, widget):
+ if not self.master_is_cmdline:
+ self.fact.exported = self.exported_checkbox.get_active()
+ self.update_cmdline()
+
def present(self):
self.window.present()
@@ -308,6 +319,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/__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..207f1fb21
--- /dev/null
+++ b/src/hamster/external/external.py
@@ -0,0 +1,237 @@
+# - 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
+import time
+
+from hamster.lib import Fact, stuff
+
+logger = logging.getLogger(__name__) # noqa: E402
+
+from hamster.lib.cache import cache
+from gi.repository import Gtk as gtk
+from gi.repository import GLib as glib
+import re
+
+try:
+ from jira.client import JIRA
+ import urllib3
+except ImportError:
+ JIRA = None
+ urllib3 = None
+
+SOURCE_NONE = ""
+SOURCE_JIRA = 'jira'
+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
+
+
+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.source = SOURCE_NONE
+ self.__on_error(error_msg + ERROR_ADDITIONAL_MESSAGE)
+ logger.warning(error_msg)
+
+ def __connect(self, 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")
+ 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', '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}
+ self.jira = JIRA(options, basic_auth=(self.jira_user, self.jira_pass), validate=True)
+ self.jira_projects = self.__jira_get_projects()
+ self.jira_issue_types = self.__jira_get_issue_types()
+ else:
+ self.source = SOURCE_NONE
+ self.__on_error("Invalid Jira credentials")
+
+ def get_activities(self, query=None):
+ query = query.strip()
+ if not self.source:
+ return []
+ elif self.source == SOURCE_JIRA:
+ activities = self.__jira_get_activities(query, self.jira_query)
+ direct_issue = None
+ 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(), fields=self.jira_fields)
+ if 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:
+ 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.info(jira_query)
+ default_jira_activities = self.__jira_get_activities('', jira_query)
+ activities.extend(default_jira_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 __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.warning(e)
+ return activities
+
+ def __jira_extract_activity_from_issue(self, issue):
+ activity = {}
+ issue_id = issue.key
+ fields = issue.fields
+ activity['name'] = str(issue_id) \
+ + ': ' + fields.summary.replace(",", " ").replace("#", "*").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:
+ activity['category'] = ""
+ if not activity['category'] or activity['category'] == "None":
+ try:
+ activity['category'] = "%s/%s (%s)" % (fields.project.key, fields.issuetype.name, fields.project.name)
+ except Exception as e:
+ logger.warning(e)
+ return activity
+
+ def __jira_get_projects(self):
+ return [project.key for project in self.jira.projects()]
+
+ def __jira_get_issue_types(self):
+ return [issuetype.name.lower() for issuetype in self.jira.issue_types()]
+
+ @cache(seconds=30)
+ 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):
+ 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)
+ 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 Exception as err:
+ logger.info(err)
+ return False
+
+ def __jira_is_issue_from_existing_project(self, issue):
+ return issue.split('-', 1)[0].upper() in self.jira_projects
+
+ 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 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:
+ issue_id = jira_match.group(1)
+ comment = self.__get_comment_to_export(fact)
+ time_worked = stuff.duration_minutes(fact.delta)
+ try:
+ self.__jira_add_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
+
+ 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/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/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/dbus.py b/src/hamster/lib/dbus.py
index f282e4fdc..36779784f 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..ed96c4199 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
@@ -217,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.
@@ -244,6 +248,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/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..c6c1bbabb 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 = 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..."))
+ 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 = self.storage.export_fact(fact.id)
+ if exported:
+ fact.exported = True
+ self.storage.update_fact(fact.id, fact, False)
+ pass
+ else:
+ logger.info("Fact not exported: %s" % fact.activity)
+
+ 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 c41b1d35c..be76b8eaa 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)
@@ -507,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/reports.py b/src/hamster/reports.py
index 69a4a258d..99659172c 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
@@ -55,6 +56,10 @@ 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)
+ elif format == "external":
+ writer = ExternalWriter(report_path)
else: #default to HTML
writer = HTMLWriter(report_path, start_date, end_date)
@@ -156,6 +161,34 @@ 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 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(_("Activity not exported: %s" % fact.activity) + "\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 468a95f9e..18ac83e3f 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,6 +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"],
+ exported=db_fact["exported"],
activity_id=db_fact["activity_id"])
def __get_fact(self, id):
@@ -410,7 +416,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
@@ -441,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"]]
@@ -502,6 +509,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 +531,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 +553,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 +572,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 +681,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()
@@ -690,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
@@ -703,7 +717,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,12 +737,19 @@ 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 '',
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,
@@ -761,6 +783,13 @@ 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 __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"""
@@ -862,7 +891,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
@@ -970,7 +1000,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)
@@ -993,6 +1023,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
@@ -1003,6 +1037,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..5d8441c67 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):
@@ -210,6 +210,12 @@ 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 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:
diff --git a/src/hamster/widgets/activityentry.py b/src/hamster/widgets/activityentry.py
index 97a3ac02e..6b126ade3 100644
--- a/src/hamster/widgets/activityentry.py
+++ b/src/hamster/widgets/activityentry.py
@@ -18,12 +18,15 @@
# along with Project Hamster. If not, see .
import logging
+
+MAX_USER_SUGGESTIONS = 10
logger = logging.getLogger(__name__) # noqa: E402
import bisect
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 +54,12 @@ def extract_search(text):
search += "@%s" % fact.category
if fact.tags:
search += " #%s" % (" #".join(fact.tags))
- return search
+ 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
@@ -227,7 +235,13 @@ 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.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")
@@ -237,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()
@@ -291,6 +303,29 @@ def on_tree_select_row(self, tree, row):
self.update_entry(label)
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(500, 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
+ suggestions.append((label, score))
+ 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):
self.todays_facts = self.storage.get_todays_facts()
@@ -321,7 +356,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 +371,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,29 +410,32 @@ 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:
+ 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))
+ 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)[: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")
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,23 +487,37 @@ def update_suggestions(self, text=""):
self.complete_tree.set_rows(res)
+ @staticmethod
+ def __bold_search(match, search):
+ 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():
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.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()
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")