openall_time_applet 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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"