openall_time_applet 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.1
1
+ 0.0.2
@@ -1,4 +1,3 @@
1
- require "socket"
2
1
  require "json"
3
2
 
4
3
  #This class handels various operations with the Openall-installation. It uses HTTP and JSON.
@@ -20,23 +19,58 @@ class Openall_time_applet::Connection
20
19
  def login
21
20
  #For some weird reason OpenAll seems to only accept multipart-post-requests??
22
21
  @http.post_multipart("index.php?c=Auth&m=validateLogin", {"username" => @args[:username], "password" => @args[:password]})
23
- @http.reconnect
24
22
 
25
23
  #Verify login by reading dashboard HTML.
24
+ @http.reconnect
26
25
  res = @http.get("index.php?c=Dashboard")
27
26
  raise _("Could not log in.") if !res.body.match(/<ul id="webticker" >/)
28
27
  end
29
28
 
30
29
  def request(args)
31
- args = {:url => args} if args.is_a?(String)
32
- res = @http.get(args[:url])
33
- parsed = JSON.parse(res.body)
30
+ @http.reconnect
31
+
32
+ #Possible to give a string instead of hash to do it simple.
33
+ args = {:url => "?c=Jsonapi&m=#{args}"} if args.is_a?(String) or args.is_a?(Symbol)
34
+ args[:url] = "?c=Jsonapi&m=#{args[:method]}" if args[:method] and !args[:url]
35
+
36
+ #Send request to OpenAll via HTTP.
37
+ if args[:post]
38
+ res = @http.post_multipart(args[:url], args[:post])
39
+ else
40
+ res = @http.get(args[:url])
41
+ end
42
+
43
+ raise _("Empty body returned from OpenAll.") if res.body.to_s.strip.length <= 0
44
+
45
+ #Parse result as JSON.
46
+ begin
47
+ parsed = JSON.parse(res.body)
48
+ rescue
49
+ raise sprintf(_("Could not parse JSON from: %s"), res.body)
50
+ end
51
+
52
+ #An error occurred in OpenAll. Make it look like an error here as well.
53
+ if parsed.is_a?(Hash) and parsed["type"] == "error"
54
+ #Hack the backtrace to include code-lines from PHP.
55
+ begin
56
+ raise "(PHP-#{parsed["class"]}) #{parsed["msg"]}"
57
+ rescue => e
58
+ newbt = parsed["bt"]
59
+ e.backtrace.each do |bt|
60
+ newbt << bt
61
+ end
62
+
63
+ e.set_backtrace(newbt)
64
+
65
+ raise e
66
+ end
67
+ end
34
68
 
35
69
  return parsed
36
70
  end
37
71
 
38
72
  def task_list
39
- return self.request("index.php?c=Jsonapi&m=task_list")
73
+ return self.request("getAllTasksForUser")
40
74
  end
41
75
 
42
76
  def destroy
data/conf/db_schema.rb CHANGED
@@ -7,10 +7,21 @@ Openall_time_applet::DB_SCHEMA = {
7
7
  {"name" => "value", "type" => "text"}
8
8
  ]
9
9
  },
10
+ "Task" => {
11
+ "columns" => [
12
+ {"name" => "id", "type" => "int", "autoincr" => true, "primarykey" => true},
13
+ {"name" => "openall_uid", "type" => "int"},
14
+ {"name" => "title", "type" => "varchar"}
15
+ ],
16
+ "indexes" => [
17
+ "openall_uid"
18
+ ]
19
+ },
10
20
  "Timelog" => {
11
21
  "columns" => [
12
22
  {"name" => "id", "type" => "int", "autoincr" => true, "primarykey" => true},
13
23
  {"name" => "openall_uid", "type" => "int"},
24
+ {"name" => "task_id", "type" => "int"},
14
25
  {"name" => "time", "type" => "int"},
15
26
  {"name" => "time_transport", "type" => "int"},
16
27
  {"name" => "descr", "type" => "text"},
@@ -18,7 +29,19 @@ Openall_time_applet::DB_SCHEMA = {
18
29
  {"name" => "sync_last", "type" => "datetime"}
19
30
  ],
20
31
  "indexes" => [
21
- "openall_uid"
32
+ "openall_uid",
33
+ "task_id"
34
+ ]
35
+ },
36
+ "Worktime" => {
37
+ "columns" => [
38
+ {"name" => "id", "type" => "int", "autoincr" => true, "primarykey" => true},
39
+ {"name" => "openall_uid", "type" => "int"},
40
+ {"name" => "task_id", "type" => "int"},
41
+ {"name" => "timestamp", "type" => "datetime"},
42
+ {"name" => "worktime", "type" => "int"},
43
+ {"name" => "transporttime", "type" => "int"},
44
+ {"name" => "comment", "type" => "text"}
22
45
  ]
23
46
  }
24
47
  }
@@ -8,6 +8,7 @@
8
8
  <property name="window_position">center</property>
9
9
  <property name="default_width">640</property>
10
10
  <property name="default_height">480</property>
11
+ <signal name="destroy" handler="on_window_destroy" swapped="no"/>
11
12
  <child>
12
13
  <object class="GtkVBox" id="vbox1">
13
14
  <property name="visible">True</property>
@@ -6,8 +6,7 @@
6
6
  <property name="can_focus">False</property>
7
7
  <property name="title" translatable="yes">Preferences</property>
8
8
  <property name="window_position">center</property>
9
- <property name="default_width">640</property>
10
- <property name="default_height">480</property>
9
+ <property name="default_width">400</property>
11
10
  <child>
12
11
  <object class="GtkVBox" id="vbox1">
13
12
  <property name="visible">True</property>
@@ -6,8 +6,7 @@
6
6
  <property name="can_focus">False</property>
7
7
  <property name="title" translatable="yes">Timelog</property>
8
8
  <property name="window_position">center</property>
