Commit c0926c4b authored by Daniel Garcia Moreno's avatar Daniel Garcia Moreno

Merge branch 'export' into 'master'

Export activities time (TXT/CSV)

This MR adds an export button and dialog to the reports UI. Reports can be simple (just the activity name and time) or detailed (everything except the IDs). They can also be in TXT or CSV format. I'm leaving out PDF for a future MR.

closes #2

See merge request !9
parents 7813be23 4fed637b
Pipeline #70094 passed with stage
in 49 seconds
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkBox" id="export-box">
<property name="width_request">500</property>
<property name="height_request">350</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">75</property>
<property name="margin_right">75</property>
<property name="margin_top">18</property>
<property name="margin_bottom">18</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">6</property>
<property name="margin_right">6</property>
<property name="label" translatable="yes">Date Range</property>
<property name="xalign">0</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkListBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_bottom">30</property>
<child>
<object class="GtkListBoxRow">
<property name="width_request">100</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="activatable">False</property>
<property name="selectable">False</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">8</property>
<property name="margin_right">8</property>
<property name="margin_top">8</property>
<property name="margin_bottom">8</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Start Date</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkMenuButton" id="export-start">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="popover">export-startdate-popover</property>
<child>
<object class="GtkLabel" id="startdate_button_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Click to select</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkListBoxRow">
<property name="width_request">100</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="activatable">False</property>
<property name="selectable">False</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">8</property>
<property name="margin_right">8</property>
<property name="margin_top">8</property>
<property name="margin_bottom">8</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">End Date</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkMenuButton" id="export-end">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="popover">export-enddate-popover</property>
<child>
<object class="GtkLabel" id="enddate_button_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Click to select</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">6</property>
<property name="margin_right">6</property>
<property name="label" translatable="yes">Settings</property>
<property name="xalign">0</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkListBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkListBoxRow">
<property name="width_request">100</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="activatable">False</property>
<property name="selectable">False</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">8</property>
<property name="margin_right">8</property>
<property name="margin_top">8</property>
<property name="margin_bottom">8</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Detailed</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkSwitch" id="export-detailed">
<property name="visible">True</property>
<property name="can_focus">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkListBoxRow">
<property name="width_request">100</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="activatable">False</property>
<property name="selectable">False</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">8</property>
<property name="margin_right">8</property>
<property name="margin_top">8</property>
<property name="margin_bottom">8</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">File Type</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkComboBoxText" id="export-filetype">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="active">0</property>
<items>
<item id="TXT" translatable="yes">TXT</item>
<item id="CSV" translatable="yes">CSV</item>
</items>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object>
<object class="GtkPopover" id="export-enddate-popover">
<property name="can_focus">False</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">12</property>
<property name="margin_bottom">12</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkCalendar" id="export-end-calendar">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="year">2019</property>
<property name="month">1</property>
<property name="day">22</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
<object class="GtkPopover" id="export-startdate-popover">
<property name="can_focus">False</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">12</property>
<property name="margin_bottom">12</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkCalendar" id="export-start-calendar">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="year">2019</property>
<property name="month">1</property>
<property name="day">22</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
</interface>
......@@ -2,6 +2,11 @@
<!-- Generated with glade 3.22.1 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<requires lib="libhandy" version="0.0"/>
<object class="GtkMenu">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<object class="GtkPopover" id="calendar-popover">
<property name="can_focus">False</property>
<child>
......@@ -84,9 +89,9 @@
<child>
<object class="HdyColumn">
<property name="visible">True</property>
<property name="expand">True</property>
<property name="linear_growth_width">400</property>
<property name="can_focus">False</property>
<property name="maximum_width">800</property>
<property name="linear_growth_width">400</property>
<child>
<object class="GtkListBox" id="activity-list">
<property name="width_request">300</property>
......@@ -234,9 +239,9 @@
<child>
<object class="HdyColumn">
<property name="visible">True</property>
<property name="expand">True</property>
<property name="linear_growth_width">400</property>
<property name="can_focus">False</property>
<property name="maximum_width">800</property>
<property name="linear_growth_width">400</property>
<child>
<object class="GtkListBox" id="report-day-list">
<property name="width_request">300</property>
......@@ -281,9 +286,9 @@
<child>
<object class="HdyColumn">
<property name="visible">True</property>
<property name="expand">True</property>
<property name="linear_growth_width">400</property>
<property name="can_focus">False</property>
<property name="maximum_width">800</property>
<property name="linear_growth_width">400</property>
<child>
<object class="GtkListBox" id="report-week-list">
<property name="width_request">300</property>
......@@ -329,9 +334,9 @@
<child>
<object class="HdyColumn">
<property name="visible">True</property>
<property name="expand">True</property>
<property name="linear_growth_width">400</property>
<property name="can_focus">False</property>
<property name="maximum_width">800</property>
<property name="linear_growth_width">400</property>
<child>
<object class="GtkListBox" id="report-month-list">
<property name="width_request">300</property>
......@@ -443,6 +448,27 @@
<property name="stack">report-stack</property>
</object>
</child>
<child>
<object class="GtkButton" id="report-export">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Export actvity report data</property>
<property name="action_name">app.report-export</property>
<property name="image_position">bottom</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">document-save-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
<object class="GtkAdjustment" id="current-hour-adj">
<property name="upper">24</property>
......@@ -539,7 +565,6 @@
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkFrame">
<property name="visible">True</property>
......@@ -551,9 +576,6 @@
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<style>
<class name="comments-frame"/>
</style>
<child>
<object class="GtkTextView" id="current-comments">
<property name="visible">True</property>
......@@ -569,6 +591,9 @@
</style>
</object>
</child>
<style>
<class name="comments-frame"/>
</style>
</object>
</child>
<child type="label">
......@@ -585,8 +610,6 @@
<property name="position">4</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="current-timer">
<property name="visible">True</property>
......@@ -612,7 +635,6 @@
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="margin">12</property>
<child>
<object class="GtkModelButton" id="menubutton_popover_report_button">
<property name="visible">True</property>
......
......@@ -6,5 +6,6 @@
<file compressed="true" preprocess="xml-stripblanks">main_window.ui</file>
<file compressed="true" preprocess="xml-stripblanks">shortcuts_overview.ui</file>
<file compressed="true" preprocess="xml-stripblanks">edit_dialog.ui</file>
<file compressed="true" preprocess="xml-stripblanks">export_dialog.ui</file>
</gresource>
</gresources>
......@@ -145,10 +145,14 @@ class DB:
yield Activity(id=row[0], name=row[1], start=row[2], stop=row[3],
comments=row[4])
def report(self, start=None, end=None, activity=None):
def report(self, start=None, end=None, activity=None, detailed=False):
c = self.connection.cursor()
query = 'SELECT activity, Sum(seconds) FROM activities'
if detailed:
query = 'SELECT * FROM activities'
else:
query = 'SELECT activity, Sum(seconds) FROM activities'
params = []
where_clause = []
if activity or start or end:
......@@ -164,7 +168,9 @@ class DB:
where_clause += ['start_date <= ?']
params += [end]
query += ' AND '.join(where_clause)
query += ' GROUP BY activity'
if not detailed:
query += ' GROUP BY activity'
for row in c.execute(query, params):
yield row
......
from gettext import gettext as _
from gi.repository import Gtk, Gdk
import csv
from datetime import datetime, timedelta
class ExportDialog(Gtk.Dialog):
db = None
main_window = None
detailed_switch = NotImplemented
file_type_menu = NotImplemented
start_date_button = NotImplemented
end_date_button = NotImplemented
start_date_calendar = NotImplemented
end_date_calendar = NotImplemented
start_date_label = NotImplemented
end_date_label = NotImplemented
file_type = None
start_date = None
end_date = None
detailed = False
patterns = {
'TXT': '.txt',
'CSV': '.csv',
}
def __init__(self, db, main_window, *args, **kwargs):
self.db = db
self.main_window = main_window
super().__init__(*args, title='Export Activity Report',
use_header_bar=True, **kwargs)
builder = Gtk.Builder()
builder.add_from_resource("/net/danigm/timetrack/export_dialog.ui")
self.detailed_switch = builder.get_object("export-detailed")
self.file_type_menu = builder.get_object("export-filetype")
self.start_date_row = builder.get_object("export-start")
self.end_date_row = builder.get_object("export-end")
self.start_date_label = builder.get_object("startdate_button_label")
self.end_date_label = builder.get_object("enddate_button_label")
self.start_date_calendar = builder.get_object("export-start-calendar")
self.end_date_calendar = builder.get_object("export-end-calendar")
self.start_date_calendar.connect("day-selected",
self.start_date_changed)
self.end_date_calendar.connect("day-selected",
self.end_date_changed)
# Set the start date to yesterday by default
yesterday = datetime.today() - timedelta(days=1)
self.start_date_calendar.select_month(yesterday.month - 1,
yesterday.year)
self.start_date_calendar.select_day(yesterday.day)
# Set the end date to tomorrow by default
tomorrow = datetime.today() + timedelta(days=1)
self.end_date_calendar.select_month(tomorrow.month - 1, tomorrow.year)
self.end_date_calendar.select_day(tomorrow.day)
self.set_deletable(False)
self.set_transient_for(main_window)
self.set_attached_to(main_window)
self.set_destroy_with_parent(True)
self.set_type_hint(Gdk.WindowTypeHint.DIALOG)
self.set_modal(True)
box = builder.get_object("export-box")
box.show_all()
self.get_content_area().add(box)
self.show_all()
header = self.get_header_bar()
cancel = Gtk.Button(label=_("Cancel"))
cancel.connect("clicked", lambda x: self.response(0))
header.pack_start(cancel)
export = Gtk.Button(label=_("Export"))
export.connect("clicked", self.open_file_dialog)
export.get_style_context().add_class('suggested-action')
header.pack_end(export)
header.show_all()
def set_default_dates(self, start, end):
self.start_date_calendar.select_month(start.month - 1, start.year)
self.start_date_calendar.select_day(start.day)
self.end_date_calendar.select_month(end.month - 1, end.year)
self.end_date_calendar.select_day(end.day)
def start_date_changed(self, *args, **kwargs):
selected_date = self.start_date_calendar.get_date()
date = datetime(selected_date.year,
selected_date.month + 1,
selected_date.day)
text = date.strftime("%d/%m/%Y")
self.start_date_label.set_text(text)
self.start_date = date
def end_date_changed(self, *args, **kwargs):
selected_date = self.end_date_calendar.get_date()
date = datetime(selected_date.year,
selected_date.month + 1,
selected_date.day)
text = date.strftime("%d/%m/%Y")
self.end_date_label.set_text(text)
self.end_date = date
def open(self):
self.run()
self.close()
def open_file_dialog(self, _):
save_dialog = Gtk.FileChooserDialog(title="Export To",
parent=self,
action=Gtk.FileChooserAction.SAVE,
buttons=(Gtk.STOCK_CANCEL,
Gtk.ResponseType.CANCEL,
Gtk.STOCK_SAVE,
Gtk.ResponseType.ACCEPT))
save_dialog.set_do_overwrite_confirmation(True)
save_dialog.set_modal(True)
save_dialog.connect("response", self.file_dialog_callback)
self.file_type = self.file_type_menu.get_active_text()
self.detailed = self.detailed_switch.get_state()
file_types = Gtk.FileFilter()
file_types.add_pattern('*' + self.patterns[self.file_type])
save_dialog.add_filter(file_types)
save_dialog.show()
def file_dialog_callback(self, save_dialog, response_id):
if response_id == Gtk.ResponseType.ACCEPT: