Commits

Andy Mikhailenko committed 86362cf

Refactoring. Added fact creation view in web UI.

Comments (0)

Files changed (7)

 from timetra.web import create_app
 
 
-#app = create_app(None)
-app = create_app(None, debug=True)
+class TestConfig(object):
+    DEBUG = True
+    # FIXME not for production
+    SECRET_KEY = 'secret123'
+
+
+app = create_app(TestConfig)
 app.run()

timetra/cli/__init__.py

 :author: Andrey Mikhaylenko
 
 """
-from argh import alias, arg, confirm, ArghParser
+from argh import alias, arg, confirm, ArghParser, CommandError
 import datetime
 
 from timetra.reporting import drift
 HAMSTER_TAG_LOG = 'auto-logged'
 
 
+def parse_activity(loose_name):
+    try:
+        return storage.parse_activity(loose_name)
+    except storage.ActivityMatchingError as e:
+        raise CommandError(failure(e))
+
+
 @arg('periods', nargs='+')
 @arg('--silent', default=False)
 def cycle(args):
 @arg('-d', '--description', default='', help='description for work periods')
 def pomodoro(args):
     yield 'Running Pomodoro timer'
-    work_activity, work_category = storage.parse_activity(args.activity)
+    work_activity, work_category = parse_activity(args.activity)
     tags = ['pomodoro', HAMSTER_TAG]
 
     work = timer.Period(args.work_duration, name=work_activity,
     # * smart "-c":
     #   * "--upto DURATION" modifier (avoids overlapping)
     assert storage.hamster_storage
-    activity, category = storage.parse_activity(args.activity)
+    activity, category = parse_activity(args.activity)
     h_act = u'{activity}@{category}'.format(**locals())
     start = None
     fact = None
             yield failure(u'Operation cancelled.')
             return
 
-    activity, category = storage.parse_activity(args.activity)
-    h_act = u'{activity}@{category}'.format(**locals())
-
     tags = [HAMSTER_TAG_LOG]
     if args.tags:
         tags = list(set(tags + args.tags.split(',')))
     if args.ppl:
         tags.extend(['with-{0}'.format(x) for x in args.ppl.split(',')])
 
-    fact = storage.Fact(h_act, tags=tags, description=args.description,
-                        start_time=start, end_time=end)
-    storage.hamster_storage.add_fact(fact)
+    fact = storage.add_fact(args.activity, tags=tags,
+                            description=args.description, start_time=start,
+                            end_time=end)
 
     # report
     delta = fact.end_time - start  # почему-то сам факт "не знает" времени начала
     delta_minutes = delta.seconds / 60
-    template = u'Logged {h_act} ({delta_minutes} min)'
-    yield success(template.format(h_act=h_act, delta_minutes=delta_minutes))
+    template = u'Logged {fact.activity}@{fact.category} ({delta_minutes} min)'
+    yield success(template.format(fact, delta_minutes=delta_minutes))
 
 
 @alias('ps')
     kwargs = {}
     if args.set_activity:
         yield u'Updating fact {0}'.format(fact)
-        activity, category = storage.parse_activity(args.set_activity)
+        activity, category = parse_activity(args.set_activity)
         kwargs['activity'] = activity
         kwargs['category'] = category
         storage.update_fact(fact, **kwargs)

timetra/storage.py

 import datetime
 from warnings import warn
 
-# FIXME ideally this should only be used in CLI module
-from argh import CommandError
-
 
 try:
     from hamster.client import Storage
 # Auxiliary API
 #
 
+class ActivityMatchingError(Exception):
+    """ Raised if no known activity unambiguously matches given pattern.
+    """
+
+
+class UnknownActivity(ActivityMatchingError):
+    """ Raised if no activity in the storage corresponds to given pattern.
+    """
+
+
+class AmbiguousActivityName(ActivityMatchingError):
+    """ Raised if more than a single known activity matches given pattern.
+    """
+
+
+class CannotCreateFact(Exception):
+    pass
+
+
 def get_hamster_activity(activity):
     """Given a mask, finds the (single) matching activity and returns its full
     name along with category name. Raises AssertionError if no matching
     if not candidates:
         # look for partial matches
         candidates = [d for d in activities if activity in d['name']]
-    assert candidates, 'unknown activity {0}'.format(activity)
-    assert len(candidates) == 1, 'ambiguous name, matches:\n{0}'.format(
-        '\n'.join((u'  - {category}: {name}'.format(**x)
-                   for x in sorted(candidates))))
+    if not candidates:
+        raise UnknownActivity('unknown activity {0}'.format(activity))
+    if 1 < len(candidates):
+        raise AmbiguousActivityName('ambiguous name, matches:\n{0}'.format(
+            '\n'.join((u'  - {category}: {name}'.format(**x)
+                       for x in sorted(candidates)))))
     return [unicode(candidates[0][x]) for x in ['name', 'category']]
 
 
         if '@' in activity_mask:
             return activity_mask.split('@')
         else:
-            try:
-                return get_hamster_activity(activity_mask)
-            except AssertionError as e:
-                raise CommandError(e)
+            return get_hamster_activity(activity_mask)
     return activity, category
 
 
         return prev, now
 
 