9
- <property name="default_width">640</property>
10
- <property name="default_height">480</property>
9
+ <property name="default_width">360</property>
11
10
  <child>
12
11
  <object class="GtkVBox" id="vbox1">
13
12
  <property name="visible">True</property>
@@ -27,7 +26,7 @@
27
26
  <object class="GtkTable" id="table1">
28
27
  <property name="visible">True</property>
29
28
  <property name="can_focus">False</property>
30
- <property name="n_rows">3</property>
29
+ <property name="n_rows">6</property>
31
30
  <property name="n_columns">2</property>
32
31
  <property name="column_spacing">3</property>
33
32
  <property name="row_spacing">3</property>
@@ -120,6 +119,64 @@
120
119
  <property name="bottom_attach">3</property>
121
120
  </packing>
122
121
  </child>
122
+ <child>
123
+ <object class="GtkLabel" id="label6">
124
+ <property name="visible">True</property>
125
+ <property name="can_focus">False</property>
126
+ <property name="xalign">0</property>
127
+ <property name="label" translatable="yes">Task</property>
128
+ </object>
129
+ <packing>
130
+ <property name="top_attach">3</property>
131
+ <property name="bottom_attach">4</property>
132
+ <property name="x_options">GTK_FILL</property>
133
+ <property name="y_options">GTK_FILL</property>
134
+ </packing>
135
+ </child>
136
+ <child>
137
+ <object class="GtkComboBox" id="cbTask">
138
+ <property name="visible">True</property>
139
+ <property name="can_focus">False</property>
140
+ </object>
141
+ <packing>
142
+ <property name="left_attach">1</property>
143
+ <property name="right_attach">2</property>
144
+ <property name="top_attach">3</property>
145
+ <property name="bottom_attach">4</property>
146
+ </packing>
147
+ </child>
148
+ <child>
149
+ <object class="GtkCheckButton" id="cbShouldSync">
150
+ <property name="label" translatable="yes">Should sync</property>
151
+ <property name="visible">True</property>
152
+ <property name="can_focus">True</property>
153
+ <property name="receives_default">False</property>
154
+ <property name="use_action_appearance">False</property>
155
+ <property name="draw_indicator">True</property>
156
+ </object>
157
+ <packing>
158
+ <property name="right_attach">2</property>
159
+ <property name="top_attach">4</property>
160
+ <property name="bottom_attach">5</property>
161
+ <property name="x_options">GTK_FILL</property>
162
+ <property name="y_options">GTK_FILL</property>
163
+ </packing>
164
+ </child>
165
+ <child>
166
+ <object class="GtkCheckButton" id="cbStartTracking">
167
+ <property name="label" translatable="yes">Start tracking after saving</property>
168
+ <property name="visible">True</property>
169
+ <property name="can_focus">True</property>
170
+ <property name="receives_default">False</property>
171
+ <property name="use_action_appearance">False</property>
172
+ <property name="draw_indicator">True</property>
173
+ </object>
174
+ <packing>
175
+ <property name="right_attach">2</property>
176
+ <property name="top_attach">5</property>
177
+ <property name="bottom_attach">6</property>
178
+ </packing>
179
+ </child>
123
180
  </object>
124
181
  </child>
125
182
  </object>
@@ -145,6 +202,22 @@
145
202
  <property name="can_focus">False</property>
146
203
  <property name="spacing">3</property>
147
204
  <property name="layout_style">end</property>
205
+ <child>
206
+ <object class="GtkButton" id="btnRemove">
207
+ <property name="label">gtk-remove</property>
208
+ <property name="visible">True</property>
209
+ <property name="can_focus">True</property>
210
+ <property name="receives_default">True</property>
211
+ <property name="use_action_appearance">False</property>
212
+ <property name="use_stock">True</property>
213
+ <signal name="clicked" handler="on_btnRemove_clicked" swapped="no"/>
214
+ </object>
215
+ <packing>
216
+ <property name="expand">False</property>
217
+ <property name="fill">False</property>
218
+ <property name="position">0</property>
219
+ </packing>
220
+ </child>
148
221
  <child>
149
222
  <object class="GtkButton" id="btnSave">
150
223
  <property name="label">gtk-save</property>
@@ -158,7 +231,7 @@
158
231
  <packing>
159
232
  <property name="expand">False</property>
160
233
  <property name="fill">False</property>
161
- <property name="position">0</property>
234
+ <property name="position">1</property>
162
235
  </packing>
163
236
  </child>
164
237
  </object>
