From 86d5190427db209bb478a49ee515097d2f30e6a2 Mon Sep 17 00:00:00 2001 From: Mike Crute Date: Mon, 18 May 2015 20:20:36 -0700 Subject: Initial import --- .gitignore | 2 + activity/__init__.py | 4 + activity/_activitybase.py | 161 +++++++++++++++++++++++++++++++++++++++ activity/action/StartActivity.py | 50 ++++++++++++ activity/action/StopActivity.py | 12 +++ activity/action/__init__.py | 5 ++ activity/parser/__init__.py | 5 ++ activity/parser/timecsv.py | 46 +++++++++++ activity/xmlrpc/StartActivity.py | 19 +++++ activity/xmlrpc/StopActivity.py | 19 +++++ activity/xmlrpc/__init__.py | 5 ++ setup.py | 37 +++++++++ 12 files changed, 365 insertions(+) create mode 100644 .gitignore create mode 100644 activity/__init__.py create mode 100644 activity/_activitybase.py create mode 100644 activity/action/StartActivity.py create mode 100644 activity/action/StopActivity.py create mode 100644 activity/action/__init__.py create mode 100644 activity/parser/__init__.py create mode 100644 activity/parser/timecsv.py create mode 100644 activity/xmlrpc/StartActivity.py create mode 100644 activity/xmlrpc/StopActivity.py create mode 100644 activity/xmlrpc/__init__.py create mode 100644 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..175e2da --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/*.egg-info +*.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 @@ +# *** Do not remove this! *** +# Although being empty, the presence of this file is important for plugins +# working correctly. + 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 @@ +import csv +from datetime import datetime +from cStringIO import StringIO + +from MoinMoin.action import ActionBase +from MoinMoin.PageEditor import PageEditor + + +class FormattedDateTime: + + def __init__(self, dt): + self.dt = dt + + @classmethod + def from_now(cls): + return cls(datetime.now()) + + @property + def time(self): + return self.dt.strftime('%H:%M:%S') + + @property + def date(self): + return self.dt.strftime('%Y-%m-%d') + + +class DataRow(list): + + _attr_map = ['start_date', 'start_time', 'end_date', 'end_time', 'task'] + + def __setattr__(self, attr, value): + pos = self._attr_map.index(attr) + self[pos] = value + + def __getattr__(self, attr): + pos = self._attr_map.index(attr) + return self[pos] + + def _make_datetime(self, date, time): + if not date or not time: + return None + + try: + return datetime.strptime(' '.join((date, time)), '%Y-%m-%d %H:%M:%S') + except ValueError as err: + return datetime.strptime(' '.join((date, time)), '%Y-%m-%d %H:%M') + + @property + def start_datetime(self): + return self._make_datetime(self.start_date, self.start_time) + + @property + def end_datetime(self): + return self._make_datetime(self.end_date, self.end_time) + + @property + def has_ended(self): + return self.end_date != '' + + def mark_ended(self, now=None): + if not now: + now = FormattedDateTime.from_now() + + if not self.has_ended: + self.end_date = now.date + self.end_time = now.time + + +def parse_rows(data): + body = StringIO(data) + + if data.startswith('#'): + body.readline() + + reader = csv.reader(body, quotechar='"') + return [DataRow(r) for r in reader] + + +class ActivityAction(ActionBase): + + def __init__(self, pagename, request): + ActionBase.__init__(self, pagename, request) + + self.page = PageEditor(request, pagename) + self.use_ticket = True + + def get_rows(self): + return parse_rows(self.page.body) + + def update_page(self, rows): + out_page = StringIO() + out_page.write('#format timecsv\n') + csv.writer(out_page, quotechar='"').writerows(rows) + + return self.page.saveText(out_page.getvalue(), self.request.rev or 0) + + def start_activity(self, description): + rows = self.get_rows() + now = FormattedDateTime.from_now() + + rows[-1].mark_ended(now) + rows.append((now.date, now.time, '', '', description)) + + return self.update_page(rows) + + def stop_activity(self): + rows = self.get_rows() + rows[-1].mark_ended() + self.update_page(rows) + + @property + def can_use_activity(self): + return self.page.pi['format'] == 'timecsv' + + +class Analysis: + + def __init__(self, data): + self.raw_data = data + self.data = {} + self.order = [] + + @staticmethod + def _to_time(value): + hours = value // 3600 + minutes = (value // 60) - (hours * 60) + + if hours == 0: + return '{} minutes'.format(minutes) + else: + return '{} hours {} minutes'.format(hours, minutes) + + def process(self): + self.raw_data.sort(key=lambda i: i.start_datetime, reverse=True) + + for row in self.raw_data: + date = row.start_datetime.date() + data = self.data.get(date, None) + + if date not in self.order: + self.order.append(date) + + if data is None: + data = self.data[date] = {} + + if not data.get(row.task): + data[row.task] = [0, False] + + if row.end_datetime: + data[row.task][0] += ( + row.end_datetime - row.start_datetime).seconds + else: + data[row.task][0] += ( + datetime.now() - row.start_datetime).seconds + + if not row.has_ended: + data[row.task][1] = True + + def __iter__(self): + for key in self.order: + 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 @@ +from MoinMoin import wikiutil +from .._activitybase import ActivityAction, FormattedDateTime + + +TEMPLATE = ''' + + + + + + + + + +
+ +
+ %(buttons_html)s +
+''' + + +class StartActivity(ActivityAction): + + def __init__(self, pagename, request): + ActivityAction.__init__(self, pagename, request) + + self.form_trigger = 'start_activity' + self.form_trigger_label = self._('Start Activity') + + def check_condition(self): + if not self.can_use_activity: + return 'This page does not support activities.' + else: + return None + + def do_action(self): + description = wikiutil.clean_input(self.form.get('activity', u'')) + return True, self.start_activity(description) + + def get_form_html(self, buttons_html): + return TEMPLATE % { + 'pagename': self.pagename, + 'comment_label': self._("Activity to start"), + 'buttons_html': buttons_html, + } + + +def execute(pagename, request): + 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 @@ +from .._activitybase import ActivityAction + + +def execute(pagename, request): + action = ActivityAction(pagename, request) + + if not action.can_use_activity: + request.theme.add_msg('This page does not support activities.', 'error') + return action.page.send_page() + + action.stop_activity() + 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 @@ +# -*- coding: iso-8859-1 -*- + +from MoinMoin.util import pysupport + +modules = 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 @@ +# -*- coding: iso-8859-1 -*- + +from MoinMoin.util import pysupport + +modules = 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 @@ +from cStringIO import StringIO +from .._activitybase import Analysis, parse_rows + + +DAY_TEMPLATE_TOP = """ +

{day}

+ + + + + + + + +""" + + +DAY_TEMPLATE_BOTTOM = """\ + +
ProjectTime Spent
+""" + + +ROW_TEMPLATE = '\t{task}{time}\n' + + +class Parser: + + def __init__(self, raw, request, **kw): + self.request = request + self.analysis = Analysis(parse_rows(raw)) + self.analysis.process() + + def format(self, formatter, **kw): + output = StringIO() + + for day, rows in self.analysis: + output.write(DAY_TEMPLATE_TOP.format(day=day.strftime('%A %B %d, %Y'))) + + for task, (time, still_active) in rows.items(): + output.write(ROW_TEMPLATE.format(task=task, time=time, + active='active' if still_active else '')) + + output.write(DAY_TEMPLATE_BOTTOM) + + 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 @@ +import xmlrpclib +from .._activitybase import ActivityAction + + +def execute(self, pagename, description): + action = ActivityAction(self._instr(pagename), self.request) + + if not action.page.exists(): + return self.noSuchPageFault() + + if not self.request.user.may.write(pagename): + return self.notAllowedFault() + + if not action.can_use_activity: + return xmlrpclib.Fault(1, "This page does not support activities.") + + action.start_activity(self._instr(description)) + + 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 @@ +import xmlrpclib +from .._activitybase import ActivityAction + + +def execute(self, pagename): + action = ActivityAction(self._instr(pagename), self.request) + + if not action.page.exists(): + return self.noSuchPageFault() + + if not self.request.user.may.write(pagename): + return self.notAllowedFault() + + if not action.can_use_activity: + return xmlrpclib.Fault(1, "This page does not support activities.") + + action.stop_activity() + + 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 @@ +# -*- coding: iso-8859-1 -*- + +from MoinMoin.util import pysupport + +modules = 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 @@ +from setuptools import setup, find_packages + +setup( + name="CruteMoinActivity", + version="1.0", + description="", + author="Michael Crute ", + license="MIT", + packages=find_packages(), + entry_points={ + "moin.plugins.action": [ + "StartActivity = activity.action.StartActivity:execute", + "StopActivity = activity.action.StopActivity:execute", + ], +# "moin.plugins.converter": [ +# ], +# "moin.plugins.events": [ +# ], +# "moin.plugins.filter": [ +# ], +# "moin.plugins.formatter": [ +# ], +# "moin.plugins.macro": [ +# ], + "moin.plugins.parser": [ + "timecsv = activity.parser.timecsv:Parser", + ], +# "moin.plugins.theme": [ +# ], +# "moin.plugins.userprefs": [ +# ], + "moin.plugins.xmlrpc": [ + "StartActivity = activity.xmlrpc.StartActivity:execute", + "StopActivity = activity.xmlrpc.StopActivity:execute", + ], + } +) -- cgit v1.2.3