+def add_fact(loose_name, tags=None, description='', start_time=None,
+             end_time=None):
+    activity, category = parse_activity(loose_name)
+    h_act = u'{activity}@{category}'.format(activity=activity,
+                                            category=category)
+    fact = Fact(h_act, tags=tags, description=description,
+                start_time=start_time, end_time=end_time)
+    fact.id = hamster_storage.add_fact(fact)
+    if not fact.id:
+        raise CannotCreateFact(u'Another activity may be running')
+    return fact
+
+
 def update_fact(fact, extra_tags=None, extra_description=None, **kwargs):
     for key, value in kwargs.items():
         setattr(fact, key, value)

timetra/web/__init__.py

 ===============
 """
 import datetime
-from flask import Blueprint, Flask, redirect, render_template, request, url_for
+from flask import (Blueprint, Flask, flash, redirect, render_template, request,
+                   url_for)
 import wtforms as wtf
 
 from timetra import storage
             self.data = [x.strip() for x in valuelist[0].split(',')]
 
 
+class AddFactForm(wtf.Form):
+    loose_name = wtf.TextField(u'Activity', [wtf.validators.Required()])
+    description = wtf.TextAreaField()
+    tags = TagListField()
+    start_time = wtf.DateTimeField(default=datetime.datetime.now)
+    end_time = wtf.DateTimeField(u'End Time', [wtf.validators.Optional()])
+
+
 class FactForm(wtf.Form):
     category = wtf.SelectField()
-    activity = wtf.TextField()
+    activity = wtf.TextField(u'Activity', [wtf.validators.Required()])
     description = wtf.TextAreaField()
     tags = TagListField()
     start_time = wtf.DateTimeField()
     end_time = wtf.DateTimeField(u'End Time', [wtf.validators.Optional()])
 
 
-
 def appraise_category(category):
     # map category types to CSS classes to tweak progress bar colour
     default_appraisal = 'info'
 
 @blueprint.route('/')
 def dashboard():
-    facts = list(reversed(storage.get_facts_for_day()))
+    # можно storage.get_facts_for_today(), но тогда в 00:00 обрезается в ноль
+    facts = list(reversed(storage.hamster_storage.get_todays_facts()))
     stats = get_stats(facts)
     return render_template('dashboard.html', facts=facts, stats=stats,
                            appraise_category=appraise_category)
                            activity=activity, facts=facts)
 
 
-@blueprint.route('facts/<int:fact_id>/edit', methods=['GET', 'POST'])
+@blueprint.route('facts/add/', methods=['GET', 'POST'])
+def add_fact():
+    data = request.form.copy()
+    form = AddFactForm(data)
+    if request.method == 'POST' and form.validate():
+        try:
+            fact = storage.add_fact(**form.data)
+        except (storage.ActivityMatchingError, storage.CannotCreateFact) as e:
+            flash(u'Error: {0}'.format(e), 'error')
+        else:
+            url = url_for('timetra.edit_fact', fact_id=fact.id)
+            message = u'Added <a href="{url}">{f.activity}@{f.category}</a>'
+            flash(message.format(url=url, f=fact), 'success')
+            return redirect(url_for('timetra.dashboard'))
+
+    return render_template('add.html', storage=storage, form=form)
+
+
+@blueprint.route('facts/<int:fact_id>/', methods=['GET', 'POST'])
 def edit_fact(fact_id):
     fact = storage.hamster_storage.get_fact(fact_id)
     form = FactForm(request.form, fact)

timetra/web/templates/add.html

+{% extends 'base.html' %}
+
+{% from "_helpers.html" import render_tags, render_delta %}
+{% from "_formhelpers.html" import render_field %}
+
+{% block heading %} Adding fact {% endblock %}
+{% block content %}
+    <form method="POST" action="" enctype="multipart/form-data" class="form-horizontal">
+
+        {% for field in form %}
+            {{ render_field(field) }}
+        {% endfor %}
+
+        <div class="form-actions">
+            <input type="submit" value="Add this fact" class="btn-success" />
+        </div>
+    </form>
+{% endblock %}

timetra/web/templates/base.html

 <html>
     <head>
         <title>Timetra</title>
-        <link href="/static/css/bootstrap.min.css" rel="stylesheet">
+        <link href="{{ url_for('static', filename='css/bootstrap.min.css') }}" rel="stylesheet">
         <style type="text/css">
             body {
                 padding-top: 50px;
             <div class="navbar-inner">
                 <div class="container">
                     <a class="brand" href="/">Timetra</a>
+
                     <form action="{{ url_for('timetra.search') }}" class="navbar-search pull-left">
                         <input name="q" type="text" class="search-query" placeholder="Search facts">
                     </form>
+
+                    <form action="{{ url_for('timetra.add_fact') }}" method="POST" class="navbar-form pull-right">
+                        <div class="input-append">
+                            <input name="loose_name" type="text" class="add-on" placeholder="New Activity"><button class="btn btn-success" type="submit">Start!</button>
+                        </div>
+                    </form>
                 </div>
             </div>
         </div>
 
         <h1>{% block heading %} Timetra {% endblock %}</h1>
 
+        {% with messages = get_flashed_messages(with_categories=true) %}
+            {% for category, message in messages %}
+                <div class="alert alert-{{ category }}">{{ message|safe }}</div>
+            {% endfor %}
+        {% endwith %}
+
         {% block content %}
             <p>(nothing here yet)</p>
         {% endblock %}

timetra/web/templates/edit.html

         {% endfor %}
 
         <div class="form-actions">
-            <input type="submit" value="Сохранить" class="btn-success" />
+            <input type="submit" value="Save changes" class="btn-success" />
         </div>
     </form>
 {% endblock %}