summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Crute <mcrute@gmail.com>2015-05-18 20:20:36 -0700
committerMike Crute <mcrute@gmail.com>2015-05-18 20:20:51 -0700
commit86d5190427db209bb478a49ee515097d2f30e6a2 (patch)
tree7028eeec0e5f2b88899e6e6696523b9eafb90280
downloadmoin_activity-86d5190427db209bb478a49ee515097d2f30e6a2.tar.bz2
moin_activity-86d5190427db209bb478a49ee515097d2f30e6a2.tar.xz
moin_activity-86d5190427db209bb478a49ee515097d2f30e6a2.zip
Initial import
-rw-r--r--.gitignore2
-rw-r--r--activity/__init__.py4
-rw-r--r--activity/_activitybase.py161
-rw-r--r--activity/action/StartActivity.py50
-rw-r--r--activity/action/StopActivity.py12
-rw-r--r--activity/action/__init__.py5
-rw-r--r--activity/parser/__init__.py5
-rw-r--r--activity/parser/timecsv.py46
-rw-r--r--activity/xmlrpc/StartActivity.py19
-rw-r--r--activity/xmlrpc/StopActivity.py19
-rw-r--r--activity/xmlrpc/__init__.py5
-rw-r--r--setup.py37
12 files changed, 365 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..175e2da
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
1/*.egg-info
2*.pyc
diff --git a/activity/__init__.py b/activity/__init__.py
new file mode 100644
index 0000000..f2d8200
--- /dev/null
+++ b/activity/__init__.py
@@ -0,0 +1,4 @@
1# *** Do not remove this! ***
2# Although being empty, the presence of this file is important for plugins
3# working correctly.
4
diff --git a/activity/_activitybase.py b/activity/_activitybase.py
new file mode 100644
index 0000000..3a0255f
--- /dev/null
+++ b/activity/_activitybase.py
@@ -0,0 +1,161 @@
1import csv
2from datetime import datetime
3from cStringIO import StringIO
4
5from MoinMoin.action import ActionBase
6from MoinMoin.PageEditor import PageEditor
7
8
9class FormattedDateTime:
10
11 def __init__(self, dt):
12 self.dt = dt
13
14 @classmethod
15 def from_now(cls):
16 return cls(datetime.now())
17
18 @property
19 def time(self):
20 return self.dt.strftime('%H:%M:%S')
21
22 @property
23 def date(self):
24 return self.dt.strftime('%Y-%m-%d')
25
26
27class DataRow(list):
28
29 _attr_map = ['start_date', 'start_time', 'end_date', 'end_time', 'task']
30
31 def __setattr__(self, attr, value):
32 pos = self._attr_map.index(attr)
33 self[pos] = value
34
35 def __getattr__(self, attr):
36 pos = self._attr_map.index(attr)
37 return self[pos]
38
39 def _make_datetime(self, date, time):
40 if not date or not time:
41 return None
42
43 try:
44 return datetime.strptime(' '.join((date, time)), '%Y-%m-%d %H:%M:%S')
45 except ValueError as err:
46 return datetime.strptime(' '.join((date, time)), '%Y-%m-%d %H:%M')
47
48 @property
49 def start_datetime(self):
50 return self._make_datetime(self.start_date, self.start_time)
51
52 @property
53 def end_datetime(self):
54 return self._make_datetime(self.end_date, self.end_time)
55
56 @property
57 def has_ended(self):
58 return self.end_date != ''
59
60 def mark_ended(self, now=None):
61 if not now:
62 now = FormattedDateTime.from_now()
63
64 if not self.has_ended:
65 self.end_date = now.date
66 self.end_time = now.time
67
68
69def parse_rows(data):
70 body = StringIO(data)
71
72 if data.startswith('#'):
73 body.readline()
74
75 reader = csv.reader(body, quotechar='"')
76 return [DataRow(r) for r in reader]
77
78
79class ActivityAction(ActionBase):
80
81 def __init__(self, pagename, request):
82 ActionBase.__init__(self, pagename, request)
83
84 self.page = PageEditor(request, pagename)
85 self.use_ticket = True
86
87 def get_rows(self):
88 return parse_rows(self.page.body)
89
90 def update_page(self, rows):
91 out_page = StringIO()
92 out_page.write('#format timecsv\n')
93 csv.writer(out_page, quotechar='"').writerows(rows)
94
95 return self.page.saveText(out_page.getvalue(), self.request.rev or 0)
96
97 def start_activity(self, description):
98 rows = self.get_rows()
99 now = FormattedDateTime.from_now()
100
101 rows[-1].mark_ended(now)
102 rows.append((now.date, now.time, '', '', description))
103
104 return self.update_page(rows)
105
106 def stop_activity(self):
107 rows = self.get_rows()
108 rows[-1].mark_ended()
109 self.update_page(rows)
110
111 @property
112 def can_use_activity(self):
113 return self.page.pi['format'] == 'timecsv'
114
115
116class Analysis:
117
118 def __init__(self, data):
119 self.raw_data = data
120 self.data = {}
121 self.order = []
122
123 @staticmethod
124 def _to_time(value):
125 hours = value // 3600
126 minutes = (value // 60) - (hours * 60)
127
128 if hours == 0:
129 return '{} minutes'.format(minutes)
130 else:
131 return '{} hours {} minutes'.format(hours, minutes)
132
133 def process(self):
134 self.raw_data.sort(key=lambda i: i.start_datetime, reverse=True)
135
136 for row in self.raw_data:
137 date = row.start_datetime.date()
138 data = self.data.get(date, None)
139
140 if date not in self.order:
141 self.order.append(date)
142
143 if data is None:
144 data = self.data[date] = {}
145
146 if not data.get(row.task):
147 data[row.task] = [0, False]
148
149 if row.end_datetime:
150 data[row.task][0] += (
151 row.end_datetime - row.start_datetime).seconds
152 else:
153 data[row.task][0] += (
154 datetime.now() - row.start_datetime).seconds
155
156 if not row.has_ended:
157 data[row.task][1] = True
158
159 def __iter__(self):
160 for key in self.order:
161 yield key, dict((k, (self._to_time(v[0]), v[1])) for k, v in self.data[key].items())
diff --git a/activity/action/StartActivity.py b/activity/action/StartActivity.py
new file mode 100644
index 0000000..34b3873
--- /dev/null
+++ b/activity/action/StartActivity.py
@@ -0,0 +1,50 @@
1from MoinMoin import wikiutil
2from .._activitybase import ActivityAction, FormattedDateTime
3
4
5TEMPLATE = '''
6<table>
7 <tr>
8 <td class="label"><label>%(comment_label)s</label></td>
9 <td class="content">
10 <input type="text" name="activity" size="80" maxlength="200">
11 </td>
12 </tr>
13 <tr>
14 <td></td>
15 <td class="buttons">
16 %(buttons_html)s
17 </td>
18 </tr>
19</table>
20'''
21
22
23class StartActivity(ActivityAction):
24
25 def __init__(self, pagename, request):
26 ActivityAction.__init__(self, pagename, request)
27
28 self.form_trigger = 'start_activity'
29 self.form_trigger_label = self._('Start Activity')
30
31 def check_condition(self):
32 if not self.can_use_activity:
33 return 'This page does not support activities.'
34 else:
35 return None
36
37 def do_action(self):
38 description = wikiutil.clean_input(self.form.get('activity', u''))
39 return True, self.start_activity(description)
40
41 def get_form_html(self, buttons_html):
42 return TEMPLATE % {
43 'pagename': self.pagename,
44 'comment_label': self._("Activity to start"),
45 'buttons_html': buttons_html,
46 }
47
48
49def execute(pagename, request):
50 StartActivity(pagename, request).render()
diff --git a/activity/action/StopActivity.py b/activity/action/StopActivity.py
new file mode 100644
index 0000000..94d65bf
--- /dev/null
+++ b/activity/action/StopActivity.py
@@ -0,0 +1,12 @@
1from .._activitybase import ActivityAction
2
3
4def execute(pagename, request):
5 action = ActivityAction(pagename, request)
6
7 if not action.can_use_activity:
8 request.theme.add_msg('This page does not support activities.', 'error')
9 return action.page.send_page()
10
11 action.stop_activity()
12 action.page.send_page()
diff --git a/activity/action/__init__.py b/activity/action/__init__.py
new file mode 100644
index 0000000..e4ed3b6
--- /dev/null
+++ b/activity/action/__init__.py
@@ -0,0 +1,5 @@
1# -*- coding: iso-8859-1 -*-
2
3from MoinMoin.util import pysupport
4
5modules = pysupport.getPackageModules(__file__)
diff --git a/activity/parser/__init__.py b/activity/parser/__init__.py
new file mode 100644
index 0000000..e4ed3b6
--- /dev/null
+++ b/activity/parser/__init__.py
@@ -0,0 +1,5 @@
1# -*- coding: iso-8859-1 -*-
2
3from MoinMoin.util import pysupport
4
5modules = pysupport.getPackageModules(__file__)
diff --git a/activity/parser/timecsv.py b/activity/parser/timecsv.py
new file mode 100644
index 0000000..7e988eb
--- /dev/null
+++ b/activity/parser/timecsv.py
@@ -0,0 +1,46 @@
1from cStringIO import StringIO
2from .._activitybase import Analysis, parse_rows
3
4
5DAY_TEMPLATE_TOP = """
6<h2>{day}</h2>
7<table class="timecsv">
8 <thead>
9 <tr>
10 <th>Project</th>
11 <th>Time Spent</th>
12 </tr>
13 </thead>
14 <tbody>
15"""
16
17
18DAY_TEMPLATE_BOTTOM = """\
19 </tbody>
20</table>
21"""
22
23
24ROW_TEMPLATE = '\t<tr class="{active}"><td>{task}</td><td>{time}</td></tr>\n'
25
26
27class Parser:
28
29 def __init__(self, raw, request, **kw):
30 self.request = request
31 self.analysis = Analysis(parse_rows(raw))
32 self.analysis.process()
33
34 def format(self, formatter, **kw):
35 output = StringIO()
36
37 for day, rows in self.analysis:
38 output.write(DAY_TEMPLATE_TOP.format(day=day.strftime('%A %B %d, %Y')))
39
40 for task, (time, still_active) in rows.items():
41 output.write(ROW_TEMPLATE.format(task=task, time=time,
42 active='active' if still_active else ''))
43
44 output.write(DAY_TEMPLATE_BOTTOM)
45
46 self.request.write(output.getvalue())
diff --git a/activity/xmlrpc/StartActivity.py b/activity/xmlrpc/StartActivity.py
new file mode 100644
index 0000000..b5e52c7
--- /dev/null
+++ b/activity/xmlrpc/StartActivity.py
@@ -0,0 +1,19 @@
1import xmlrpclib
2from .._activitybase import ActivityAction
3
4
5def execute(self, pagename, description):
6 action = ActivityAction(self._instr(pagename), self.request)
7
8 if not action.page.exists():
9 return self.noSuchPageFault()
10
11 if not self.request.user.may.write(pagename):
12 return self.notAllowedFault()
13
14 if not action.can_use_activity:
15 return xmlrpclib.Fault(1, "This page does not support activities.")
16
17 action.start_activity(self._instr(description))
18
19 return self._outstr('OK')
diff --git a/activity/xmlrpc/StopActivity.py b/activity/xmlrpc/StopActivity.py
new file mode 100644
index 0000000..b26d8dd
--- /dev/null
+++ b/activity/xmlrpc/StopActivity.py
@@ -0,0 +1,19 @@
1import xmlrpclib
2from .._activitybase import ActivityAction
3
4
5def execute(self, pagename):
6 action = ActivityAction(self._instr(pagename), self.request)
7
8 if not action.page.exists():
9 return self.noSuchPageFault()
10
11 if not self.request.user.may.write(pagename):
12 return self.notAllowedFault()
13
14 if not action.can_use_activity:
15 return xmlrpclib.Fault(1, "This page does not support activities.")
16
17 action.stop_activity()
18
19 return self._outstr('OK')
diff --git a/activity/xmlrpc/__init__.py b/activity/xmlrpc/__init__.py
new file mode 100644
index 0000000..e4ed3b6
--- /dev/null
+++ b/activity/xmlrpc/__init__.py
@@ -0,0 +1,5 @@
1# -*- coding: iso-8859-1 -*-
2
3from MoinMoin.util import pysupport
4
5modules = pysupport.getPackageModules(__file__)
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..34b421f
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,37 @@
1from setuptools import setup, find_packages
2
3setup(
4 name="CruteMoinActivity",
5 version="1.0",
6 description="",
7 author="Michael Crute <mcrute@gmail.com>",
8 license="MIT",
9 packages=find_packages(),
10 entry_points={
11 "moin.plugins.action": [
12 "StartActivity = activity.action.StartActivity:execute",
13 "StopActivity = activity.action.StopActivity:execute",
14 ],
15# "moin.plugins.converter": [
16# ],
17# "moin.plugins.events": [
18# ],
19# "moin.plugins.filter": [
20# ],
21# "moin.plugins.formatter": [
22# ],
23# "moin.plugins.macro": [
24# ],
25 "moin.plugins.parser": [
26 "timecsv = activity.parser.timecsv:Parser",
27 ],
28# "moin.plugins.theme": [
29# ],
30# "moin.plugins.userprefs": [
31# ],
32 "moin.plugins.xmlrpc": [
33 "StartActivity = activity.xmlrpc.StartActivity:execute",
34 "StopActivity = activity.xmlrpc.StopActivity:execute",
35 ],
36 }
37)