diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..8077e13f2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,21 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true + +# Matches multiple files with brace expansion notation +# Set default charset +[*.{js,py}] +charset = utf-8 + +# 4 space indentation +[*.py] +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/README.md b/README.md index 4a3188df2..fd18df107 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,51 @@ Hamster is time tracking for individuals. It helps you to keep track of how much time you have spent during the day on activities you choose to track. + + +hamster-time-tracker v1.04 was not usable under openSUSE Leap-15, +where python2-gconf is not available any longer. +The [rewrite of hamster](https://github.com/projecthamster/hamster-gtk) +is progressing well, but it is still listed as alpha. + +This repo is a fork from +[hamster](https://github.com/projecthamster/hamster) project, +that still used hamster.db, already migrated to Gtk3 and did not +depend on python-gconf. + +Lots of the gui ease of use has been lost, especially for tags handling, +start/restart of activities, and the stats display is minimal now. +But at least backward compatibility seems good. +Seems enough to wait for the rewrite. + +The fork base commit is [the latest one from master branch](https://github.com/projecthamster/hamster/commit/c3e5fb761c88fdecfd1566cac8b6836228a27cce). + +After a little tweaking, it works now, +but has not been thouroughly tested. +Backup `hamster.db` first, +and keep track of activities on a text file too for some days ! + +To use the development version: +``` +pkill -f hamster-service +pkill -f hamster-windows-service +src/hamster-service & +src/hamster-windows-service & +src/hamster-cli +``` + +To install, python2 is still necessary (for waf). +Adapt the paths below to your system, +`sudo rm/mv` commands, beware ! +``` +./waf configure build --prefix=/usr && sudo ./waf install +sudo rm -rf /usr/lib/python3.6/site-packages/hamster +sudo mv /usr/lib/python2.7/site-packages/hamster /usr/lib/python3.6/site-packages/ +``` + + +*[README from the original repo below]* + **IMPORTANT** Project Hamster is undergoing a period of major transition. Unless someone steps up to the task, this repository will remain unmaintained as the diff --git a/po/de.po b/po/de.po index fe18ddf4c..4b6c517cf 100644 --- a/po/de.po +++ b/po/de.po @@ -6,51 +6,50 @@ # Mario Blättermann , 2009-2012. # Christian Kirbach , 2010, 2011. # Wolfgang Stöggl , 2011. +# Benjamin Steinwender , 2017. # -#: ../src/hamster-cli:342 msgid "" msgstr "" "Project-Id-Version: Hamster master\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2012-12-02 19:20+0100\n" -"PO-Revision-Date: 2012-02-20 10:35+0100\n" -"Last-Translator: Mario Blättermann \n" +"POT-Creation-Date: 2017-04-15 08:21+0200\n" +"PO-Revision-Date: 2017-04-15 08:27+0200\n" +"Last-Translator: Benjamin Steinwender \n" "Language-Team: Deutsch \n" -"Language: \n" +"Language: de_DE\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -"X-Poedit-Language: German\n" -"X-Poedit-Country: GERMANY\n" +"X-Generator: Poedit 1.8.12\n" -#: ../data/edit_activity.ui.h:1 ../data/today.ui.h:15 -msgid "Add Earlier Activity" -msgstr "Neue Tätigkeit hinzufügen" +#: ../data/date_range.ui.h:1 +msgid "Today:" +msgstr "Heute:" -#: ../data/edit_activity.ui.h:2 ../data/range_pick.ui.h:5 -msgid "to" -msgstr "zu" +#: ../data/date_range.ui.h:2 +msgid "Week:" +msgstr "Woche:" -#: ../data/edit_activity.ui.h:3 -msgid "in progress" -msgstr "im Gange" +#: ../data/date_range.ui.h:3 +msgid "Month:" +msgstr "Monat:" -#: ../data/edit_activity.ui.h:4 -msgid "Description:" -msgstr "Beschreibung:" +#: ../data/date_range.ui.h:4 +msgid "Range:" +msgstr "Datumsbereich:" -#: ../data/edit_activity.ui.h:5 -msgid "Time:" -msgstr "Zeit:" +#: ../data/date_range.ui.h:5 +msgid "to" +msgstr "bis" -#: ../data/edit_activity.ui.h:6 -msgid "Activity:" -msgstr "Tätigkeit:" +#: ../data/date_range.ui.h:6 +msgid "Apply" +msgstr "Anwenden" -#: ../data/edit_activity.ui.h:7 -msgid "Tags:" -msgstr "Schlagworte:" +#: ../data/edit_activity.ui.h:1 +msgid "Add Earlier Activity" +msgstr "Neue Tätigkeit hinzufügen" #: ../data/hamster-time-tracker.schemas.in.h:1 msgid "Stop tracking on idle" @@ -166,9 +165,8 @@ msgid "Toggle visibility of the hamster application window." msgstr "Die Sichtbarkeit des Hamster-Anwendungsfensters ein-/ausschalten." #: ../data/hamster-time-tracker.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 +#: ../data/hamster-windows-service.desktop.in.in.h:1 ../src/hamster/about.py:31 +#: ../src/hamster/about.py:32 msgid "Time Tracker" msgstr "Zeiterfassung" @@ -185,77 +183,6 @@ msgstr "Zeiterfassungsübersicht" msgid "The overview window of hamster time tracker" msgstr "Das Übersichtsfenster der Hamster-Zeiterfassung" -#: ../data/overview_totals.ui.h:1 -msgid "Show Statistics" -msgstr "Statistiken zeigen" - -#: ../data/overview_totals.ui.h:2 -msgid "Categories" -msgstr "Kategorien" - -#: ../data/overview_totals.ui.h:3 ../data/overview.ui.h:9 -msgid "Activities" -msgstr "Tätigkeiten" - -#: ../data/overview_totals.ui.h:4 ../src/hamster-cli:278 -#: ../src/hamster/reports.py:319 ../src/hamster/today.py:150 -msgid "Tags" -msgstr "Schlagworte" - -#: ../data/overview_totals.ui.h:5 -msgid "No data for this interval" -msgstr "Keine Daten für diese Zeitspanne" - -#: ../data/overview.ui.h:1 -msgid "Save report..." -msgstr "Bericht speichern …" - -#: ../data/overview.ui.h:2 -msgid "Day" -msgstr "Tag" - -#: ../data/overview.ui.h:3 -msgid "Week" -msgstr "Woche" - -#: ../data/overview.ui.h:4 -msgid "Month" -msgstr "Monat" - -#: ../data/overview.ui.h:5 -msgid "Overview — Hamster" -msgstr "Übersicht — Hamster" - -#: ../data/overview.ui.h:6 -msgid "_Overview" -msgstr "Ü_bersicht" - -#: ../data/overview.ui.h:7 ../src/hamster-cli:276 -#: ../src/hamster/preferences.py:212 ../src/hamster/reports.py:317 -#: ../src/hamster/today.py:144 -msgid "Activity" -msgstr "Tätigkeit" - -#: ../data/overview.ui.h:8 -msgid "_View" -msgstr "_Ansicht" - -#: ../data/overview.ui.h:10 ../src/hamster/reports.py:308 -msgid "Totals" -msgstr "Gesamt" - -#: ../data/overview.ui.h:11 -msgid "Remove" -msgstr "Entfernen" - -#: ../data/overview.ui.h:12 -msgid "Add new" -msgstr "Neu hinzufügen" - -#: ../data/overview.ui.h:13 -msgid "Edit" -msgstr "Bearbeiten" - #: ../data/preferences.ui.h:1 msgid "Time Tracker Preferences" msgstr "hamster-time-tracker Einstellungen" @@ -333,279 +260,75 @@ msgstr "" msgid "Categories and Tags" msgstr "Kategorien und Schlagworte" -#: ../data/preferences.ui.h:22 -msgid "Resume the last activity when returning to a workspace" -msgstr "Bei Rückkehr auf eine Arbeitsfläche letzte Tätigkeit wiederaufnehmen" - -#: ../data/preferences.ui.h:23 -msgid "Start new activity when switching workspaces:" -msgstr "Beim Arbeitsflächenwechsel neue Tätigkeit starten" - -#: ../data/preferences.ui.h:24 -msgid "Workspaces" -msgstr "Arbeitsflächen" - -#: ../data/range_pick.ui.h:1 -msgid "Day:" -msgstr "Tag:" - -#: ../data/range_pick.ui.h:2 -msgid "Week:" -msgstr "Woche:" - -#: ../data/range_pick.ui.h:3 -msgid "Month:" -msgstr "Monat:" - -#: ../data/range_pick.ui.h:4 -msgid "Range:" -msgstr "Datumsbereich:" - -#: ../data/range_pick.ui.h:6 -msgid "Apply" -msgstr "Anwenden" - -#: ../data/today.ui.h:2 -msgid "_Tracking" -msgstr "_Erfassung" - -#: ../data/today.ui.h:3 -msgid "Add earlier activity" -msgstr "Frühere Tätigkeit hinzufügen" - -#: ../data/today.ui.h:4 -msgid "Overview" -msgstr "Übersicht" - -#: ../data/today.ui.h:5 -msgid "Statistics" -msgstr "Statistiken" - -#: ../data/today.ui.h:6 -msgid "_Edit" -msgstr "_Bearbeiten" - -#: ../data/today.ui.h:7 -msgid "_Help" -msgstr "_Hilfe" - -#: ../data/today.ui.h:8 -msgid "Contents" -msgstr "Inhalt" - -#: ../data/today.ui.h:9 -msgid "Sto_p tracking" -msgstr "Erfassung _anhalten" - -#: ../data/today.ui.h:10 -msgid "S_witch" -msgstr "_Wechseln" - -#: ../data/today.ui.h:11 -msgid "Start _Tracking" -msgstr "Erfassung s_tarten" - -#: ../data/today.ui.h:12 -msgid "Start new activity" -msgstr "Neue Tätigkeit starten" - -#: ../data/today.ui.h:13 -msgid "Today" -msgstr "Heute" - -#: ../data/today.ui.h:14 -msgid "totals" -msgstr "Gesamt" - -#: ../data/today.ui.h:16 -msgid "Show Overview" -msgstr "Übersicht anzeigen" - -#: ../src/hamster-cli:254 ../src/hamster/today.py:289 -msgid "No activity" -msgstr "Keine Tätigkeit" - -#: ../src/hamster-cli:277 ../src/hamster/preferences.py:155 -#: ../src/hamster/reports.py:318 -msgid "Category" -msgstr "Kategorie" - -#: ../src/hamster-cli:279 ../src/hamster/reports.py:323 -msgid "Description" -msgstr "Beschreibung" - -#: ../src/hamster-cli:280 ../src/hamster/reports.py:320 -msgid "Start" -msgstr "Beginn" - -#: ../src/hamster-cli:281 ../src/hamster/reports.py:321 -msgid "End" -msgstr "Ende" - -#: ../src/hamster-cli:282 ../src/hamster/reports.py:322 -msgid "Duration" -msgstr "Dauer" - -#: ../src/hamster-cli:308 -#, fuzzy -msgid "Uncategorized" -msgstr "Kategorien" - -#: ../src/hamster/about.py:42 +#: ../src/hamster/about.py:34 msgid "Project Hamster — track your time" msgstr "Projekt-Hamster - Erfassen Sie Ihre Zeit" -#: ../src/hamster/about.py:43 +#: ../src/hamster/about.py:35 msgid "Copyright © 2007–2010 Toms Bauģis and others" msgstr "Copyright © 2007-2010 Toms Baugis und andere" -#: ../src/hamster/about.py:45 +#: ../src/hamster/about.py:37 msgid "Project Hamster Website" msgstr "Hamster-Projektwebsite" -#: ../src/hamster/about.py:46 +#: ../src/hamster/about.py:38 msgid "About Time Tracker" msgstr "Info zur Zeiterfassung" -#: ../src/hamster/about.py:56 +#: ../src/hamster/about.py:48 msgid "translator-credits" msgstr "" "Matthias Mailänder https://launchpad.net/~mailaender\n" "Hendrik Richter \n" "Mario Blättermann \n" -"Christian Kirbach " - -#: ../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 "Unsortiert" - -#. defaults -#: ../src/hamster/db.py:937 -msgid "Work" -msgstr "Arbeit" - -#: ../src/hamster/db.py:938 -msgid "Reading news" -msgstr "Nachrichten lesen" - -#: ../src/hamster/db.py:939 -msgid "Checking stocks" -msgstr "Aktienkurse überprüfen" +"Christian Kirbach \n" +"Benjamin Steinwender " -#: ../src/hamster/db.py:940 -msgid "Super secret project X" -msgstr "Supergeheimprojekt X" - -#: ../src/hamster/db.py:941 -msgid "World domination" -msgstr "Weltherrschaft" - -#: ../src/hamster/db.py:943 -msgid "Day-to-day" -msgstr "Tag für Tag" - -#: ../src/hamster/db.py:944 -msgid "Lunch" -msgstr "Mittagessen" - -#: ../src/hamster/db.py:945 -msgid "Watering flowers" -msgstr "Blumen gießen" - -#: ../src/hamster/db.py:946 -msgid "Doing handstands" -msgstr "Handstand machen" - -#: ../src/hamster/edit_activity.py:75 +#: ../src/hamster/edit_activity.py:81 msgid "Update activity" msgstr "Tätigkeit aktualisieren" -#. duration in round hours -#: ../src/hamster/lib/stuff.py:57 -#, python-format -msgid "%dh" -msgstr "%dh" +#: ../src/hamster/overview.py:86 +msgid "Export..." +msgstr "Exportieren …" -#. duration less than hour -#: ../src/hamster/lib/stuff.py:60 -#, python-format -msgid "%dmin" -msgstr "%dmin" +#: ../src/hamster/overview.py:88 +msgid "Tracking Settings" +msgstr "Erfassungseinstellungen" -#. x hours, y minutes -#: ../src/hamster/lib/stuff.py:63 -#, python-format -msgid "%dh %dmin" -msgstr "%dh %dmin" +#: ../src/hamster/overview.py:254 +msgid "Click to see stats" +msgstr "Klicken Sie für Statistiken" -#. 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. – %(end_d)s. %(start_B)s %(end_Y)s" - -#: ../src/hamster/overview_activities.py:88 -msgctxt "overview list" -msgid "%A, %b %d" -msgstr "%A, %d.%b." - -#: ../src/hamster/overview_totals.py:161 -#, python-format -msgid "%s hours tracked total" -msgstr "%s Stunden insgesamt erfasst" +#: ../src/hamster/preferences.py:52 ../src/hamster/reports.py:89 +#: ../src/hamster/reports.py:126 ../src/hamster/reports.py:255 +msgid "Unsorted" +msgstr "Unsortiert" #. Translators: 'None' refers here to the Todo list choice in Hamster preferences (Tracking tab) -#: ../src/hamster/preferences.py:113 +#: ../src/hamster/preferences.py:93 msgid "None" msgstr "Nichts" -#: ../src/hamster/preferences.py:130 ../src/hamster/preferences.py:208 +#: ../src/hamster/preferences.py:111 msgid "Name" msgstr "Name" -#: ../src/hamster/preferences.py:664 +#: ../src/hamster/preferences.py:136 ../src/hamster/reports.py:316 +msgid "Category" +msgstr "Kategorie" + +#: ../src/hamster/preferences.py:542 msgid "New category" msgstr "Neue Kategorie" -#: ../src/hamster/preferences.py:677 +#: ../src/hamster/preferences.py:557 msgid "New activity" msgstr "Neue Tätigkeit" #. notify interval slider value label -#: ../src/hamster/preferences.py:738 +#: ../src/hamster/preferences.py:584 #, python-format msgid "%(interval_minutes)d minute" msgid_plural "%(interval_minutes)d minutes" @@ -613,46 +336,46 @@ msgstr[0] "%(interval_minutes)d Minute" msgstr[1] "%(interval_minutes)d Minuten" #. notify interval slider value label -#: ../src/hamster/preferences.py:743 +#: ../src/hamster/preferences.py:589 msgid "Never" msgstr "Nie" #. column title in the TSV export format -#: ../src/hamster/reports.py:148 +#: ../src/hamster/reports.py:147 msgid "activity" msgstr "Tätigkeit" #. column title in the TSV export format -#: ../src/hamster/reports.py:150 +#: ../src/hamster/reports.py:149 msgid "start time" msgstr "Beginn" #. column title in the TSV export format -#: ../src/hamster/reports.py:152 +#: ../src/hamster/reports.py:151 msgid "end time" msgstr "Ende" #. column title in the TSV export format -#: ../src/hamster/reports.py:154 +#: ../src/hamster/reports.py:153 msgid "duration minutes" msgstr "Dauer in Minuten" #. column title in the TSV export format -#: ../src/hamster/reports.py:156 +#: ../src/hamster/reports.py:155 msgid "category" msgstr "Kategorie" #. column title in the TSV export format -#: ../src/hamster/reports.py:158 +#: ../src/hamster/reports.py:157 msgid "description" msgstr "Beschreibung" #. column title in the TSV export format -#: ../src/hamster/reports.py:160 ../src/hamster/reports.py:312 +#: ../src/hamster/reports.py:159 ../src/hamster/reports.py:310 msgid "tags" msgstr "Schlagworte" -#: ../src/hamster/reports.py:207 +#: ../src/hamster/reports.py:206 #, python-format msgid "" "Activity report for %(start_B)s %(start_d)s, %(start_Y)s – %(end_B)s " @@ -661,7 +384,7 @@ msgstr "" "Tätigkeitsprotokoll für %(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:208 #, python-format msgid "" "Activity report for %(start_B)s %(start_d)s – %(end_B)s %(end_d)s, %(end_Y)s" @@ -669,12 +392,12 @@ msgstr "" "Tätigkeitsprotokoll für %(start_B)s %(start_d)s – %(end_B)s %(end_d)s, " "%(end_Y)s" -#: ../src/hamster/reports.py:211 +#: ../src/hamster/reports.py:210 #, python-format msgid "Activity report for %(start_B)s %(start_d)s, %(start_Y)s" msgstr "Tätigkeitsprotokoll für %(start_B)s %(start_d)s, %(start_Y)s" -#: ../src/hamster/reports.py:213 +#: ../src/hamster/reports.py:212 #, python-format msgid "Activity report for %(start_B)s %(start_d)s – %(end_d)s, %(end_Y)s" msgstr "Tätigkeitsprotokoll für %(start_B)s %(start_d)s – %(end_d)s, %(end_Y)s" @@ -682,205 +405,362 @@ msgstr "Tätigkeitsprotokoll für %(start_B)s %(start_d)s – %(end_d)s, %(end_Y #. 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:264 ../src/hamster/reports.py:296 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:304 msgid "Totals by Day" msgstr "Gesamt nach Tag" -#: ../src/hamster/reports.py:307 +#: ../src/hamster/reports.py:305 msgid "Activity Log" msgstr "Tätigkeitsprotokoll" -#: ../src/hamster/reports.py:310 +#: ../src/hamster/reports.py:306 +msgid "Totals" +msgstr "Gesamt" + +#: ../src/hamster/reports.py:308 msgid "activities" msgstr "Tätigkeiten" -#: ../src/hamster/reports.py:311 +#: ../src/hamster/reports.py:309 msgid "categories" msgstr "Kategorien" -#: ../src/hamster/reports.py:314 +#: ../src/hamster/reports.py:312 msgid "Distinguish:" msgstr "Unterscheiden:" -#: ../src/hamster/reports.py:316 +#: ../src/hamster/reports.py:314 msgid "Date" msgstr "Datum" -#: ../src/hamster/reports.py:326 +#: ../src/hamster/reports.py:315 +msgid "Activity" +msgstr "Tätigkeit" + +#: ../src/hamster/reports.py:317 +msgid "Tags" +msgstr "Schlagworte" + +#: ../src/hamster/reports.py:318 +msgid "Start" +msgstr "Beginn" + +#: ../src/hamster/reports.py:319 +msgid "End" +msgstr "Ende" + +#: ../src/hamster/reports.py:320 +msgid "Duration" +msgstr "Dauer" + +#: ../src/hamster/reports.py:321 +msgid "Description" +msgstr "Beschreibung" + +#: ../src/hamster/reports.py:324 msgid "Show template" msgstr "Vorlage anzeigen" -#: ../src/hamster/reports.py:327 +#: ../src/hamster/reports.py:325 #, python-format msgid "You can override it by storing your version in %(home_folder)s" msgstr "" "Sie können dies überschreiben, indem Sie Ihre Version in %(home_folder)s " "speichern" -#: ../src/hamster/stats.py:147 -msgctxt "years" -msgid "All" -msgstr "Alle" +#: ../src/hamster/widgets/reportchooserdialog.py:40 +msgid "Save Report — Time Tracker" +msgstr "Bericht speichern — Zeiterfassung" -#: ../src/hamster/stats.py:177 -msgid "" -"There is no data to generate statistics yet.\n" -"A week of usage would be nice!" -msgstr "" -"Es gibt noch keine Daten zur Erstellung einer Statistik.\n" -"Eine Woche Benutzung wäre wünschenswert!" +#: ../src/hamster/widgets/reportchooserdialog.py:58 +msgid "HTML Report" +msgstr "HTML-Bericht" -#: ../src/hamster/stats.py:180 -msgid "Collecting data — check back after a week has passed!" -msgstr "" -"Daten werden noch gesammelt — Bitte warten Sie, bis eine Woche vergangen ist!" +#: ../src/hamster/widgets/reportchooserdialog.py:66 +msgid "Tab-Separated Values (TSV)" +msgstr "Durch Tabulatoren getrennte Werte (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:74 +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:81 +msgid "iCal" +msgstr "iCal" -#: ../src/hamster/stats.py:338 -#, python-format -msgid "First activity was recorded on %s." -msgstr "Die erste Tätigkeit wurde am %s aufgezeichnet." +#. title in the report file name +#: ../src/hamster/widgets/reportchooserdialog.py:98 +msgid "Time track" +msgstr "Zeiterfassung" -#: ../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 Jahr" -msgstr[1] "%(num)s Jahre" +#~ msgid "in progress" +#~ msgstr "im Gange" -#. 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 "" -"Die bisher erfasste Zeit ist %(human_days)s Kalendertage (%(human_years)s) " -"oder %(working_days)s Arbeitstage (%(working_years)s)." +#~ msgid "Description:" +#~ msgstr "Beschreibung:" -#. 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 "Time:" +#~ msgstr "Zeit:" -#: ../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] "" -"Die längste zusammenhängende Tätigkeit geschah am %(date)s und dauerte " -"%(hours)s Stunde." -msgstr[1] "" -"Die längste zusammenhängende Tätigkeit geschah am %(date)s und dauerte " -"%(hours)s Stunden." - -#. 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] "Es gibt %s Aufzeichnung." -msgstr[1] "Es gibt %s Aufzeichnungen." +#~ msgid "Activity:" +#~ msgstr "Tätigkeit:" -#: ../src/hamster/stats.py:407 -msgid "Hamster would like to observe you some more!" -msgstr "Hamster möchte Sie gern ein wenig genauer beobachten!" +#~ msgid "Tags:" +#~ msgstr "Schlagworte:" -#: ../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 "" -"Da %s Prozent aller Aufgaben vor 9 Uhr begonnen wurden, sind Sie sicherlich " -"ein Frühaufsteher." +#~ msgid "Show Statistics" +#~ msgstr "Statistiken zeigen" -#: ../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 "" -"Da %s Prozent aller Aufgaben nach 23 Uhr begonnen wurden, sind Sie " -"sicherlich eine Nachteule." +#~ msgid "Categories" +#~ msgstr "Kategorien" -#: ../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 "" -"Da %s Prozent aller Aufgaben weniger als 15 Minuten in Anspruch nahmen, " -"scheinen Sie eine fleißige Biene zu sein." +#~ msgid "Activities" +#~ msgstr "Tätigkeiten" -#: ../src/hamster/today.py:243 -msgid "No records today" -msgstr "Heute keine Aufzeichnungen" +#~ msgid "No data for this interval" +#~ msgstr "Keine Daten für diese Zeitspanne" -#: ../src/hamster/today.py:250 -#, python-format -msgid "%(category)s: %(duration)s" -msgstr "%(category)s: %(duration)s, " +#~ msgid "Day" +#~ msgstr "Tag" -#. duration in main drop-down per category in hours -#: ../src/hamster/today.py:253 -#, python-format -msgid "%sh" -msgstr "%sh" +#~ msgid "Week" +#~ msgstr "Woche" -#: ../src/hamster/today.py:280 -msgid "Just started" -msgstr "Soeben gestartet" +#~ msgid "Month" +#~ msgstr "Monat" -#: ../src/hamster/widgets/reportchooserdialog.py:39 -msgid "Save Report — Time Tracker" -msgstr "Bericht speichern — Zeiterfassung" +#~ msgid "Overview — Hamster" +#~ msgstr "Übersicht — Hamster" -#: ../src/hamster/widgets/reportchooserdialog.py:57 -msgid "HTML Report" -msgstr "HTML-Bericht" +#~ msgid "_Overview" +#~ msgstr "Ü_bersicht" -#: ../src/hamster/widgets/reportchooserdialog.py:65 -msgid "Tab-Separated Values (TSV)" -msgstr "Durch Tabulatoren getrennte Werte (TSV)" +#~ msgid "_View" +#~ msgstr "_Ansicht" -#: ../src/hamster/widgets/reportchooserdialog.py:73 -msgid "XML" -msgstr "XML" +#~ msgid "Remove" +#~ msgstr "Entfernen" -#: ../src/hamster/widgets/reportchooserdialog.py:80 -msgid "iCal" -msgstr "iCal" +#~ msgid "Add new" +#~ msgstr "Neu hinzufügen" -#. title in the report file name -#: ../src/hamster/widgets/reportchooserdialog.py:97 -msgid "Time track" -msgstr "Zeiterfassung" +#~ msgid "Edit" +#~ msgstr "Bearbeiten" + +#~ msgid "Resume the last activity when returning to a workspace" +#~ msgstr "" +#~ "Bei Rückkehr auf eine Arbeitsfläche letzte Tätigkeit wiederaufnehmen" + +#~ msgid "Start new activity when switching workspaces:" +#~ msgstr "Beim Arbeitsflächenwechsel neue Tätigkeit starten" + +#~ msgid "Workspaces" +#~ msgstr "Arbeitsflächen" + +#~ msgid "Day:" +#~ msgstr "Tag:" + +#~ msgid "_Tracking" +#~ msgstr "_Erfassung" + +#~ msgid "Add earlier activity" +#~ msgstr "Frühere Tätigkeit hinzufügen" + +#~ msgid "Overview" +#~ msgstr "Übersicht" + +#~ msgid "Statistics" +#~ msgstr "Statistiken" + +#~ msgid "_Edit" +#~ msgstr "_Bearbeiten" + +#~ msgid "_Help" +#~ msgstr "_Hilfe" + +#~ msgid "Contents" +#~ msgstr "Inhalt" + +#~ msgid "Sto_p tracking" +#~ msgstr "Erfassung _anhalten" + +#~ msgid "S_witch" +#~ msgstr "_Wechseln" + +#~ msgid "Start _Tracking" +#~ msgstr "Erfassung s_tarten" + +#~ msgid "Start new activity" +#~ msgstr "Neue Tätigkeit starten" + +#~ msgid "totals" +#~ msgstr "Gesamt" + +#~ msgid "Show Overview" +#~ msgstr "Übersicht anzeigen" + +#~ msgid "No activity" +#~ msgstr "Keine Tätigkeit" + +#, fuzzy +#~ msgid "Uncategorized" +#~ msgstr "Kategorien" + +#~ msgid "Work" +#~ msgstr "Arbeit" + +#~ msgid "Reading news" +#~ msgstr "Nachrichten lesen" + +#~ msgid "Checking stocks" +#~ msgstr "Aktienkurse überprüfen" + +#~ msgid "Super secret project X" +#~ msgstr "Supergeheimprojekt X" + +#~ msgid "World domination" +#~ msgstr "Weltherrschaft" + +#~ msgid "Day-to-day" +#~ msgstr "Tag für Tag" + +#~ msgid "Lunch" +#~ msgstr "Mittagessen" + +#~ msgid "Watering flowers" +#~ msgstr "Blumen gießen" + +#~ msgid "Doing handstands" +#~ msgstr "Handstand machen" + +#~ 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. – %(end_d)s. %(start_B)s %(end_Y)s" + +#~ msgctxt "overview list" +#~ msgid "%A, %b %d" +#~ msgstr "%A, %d.%b." + +#~ msgid "%s hours tracked total" +#~ msgstr "%s Stunden insgesamt erfasst" + +#~ msgctxt "years" +#~ msgid "All" +#~ msgstr "Alle" + +#~ msgid "" +#~ "There is no data to generate statistics yet.\n" +#~ "A week of usage would be nice!" +#~ msgstr "" +#~ "Es gibt noch keine Daten zur Erstellung einer Statistik.\n" +#~ "Eine Woche Benutzung wäre wünschenswert!" + +#~ msgid "Collecting data — check back after a week has passed!" +#~ msgstr "" +#~ "Daten werden noch gesammelt — Bitte warten Sie, bis eine Woche vergangen " +#~ "ist!" + +#~ 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 "Die erste Tätigkeit wurde am %s aufgezeichnet." + +#~ msgid "%(num)s year" +#~ msgid_plural "%(num)s years" +#~ msgstr[0] "%(num)s Jahr" +#~ msgstr[1] "%(num)s Jahre" + +#~ msgid "" +#~ "Time tracked so far is %(human_days)s human days (%(human_years)s) or " +#~ "%(working_days)s working days (%(working_years)s)." +#~ msgstr "" +#~ "Die bisher erfasste Zeit ist %(human_days)s Kalendertage " +#~ "(%(human_years)s) oder %(working_days)s Arbeitstage (%(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] "" +#~ "Die längste zusammenhängende Tätigkeit geschah am %(date)s und dauerte " +#~ "%(hours)s Stunde." +#~ msgstr[1] "" +#~ "Die längste zusammenhängende Tätigkeit geschah am %(date)s und dauerte " +#~ "%(hours)s Stunden." + +#~ msgid "There is %s record." +#~ msgid_plural "There are %s records." +#~ msgstr[0] "Es gibt %s Aufzeichnung." +#~ msgstr[1] "Es gibt %s Aufzeichnungen." + +#~ msgid "Hamster would like to observe you some more!" +#~ msgstr "Hamster möchte Sie gern ein wenig genauer beobachten!" + +#~ msgid "" +#~ "With %s percent of all activities starting before 9am, you seem to be an " +#~ "early bird." +#~ msgstr "" +#~ "Da %s Prozent aller Aufgaben vor 9 Uhr begonnen wurden, sind Sie " +#~ "sicherlich ein Frühaufsteher." + +#~ msgid "" +#~ "With %s percent of all activities starting after 11pm, you seem to be a " +#~ "night owl." +#~ msgstr "" +#~ "Da %s Prozent aller Aufgaben nach 23 Uhr begonnen wurden, sind Sie " +#~ "sicherlich eine Nachteule." + +#~ msgid "" +#~ "With %s percent of all activities being shorter than 15 minutes, you seem " +#~ "to be a busy bee." +#~ msgstr "" +#~ "Da %s Prozent aller Aufgaben weniger als 15 Minuten in Anspruch nahmen, " +#~ "scheinen Sie eine fleißige Biene zu sein." + +#~ msgid "No records today" +#~ msgstr "Heute keine Aufzeichnungen" + +#~ msgid "%(category)s: %(duration)s" +#~ msgstr "%(category)s: %(duration)s, " + +#~ msgid "%sh" +#~ msgstr "%sh" + +#~ msgid "Just started" +#~ msgstr "Soeben gestartet" #~ msgid "Show activities window" #~ msgstr "Aktivitäten-Fenster anzeigen" diff --git a/src/hamster-cli b/src/hamster-cli index 9c886c27f..e856422d1 100755 --- a/src/hamster-cli +++ b/src/hamster-cli @@ -1,4 +1,4 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 # - coding: utf-8 - # Copyright (C) 2010 Matías Ribecky @@ -142,7 +142,7 @@ class HamsterClient(object): "/org/gnome/Hamster/WindowServer") getattr(server, window_name)() else: - print "Running in devel mode" + print("Running in devel mode") from gi.repository import Gtk as gtk from hamster.lib.configuration import dialogs getattr(dialogs, window_name).show() @@ -173,7 +173,7 @@ class HamsterClient(object): formats = "html tsv xml ical".split() chosen = sys.argv[-1] formats = [f for f in formats if not chosen or f.startswith(chosen)] - print "\n".join(formats) + print("\n".join(formats)) def toggle(self): @@ -187,7 +187,7 @@ class HamsterClient(object): def start(self, *args): '''Start a new activity.''' if not args: - print "Error: please specify activity" + print("Error: please specify activity") return activity = args[0] @@ -216,7 +216,7 @@ class HamsterClient(object): facts = self.storage.get_facts(start_time, end_time) writer = reports.simple(facts, start_time, end_time, export_format) - print writer.export() + print(writer.export()) def _activities(self, search=""): @@ -225,24 +225,24 @@ class HamsterClient(object): activity, category = search.split("@") for cat in self.storage.get_categories(): if not category or cat['name'].lower().startswith(category.lower()): - print "%s@%s" % (activity.encode("utf8"), cat['name'].encode("utf8")) + print("{}@{}".format(activity, cat['name'])) else: for activity in self.storage.get_activities(search): - print activity['name'].encode('utf8') + print(activity['name']) if activity['category']: - print '%s@%s' % (activity['name'].encode('utf8'), activity['category'].encode('utf8')) + print("{}@{}".format(activity['name'], activity['category'])) def activities(self, *args): '''Print the names of all the activities.''' search = args[0] if args else "" for activity in self.storage.get_activities(search): - print '%s@%s' % (activity['name'].encode('utf8'), activity['category'].encode('utf8')) + print("{}@{}".format(activity['name'], activity['category'])) def categories(self, *args): '''Print the names of all the categories.''' for category in self.storage.get_categories(): - print category['name'].encode('utf8') + print(category['name']) def list(self, *times): @@ -258,10 +258,10 @@ class HamsterClient(object): """prints current activity. kinda minimal right now""" facts = self.storage.get_todays_facts() if facts and not facts[-1].end_time: - print "%s %s" % (unicode(facts[-1]).encode("utf-8").strip(), - stuff.format_duration(facts[-1].delta, human=False)) + print("{} {}".format(str(facts[-1]).strip(), + stuff.format_duration(facts[-1].delta, human=False))) else: - print _("No activity") + print((_("No activity"))) def search(self, *args): @@ -307,11 +307,11 @@ class HamsterClient(object): cols = ["{{{col}: <{len}}}".format(col=col, len=widths[col]) for col in cols] fact_line = " | ".join(cols) - row_width = sum([val + 3 for val in widths.values()]) + row_width = sum(val + 3 for val in list(widths.values())) - print - print fact_line.format(**headers) - print "-" * min(row_width, 80) + print() + print(fact_line.format(**headers)) + print("-" * min(row_width, 80)) by_cat = {} for fact in facts: @@ -320,26 +320,26 @@ class HamsterClient(object): by_cat[cat] += fact.delta pretty_fact = fact_dict(fact, print_with_date) - print fact_line.format(**pretty_fact) + print(fact_line.format(**pretty_fact)) if pretty_fact['description']: for line in word_wrap(pretty_fact['description'], 76): - print " %s" % line + print(" {}".format(line)) if pretty_fact['tags']: for line in word_wrap(pretty_fact['tags'], 76): - print " %s" % line + print(" {}".format(line)) - print "-" * min(row_width, 80) + print("-" * min(row_width, 80)) cats = [] - for cat, duration in sorted(by_cat.iteritems(), key=lambda x: x[1], reverse=True): + for cat, duration in sorted(by_cat.items(), key=lambda x: x[1], reverse=True): cats.append("%s: %s" % (cat, "%.1fh" % (stuff.duration_minutes(duration) / 60.0))) for line in word_wrap(", ".join(cats), 80): - print line + print(line) - print + print() diff --git a/src/hamster-service b/src/hamster-service index d3e260e18..5a316cbae 100755 --- a/src/hamster-service +++ b/src/hamster-service @@ -1,4 +1,4 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 # nicked off gwibber from gi.repository import GObject as gobject @@ -13,7 +13,7 @@ DBusGMainLoop(set_as_default=True) loop = gobject.MainLoop() if "org.gnome.Hamster" in dbus.SessionBus().list_names(): - print "Found hamster-service already running, exiting" + print("Found hamster-service already running, exiting") quit() from hamster.lib import i18n @@ -88,7 +88,7 @@ class Storage(db.Storage, dbus.service.Object): # anyway. should make updating simpler def _on_us_change(self, monitor, gio_file, event_uri, event): if event == gio.FileMonitorEvent.CHANGES_DONE_HINT: - print "`%s` has changed. Quitting!" % __file__ + print("`{}` has changed. Quitting!".format(__file__)) self.Quit() @dbus.service.signal("org.gnome.Hamster") @@ -301,6 +301,6 @@ class Storage(db.Storage, dbus.service.Object): if __name__ == '__main__': - print "hamster-service up" + print("hamster-service up") storage = Storage(loop) loop.run() diff --git a/src/hamster-windows-service b/src/hamster-windows-service index ce5306bc0..03383cc2a 100755 --- a/src/hamster-windows-service +++ b/src/hamster-windows-service @@ -1,4 +1,4 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 # nicked off hamster-service from gi.repository import GObject as gobject @@ -10,7 +10,7 @@ DBusGMainLoop(set_as_default=True) loop = gobject.MainLoop() if "org.gnome.Hamster.WindowServer" in dbus.SessionBus().list_names(): - print "Found hamster-window-service already running, exiting" + print("Found hamster-window-service already running, exiting") quit() @@ -54,11 +54,11 @@ if __name__ == '__main__': from hamster.lib import i18n i18n.setup_i18n() - glib.set_prgname(unicode(_("hamster-windows-service")).encode("utf-8")) + glib.set_prgname(str(_("hamster-windows-service"))) from hamster.lib.configuration import runtime, dialogs, conf, load_ui_file window_server = WindowServer(loop) - print "hamster-window-service up" + print("hamster-window-service up") loop.run() diff --git a/src/hamster/about.py b/src/hamster/about.py index 6e2f29afe..9c49764c8 100644 --- a/src/hamster/about.py +++ b/src/hamster/about.py @@ -31,8 +31,8 @@ def __init__(self, parent = None): "program-name" : _("Time Tracker"), "name" : _("Time Tracker"), #this should be deprecated in gtk 2.10 "version" : runtime.version, - "comments" : _(u"Project Hamster — track your time"), - "copyright" : _(u"Copyright © 2007–2010 Toms Bauģis and others"), + "comments" : _("Project Hamster — track your time"), + "copyright" : _("Copyright © 2007–2010 Toms Bauģis and others"), "website" : "http://projecthamster.wordpress.com/", "website-label" : _("Project Hamster Website"), "title": _("About Time Tracker"), diff --git a/src/hamster/client.py b/src/hamster/client.py index df93ceee1..1332ab1f1 100644 --- a/src/hamster/client.py +++ b/src/hamster/client.py @@ -55,10 +55,10 @@ class Storage(gobject.GObject): The rest is hopefully obvious. But if not, please file bug reports! """ __gsignals__ = { - "tags-changed": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), - "facts-changed": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), - "activities-changed": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), - "toggle-called": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), + "tags-changed": (gobject.SignalFlags.RUN_LAST, gobject.TYPE_NONE, ()), + "facts-changed": (gobject.SignalFlags.RUN_LAST, gobject.TYPE_NONE, ()), + "activities-changed": (gobject.SignalFlags.RUN_LAST, gobject.TYPE_NONE, ()), + "toggle-called": (gobject.SignalFlags.RUN_LAST, gobject.TYPE_NONE, ()), } def __init__(self): diff --git a/src/hamster/edit_activity.py b/src/hamster/edit_activity.py index 68bbf5d34..8d7460660 100644 --- a/src/hamster/edit_activity.py +++ b/src/hamster/edit_activity.py @@ -26,9 +26,10 @@ """ TODO: hook into notifications and refresh our days if some evil neighbour edit fact window has dared to edit facts """ -import widgets +from hamster import widgets from hamster.lib.configuration import runtime, conf, load_ui_file -from lib import Fact +from hamster.lib.stuff import datetime_to_hamsterday +from hamster.lib import Fact class CustomFactController(gobject.GObject): __gsignals__ = { @@ -41,21 +42,18 @@ def __init__(self, parent=None, fact_date=None, 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.parent, self.fact_id = parent, fact_id + self.parent = parent + self.fact_id = fact_id - #TODO - should somehow hint that time is not welcome here self.activity = widgets.ActivityEntry() self.activity.connect("changed", self.on_activity_changed) self.get_widget("activity_box").add(self.activity) - day_start = conf.get("day_start_minutes") - self.day_start = dt.time(day_start / 60, day_start % 60) + self.day_start = conf.day_start self.date = fact_date if not self.date: - self.date = (dt.datetime.now() - dt.timedelta(hours=self.day_start.hour, - minutes=self.day_start.minute)).date() - + self.date = datetime_to_hamsterday(dt.datetime.now()) self.dayline = widgets.DayLine() self._gui.get_object("day_preview").add(self.dayline) @@ -63,11 +61,8 @@ def __init__(self, parent=None, fact_date=None, fact_id=None): self.activity.grab_focus() if fact_id: fact = runtime.storage.get_fact(fact_id) - label = fact.start_time.strftime("%H:%M") - if fact.end_time: - label += fact.end_time.strftime(" %H:%M") - - label += " " + fact.serialized_name() + self.activity.original_fact = fact + label = fact.serialized(prepend_date=False) with self.activity.handler_block(self.activity.checker): self.activity.set_text(label) self.activity.select_region(len(label) - len(fact.serialized_name()), -1) @@ -111,20 +106,14 @@ def show(self): def figure_description(self): buf = self.get_widget('description').get_buffer() - description = buf.get_text(buf.get_start_iter(), buf.get_end_iter(), 0)\ - .decode("utf-8") + description = buf.get_text(buf.get_start_iter(), buf.get_end_iter(), 0) return description.strip() def localized_fact(self): """makes sure fact is in our date""" fact = Fact(self.activity.get_text()) - if fact.start_time: - fact.start_time = dt.datetime.combine(self.date, fact.start_time.time()) - - if fact.end_time: - fact.end_time = dt.datetime.combine(self.date, fact.end_time.time()) - + fact.date = self.date return fact diff --git a/src/hamster/external.py b/src/hamster/external.py index fe7c35bf4..f6786f4af 100644 --- a/src/hamster/external.py +++ b/src/hamster/external.py @@ -139,6 +139,6 @@ def get_eds_tasks(): if task.get_status() in [ecal.ICAL_STATUS_NONE, ecal.ICAL_STATUS_INPROCESS]: tasks.append({'name': task.get_summary(), 'category' : category}) return tasks - except Exception, e: + except Exception as e: logging.warn(e) return [] diff --git a/src/hamster/lib/__init__.py b/src/hamster/lib/__init__.py index bc93130a4..930e15e43 100644 --- a/src/hamster/lib/__init__.py +++ b/src/hamster/lib/__init__.py @@ -1,7 +1,47 @@ import calendar import datetime as dt +import logging import re +from hamster.lib.stuff import ( + datetime_to_hamsterday, + hamsterday_time_to_datetime, +) + + +DATE_FMT = "%d-%m-%Y" +TIME_FMT = "%H:%M" + + +# match #tag followed by any space or # that will be ignored +# tag must not contain #, comma, or any space character +tag_re = re.compile(r""" + [\s,]* # any spaces or commas (or nothing) + \# # hash character + ([^#\s]+) # the tag (anything but # or spaces) + [\s#,]* # any spaces, #, or commas (or nothing) + $ # end of text +""", flags=re.VERBOSE) + + +# match time, such as "01:32", "13.56" or "0116" +time_re = re.compile(r""" + ^ # start of string + (?P[0-1]?[0-9] | [2][0-3]) # hour (2 digits, between 00 and 23) + [:,\.]? # separator can be colon, + # dot, comma, or nothing + (?P[0-5][0-9]) # minute (2 digits, between 00 and 59) + $ # end of string +""", flags=re.VERBOSE) + + +def extract_time(match): + """extract time from a time_re match.""" + hour = int(match.group('hour')) + minute = int(match.group('minute')) + return dt.time(hour, minute) + + def figure_time(str_time): if not str_time or not str_time.strip(): return None @@ -9,7 +49,7 @@ def figure_time(str_time): # strip everything non-numeric and consider hours to be first number # and minutes - second number numbers = re.split("\D", str_time) - numbers = filter(lambda x: x!="", numbers) + numbers = [x for x in numbers if x!=""] hours, minutes = None, None @@ -29,12 +69,36 @@ def figure_time(str_time): class Fact(object): - def __init__(self, activity, category = "", description = "", tags = "", + def __init__(self, activity="", category = "", description = "", tags = "", start_time = None, end_time = None, id = None, delta = None, - date = None, activity_id = None): - """the category, description and tags can be either passed in explicitly - or by using the "activity@category, description #tag #tag" syntax. - explicitly stated values will take precedence over derived ones""" + date = None, activity_id = None, initial_fact=None): + """Homogeneous chunk of activity. + The category, description and tags can be either passed in explicitly + or by passing a string with the + "activity@category, description #tag #tag" + syntax as the first argument (activity), for historical reasons. + Explicitly stated values will take precedence over derived ones. + Note: this works only if the new values are not None or "" ! + In this case, one should separately state e.g. + fact = Fact(initial_fact=) + fact.end_time = None + TODO: should rework that, + but the new hamster-* should be ready in few years... + initial_fact (Fact or str): optional starting point. + This is the same as calling + Fact(str(initial_fact), ...) + + """ + + # Previously the "activity" argument could only be a string, + # actually containing all the "fact" information. + # Now allow to pass an inital fact, that will be copied, + # and overridden by any given keyword argument + # Note: currently, the genuine activity (before the @) + # is the same as in the original fact + if initial_fact: + activity = initial_fact.serialized() + self.original_activity = activity # unparsed version, mainly for trophies right now self.activity = None self.category = None @@ -45,59 +109,94 @@ def __init__(self, activity, category = "", description = "", tags = "", self.id = id self.ponies = False self.delta = delta - self.date = date self.activity_id = activity_id - for key, val in parse_fact(activity).iteritems(): + phase = "start_time" if date else "date" + for key, val in parse_fact(activity, phase, {}, date).items(): setattr(self, key, val) - # override implicit with explicit - self.category = category.replace(",", "") or self.category or None + self.category = (category.replace(",", "").strip() or + (self.category and self.category.strip())) self.description = (description or self.description or "").strip() or None self.tags = tags or self.tags or [] self.start_time = start_time or self.start_time or None self.end_time = end_time or self.end_time or None - - def __iter__(self): - keys = { + # TODO: might need some cleanup + def as_dict(self): + date = self.date + return { 'id': int(self.id) if self.id else "", 'activity': self.activity, 'category': self.category, 'description': self.description, - 'tags': [tag.encode("utf-8").strip() for tag in self.tags], - 'date': calendar.timegm(self.date.timetuple()) if self.date else "", - 'start_time': self.start_time if isinstance(self.start_time, basestring) else calendar.timegm(self.start_time.timetuple()), - 'end_time': self.end_time if isinstance(self.end_time, basestring) else calendar.timegm(self.end_time.timetuple()) if self.end_time else "", + 'tags': [tag.strip() for tag in self.tags], + 'date': calendar.timegm(date.timetuple()) if date else "", + 'start_time': self.start_time if isinstance(self.start_time, str) else calendar.timegm(self.start_time.timetuple()), + 'end_time': self.end_time if isinstance(self.end_time, str) else calendar.timegm(self.end_time.timetuple()) if self.end_time else "", 'delta': self.delta.seconds + self.delta.days * 24 * 60 * 60 if self.delta else "" #duration in seconds } - return iter(keys.items()) + @property + def date(self): + """hamster day, determined from start_time. + + Note: Setting date is a one-shot modification of + the start_time and end_time (if defined), + to match the given value. + Any subsequent modification of start_time + can result in different self.date. + """ + return datetime_to_hamsterday(self.start_time) + + @date.setter + def date(self, value): + if self.start_time: + self.start_time = hamsterday_time_to_datetime(value, self.start_time.time()) + if self.end_time: + self.end_time = hamsterday_time_to_datetime(value, self.end_time.time()) + def serialized_name(self): res = self.activity if self.category: res += "@%s" % self.category - if self.description or self.tags: - res += "%s, %s" % (" ".join(["#%s" % tag for tag in self.tags]), - self.description or "") + if self.description: + res += ", %s" % self.description + + if self.tags: + res += " %s" % " ".join("#%s" % tag for tag in self.tags) return res - def __str__(self): + + def serialized_time(self, prepend_date=True): time = "" if self.start_time: - time = self.start_time.strftime("%d-%m-%Y %H:%M") + if prepend_date: + time += self.date.strftime(DATE_FMT) + " " + time += self.start_time.strftime(TIME_FMT) if self.end_time: - time = "%s - %s" % (time, self.end_time.strftime("%H:%M")) - return "%s %s" % (time, self.serialized_name()) + time = "%s-%s" % (time, self.end_time.strftime(TIME_FMT)) + return time + def serialized(self, prepend_date=True): + """Return a string fully representing the fact.""" + name = self.serialized_name() + datetime = self.serialized_time(prepend_date) + return "%s %s" % (datetime, name) -def parse_fact(text, phase=None): + def __str__(self): + return self.serialized_time(prepend_date=True) + + + + +def parse_fact(text, phase=None, res=None, date=None): """tries to extract fact fields from the string the optional arguments in the syntax makes us actually try parsing values and fallback to next phase @@ -107,53 +206,95 @@ def parse_fact(text, phase=None): TODO - While we are now bit cooler and going recursively, this code still looks rather awfully spaghetterian. What is the real solution? + + Tentative syntax: + [date] start_time[-end_time] activity[@category][, description]{[,] { })#tag} + According to the legacy tests, # were allowed in the description """ now = dt.datetime.now() # determine what we can look for phases = [ + "date", # hamster day "start_time", "end_time", + "tags", "activity", "category", - "tags", ] phase = phase or phases[0] phases = phases[phases.index(phase):] - res = {} + if res is None: + res = {} text = text.strip() if not text: - return {} + return res fragment = re.split("[\s|#]", text, 1)[0].strip() - def next_phase(fragment, phase): - res.update(parse_fact(text[len(fragment):], phase)) - return res + # remove a fragment assumed to be at the beginning of text + remove_fragment = lambda text, fragment: text[len(fragment):] + + if "date" in phases: + # if there is any date given, it must be at the front + try: + date = dt.datetime.strptime(fragment, DATE_FMT).date() + remaining_text = remove_fragment(text, fragment) + except ValueError: + date = now.date() + remaining_text = text + return parse_fact(remaining_text, "start_time", res, date) if "start_time" in phases or "end_time" in phases: - # looking for start or end time + # -delta ? delta_re = re.compile("^-[0-9]{1,3}$") - time_re = re.compile("^([0-1]?[0-9]|[2][0-3]):([0-5][0-9])$") - time_range_re = re.compile("^([0-1]?[0-9]|[2][0-3]):([0-5][0-9])-([0-1]?[0-9]|[2][0-3]):([0-5][0-9])$") - if delta_re.match(fragment): + # TODO untested + # delta_re was probably thought to be used + # alone or together with a start_time + # but using "now" prevents the latter res[phase] = now + dt.timedelta(minutes=int(fragment)) - return next_phase(fragment, phases[phases.index(phase)+1]) - - elif time_re.match(fragment): - res[phase] = dt.datetime.combine(now.date(), dt.datetime.strptime(fragment, "%H:%M").time()) - return next_phase(fragment, phases[phases.index(phase)+1]) + remaining_text = remove_fragment(text, fragment) + return parse_fact(remaining_text, phases[phases.index(phase)+1], res, date) + + # only starting time ? + m = re.search(time_re, fragment) + if m: + time = extract_time(m) + res[phase] = hamsterday_time_to_datetime(date, time) + remaining_text = remove_fragment(text, fragment) + return parse_fact(remaining_text, phases[phases.index(phase)+1], res, date) + + # start-end ? + start, __, end = fragment.partition("-") + m_start = re.search(time_re, start) + m_end = re.search(time_re, end) + if m_start and m_end: + start_time = extract_time(m_start) + end_time = extract_time(m_end) + res["start_time"] = hamsterday_time_to_datetime(date, start_time) + res["end_time"] = hamsterday_time_to_datetime(date, end_time) + remaining_text = remove_fragment(text, fragment) + return parse_fact(remaining_text, "tags", res, date) - elif time_range_re.match(fragment) and phase == "start_time": - start, end = fragment.split("-") - res["start_time"] = dt.datetime.combine(now.date(), dt.datetime.strptime(start, "%H:%M").time()) - res["end_time"] = dt.datetime.combine(now.date(), dt.datetime.strptime(end, "%H:%M").time()) - phase = "activity" - return next_phase(fragment, "activity") + if "tags" in phases: + # Need to start from the end, because + # the description can hold some '#' characters + tags = [] + remaining_text = text + while True: + m = re.search(tag_re, remaining_text) + if not m: + break + tag = m.group(1) + tags.append(tag) + # strip the matched string (including #) + remaining_text = remaining_text[:m.start()] + res["tags"] = tags + return parse_fact(remaining_text, "activity", res, date) if "activity" in phases: activity = re.split("[@|#|,]", text, 1)[0] @@ -162,26 +303,13 @@ def next_phase(fragment, phase): return res res["activity"] = activity - return next_phase(activity, "category") + remaining_text = remove_fragment(text, activity) + return parse_fact(remaining_text, "category", res, date) if "category" in phases: - category = re.split("[#|,]", text, 1)[0] - if category.lstrip().startswith("@"): - res["category"] = category.lstrip("@ ") - return next_phase(category, "tags") - - return next_phase("", "tags") - - if "tags" in phases: - tags, desc = text.split(",", 1) if "," in text else (text, None) - - tags = [tag.strip() for tag in re.split("[#]", tags) if tag.strip()] - if tags: - res["tags"] = tags - - if (desc or "").strip(): - res["description"] = desc.strip() - + category, _, description = text.partition(",") + res["category"] = category.lstrip("@").strip() or None + res["description"] = description.strip() or None return res return {} diff --git a/src/hamster/lib/charting.py b/src/hamster/lib/charting.py index 60c49ed5c..e88adb313 100644 --- a/src/hamster/lib/charting.py +++ b/src/hamster/lib/charting.py @@ -22,7 +22,7 @@ from gi.repository import Pango as pango import datetime as dt import time -import graphics, stuff +from hamster.lib import graphics, stuff import locale class Bar(graphics.Sprite): diff --git a/src/hamster/lib/configuration.py b/src/hamster/lib/configuration.py index e3630878b..f86d21112 100644 --- a/src/hamster/lib/configuration.py +++ b/src/hamster/lib/configuration.py @@ -30,6 +30,9 @@ from gi.repository import GObject as gobject from gi.repository import Gtk as gtk + +import gi +gi.require_version('GConf', '2.0') from gi.repository import GConf as gconf import logging @@ -39,7 +42,7 @@ class Controller(gobject.GObject): __gsignals__ = { - "on-close": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), + "on-close": (gobject.SignalFlags.RUN_LAST, gobject.TYPE_NONE, ()), } def __init__(self, parent=None, ui_file=""): @@ -129,7 +132,7 @@ def __init__(self, get_dialog_class): self.dialog_close_handlers = {} def on_close_window(self, dialog): - for key, assoc_dialog in list(self.dialogs.iteritems()): + for key, assoc_dialog in list(self.dialogs.items()): if dialog == assoc_dialog: del self.dialogs[key] @@ -212,7 +215,7 @@ class GConfStore(gobject.GObject, Singleton): } __gsignals__ = { - "conf-changed": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT)) + "conf-changed": (gobject.SignalFlags.RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT)) } def __init__(self): gobject.GObject.__init__(self) @@ -332,5 +335,12 @@ def set(self, key, value): return True + @property + def day_start(self): + """Start of the hamster day.""" + day_start_minutes = self.get("day_start_minutes") + hours, minutes = divmod(day_start_minutes, 60) + return dt.time(hours, minutes) + conf = GConfStore() diff --git a/src/hamster/lib/desktop.py b/src/hamster/lib/desktop.py index f60905a42..c352051df 100644 --- a/src/hamster/lib/desktop.py +++ b/src/hamster/lib/desktop.py @@ -56,7 +56,7 @@ def check_hamster(self): todays_facts = self.storage._Storage__get_todays_facts() self.check_user(todays_facts) trophies.check_ongoing(todays_facts) - except Exception, e: + except Exception as e: logging.error("Error while refreshing: %s" % e) finally: # we want to go on no matter what, so in case of any error we find out about it sooner return True @@ -79,13 +79,13 @@ def check_user(self, todays_facts): duration = delta.seconds / 60 if duration and duration % interval == 0: - message = _(u"Working on %s") % last_activity['name'] + message = _("Working on %s") % last_activity['name'] self.notify_user(message) elif self.conf_notify_on_idle: #if we have no last activity, let's just calculate duration from 00:00 if (now.minute + now.hour * 60) % interval == 0: - self.notify_user(_(u"No activity")) + self.notify_user(_("No activity")) def notify_user(self, summary="", details=""): diff --git a/src/hamster/lib/graphics.py b/src/hamster/lib/graphics.py index 9056b6b19..faa65286e 100644 --- a/src/hamster/lib/graphics.py +++ b/src/hamster/lib/graphics.py @@ -22,7 +22,7 @@ import re try: - import pytweener + from hamster.lib import pytweener except: # we can also live without tweener. Scene.animate will not work pytweener = None @@ -61,7 +61,7 @@ def parse(self, color): assert color is not None #parse color into rgb values - if isinstance(color, basestring): + if isinstance(color, str): match = self.hex_color_long.match(color) if match: color = [int(color, 16) / 65535.0 for color in match.groups()] @@ -99,7 +99,7 @@ def gdk(self, color): def hex(self, color): c = self.parse(color) - return "#" + "".join(["%02x" % (color * 255) for color in c]) + return "#" + "".join("{:02x}".format(int(color) * 255) for color in c) def is_light(self, color): """tells you if color is dark or light, so you can up or down the @@ -190,7 +190,7 @@ class Graphics(object): See http://cairographics.org/documentation/pycairo/2/reference/context.html for detailed description of the cairo drawing functions. """ - __slots__ = ('context', 'colors', 'extents', 'paths', '_last_matrix', + __slots__ = ('context', 'extents', 'paths', '_last_matrix', '__new_instructions', '__instruction_cache', 'cache_surface', '_cache_layout') colors = Colors # pointer to the color utilities instance @@ -442,7 +442,7 @@ def create_layout(self, size = None): if not self.context: # TODO - this is rather sloppy as far as exception goes # should explain better - raise "Can not create layout without existing context!" + raise Exception("Can not create layout without existing context!") layout = pangocairo.create_layout(self.context) font_desc = pango.FontDescription(_font_desc) @@ -676,10 +676,10 @@ def log(self, *lines): """will print out the lines in console if debug is enabled for the specific sprite""" if getattr(self, "debug", False): - print dt.datetime.now().time(), + print(dt.datetime.now().time(), end=' ') for line in lines: - print line, - print + print(line, end=' ') + print() def _add(self, sprite, index = None): """add one sprite at a time. used by add_child. split them up so that @@ -1489,7 +1489,7 @@ def __init__(self, text = "", size = None, color = None, def __setattr__(self, name, val): if name == "font_desc": - if isinstance(val, basestring): + if isinstance(val, str): val = pango.FontDescription(val) elif isinstance(val, pango.FontDescription): val = val.copy() diff --git a/src/hamster/lib/i18n.py b/src/hamster/lib/i18n.py index 34eb91bb2..477d91e25 100644 --- a/src/hamster/lib/i18n.py +++ b/src/hamster/lib/i18n.py @@ -25,7 +25,7 @@ def setup_i18n(): module.bind_textdomain_codeset('hamster-time-tracker','utf8') - gettext.install("hamster-time-tracker", locale_dir, unicode = True) + gettext.install("hamster-time-tracker", locale_dir) else: gettext.install("hamster-time-tracker-uninstalled") diff --git a/src/hamster/lib/layout.py b/src/hamster/lib/layout.py index d2a0e360b..3a170a70e 100644 --- a/src/hamster/lib/layout.py +++ b/src/hamster/lib/layout.py @@ -12,7 +12,7 @@ from gi.repository import Pango as pango from collections import defaultdict -import graphics +from hamster.lib import graphics class Widget(graphics.Sprite): diff --git a/src/hamster/lib/pytweener.py b/src/hamster/lib/pytweener.py index c2449f2b8..393f899cb 100644 --- a/src/hamster/lib/pytweener.py +++ b/src/hamster/lib/pytweener.py @@ -227,7 +227,7 @@ def color_update(fraction): self.start_value = self.decode_func(start_value) self.change = self.decode_func(target_value) - self.start_value - elif isinstance(start_value, basestring) \ + elif isinstance(start_value, str) \ and (self.hex_color_normal.match(start_value) or self.hex_color_short.match(start_value)): self.update = color_update if self.hex_color_normal.match(start_value): @@ -357,17 +357,17 @@ def __init__(self, a, b, c): total = dt.datetime.now() t = dt.datetime.now() - print "Adding %d tweens..." % object_count + print("Adding %d tweens..." % object_count) for i, o in enumerate(objects): tweener.add_tween(o, a = i, b = i, c = i, duration = 0.1 * update_times, easing=Easing.Circ.ease_in_out) - print dt.datetime.now() - t + print(dt.datetime.now() - t) t = dt.datetime.now() - print "Updating %d times......" % update_times + print("Updating %d times......" % update_times) for i in range(update_times): #update 1000 times tweener.update(0.1) - print dt.datetime.now() - t + print(dt.datetime.now() - t) diff --git a/src/hamster/lib/stuff.py b/src/hamster/lib/stuff.py index c492cb039..43789a7f7 100644 --- a/src/hamster/lib/stuff.py +++ b/src/hamster/lib/stuff.py @@ -23,6 +23,8 @@ import gi import logging +logger = logging.getLogger(__name__) # noqa: E402 +import datetime as dt gi.require_version('Gtk', '3.0') from gi.repository import Gtk as gtk @@ -36,6 +38,44 @@ import locale import os + +def datetime_to_hamsterday(civil_date_time): + """Return the hamster day corresponding to a given civil datetime. + + The hamster day start is taken into account. + """ + + # work around cyclic imports + from hamster.lib.configuration import conf + + if civil_date_time.time() < conf.day_start: + # early morning, between midnight and day_start + # => the hamster day is the previous civil day + hamster_date_time = civil_date_time - dt.timedelta(days=1) + else: + hamster_date_time = civil_date_time + # return only the date + return hamster_date_time.date() + + +def hamsterday_time_to_datetime(hamsterday, time): + """Return the civil datetime corresponding to a given hamster day and time. + + The hamster day start is taken into account. + """ + + # work around cyclic imports + from hamster.lib.configuration import conf + + if time < conf.day_start: + # early morning, between midnight and day_start + # => the hamster day is the previous civil day + civil_date = hamsterday + dt.timedelta(days=1) + else: + civil_date = hamsterday + return dt.datetime.combine(civil_date, time) + + def format_duration(minutes, human = True): """formats duration in a human readable format. accepts either minutes or timedelta""" @@ -49,6 +89,11 @@ def format_duration(minutes, human = True): else: return "00:00" + if minutes < 0: + # format_duration did not work for negative values anyway + # return a warning + return "NEGATIVE" + hours = minutes / 60 minutes = minutes % 60 formatted_duration = "" @@ -85,19 +130,19 @@ def format_range(start_date, end_date): # 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 - title = (u"%(start_B)s %(start_d)s, %(start_Y)s – %(end_B)s %(end_d)s, %(end_Y)s".encode('utf-8')) % dates_dict + title = ("%(start_B)s %(start_d)s, %(start_Y)s – %(end_B)s %(end_d)s, %(end_Y)s") % dates_dict elif start_date.month != end_date.month: # 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 - title = (u"%(start_B)s %(start_d)s – %(end_B)s %(end_d)s, %(end_Y)s".encode('utf-8')) % dates_dict + title = ("%(start_B)s %(start_d)s – %(end_B)s %(end_d)s, %(end_Y)s") % dates_dict else: # 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 - title = (u"%(start_B)s %(start_d)s – %(end_d)s, %(end_Y)s".encode('utf-8')) % dates_dict + title = ("%(start_B)s %(start_d)s – %(end_d)s, %(end_Y)s") % dates_dict return title @@ -140,14 +185,14 @@ def zero_hour(date): # see bug 562298 def locale_from_utf8(utf8_str): try: - retval = unicode (utf8_str, "utf-8").encode(locale.getpreferredencoding()) + retval = str (utf8_str, "utf-8").encode(locale.getpreferredencoding()) except: retval = utf8_str return retval def locale_to_utf8(locale_str): try: - retval = unicode (locale_str, locale.getpreferredencoding()).encode("utf-8") + retval = str (locale_str, locale.getpreferredencoding()).encode("utf-8") except: retval = locale_str return retval diff --git a/src/hamster/lib/trophies.py b/src/hamster/lib/trophies.py index f2bd692d8..e7f511b67 100644 --- a/src/hamster/lib/trophies.py +++ b/src/hamster/lib/trophies.py @@ -31,7 +31,7 @@ storage = None from hamster.lib import Fact -import stuff +from hamster.lib import stuff import datetime as dt def unlock(achievement_id): diff --git a/src/hamster/overview.py b/src/hamster/overview.py index 02a6ab307..643c42fcf 100755 --- a/src/hamster/overview.py +++ b/src/hamster/overview.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright (C) 2014 Toms Bauģis @@ -24,10 +24,14 @@ import webbrowser from collections import defaultdict +from math import ceil from gi.repository import Gtk as gtk from gi.repository import Gdk as gdk from gi.repository import GObject as gobject + +import gi +gi.require_version('PangoCairo', '1.0') from gi.repository import PangoCairo as pangocairo from gi.repository import Pango as pango import cairo @@ -45,8 +49,8 @@ from hamster.lib.pytweener import Easing -from widgets.dates import RangePick -from widgets.facttree import FactTree +from hamster.widgets.dates import RangePick +from hamster.widgets.facttree import FactTree class HeaderBar(gtk.HeaderBar): @@ -132,7 +136,7 @@ def __init__(self, width=0, height=0, vertical=None, **kwargs): def set_items(self, items): """expects a list of key, value to work with""" res = [] - max_value = sum((rec[1] for rec in items)) + max_value = max(sum((rec[1] for rec in items)), 1) for key, val in items: res.append((key, val, val * 1.0 / max_value)) self._items = res @@ -202,13 +206,13 @@ def __init__(self, **kwargs): self.layout.set_markup("Hamster") # dummy self.label_height = self.layout.get_pixel_size()[1] - self._max = 0 + self._max = dt.timedelta(0) def set_values(self, values): """expects a list of 2-tuples""" self.values = values self.height = len(self.values) * 14 - self._max = max(rec[1] for rec in values) if values else 0 + self._max = max(rec[1] for rec in values) if values else dt.timedelta(0) def _draw(self, context, opacity, matrix): g = graphics.Graphics(context) @@ -217,16 +221,23 @@ def _draw(self, context, opacity, matrix): for i, (label, value) in enumerate(self.values): g.set_color("#333") - self.layout.set_markup(stuff.escape_pango(label)) + duration_str = stuff.format_duration(value, human=False) + markup = stuff.escape_pango('{}, {}'.format(label, duration_str)) + self.layout.set_markup(markup) label_w, label_h = self.layout.get_pixel_size() + bar_start_x = 150 # pixels + margin = 10 # pixels y = int(i * label_h * 1.5) - g.move_to(100 - label_w, y) + g.move_to(bar_start_x - margin - label_w, y) pangocairo.show_layout(context, self.layout) - w = (self.alloc_w - 110) * value.total_seconds() / self._max.total_seconds() - w = max(1, int(round(w))) - g.rectangle(110, y, int(w), int(label_h)) + if self._max > dt.timedelta(0): + w = ceil((self.alloc_w - bar_start_x) * value.total_seconds() / + self._max.total_seconds()) + else: + w = 1 + g.rectangle(bar_start_x, y, int(w), int(label_h)) g.fill("#999") g.restore_context() @@ -295,8 +306,8 @@ def set_facts(self, facts): totals["tag"][tag] += fact.delta - for key, group in totals.iteritems(): - totals[key] = sorted(group.iteritems(), key=lambda x: x[1], reverse=True) + for key, group in totals.items(): + totals[key] = sorted(group.items(), key=lambda x: x[1], reverse=True) self.totals = totals self.activities_chart.set_values(totals['activity']) @@ -304,7 +315,13 @@ def set_facts(self, facts): self.tag_chart.set_values(totals['tag']) self.stacked_bar.set_items([(cat, delta.total_seconds() / 60.0) for cat, delta in totals['category']]) - self.category_totals.markup = ", ".join("%s: %s" % (stuff.escape_pango(cat), stuff.format_duration(hours)) for cat, hours in totals['category']) + + 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']) + + def on_click(self, scene, sprite, event): self.collapsed = not self.collapsed @@ -402,8 +419,9 @@ def __init__(self, parent = None): self.totals = Totals() main.pack_start(self.totals, False, True, 1) - date_range = stuff.week(dt.datetime.today()) # TODO - do the hamster day - self.header_bar.range_pick.set_range(*date_range) + # FIXME: should store and recall date_range from hamster.lib.configuration.conf + hamster_day = stuff.datetime_to_hamsterday(dt.datetime.today()) + self.header_bar.range_pick.set_range(hamster_day) self.header_bar.range_pick.connect("range-selected", self.on_range_selected) self.header_bar.add_activity_button.connect("clicked", self.on_add_activity_clicked) self.header_bar.search_button.connect("toggled", self.on_search_toggled) @@ -416,6 +434,9 @@ def __init__(self, parent = None): self.facts = [] self.find_facts() + + # update every minute (necessary if an activity is running) + gobject.timeout_add_seconds(60, self.on_timeout) self.window.show_all() @@ -455,7 +476,6 @@ def find_facts(self): self.fact_tree.set_facts(self.facts) self.totals.set_facts(self.facts) - def on_range_selected(self, button, range_type, start, end): self.find_facts() @@ -491,6 +511,11 @@ def on_search_toggled(self, button): if active: self.filter_entry.grab_focus() + def on_timeout(self): + # TODO: should update only the running FactTree row (if any), and totals + self.find_facts() + # The timeout will stop if returning False + return True def on_prefs_clicked(self, menu): dialogs.prefs.show(self) @@ -511,7 +536,7 @@ def on_report_chosen(widget, format, path): webbrowser.open_new("file://%s" % path) else: try: - gtk.show_uri(gdk.Screen(), "file://%s" % os.path.split(path)[0], 0L) + gtk.show_uri(gdk.Screen(), "file://%s" % os.path.split(path)[0], 0) except: pass # bug 626656 - no use in capturing this one i think diff --git a/src/hamster/preferences.py b/src/hamster/preferences.py index 52c06cfbe..ee94730eb 100755 --- a/src/hamster/preferences.py +++ b/src/hamster/preferences.py @@ -75,8 +75,8 @@ def load(self, category_id): appearances = ["text", "icon", "both"] from hamster.lib.configuration import runtime, conf -import widgets -from lib import stuff, trophies +from hamster import widgets +from hamster.lib import stuff, trophies @@ -205,9 +205,7 @@ def load_config(self, *args): self.get_widget("notify_on_idle").set_active(conf.get("notify_on_idle")) self.get_widget("notify_on_idle").set_sensitive(conf.get("notify_interval") <=120) - day_start = conf.get("day_start_minutes") - day_start = dt.time(day_start / 60, day_start % 60) - self.day_start.set_time(day_start) + self.day_start.set_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)) @@ -221,8 +219,7 @@ def load_config(self, *args): 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) \ - .decode("utf-8") + updated_tags = buf.get_text(buf.get_start_iter(), buf.get_end_iter(), 0) if updated_tags == self.tags: return @@ -265,8 +262,8 @@ def on_category_list_drag_motion(self, treeview, drag_context, x, y, eventtime): except: return - drop_yes = ("drop_yes", gtk.TARGET_SAME_APP, 0) - drop_no = ("drop_no", gtk.TARGET_SAME_APP, 0) + drop_yes = ("drop_yes", gtk.TargetFlags.SAME_APP, 0) + drop_no = ("drop_no", gtk.TargetFlags.SAME_APP, 0) if drop_position != gtk.TREE_VIEW_DROP_AFTER and \ drop_position != gtk.TREE_VIEW_DROP_BEFORE: @@ -295,7 +292,6 @@ def on_category_drop(self, treeview, context, x, y, selection, # callbacks def category_edited_cb(self, cell, path, new_text, model): - new_text = new_text.decode("utf-8") id = model[path][0] if id == -1: return False #ignoring unsorted category @@ -319,8 +315,6 @@ def category_edited_cb(self, cell, path, new_text, model): def activity_name_edited_cb(self, cell, path, new_text, model): - new_text = new_text.decode("utf-8") - id = model[path][0] category_id = model[path][2] @@ -539,7 +533,7 @@ def on_category_add_clicked(self, button): """ appends row, jumps to it and allows user to input name """ new_category = self.category_store.insert_before(self.category_store.unsorted_category, - [-2, _(u"New category")]) + [-2, _("New category")]) model = self.category_tree.get_model() @@ -554,7 +548,7 @@ def on_activity_add_clicked(self, button): """ appends row, jumps to it and allows user to input name """ category_id = self._get_selected_category() - new_activity = self.activity_store.append([-1, _(u"New activity"), category_id]) + new_activity = self.activity_store.append([-1, _("New activity"), category_id]) (model, iter) = self.selection.get_selected() @@ -586,7 +580,7 @@ def on_notify_interval_format_value(self, slider, value): value) % {'interval_minutes': value} else: # notify interval slider value label - label = _(u"Never") + label = _("Never") return label diff --git a/src/hamster/reports.py b/src/hamster/reports.py index 82dff924b..69314c802 100644 --- a/src/hamster/reports.py +++ b/src/hamster/reports.py @@ -42,7 +42,7 @@ from calendar import timegm -from StringIO import StringIO +from io import StringIO, IOBase def simple(facts, start_date, end_date, format, path = None): facts = copy.deepcopy(facts) # dont want to do anything bad to the input @@ -85,7 +85,7 @@ def write_report(self, facts): try: for fact in facts: fact.activity= fact.activity - fact.description = (fact.description or u"") + fact.description = (fact.description or "") fact.category = (fact.category or _("Unsorted")) if self.datetime_format: @@ -100,7 +100,7 @@ def write_report(self, facts): self._finish(facts) finally: - if isinstance(self.file, file): + if isinstance(self.file, IOBase): self.file.close() def _start(self, facts): @@ -203,13 +203,13 @@ def __init__(self, path, start_date, end_date): dates_dict.update(stuff.dateDict(end_date, "end_")) if start_date.year != end_date.year: - self.title = _(u"Activity report for %(start_B)s %(start_d)s, %(start_Y)s – %(end_B)s %(end_d)s, %(end_Y)s") % dates_dict + self.title = _("Activity report for %(start_B)s %(start_d)s, %(start_Y)s – %(end_B)s %(end_d)s, %(end_Y)s") % dates_dict elif start_date.month != end_date.month: - self.title = _(u"Activity report for %(start_B)s %(start_d)s – %(end_B)s %(end_d)s, %(end_Y)s") % dates_dict + self.title = _("Activity report for %(start_B)s %(start_d)s – %(end_B)s %(end_d)s, %(end_Y)s") % dates_dict elif start_date == end_date: - self.title = _(u"Activity report for %(start_B)s %(start_d)s, %(start_Y)s") % dates_dict + self.title = _("Activity report for %(start_B)s %(start_d)s, %(start_Y)s") % dates_dict else: - self.title = _(u"Activity report for %(start_B)s %(start_d)s – %(end_d)s, %(end_Y)s") % dates_dict + self.title = _("Activity report for %(start_B)s %(start_d)s – %(end_d)s, %(end_Y)s") % dates_dict # read the template, allow override @@ -283,7 +283,7 @@ def _finish(self, facts): # group by date by_date = [] for date, date_facts in itertools.groupby(facts, lambda fact:fact.date): - by_date.append((date, [dict(fact) for fact in date_facts])) + by_date.append((date, [fact.as_dict() for fact in date_facts])) by_date = dict(by_date) date_facts = [] @@ -326,15 +326,15 @@ def _finish(self, facts): start_date = timegm(self.start_date.timetuple()), end_date = timegm(self.end_date.timetuple()), - facts = json_dumps([dict(fact) for fact in facts]), + facts = json_dumps([fact.as_dict() for fact in facts]), date_facts = json_dumps(date_facts), all_activities_rows = "\n".join(self.fact_rows) ) - for key, val in data.iteritems(): - if isinstance(val, basestring): - data[key] = val.encode("utf-8") + for key, val in data.items(): + if isinstance(val, str): + data[key] = val self.file.write(Template(self.main_template).safe_substitute(data)) diff --git a/src/hamster/storage/db.py b/src/hamster/storage/db.py index 505d34123..d59d3f57e 100644 --- a/src/hamster/storage/db.py +++ b/src/hamster/storage/db.py @@ -34,14 +34,14 @@ import os, time import datetime -import storage +from hamster.storage import storage from shutil import copy as copyfile import itertools import datetime as dt try: from gi.repository import Gio as gio except ImportError: - print "Could not import gio - requires pygobject. File monitoring will be disabled" + print("Could not import gio - requires pygobject. File monitoring will be disabled") gio = None from hamster.lib import Fact @@ -80,7 +80,7 @@ def on_db_file_change(monitor, gio_file, event_uri, event): self.con = None if event in (gio.FileMonitorEvent.CHANGES_DONE_HINT, gio.FileMonitorEvent.CREATED): - print "DB file has been modified externally. Calling all stations" + print("DB file has been modified externally. Calling all stations") self.dispatch_overwrite() # plan "b" – synchronize the time tracker's database from external source while the tracker is running @@ -102,11 +102,11 @@ def __init_db_file(self, database_dir): from xdg.BaseDirectory import xdg_data_home database_dir = os.path.realpath(os.path.join(xdg_data_home, "hamster-applet")) except ImportError: - print "Could not import xdg - will store hamster.db in home folder" + print("Could not import xdg - will store hamster.db in home folder") database_dir = os.path.realpath(os.path.expanduser("~")) if not os.path.exists(database_dir): - os.makedirs(database_dir, 0744) + os.makedirs(database_dir, 0o744) # handle the move to xdg_data_home old_db_file = os.path.expanduser("~/.gnome2/hamster-applet/hamster.db") @@ -140,7 +140,7 @@ def __init_db_file(self, database_dir): copyfile(os.path.join(data_dir, 'hamster.db'), db_path) #change also permissions - sometimes they are 444 - os.chmod(db_path, 0664) + os.chmod(db_path, 0o664) return db_path @@ -633,10 +633,9 @@ def __last_insert_rowid(self): def __get_todays_facts(self): try: from hamster.lib.configuration import conf - day_start = conf.get("day_start_minutes") + day_start = conf.day_start except: - day_start = 5 * 60 # default day start to 5am - day_start = dt.time(day_start / 60, day_start % 60) + day_start = dt.time(5, 0) # default: 5:00 today = (dt.datetime.now() - dt.timedelta(hours = day_start.hour, minutes = day_start.minute)).date() return self.__get_facts(today) @@ -646,9 +645,12 @@ def __get_facts(self, date, end_date = None, search_terms = ""): try: from hamster.lib.configuration import conf day_start = conf.get("day_start_minutes") + day_start_h, day_start_m = divmod(day_start) except: - day_start = 5 * 60 # default day start to 5am - day_start = dt.time(day_start / 60, day_start % 60) + # default day start to 5am + day_start_h = 5 + day_start_m = 0 + day_start = dt.time(day_start_h, day_start_m) split_time = day_start datetime_from = dt.datetime.combine(date, split_time) @@ -780,7 +782,7 @@ def __get_activities(self, search): """ search = search.lower() search = search.replace('\\', '\\\\').replace('%', '\\%').replace('_', '\\_') - activities = self.fetchall(query, (u'%s%%' % search, )) + activities = self.fetchall(query, ('%s%%' % search, )) return activities @@ -999,7 +1001,7 @@ def run_fixtures(self): if version < current_version: #lock down current version self.execute("UPDATE version SET version = %d" % current_version) - print "updated database from version %d to %d" % (version, current_version) + print("updated database from version %d to %d" % (version, current_version)) # oldtimer – database version structure had been performed on startup (thus we know that user has been on at least 2 versions) if trophies: diff --git a/src/hamster/widgets/__init__.py b/src/hamster/widgets/__init__.py index 6618b7153..58b7dbef6 100644 --- a/src/hamster/widgets/__init__.py +++ b/src/hamster/widgets/__init__.py @@ -23,21 +23,13 @@ from gi.repository import Pango as pango # import our children -from activityentry import ActivityEntry - -from timeinput import TimeInput - -from dayline import DayLine - -from tags import Tag -from tags import TagBox -from tags import TagsEntry - -from reportchooserdialog import ReportChooserDialog - -from facttree import FactTree - -from dates import RangePick +from hamster.widgets.activityentry import ActivityEntry +from hamster.widgets.timeinput import TimeInput +from hamster.widgets.dayline import DayLine +from hamster.widgets.tags import Tag, TagBox, TagsEntry +from hamster.widgets.reportchooserdialog import ReportChooserDialog +from hamster.widgets.facttree import FactTree +from hamster.widgets.dates import RangePick # handy wrappers diff --git a/src/hamster/widgets/activityentry.py b/src/hamster/widgets/activityentry.py index 7a8c3f61b..dbc0c9ebd 100644 --- a/src/hamster/widgets/activityentry.py +++ b/src/hamster/widgets/activityentry.py @@ -28,13 +28,14 @@ from gi.repository import PangoCairo as pangocairo from gi.repository import Pango as pango from collections import defaultdict +from copy import deepcopy from hamster import client from hamster.lib import Fact, looks_like_time from hamster.lib import stuff from hamster.lib import graphics - +import logging def extract_search(text): @@ -185,7 +186,7 @@ def on_enter_frame(self, scene, context): description_color = graphics.Colors.contrast(color, 50) description_color = graphics.Colors.hex(description_color) - label += ' - %s' % (description_color, row.description) + label += ' [%s]' % (description_color, row.description) self.label.show(g, label, color=color) @@ -194,9 +195,12 @@ def on_enter_frame(self, scene, context): class ActivityEntry(gtk.Entry): - def __init__(self, **kwargs): + def __init__(self, updating=True, **kwargs): gtk.Entry.__init__(self) + # to be set by the caller, if editing an existing fact + self.original_fact = None + self.popup = gtk.Window(type = gtk.WindowType.POPUP) box = gtk.Frame() box.set_shadow_type(gtk.ShadowType.IN) @@ -245,7 +249,7 @@ def on_focus_out(self, entry, event): self.popup.hide() def on_icon_press(self, entry, icon, event): - self.show_suggestions("") + self.show_suggestions(self.get_text()) def on_key_press(self, entry, event=None): if event.keyval in (gdk.KEY_BackSpace, gdk.KEY_Delete): @@ -299,7 +303,8 @@ def load_suggestions(self): label += "@%s" % rec["category"] suggestions[label] += 0 - self.suggestions = sorted(suggestions.iteritems(), key=lambda x: x[1], reverse=True) + # list of (label, score), higher scores first + self.suggestions = sorted(suggestions.items(), key=lambda x: x[1], reverse=True) def complete_first(self): text = self.get_text() @@ -335,39 +340,59 @@ def update_suggestions(self, text=""): [start_time] | [-end_time] | activity | [@category] | [#tag] """ - now = dt.datetime.now() - - text = text.lstrip() - - time_re = re.compile("^([0-1]?[0-9]|[2][0-3]):([0-5][0-9])$") - time_range_re = re.compile("^([0-1]?[0-9]|[2][0-3]):([0-5][0-9])-([0-1]?[0-9]|[2][0-3]):([0-5][0-9])$") - delta_re = re.compile("^-[0-9]{1,3}$") - - # when the time is filled, we need to make sure that the chunks parse correctly - + # list of tuples (description, variant) + variants = [] - delta_fragment_re = re.compile("^-[0-9]{0,3}$") - + if self.original_fact: + # editing an existing fact + if self.original_fact.end_time is None: + description = "stop now" + now = dt.datetime.now() + variant_fact = Fact(initial_fact=self.original_fact, end_time=now) + else: + # FIXME: that one should be available only for the last entry + description = "keep up (caveat: is it the last entry ?)" + # Do not use Fact(..., end_time=None): it would be a no-op + variant_fact = Fact(initial_fact=self.original_fact) + variant_fact.end_time = None + variant = variant_fact.serialized(prepend_date=False) + variants.append((description, variant)) + + else: + # brand new fact + description = "start now" + now = dt.datetime.now() + variant = now.strftime("%H:%M ") + variants.append((description, variant)) + + prev_fact = self.todays_facts[-1] if self.todays_facts else None + if prev_fact and prev_fact.end_time: + since = stuff.format_duration(now - prev_fact.end_time) + description = "from previous activity, %s ago" % since + variant = prev_fact.end_time.strftime("%H:%M ") + variants.append((description, variant)) + + description = "start activity -n minutes ago (1 or 3 digits allowed)" + variant = "-" + variants.append((description, variant)) + + text = text.strip() + if text: + description = "clear" + variant = "" + variants.append((description, variant)) - templates = { - "start_time": "", - "start_delta": ("start activity -n minutes ago", "-"), - } + res = [] + for (description, variant) in variants: + res.append(DataRow(variant, description=description)) - # need to set the start_time template before - prev_fact = self.todays_facts[-1] if self.todays_facts else None - if prev_fact and prev_fact.end_time: - templates["start_time"] = ("from previous activity %s ago" % stuff.format_duration(now - prev_fact.end_time), - prev_fact.end_time.strftime("%H:%M ")) - - variants = [] fact = Fact(text) # figure out what we are looking for # time -> activity[@category] -> tags -> description - # presence of each next attribute means that we are not looking for the previous one + # presence of an attribute means that we are not looking for the previous one # we still might be looking for the current one though looking_for = "start_time" fields = ["start_time", "end_time", "activity", "category", "tags", "description", "done"] @@ -382,60 +407,28 @@ def update_suggestions(self, text=""): fragments = [f for f in re.split("[\s|#]", text)] current_fragment = fragments[-1] if fragments else "" - if not text.strip(): - variants = [templates[name] for name in ("start_time", - "start_delta") if templates[name]] - elif looking_for == "start_time" and text == "-": - if len(current_fragment) > 1: # avoid blank "-" - templates["start_delta"] = ("%s minutes ago" % (-int(current_fragment)), current_fragment) - variants.append(templates["start_delta"]) - - - res = [] - for (description, variant) in variants: - res.append(DataRow(variant, description=description)) - - # regular activity - if (looking_for in ("start_time", "end_time") and not looks_like_time(text.split(" ")[-1])) or \ - looking_for in ("activity", "category"): - search = extract_search(text) + search = extract_search(text) - matches = [] - for match, score in self.suggestions: - if search in match: - if match.startswith(search): - score += 10**8 # boost beginnings - matches.append((match, score)) + matches = [] + for match, score in self.suggestions: + if search in match: + if match.startswith(search): + score += 10**8 # boost beginnings + matches.append((match, score)) - matches = sorted(matches, key=lambda x: x[1], reverse=True)[:7] # need to limit these guys, sorry + # need to limit these guys, sorry + matches = sorted(matches, key=lambda x: x[1], reverse=True)[:7] - 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) - label += " " + match - - res.append(DataRow(markup_label, match, label)) - - if not res: - # in case of nothing to show, add preview so that the user doesn't - # think they are lost + 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") - if fact.activity: - label += " " + fact.activity - if fact.category: - label += "@" + fact.category - - if fact.tags: - label += " #" + " #".join(fact.tags) + markup_label = label + " " + (stuff.escape_pango(match).replace(search, "%s" % search) if search else match) + label += " " + match - res.append(DataRow(stuff.escape_pango(label), description="Start tracking")) + res.append(DataRow(markup_label, match, label)) self.complete_tree.set_rows(res) diff --git a/src/hamster/widgets/dayline.py b/src/hamster/widgets/dayline.py index a70103cff..cc98a8af3 100644 --- a/src/hamster/widgets/dayline.py +++ b/src/hamster/widgets/dayline.py @@ -90,8 +90,7 @@ def __init__(self, start_time = None): graphics.Scene.__init__(self) self.set_can_focus(False) # no interaction - day_start = conf.get("day_start_minutes") - self.day_start = dt.time(day_start / 60, day_start % 60) + self.day_start = conf.day_start start_time = start_time or dt.datetime.now() diff --git a/src/hamster/widgets/reportchooserdialog.py b/src/hamster/widgets/reportchooserdialog.py index 7bcac9f1b..5ff2822f3 100644 --- a/src/hamster/widgets/reportchooserdialog.py +++ b/src/hamster/widgets/reportchooserdialog.py @@ -18,9 +18,6 @@ # along with Project Hamster. If not, see . -import pygtk -pygtk.require('2.0') - import os from gi.repository import GObject as gobject from gi.repository import Gtk as gtk @@ -37,7 +34,7 @@ def __init__(self): gtk.Dialog.__init__(self) - self.dialog = gtk.FileChooserDialog(title = _(u"Save Report — Time Tracker"), + self.dialog = gtk.FileChooserDialog(title = _("Save Report — Time Tracker"), parent = self, action = gtk.FileChooserAction.SAVE, buttons=(gtk.STOCK_CANCEL, diff --git a/src/hamster/widgets/tags.py b/src/hamster/widgets/tags.py index 2b821ac7e..5a6344ae0 100644 --- a/src/hamster/widgets/tags.py +++ b/src/hamster/widgets/tags.py @@ -79,7 +79,7 @@ def refresh_tags(self, event): def get_tags(self): # splits the string by comma and filters out blanks - return [tag.strip() for tag in self.get_text().decode('utf8', 'replace').split(",") if tag.strip()] + return [tag.strip() for tag in self.get_text().split(",") if tag.strip()] def on_tag_selected(self, tag_box, tag): cursor_tag = self.get_cursor_tag() @@ -204,7 +204,7 @@ def get_cursor_tag(self): else: cursor = self.get_position() - text = self.get_text().decode('utf8', 'replace') + text = self.get_text() return text[text.rfind(",", 0, cursor)+1:max(text.find(",", cursor+1)+1, len(text))].strip() diff --git a/src/hamster/widgets/timeinput.py b/src/hamster/widgets/timeinput.py index e689bceec..deefb7e51 100644 --- a/src/hamster/widgets/timeinput.py +++ b/src/hamster/widgets/timeinput.py @@ -103,7 +103,7 @@ def figure_time(self, str_time): # strip everything non-numeric and consider hours to be first number # and minutes - second number numbers = re.split("\D", str_time) - numbers = filter(lambda x: x!="", numbers) + numbers = [x for x in numbers if x!=""] hours, minutes = None, None diff --git a/tests/stuff_test.py b/tests/stuff_test.py index afed71b57..e7f458be1 100644 --- a/tests/stuff_test.py +++ b/tests/stuff_test.py @@ -1,15 +1,38 @@ import sys, os.path # a convoluted line to add hamster module to absolute path -sys.path.insert(0, os.path.realpath(os.path.join(os.path.dirname(__file__), ".."))) +sys.path.insert(0, os.path.realpath(os.path.join(os.path.dirname(__file__), "../src"))) import unittest from hamster.lib import Fact + class TestActivityInputParsing(unittest.TestCase): + def test_datetime_to_hamsterday(self): + import datetime as dt + from hamster.lib import datetime_to_hamsterday + date_time = dt.datetime(2018, 8, 13, 23, 10) # 2018-08-13 23:10 + expected = dt.date(2018, 8, 13) + self.assertEqual(datetime_to_hamsterday(date_time), expected) + date_time = dt.datetime(2018, 8, 14, 0, 10) # 2018-08-14 0:10 + expected = dt.date(2018, 8, 13) + self.assertEqual(datetime_to_hamsterday(date_time), expected) + + def test_hamsterday_time_to_datetime(self): + import datetime as dt + from hamster.lib import hamsterday_time_to_datetime + hamsterday = dt.date(2018, 8, 13) + time = dt.time(23, 10) + expected = dt.datetime(2018, 8, 13, 23, 10) # 2018-08-13 23:10 + self.assertEqual(hamsterday_time_to_datetime(hamsterday, time), expected) + hamsterday = dt.date(2018, 8, 13) + time = dt.time(0, 10) + expected = dt.datetime(2018, 8, 14, 0, 10) # 2018-08-14 00:10 + self.assertEqual(hamsterday_time_to_datetime(hamsterday, time), expected) + def test_plain_name(self): # plain activity name - activity = Fact("just a simple case") - self.assertEquals(activity.activity, "just a simple case") + activity = Fact("just a simple case with ütf-8") + self.assertEqual(activity.activity, "just a simple case with ütf-8") assert activity.category is None assert activity.start_time is None @@ -20,8 +43,8 @@ def test_plain_name(self): def test_with_start_time(self): # with time activity = Fact("12:35 with start time") - self.assertEquals(activity.activity, "with start time") - self.assertEquals(activity.start_time.strftime("%H:%M"), "12:35") + self.assertEqual(activity.activity, "with start time") + self.assertEqual(activity.start_time.strftime("%H:%M"), "12:35") #rest must be empty assert activity.category is None @@ -31,9 +54,9 @@ def test_with_start_time(self): def test_with_start_and_end_time(self): # with time activity = Fact("12:35-14:25 with start-end time") - self.assertEquals(activity.activity, "with start-end time") - self.assertEquals(activity.start_time.strftime("%H:%M"), "12:35") - self.assertEquals(activity.end_time.strftime("%H:%M"), "14:25") + self.assertEqual(activity.activity, "with start-end time") + self.assertEqual(activity.start_time.strftime("%H:%M"), "12:35") + self.assertEqual(activity.end_time.strftime("%H:%M"), "14:25") #rest must be empty assert activity.category is None @@ -41,18 +64,18 @@ def test_with_start_and_end_time(self): def test_category(self): # plain activity name - activity = Fact("just a simple case@hamster") - self.assertEquals(activity.activity, "just a simple case") - self.assertEquals(activity.category, "hamster") + activity = Fact("just a simple case@hämster") + self.assertEqual(activity.activity, "just a simple case") + self.assertEqual(activity.category, "hämster") assert activity.start_time is None assert activity.end_time is None assert activity.description is None def test_description(self): # plain activity name - activity = Fact("case, with added description") - self.assertEquals(activity.activity, "case") - self.assertEquals(activity.description, "with added description") + activity = Fact("case, with added descriptiön") + self.assertEqual(activity.activity, "case") + self.assertEqual(activity.description, "with added descriptiön") assert activity.category is None assert activity.start_time is None assert activity.end_time is None @@ -60,23 +83,33 @@ def test_description(self): def test_tags(self): # plain activity name - activity = Fact("case, with added #de description #and, #some #tags") - self.assertEquals(activity.activity, "case") - self.assertEquals(activity.description, "with added #de description") - self.assertEquals(set(activity.tags), set(["and", "some", "tags"])) + activity = Fact("case, with added #de description #and, #some #tägs") + self.assertEqual(activity.activity, "case") + self.assertEqual(activity.description, "with added #de description") + self.assertEqual(set(activity.tags), set(["and", "some", "tägs"])) assert activity.category is None assert activity.start_time is None assert activity.end_time is None def test_full(self): # plain activity name - activity = Fact("1225-1325 case@cat, description #ta non-tag #tag #bag") - self.assertEquals(activity.start_time.strftime("%H:%M"), "12:25") - self.assertEquals(activity.end_time.strftime("%H:%M"), "13:25") - self.assertEquals(activity.activity, "case") - self.assertEquals(activity.category, "cat") - self.assertEquals(activity.description, "description #ta non-tag") - self.assertEquals(set(activity.tags), set(["bag", "tag"])) + activity = Fact("1225-1325 case@cat, description #ta non-tag #tag #bäg") + self.assertEqual(activity.start_time.strftime("%H:%M"), "12:25") + self.assertEqual(activity.end_time.strftime("%H:%M"), "13:25") + self.assertEqual(activity.activity, "case") + self.assertEqual(activity.category, "cat") + self.assertEqual(activity.description, "description #ta non-tag") + self.assertEqual(set(activity.tags), set(["bäg", "tag"])) + + def test_initial_fact(self): + fact = Fact("12:25-13:25 case@cat, description #tag #bäg") + fact_copy = Fact(initial_fact=fact) + self.assertEqual(fact_copy.start_time.strftime("%H:%M"), "12:25") + self.assertEqual(fact_copy.end_time.strftime("%H:%M"), "13:25") + self.assertEqual(fact_copy.activity, "case") + self.assertEqual(fact_copy.category, "cat") + self.assertEqual(fact_copy.description, "description") + self.assertEqual(set(fact_copy.tags), set(["bäg", "tag"])) if __name__ == '__main__': unittest.main()