@@ -0,0 +1,110 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <interface>
3
+ <requires lib="gtk+" version="2.24"/>
4
+ <!-- interface-naming-policy project-wide -->
5
+ <object class="GtkWindow" id="window">
6
+ <property name="can_focus">False</property>
7
+ <property name="title" translatable="yes">Week status</property>
8
+ <property name="window_position">center</property>
9
+ <property name="default_width">440</property>
10
+ <child>
11
+ <object class="GtkVBox" id="vbox1">
12
+ <property name="visible">True</property>
13
+ <property name="can_focus">False</property>
14
+ <child>
15
+ <object class="GtkHBox" id="hbox1">
16
+ <property name="visible">True</property>
17
+ <property name="can_focus">False</property>
18
+ <child>
19
+ <object class="GtkHButtonBox" id="hbuttonbox1">
20
+ <property name="visible">True</property>
21
+ <property name="can_focus">False</property>
22
+ <property name="layout_style">start</property>
23
+ <child>
24
+ <object class="GtkButton" id="btnPrevious">
25
+ <property name="label">gtk-media-previous</property>
26
+ <property name="visible">True</property>
27
+ <property name="can_focus">True</property>
28
+ <property name="receives_default">True</property>
29
+ <property name="use_action_appearance">False</property>
30
+ <property name="use_stock">True</property>
31
+ <signal name="clicked" handler="on_btnPrevious_clicked" swapped="no"/>
32
+ </object>
33
+ <packing>
34
+ <property name="expand">False</property>
35
+ <property name="fill">False</property>
36
+ <property name="position">0</property>
37
+ </packing>
38
+ </child>
39
+ </object>
40
+ <packing>
41
+ <property name="expand">True</property>
42
+ <property name="fill">True</property>
43
+ <property name="position">0</property>
44
+ </packing>
45
+ </child>
46
+ <child>
47
+ <object class="GtkLabel" id="label2">
48
+ <property name="visible">True</property>
49
+ <property name="can_focus">False</property>
50
+ <property name="label" translatable="yes">Week</property>
51
+ </object>
52
+ <packing>
53
+ <property name="expand">True</property>
54
+ <property name="fill">True</property>
55
+ <property name="position">1</property>
56
+ </packing>
57
+ </child>
58
+ <child>
59
+ <object class="GtkHButtonBox" id="hbuttonbox2">
60
+ <property name="visible">True</property>
61
+ <property name="can_focus">False</property>
62
+ <property name="layout_style">end</property>
63
+ <child>
64
+ <object class="GtkButton" id="btnNext">
65
+ <property name="label">gtk-media-next</property>
66
+ <property name="visible">True</property>
67
+ <property name="can_focus">True</property>
68
+ <property name="receives_default">True</property>
69
+ <property name="use_action_appearance">False</property>
70
+ <property name="use_stock">True</property>
71
+ <signal name="clicked" handler="on_btnNext_clicked" swapped="no"/>
72
+ </object>
73
+ <packing>
74
+ <property name="expand">False</property>
75
+ <property name="fill">False</property>
76
+ <property name="position">0</property>
77
+ </packing>
78
+ </child>
79
+ </object>
80
+ <packing>
81
+ <property name="expand">True</property>
82
+ <property name="fill">True</property>
83
+ <property name="position">2</property>
84
+ </packing>
85
+ </child>
86
+ </object>
87
+ <packing>
88
+ <property name="expand">False</property>
89
+ <property name="fill">True</property>
90
+ <property name="position">0</property>
91
+ </packing>
92
+ </child>
93
+ <child>
94
+ <object class="GtkHBox" id="boxContent">
95
+ <property name="visible">True</property>
96
+ <property name="can_focus">False</property>
97
+ <child>
98
+ <placeholder/>
99
+ </child>
100
+ </object>
101
+ <packing>
102
+ <property name="expand">True</property>
103
+ <property name="fill">True</property>
104
+ <property name="position">1</property>
105
+ </packing>
106
+ </child>
107
+ </object>
108
+ </child>
109
+ </object>
110
+ </interface>
data/gui/trayicon.rb CHANGED
@@ -7,7 +7,9 @@ class Openall_time_applet::Gui::Trayicon
7
7
  @ti = Gtk::StatusIcon.new
8
8
  @ti.file = "../gfx/icon_time.png"
9
9
  @ti.signal_connect("popup-menu", &self.method(:on_statusicon_rightclick))
10
-
10
+ end
11
+
12
+ def on_statusicon_rightclick(tray, button, time)
11
13
  #Build rightclick-menu for tray-icon.
12
14
  timelog_new = Gtk::ImageMenuItem.new(Gtk::Stock::NEW)
13
15
  timelog_new.label = _("New timelog")
@@ -17,24 +19,63 @@ class Openall_time_applet::Gui::Trayicon
17
19
  overview.label = _("Overview")
18
20
  overview.signal_connect("activate", &self.method(:on_overview_activate))
19
21
 
22
+ worktime_overview = Gtk::ImageMenuItem.new(Gtk::Stock::HOME)
23
+ worktime_overview.label = _("Time overview")
24
+ worktime_overview.signal_connect("activate", &self.method(:on_worktimeOverview_activate))
25
+
20
26
  pref = Gtk::ImageMenuItem.new(Gtk::Stock::PREFERENCES)
21
27
  pref.signal_connect("activate", &self.method(:on_preferences_activate))
22
28
 
23
29
  quit = Gtk::ImageMenuItem.new(Gtk::Stock::QUIT)
24
30
  quit.signal_connect("activate", &self.method(:on_quit_activate))
25
31
 
26
- @menu = Gtk::Menu.new
27
- @menu.append(timelog_new)
28
- @menu.append(overview)
29
- @menu.append(Gtk::SeparatorMenuItem.new)
30
- @menu.append(pref)
31
- @menu.append(Gtk::SeparatorMenuItem.new)
32
- @menu.append(quit)
33
- @menu.show_all
34
- end
35
-
36
- def on_statusicon_rightclick(tray, button, time)
37
- @menu.popup(nil, nil, button, time)
32
+ sync = Gtk::ImageMenuItem.new(Gtk::Stock::HARDDISK)
33
+ sync.label = _("Synchronize with OpenAll")
34
+ sync.signal_connect("activate", &self.method(:on_sync_activate))
35
+
36
+ menu = Gtk::Menu.new
37
+ menu.append(timelog_new)
38
+ menu.append(overview)
39
+ menu.append(worktime_overview)
40
+ menu.append(Gtk::SeparatorMenuItem.new)
41
+ menu.append(pref)
42
+ menu.append(Gtk::SeparatorMenuItem.new)
43
+
44
+ #Make a list of all timelogs in the menu.
45
+ @args[:oata].ob.list(:Timelog, {"orderby" => "id"}) do |timelog|
46
+ label = sprintf(_("Track: %s"), timelog.descr_short)
47
+
48
+ #If this is the active timelog, make the label bold, by getting the label-child and using HTML-markup on it.
49
+ if @args[:oata].timelog_active and @args[:oata].timelog_active.id == timelog.id
50
+ mi = Gtk::ImageMenuItem.new(Gtk::Stock::MEDIA_RECORD)
51
+ mi.children[0].markup = "<b>#{label}</b>"
52
+ mi.signal_connect("activate", &self.method(:on_stopTracking_activate))
53
+ else
54
+ mi = Gtk::MenuItem.new(label)
55
+ #Change the active timelog, when the timelog is clicked.
56
+ mi.signal_connect("activate") do
57
+ @args[:oata].timelog_active = timelog
58
+ end
59
+ end
60
+
61
+ menu.append(mi)
62
+ end
63
+
64
+ if @args[:oata].timelog_active
65
+ menu.append(Gtk::SeparatorMenuItem.new)
66
+
67
+ #If tracking is active, then show how many seconds has been tracked until now in menu as an item.
68
+ secs = Time.now.to_i - @args[:oata].timelog_active_time.to_i
69
+ label = Gtk::MenuItem.new(sprintf(_("%s seconds"), secs))
70
+ menu.append(label)
71
+ end
72
+
73
+ menu.append(Gtk::SeparatorMenuItem.new)
74
+ menu.append(sync)
75
+ menu.append(quit)
76
+ menu.show_all
77
+
78
+ menu.popup(nil, nil, button, time)
38
79
  end
39
80
 
40
81
  def on_preferences_activate(*args)
@@ -49,7 +90,19 @@ class Openall_time_applet::Gui::Trayicon
49
90
  @args[:oata].show_overview
50
91
  end
51
92
 
93
+ def on_worktimeOverview_activate(*args)
94
+ @args[:oata].show_worktime_overview
95
+ end
96
+
52
97
  def on_quit_activate(*args)
53
- Gtk.main_quit
98
+ @args[:oata].destroy
99
+ end
100
+
101
+ def on_sync_activate(*args)
102
+ @args[:oata].sync
103
+ end
104
+
105
+ def on_stopTracking_activate(*args)
106
+ @args[:oata].timelog_stop_tracking
54
107
  end
55
108
  end
data/gui/win_overview.rb CHANGED
@@ -8,21 +8,27 @@ class Openall_time_applet::Gui::Win_overview
8
8
  @gui.translate
9
9
  @gui.connect_signals{|h| method(h)}
10
10
 
11
- @gui["tvTimelogs"].init([_("ID"), _("Description"), _("Time"), _("Transport"), _("Needs sync")])
11
+ @gui["tvTimelogs"].init([_("ID"), _("Description"), _("Time"), _("Transport"), _("Needs sync"), _("Task")])
12
12
  @gui["tvTimelogs"].columns[0].visible = false
13
13
  self.reload_timelogs
14
14
 
15
+ #Reload the treeview if something happened to a timelog.
16
+ @reload_id = @args[:oata].ob.connect("object" => :Timelog, "signals" => ["add", "update", "delete"], &self.method(:reload_timelogs))
17
+
15
18
  @gui["window"].show_all
16
19
  end
17
20
 
18
21
  def reload_timelogs
22
+ @gui["tvTimelogs"].model.clear
19
23
  @args[:oata].ob.list(:Timelog, {"orderby" => "id"}) do |timelog|
20
- descr = timelog[:descr].to_s.gsub("\n", " ").gsub(/\s{2,}/, " ")
21
- descr = Knj::Strings.shorten(descr, 20)
22
-
23
- sync_need = Knj::Strings.yn_str(timelog[:sync_need], _("Yes"), _("No"))
24
-
25
- @gui["tvTimelogs"].append([timelog.id, descr, timelog[:time], timelog[:time_transport], sync_need])
24
+ @gui["tvTimelogs"].append([
25
+ timelog.id,
26
+ timelog.descr_short,
27
+ timelog.time_as_human,
28
+ timelog.time_transport_as_human,
29
+ Knj::Strings.yn_str(timelog[:sync_need], _("Yes"), _("No")),
30
+ timelog.task_name
31
+ ])
26
32
  end
27
33
  end
28
34
 
@@ -35,4 +41,9 @@ class Openall_time_applet::Gui::Win_overview
35
41
  win_timelog_edit.gui["window"].modal = @gui["window"]
36
42
  win_timelog_edit.gui["window"].transient_for = @gui["window"]
37
43
  end
44
+
45
+ def on_window_destroy
46
+ #Unconnect reload-event. Else it will crash on call to destroyed object. Also frees up various ressources.
47
+ @args[:oata].ob.unconnect("object" => :Timelog, "conn_id" => @reload_id)
48
+ end
38
49
  end
@@ -13,6 +13,7 @@ class Openall_time_applet::Gui::Win_preferences
13
13
  @gui["window"].show_all
14
14
  end
15
15
 
16
+ #Loads values from database into widgets.
16
17
  def load_values
17
18
  @gui["txtHost"].text = Knj::Opts.get("openall_host")
18
19
  @gui["txtPort"].text = Knj::Opts.get("openall_port")
@@ -26,6 +27,7 @@ class Openall_time_applet::Gui::Win_preferences
26
27
  end
27
28
  end
28
29
 
30
+ #Saves values from widgets into database.
29
31
  def on_btnSave_clicked
30
32
  Knj::Opts.set("openall_host", @gui["txtHost"].text)
31
33
  Knj::Opts.set("openall_port", @gui["txtPort"].text)
@@ -35,12 +37,15 @@ class Openall_time_applet::Gui::Win_preferences
35
37
  @gui["window"].destroy
36
38
  end
37
39
 
40
+ #Tries to connect to OpenAll with the given information and receive a task-list as well to validate information and connectivity.
38
41
  def on_btnTest_clicked
39
42
  ws = Knj::Gtk2::StatusWindow.new("transient_for" => @gui["window"])
40
43
  ws.label = _("Connecting and logging in...")
41
44
 
45
+ #Do the stuff in thread so GUI wont lock.
42
46
  Knj::Thread.new do
43
47
  begin
48
+ #Connect to OpenAll, log in, get a list of tasks to test the information and connection.
44
49
  @args[:oata].oa_conn do |conn|
45
50
  ws.percent = 0.3
46
51
  ws.label = _("Getting task-list.")
@@ -50,12 +55,19 @@ class Openall_time_applet::Gui::Win_preferences
50
55
  ws.percent = 1
51
56
  end
52
57
  rescue => e
53
- raise e
54
- Knj::Gtk2.msgbox(e.message, "warning", _("Error"))
58
+ #Show error for user if error occurrs.
59
+ Knj::Gtk2.msgbox(
60
+ "msg" => Knj::Errors.error_str(e),
61
+ "type" => "warning",
62
+ "title" => _("Error"),
63
+ "run" => false,
64
+ "transient_for" => @gui["window"]
65
+ )
55
66
  ensure
67
+ #Be sure that the status-window will be closed.
56
68
  Knj::Thread.new do
57
69
  sleep 1.5
58
- ws.destroy
70
+ ws.destroy if ws
59
71
  end
60
72
  end
61
73
  end
@@ -8,22 +8,88 @@ class Openall_time_applet::Gui::Win_timelog_edit
8
8
  @gui.translate
9
9
  @gui.connect_signals{|h| method(h)}
10
10
 
11
+ tasks_opts = [_("None")] + @args[:oata].ob.list(:Task, {"orderby" => "openall_uid"})
12
+ @gui["cbTask"].init(tasks_opts)
13
+
14
+ #We are editting a timelog - set widget-values.
15
+ @timelog = @args[:timelog]
16
+
17
+ if @timelog
18
+ @gui["txtDescr"].text = @timelog[:descr]
19
+ @gui["txtTime"].text = @timelog.time_as_human
20
+ @gui["txtTimeTransport"].text = @timelog.time_transport_as_human
21
+ @gui["cbTask"].sel = @timelog.task if @timelog.task
22
+ @gui["cbShouldSync"].active = Knj::Strings.yn_str(@timelog[:sync_need], true, false)
23
+ else
24
+ @gui["btnRemove"].visible = false
25
+ end
26
+
27
+ #Show the window.
11
28
  @gui["window"].show_all
12
29
  end
13
30
 
14
31
  def on_btnSave_clicked(*args)
32
+ #Generate task-ID based on widget-value.
33
+ task = @gui["cbTask"].sel
34
+ if task
35
+ task_id = task.id
36
+ else
37
+ task_id = 0
38
+ end
39
+
40
+ #Get times as integers based on widget-values.
41
+ if @gui["txtTime"].text == ""
42
+ time_secs = 0
43
+ else
44
+ begin
45
+ time_secs = Knj::Strings.human_time_str_to_secs(@gui["txtTime"].text)
46
+ rescue => e
47
+ Knj::Gtk2.msgbox(_("You have entered an invalid time-format.") + "\n\n" + Knj::Errors.error_str(e))
48
+ return nil
49
+ end
50
+ end
51
+
52
+ if @gui["txtTimeTransport"].text == ""
53
+ time_transport_secs = 0
54
+ else
55
+ begin
56
+ time_transport_secs = Knj::Strings.human_time_str_to_secs(@gui["txtTimeTransport"].text)
57
+ rescue => e
58
+ Knj::Gtk2.msgbox(_("You have entered an invalid transport-time-format.") + "\n\n" + Knj::Errors.error_str(e))
59
+ return nil
60
+ end
61
+ end
62
+
63
+ #Generate hash for updating dataabase.
15
64
  save_hash = {
16
65
  :descr => @gui["txtDescr"].text,
17
- :time => @gui["txtTime"].text,
18
- :time_transport => @gui["txtTimeTransport"].text
66
+ :time => time_secs,
67
+ :time_transport => time_transport_secs,
68
+ :task_id => task_id,
69
+ :sync_need => Knj::Strings.yn_str(@gui["cbShouldSync"].active?, 1, 0)
19
70
  }
20
71
 
72
+ #Update or add the timelog.
21
73
  if @timelog
22
74
  @timelog.update(save_hash)
23
75
  else
24
76
  @timelog = @args[:oata].ob.add(:Timelog, save_hash)
25
77
  end
26
78
 
79
+ #Start tracking the current timelog if the checkbox has been checked.
80
+ if @gui["cbStartTracking"].active?
81
+ @args[:oata].timelog_active = @timelog
82
+ end
83
+
84
+ @gui["window"].destroy
85
+ end
86
+
87
+ def on_btnRemove_clicked(*args)
88
+ if Knj::Gtk2.msgbox(_("Do you want to remove this timelog? This will not delete the timelog on OpenAll."), "yesno") != "yes"
89
+ return nil
90
+ end
91
+
92
+ @args[:oata].ob.delete(@timelog)
27
93
  @gui["window"].destroy
28
94
  end
29
95
  end
@@ -0,0 +1,76 @@
1
+ class Openall_time_applet::Gui::Win_worktime_overview
2
+ def initialize(args)
3
+ @args = args
4
+
5
+ @gui = Gtk::Builder.new.add("../glade/win_worktime_overview.glade")
6
+ @gui.translate
7
+ @gui.connect_signals{|h| method(h)}
8
+
9
+ self.build_week(Knj::Datet.new)
10
+
11
+ @gui["window"].show_all
12
+ end
13
+
14
+ def build_week(date)
15
+ stats = {
16
+ :task_total => {},
17
+ :days_total => {}
18
+ }
19
+
20
+ @args[:oata].ob.list(:Worktime, {"timestamp_month" => date}) do |wt|
21
+ task = wt.task
22
+ date = wt.timestamp
23
+
24
+ stats[:task_total][task.id] = {:secs => 0} if !stats[:task_total].key?(task.id)
25
+ stats[:task_total][task.id][:secs] += wt[:worktime].to_i
26
+
27
+ stats[:days_total][date.date] = {:secs => 0, :tasks => {}} if !stats[:days_total].key?(date.date)
28
+ stats[:days_total][date.date][:secs] += wt[:worktime].to_i
29
+ stats[:days_total][date.date][:tasks][task.id] = task
30
+ end
31
+
32
+ table = Gtk::Table.new(4, 4)
33
+ row = 0
34
+
35
+ stats[:days_total].keys.sort.each do |day_no|
36
+ date = Knj::Datet.in(Time.new(date.year, date.month, day_no))
37
+ day_title = Gtk::Label.new
38
+ day_title.markup = "<b>#{date.out(:time => false)}</b>"
39
+ day_title.xalign = 0
40
+
41
+ day_sum_float = stats[:days_total][day_no][:secs].to_f / 3600.to_f
42
+ day_sum = Gtk::Label.new
43
+ day_sum.markup = "<b>#{Knj::Locales.number_out(day_sum_float, 2)}</b>"
44
+ day_sum.xalign = 1
45
+
46
+ table.attach(day_title, 0, 2, row, row + 1)
47
+ table.attach(day_sum, 3, 4, row, row + 1)
48
+ row += 1
49
+
50
+ stats[:days_total][day_no][:tasks].each do |task_id, task|
51
+ task_title = Gtk::Label.new(task.title)
52
+ task_title.xalign = 0
53
+
54
+ task_sum_float = stats[:task_total][task_id][:secs].to_f / 3600.to_f
55
+ task_sum = Gtk::Label.new(Knj::Locales.number_out(task_sum_float, 2))
56
+ task_sum.xalign = 1
57
+
58
+ table.attach(Gtk::Label.new(""), 0, 1, row, row + 1)
59
+ table.attach(task_title, 1, 2, row, row + 1)
60
+ table.attach(Gtk::Label.new("Company"), 2, 3, row, row + 1)
61
+ table.attach(task_sum, 3, 4, row, row + 1)
62
+ row += 1
63
+ end
64
+ end
65
+
66
+ @gui["boxContent"].pack_start(table)
67
+ end
68
+
69
+ def on_btnNext_clicked
70
+ print "Next.\n"
71
+ end
72
+
73
+ def on_btnPrevious_clicked
74
+ print "Previous.\n"
75
+ end
76
+ end
@@ -1,27 +1,38 @@
1
1
  require "rubygems"
2
- require "knjrbfw"
2
+
3
+ if ENV["HOME"] == "/home/kaspernj"
4
+ #For development.
5
+ require "/home/kaspernj/Dev/Ruby/knjrbfw/lib/knjrbfw"
6
+ else
7
+ require "knjrbfw"
8
+ end
9
+
3
10
  require "gtk2"
4
11
  require "sqlite3"
5
12
  require "gettext"
6
13
  require "base64"
7
14
 
8
15
  require "knj/gtk2"
16
+ require "knj/gtk2_cb"
9
17
  require "knj/gtk2_tv"
10
18
  require "knj/gtk2_statuswindow"
11
19
 
12
20
  class Openall_time_applet
21
+ #Shortcut to start the application. Used by the Ubuntu-package.
13
22
  def self.exec
14
23
  require "#{File.dirname(__FILE__)}/../bin/openall_time_applet"
15
24
  end
16
25
 
17
26
  #Subclass controlling autoloading of models.
18
27
  class Models
28
+ #Autoloader for subclasses.
19
29
  def self.const_missing(name)
20
30
  require "../models/#{name.to_s.downcase}.rb"
21
31
  return Openall_time_applet::Models.const_get(name)
22
32
  end
23
33
  end
24
34
 
35
+ #Subclass holding all GUI-subclasses and autoloading of them.
25
36
  class Gui
26
37
  #Autoloader for subclasses.
27
38
  def self.const_missing(name)
@@ -47,7 +58,7 @@ class Openall_time_applet
47
58
  end
48
59
 
49
60
  #Various readable variables.
50
- attr_reader :db, :ob
61
+ attr_reader :db, :ob, :timelog_active, :timelog_active_time
51
62
 
52
63
  #Config controlling paths and more.
53
64
  CONFIG = {
@@ -63,7 +74,8 @@ class Openall_time_applet
63
74
  @db = Knj::Db.new(
64
75
  :type => "sqlite3",
65
76
  :path => CONFIG[:db_path],
66
- :return_keys => "symbols"
77
+ :return_keys => "symbols",
78
+ :index_append_table_name => true
67
79
  )
68
80
 
69
81
  #Models-handeler.
@@ -74,17 +86,28 @@ class Openall_time_applet
74
86
  :class_pre => "",
75
87
  :module => Openall_time_applet::Models
76
88
  )
89
+ @ob.events.connect(:no_name) do |event, classname|
90
+ _("not set")
91
+ end
77
92
 
78
93
  #Options used to save various information (Openall-username and such).
79
94
  Knj::Opts.init("knjdb" => @db, "table" => "Option")
95
+
96
+ #Set crash-operation to save tracked time instead of loosing it.
97
+ Kernel.at_exit(&self.method(:destroy))
80
98
  end
81
99
 
82
100
  #Updates the database according to the db-schema.
83
101
  def update_db
84
102
  require "../conf/db_schema.rb"
85
- rev = Knj::Db::Revision.new.init_db("db" => @db, "schema" => Openall_time_applet::DB_SCHEMA)
103
+ Knj::Db::Revision.new.init_db("db" => @db, "schema" => Openall_time_applet::DB_SCHEMA)
86
104
  end
87
105
 
106
+ #Creates a connection to OpenAll, logs in, yields the connection and destroys it again.
107
+ #===Examples
108
+ # oata.oa_conn do |conn|
109
+ # task_list = conn.task_list
110
+ # end
88
111
  def oa_conn
89
112
  begin
90
113
  conn = Openall_time_applet::Connection.new(
@@ -122,6 +145,91 @@ class Openall_time_applet
122
145
  def show_overview
123
146
  Openall_time_applet::Gui::Win_overview.new(:oata => self)
124
147
  end
148
+
149
+ def show_worktime_overview
150
+ Openall_time_applet::Gui::Win_worktime_overview.new(:oata => self)
151
+ end
152
+
153
+ #Updates the task-cache.
154
+ def update_task_cache
155
+ @ob.static(:Task, :update_cache, {:oata => self})
156
+ end
157
+
158
+ #Updates the worktime-cache.
159
+ def update_worktime_cache
160
+ @ob.static(:Worktime, :update_cache, {:oata => self})
161
+ end
162
+
163
+ #Pushes time-updates to OpenAll.
164
+ def push_time_updates
165
+ @ob.static(:Timelog, :push_time_updates, {:oata => self})
166
+ end
167
+
168
+ #Refreshes task-cache, create missing worktime from timelogs and push tracked time to timelogs. Shows a status-window while doing so.
169
+ def sync
170
+ sw = Knj::Gtk2::StatusWindow.new
171
+
172
+ if @timelog_active
173
+ timelog_active = @timelog_active
174
+ self.timelog_stop_tracking
175
+ end
176
+
177
+ Knj::Thread.new do
178
+ begin
179
+ sw.label = _("Updating task-cache.")
180
+ self.update_task_cache
181
+ sw.percent = 0.3
182
+
183
+ sw.label = _("Updating worktime-cache.")
184
+ self.update_worktime_cache
185
+ sw.percent = 0.66
186
+
187
+ sw.label = _("Pushing time-updates.")
188
+ self.push_time_updates
189
+ sw.percent = 1
190
+
191
+ sw.label = _("Done")
192
+
193
+ sleep 1
194
+ rescue => e
195
+ Knj::Gtk2.msgbox("msg" => Knj::Errors.error_str(e), "type" => "warning", "title" => _("Error"), "run" => false)
196
+ ensure
197
+ sw.destroy
198
+ self.timelog_active = timelog_active if timelog_active
199
+ end
200
+ end
201
+ end
202
+
203
+ #Stops tracking a timelog. Saves time tracked and sets sync-flag.
204
+ def timelog_stop_tracking
205
+ if @timelog_active
206
+ secs_passed = Time.now.to_i - @timelog_active_time.to_i
207
+ @timelog_active.update(
208
+ :time => @timelog_active[:time].to_i + secs_passed,
209
+ :sync_need => 1
210
+ )
211
+ end
212
+
213
+ @timelog_active = nil
214
+ @timelog_active_time = nil
215
+ end
216
+
217
+ #Sets a new timelog to track. Stops tracking of previous timelog if already tracking.
218
+ def timelog_active=(timelog)
219
+ self.timelog_stop_tracking
220
+
221
+ @timelog_active = timelog
222
+ @timelog_active_time = Time.new
223
+ end
224
+
225
+ #Saves tracking-status if tracking. Stops Gtks main loop.
226
+ def destroy
227
+ self.timelog_stop_tracking
228
+
229
+ #Use quit-variable to avoid Gtk-warnings.
230
+ Gtk.main_quit if @quit != true
231
+ @quit = true
232
+ end
125
233
  end
126
234
 
127
235
  #Gettext support.
data/models/task.rb ADDED
@@ -0,0 +1,26 @@
1
+ class Openall_time_applet::Models::Task < Knj::Datarow
2
+ has_many [
3
+ [:Timelog, :task_id, :timelogs]
4
+ ]
5
+
6
+ def self.update_cache(d, args)
7
+ res = nil
8
+ args[:oata].oa_conn do |conn|
9
+ res = conn.request(:getAllTasksForUser)
10
+ end
11
+
12
+ res.each do |task_data|
13
+ task = self.ob.get_by(:Task, {"openall_uid" => task_data["uid"]})
14
+ task_data = {
15
+ :openall_uid => task_data["uid"],
16
+ :title => task_data["title"]
17
+ }
18
+
19
+ if task
20
+ task.update(task_data)
21
+ else
22
+ task = self.ob.add(:Task, task_data)
23
+ end
24
+ end
25
+ end
26
+ end
data/models/timelog.rb CHANGED
@@ -1,3 +1,68 @@
1
1
  class Openall_time_applet::Models::Timelog < Knj::Datarow
2
+ has_one [
3
+ :Task
4
+ ]
2
5
 
6
+ #Treat data before inserting into database.
7
+ def self.add(d)
8
+ d.data[:time] = 0 if d.data[:time].to_s.strip.length <= 0
9
+ d.data[:time_transport] = 0 if d.data[:time_transport].to_s.strip.length <= 0
10
+ end
11
+
12
+ #Pushes timelogs and time to OpenAll.
13
+ def self.push_time_updates(d, args)
14
+ args[:oata].oa_conn do |conn|
15
+ #Go through timelogs that needs syncing and has a task set.
16
+ self.ob.list(:Timelog, {"sync_need" => 1, "task_id_not" => 0}) do |timelog|
17
+ secs_sum = timelog[:time].to_i + timelog[:time_transport].to_i
18
+ next if secs_sum <= 0
19
+
20
+ #The timelog has not yet been created in OpenAll - create it!
21
+ if timelog[:openall_uid].to_i == 0
22
+ res = conn.request(
23
+ :method => :createWorktime,
24
+ :post => {
25
+ :task_uid => timelog.task[:openall_uid].to_i,
26
+ :comment => timelog[:descr]
27
+ }
28
+ )
29
+ timelog[:openall_uid] = res["worktime_uid"]
30
+ end
31
+
32
+ #Push latest work-time.
33
+ res = conn.request(
34
+ :method => :pushTimeToWorktime,
35
+ :post => {
36
+ :worktime_uid => timelog[:openall_uid].to_i,
37
+ :secs => timelog[:time].to_i,
38
+ :secs_transport => timelog[:time_transport].to_i
39
+ }
40
+ )
41
+ timelog.update(
42
+ :time => 0,
43
+ :time_transport => 0,
44
+ :sync_need => 0,
45
+ :sync_last => Time.now
46
+ )
47
+ end
48
+ end
49
+ end
50
+
51
+ #Returns a short one-line short description.
52
+ def descr_short
53
+ descr = self[:descr].to_s.gsub("\n", " ").gsub(/\s{2,}/, " ")
54
+ descr = Knj::Strings.shorten(descr, 20)
55
+ descr = "[#{_("no description")}]" if descr.to_s.strip.length <= 0
56
+ return descr
57
+ end
58
+
59
+ #Returns the time as a human readable format.
60
+ def time_as_human
61
+ return Knj::Strings.secs_to_human_time_str(self[:time])
62
+ end
63
+
64
+ #Returns the transport-time as a human readable format.
65
+ def time_transport_as_human
66
+ return Knj::Strings.secs_to_human_time_str(self[:time_transport])
67
+ end
3
68
  end
@@ -0,0 +1,39 @@
1
+ class Openall_time_applet::Models::Worktime < Knj::Datarow
2
+ has_one [
3
+ :Task
4
+ ]
5
+
6
+ def self.update_cache(d, args)
7
+ res = nil
8
+ args[:oata].oa_conn do |conn|
9
+ res = conn.request(:getLatestWorktimes)
10
+ end
11
+
12
+ #Update all worktimes.
13
+ found = []
14
+ res.each do |wt_d|
15
+ found << wt_d["uid"]
16
+ task = self.ob.get_by(:Task, {"openall_uid" => wt_d["task_uid"]})
17
+
18
+ save_hash = {
19
+ :openall_uid => wt_d["uid"],
20
+ :task_id => task.id,
21
+ :timestamp => Knj::Datet.in(wt_d["timestamp"]),
22
+ :worktime => Knj::Strings.human_time_str_to_secs(wt_d["worktime"]),
23
+ :transporttime => Knj::Strings.human_time_str_to_secs(wt_d["transporttime"]),
24
+ :comment => wt_d["comment"]
25
+ }
26
+
27
+ wt = self.ob.get_by(:Worktime, {"openall_uid" => wt_d["uid"]})
28
+ if wt
29
+ wt.update(save_hash)
30
+ else
31
+ wt = self.ob.add(:Worktime, save_hash)
32
+ end
33
+ end
34
+
35
+ #Delete the ones not given.
36
+ list = self.ob.list(:Worktime, {"openall_uid_not" => found})
37
+ self.ob.deletes(list)
38
+ end
39
+ end
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{openall_time_applet}
8
- s.version = "0.0.1"
8
+ s.version = "0.0.2"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Kasper Johansen"]
12
- s.date = %q{2012-05-19}
12
+ s.date = %q{2012-05-21}
13
13
  s.default_executable = %q{openall_time_applet.rb}
14
14
  s.description = %q{Off-line time-tracking for OpenAll with syncing when online.}
15
15
  s.email = %q{k@spernj.org}
@@ -34,14 +34,18 @@ Gem::Specification.new do |s|
34
34
  "glade/win_overview.glade",
35
35
  "glade/win_preferences.glade",
36
36
  "glade/win_timelog_edit.glade",
37
+ "glade/win_worktime_overview.glade",
37
38
  "gui/trayicon.rb",
38
39
  "gui/win_overview.rb",
39
40
  "gui/win_preferences.rb",
40
41
  "gui/win_timelog_edit.rb",
42
+ "gui/win_worktime_overview.rb",
41
43
  "lib/openall_time_applet.rb",
42
44
  "locales/da_DK/LC_MESSAGES/default.mo",
43
45
  "locales/da_DK/LC_MESSAGES/default.po",
46
+ "models/task.rb",
44
47
  "models/timelog.rb",
48
+ "models/worktime.rb",
45
49
  "openall_time_applet.gemspec",
46
50
  "spec/openall_time_applet_spec.rb",
47
51
  "spec/spec_helper.rb"
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: openall_time_applet
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 0.0.1
5
+ version: 0.0.2
6
6
  platform: ruby
7
7
  authors:
8
8
  - Kasper Johansen
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2012-05-19 00:00:00 +02:00
13
+ date: 2012-05-21 00:00:00 +02:00
14
14
  default_executable: openall_time_applet.rb
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
@@ -148,14 +148,18 @@ files:
148
148
  - glade/win_overview.glade
149
149
  - glade/win_preferences.glade
150
150
  - glade/win_timelog_edit.glade
151
+ - glade/win_worktime_overview.glade
151
152
  - gui/trayicon.rb
152
153
  - gui/win_overview.rb
153
154
  - gui/win_preferences.rb
154
155
  - gui/win_timelog_edit.rb
156
+ - gui/win_worktime_overview.rb
155
157
  - lib/openall_time_applet.rb
156
158
  - locales/da_DK/LC_MESSAGES/default.mo
157
159
  - locales/da_DK/LC_MESSAGES/default.po
160
+ - models/task.rb
158
161
  - models/timelog.rb
162
+ - models/worktime.rb
159
163
  - openall_time_applet.gemspec
160
164
  - spec/openall_time_applet_spec.rb
161
165
  - spec/spec_helper.rb
@@ -173,7 +177,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
173
177
  requirements:
174
178
  - - ">="
175
179
  - !ruby/object:Gem::Version
176
- hash: -1333764541949504074
180
+ hash: 55925093073077767
177
181
  segments:
178
182
  - 0
179
183
  version: "0"