Andy Mikhailenko avatar Andy Mikhailenko committed eae8f01

Updated extension "needs" (postponed bulk commit, unsure about the original date).

Comments (0)

Files changed (9)

orgtool/ext/needs/__init__.py

 # -*- coding: utf-8 -*-
-
+"""
+Needs management
+================
+"""
 from tool.plugins import BasePlugin
-from tool.ext.templating import register_templates
-from commands import ls, add, mv, rm
+#from tool.ext.templating import register_templates
+from commands import (
+    list_needs, view_need, add_need, rename_need, mark_need, move_need,
+    delete_need
+)
 import admin
 
 
-class NeedsPlugin(BasePlugin):
-    identity = 'needs'
-    commands = [ls, add, mv, rm]
+class NeedsCLI(BasePlugin):
+    "A stripped-down lightweight needs management plugin (CLI only)"
+    features = 'needs'
+    requires = ['{document_storage}']
+    commands = [
+        list_needs, view_need, add_need, rename_need, mark_need, move_need,
+        delete_need
+    ]
+
+
+class NeedsPlugin(NeedsCLI):
+    "The basic full-blown plugin for needs management."
+    requires = NeedsCLI.requires + ['{templating}']
 
     def make_env(self):
-        register_templates(__name__)
+        templating = self.app.get_feature('templating')
+        templating.register_templates(__name__)

orgtool/ext/needs/commands.py

 Commands
 ========
 
+These commands form a powerful command-line interface for manipulating needs.
 """
 import re
 from itertools import chain
 
-from tool.cli import arg, alias
-from tool.ext.documents import default_storage
+from tool import app
+from tool.cli import (
+    # commands
+    arg, alias, CommandError, confirm,
+    # colors
+    Fore, Back, Style
+)
 
-from schema import Need, SystemUnderDevelopment
+from .schema import Need, SystemUnderDevelopment
+from .helpers import *
 
 
 __all__ = ['ls', 'add', 'mv', 'rm']
 
 
-class NotFound(Exception):
-    pass
+@alias('view')
+@arg('-k', '--primary-key')
+@arg('-q', '--query')
+@arg('-p', '--project')
+def view_need(args):
+    fix_unicode(args, 'project', 'query')
+    if (not (args.primary_key or args.query) or
+        (args.primary_key and args.query)):
+        raise CommandError('Please specify either --query or --primary-key')
+    need = None
+    try:
+        project = get_single(find_projects, query=args.project)
+    except MultipleMatches:
+        project = None
+    if args.query:
+        try:
+            need = get_single(find_needs, query=args.query, project=project)
+        except MultipleMatches as e:
+            yield(str(e))
+            raise NotImplementedError # TODO
+    if args.primary_key:
+        db = app.get_feature('document_storage').default_db
+        need = db.get(Need, args.primary_key)
+    if need:
+        yield(need.dump())
 
-class MultipleMatches(Exception):
-    pass
-
-
-def find_projects(query=None, exclude=None):
-    db = default_storage()
-    suds = SystemUnderDevelopment.objects(db)
-    if query:
-        suds = suds.where(summary__matches_caseless=query)
-    if exclude:
-        suds = suds.where_not(summary__matches_caseless=exclude)
-    return suds
-
-def find_needs(query=None, exclude=None, project=None):
-    db = default_storage()
-    if project:
-        needs = project.needs
-        if needs and query:
-            # FIXME sud.needs should return a query instead of list (Docu bug)
-            needs = [n for n in needs if re.match(ur'.*{0}.*'.format(query),
-                                                  n.summary,
-                                                  re.UNICODE|re.IGNORECASE)]
-        if needs:
-            for need in needs:
-                yield need
-    else:
-        needs = Need.objects(db)
-        if query:
-            needs = needs.where(summary__matches_caseless=query)
-        if exclude:
-            needs = needs.where_not(summary__matches_caseless=exclude)
-        for need in needs:
-            yield need
-
-def ensure_results(finder, *args, **kwargs):
-    items = list(finder(*args, **kwargs))
-    if not items:
-        raise NotFound('No matching items.')
-    return items
-
-def get_single(finder, *args, **kwargs):
-    items = list(ensure_results(finder, *args, **kwargs))
-    if 1 < len(items):
-        print(u'More than one object matching {0} {1}:'.format(args, kwargs))
-        for item in items:
-            print('- {0}'.format(item))
-        raise MultipleMatches()
-    return items[0]
-
-def fix_unicode(namespace, *argnames):
-    for argname in argnames:
-        value = getattr(namespace, argname)
-        if isinstance(value, str):
-            decoded_value = value.decode('utf-8')
-            setattr(namespace, argname, decoded_value)
-
-def remove_need(need, dry_run=False):
-    "Safely removes the Need object from the database."
-    projects = need.get_projects()
-    for project in projects:
-        print(u'  unlinking from {summary}'.format(**project))
-        pks = [n.pk for n in project.needs]
-        project.needs.pop(pks.index(need.pk))
-        if not dry_run:
-            project.save()
-    if not dry_run:
-        need.delete()
-
-@alias('list')
-@arg('-q', '--query', help='Filter by summary (case-insensitive)')
-@arg('-p', '--project', help='Filter by project summary (case-insensitive)')
-@arg('-Q', '--exclude', help='Exclude given patterns from query by summary')
-@arg('-P', '--exclude-project',
+@alias('ls')
+@arg('-q', '--query', nargs='+', help='Filter by summary (case-insensitive)')
+@arg('-p', '--project', nargs='+',
+     help='Filter by project summary (case-insensitive)')
+@arg('-Q', '--exclude', nargs='+',
+     help='Exclude given patterns from query by summary')
+@arg('-P', '--exclude-project', nargs='+',
      help='Exclude given patterns from project query by project summary')
 @arg('-k', '--show-keys', default=False,
      help='Display primary keys along with items')
      help='Display orphaned items along with those bound to projects')
 @arg('-o', '--only-orphans', default=False,
      help='Display only orphaned needs (without projects)')
-def ls(args):
+@arg('--no-headings', default=False, help='Hide project headings')
+@arg('--done', default=False, help='Only display satisfied needs')
+@arg('--todo', default=False, help='Only display unsatisfied needs')
+def list_needs(args):
     """Lists all existing needs. By default needs are grouped by project (the
     relation is m2m so any need may appear multiple times within a query).
     Orphaned needs (without projects) are not displayed unless the switches
 
     Note that --only-orphans works slowly due to implementation details.
     """
-    fix_unicode(args, 'project', 'query')
+    for func in flatten_nargs_plus, fix_unicode:
+        func(args, 'project', 'query', 'exclude', 'exclude_project')
     assert not all([args.project, args.all]), (
-        'Switch --project cannot be used together with --no-grouping.')
+        'Switch --project cannot be used together with --all.')
     assert not all([args.project, args.only_orphans]), (
         'Switch --project cannot be used together with --only-orphans.')
     if args.all or args.only_orphans:
         except NotFound as e:
             return
 
+    extra = {}
+    if args.todo and not args.done:
+        extra = {'is_satisfied': False}
+    elif args.done and not args.todo:
+        extra = {'is_satisfied': True}
+
+    show_heading = (not args.no_headings and
+                    not args.all and
+                    not args.only_orphans)
+
     for project in projects:
         needs = list(find_needs(project=project, query=args.query,
-                                exclude=args.exclude))
+                                exclude=args.exclude, extra=extra))
         if not needs:
             continue
 
-        if not args.all and not args.only_orphans:
-            print(u'{0}:'.format(project or '(Orphaned needs)'))
-            print('')
+        if show_heading:
+            tmpl = (Fore.YELLOW + Style.BRIGHT + u'{0}:' +
+                    Style.NORMAL + Fore.RESET)
+            yield tmpl.format(project or '(Orphaned needs)')
+            yield('')
 
-        tmpl = u'{pk} {summary}' if args.show_keys else u'- {summary}'
+        if args.show_keys:
+            tmpl = u'{style}{flag} {pk} {summary}'
+        else:
+            tmpl = u'{style}{flag} {summary}'
+        tmpl = tmpl + Fore.RESET + Style.NORMAL
+
         for need in needs:
             if project is None and args.only_orphans:
                 if need.get_projects().count():
                     continue
-            print(tmpl.format(pk=need.pk, **need))
-        print('')
+            flag = '+' if need.is_satisfied else '-'
+            style = Style.DIM if need.is_satisfied else Style.NORMAL
+            line = tmpl.format(pk=need.pk, style=style, flag=flag, **need)
+            hl_tmpl = ur'{hl_back}{hl_style}\1{line_style}{line_back}'.format(
+                hl_back=Back.BLUE, hl_style=Style.NORMAL,
+                line_back=Back.RESET, line_style=style)
+            hl = re.sub(ur'(?ui)({0})'.format(args.query), hl_tmpl, line)
+            yield(hl)
 
-@arg('summary')
-@arg('-p', '--project', help='Project name to which the need should be added.')
-def add(args):
+        if show_heading:
+            yield('')
+
+@alias('add')
+@arg('summary', nargs='+')
+@arg('-p', '--project', nargs='+', help='Target project name')
+def add_need(args):
     """Creates a need with given text. Project name can be incomplete but
     unambiguous.
     """
-    fix_unicode(args, 'summary', 'project')
-    db = default_storage()
+    for func in flatten_nargs_plus, fix_unicode:
+        func(args, 'project', 'summary')
+    db = app.get_feature('document_storage').default_db
     # FIXME Docu seems to fail at filling default values such as "is_satisfied"
     need = Need(summary=args.summary, is_satisfied=False)
     project = None
         qs = SystemUnderDevelopment.objects(db)
         qs = qs.where(summary__matches_caseless=args.project)
         if not qs:
-            print('No projects matching "{0}"'.format(args.project))
+            yield('No projects matching "{0}"'.format(args.project))
             return
         if 1 < len(qs):
-            print('Found {0} projects matching "{1}". Which to use?'.format(
+            yield('Found {0} projects matching "{1}". Which to use?'.format(
                     len(qs), args.project))
             for candidate in qs:
-                print(u'- {summary}'.format(**candidate))
+                yield(u'- {summary}'.format(**candidate))
             return
         project = qs[0]
 
     # TODO: check for duplicates; require --force to add with dupes
     pk = need.save(db)    # redundant?
-    print(u'Added need: {0}'.format(need.summary))
+    yield(u'Added need: {0}'.format(need.summary))
     if project:
         project.needs.append(need.pk)
         project.save()
-        print(u'  to project: {0}.'.format(project.summary))
-    print('  primary key: {0}.'.format(pk))
+        yield(u'  to project: {0}.'.format(project.summary))
+    yield('  primary key: {0}.'.format(pk))
 
+@alias('ren')
+#@arg('-k', '--primary-key')
+@arg('-p', '--project')
+@arg('-q', '--query')
+@arg('-k', '--primary-key')
+@arg('-d', '--dry-run', default=False)
+#@arg('-t', '--to')
+@arg('to', nargs='+')
+def rename_need(args):
+    """Renames matching item. Requires exactly one match.
+    If `summary` is in the form "/foo/bar/", it is interpreted as a regular
+    expression.
+
+    Usage::
+
+        $ needs rename -q "teh stuff" the stuff
+        $ needs rename -q "teh stuff" /teh/the/
+
+    """
+    for func in flatten_nargs_plus, fix_unicode:
+        func(args, 'query', 'project', 'to')
+    summary = args.to.strip() if args.to else None
+    assert summary
+    if args.primary_key:
+        db = app.get_feature('document_storage').default_db
+        need = db.get(Need, args.primary_key)
+    else:
+        try:
+            # TODO: support batch renames (with regex only?)
+            need = get_single(find_needs, query=args.query)
+        except (NotFound, MultipleMatches) as e:
+            yield(u'Bad query "{0}": {1}'.format(args.query, e))
+
+    # if new value looks like a regex, then use it that way
+    match = re.match('^/([^/]+?)/([^/]+?)/$', summary)
+    if match:
+        old, new = match.groups()
+        yield(u'Using regex: replacing "{old}" with "{new}"'.format(**locals()))
+        summary = re.sub(old, new, need.summary)
+
+    if need.summary == summary:
+        yield(u'Nothing changed.')
+    else:
+        yield(u'Renaming "{0}" to "{1}"'.format(need.summary, summary))
+        if args.dry_run:
+            yield('Simulation: nothing was actually changed in the database.')
+        else:
+            need.summary = summary
+            need.save()
+
+@alias('mv')
 @arg('-q', '--query', help='source need\'s summary must match this')
 @arg('-p', '--project', help='source project\'s summary must match this')
 @arg('-k', '--primary-key', help='source need\'s primary key')
 @arg('-t', '--target-project', default='')
 @arg('--steal', default=False, help='steal needs from other projects (if any)')
 @arg('-d', '--dry-run', default=False)
-def mv(args):
+def move_need(args):
     """Moves matching needs to given target project. If the target is empty,
     does nothing. If the target is empty and --steal flag is set, the needs
     become orphaned (without projects). If target is specified and --steal flag
     is set, the needs are moved from old project to the target; otherwise they
     become listed in both.
     """
-    fix_unicode(args, 'query', 'project', 'target_project')
+    for func in flatten_nargs_plus, fix_unicode:
+        func(args, 'query', 'project', 'target_project')
     if args.target_project:
         try:
             target = get_single(find_projects, args.target_project)
         except (NotFound, MultipleMatches) as e:
-            print(u'Bad target "{0}": {1}'.format(args.target_project, e))
+            yield(u'Bad target "{0}": {1}'.format(args.target_project, e))
             return
-        print(u'Moving matching needs to project {0.summary}...'.format(target))
+        yield(u'Moving matching needs to project {0.summary}...'.format(target))
     else:
         target = None
         if not args.steal:
-            print('Cannot use empty --target-project without --steal.')
+            yield('Cannot use empty --target-project without --steal.')
             return
-        print(u'Unlinking matching needs from their projects...')
+        yield(u'Unlinking matching needs from their projects...')
 
     # iterate *source* projects (if none specified, use dummy None project)
     projects = find_projects(query=args.project) if args.project else [None]
             continue
         needs = find_needs(project=project, query=args.query)
         for need in needs:
-            print(u'* {0}'.format(need.summary))
+            yield(u'* {0}'.format(need.summary))
             if args.primary_key:
-                print(u'  primary key: {0}'.format(need.pk))
-            if target:
+                yield(u'  primary key: {0}'.format(need.pk))
+            if target and target.needs:  # target.needs may be None
                 if need not in target.needs:
                     target.needs.append(need)
-                    print(u'  added to {0}'.format(target.summary))
+                    yield(u'  added to {0}'.format(target.summary))
             if args.steal:
                 # remove item from current project (and leave in others; if
                 # user needs to remove from certain or all projects, they
                     pks = [x.pk for x in p.needs]
                     p.needs.pop(pks.index(need.pk))
                     p.save()
-                    print(u'  unlinked from {0}'.format(p.summary))
+                    yield(u'  unlinked from {0}'.format(p.summary))
     if target:
         target.save()
         pass
 
     if args.dry_run:
-        print('Simulation: nothing was actually changed in the database.')
+        yield('Simulation: nothing was actually changed in the database.')
 
+@alias('rm')
 @arg('-p', '--project')
 @arg('-q', '--query')
 @arg('-k', '--primary-key')
 @arg('-d', '--dry-run', default=False)
-def rm(args):
+def delete_need(args):
     """Deletes needs with given primary key or with summary matching given
     query. If the query matches more than one item, the operation is cancelled.
     """
+    for func in flatten_nargs_plus, fix_unicode:
+        func(args, 'query', 'project')
     if args.primary_key:
-        print('Deleting need with primary key {0}'.format(args.primary_key))
-        db = default_storage()
+        yield('Deleting need with primary key {0}'.format(args.primary_key))
+        db = app.get_feature('document_storage').default_db
         need = db.get(Need, args.primary_key)
-        remove_need(need, dry_run=args.dry_run)
+        if confirm(u'Delete need {summary}'.format(**need)):
+            if not args.dry_run:
+                remove_need(need, dry_run=args.dry_run)
+        else:
+            yield('Operation cancelled.')
     elif args.query or args.project:
         try:
             needs = ensure_results(find_needs, project=args.project,
                                    query=args.query)
         except NotFound as e:
-            print('Cannot delete items: {0}'.format(e))
-            return
-        print('Deleting needs:')
+            raise CommandError('Cannot delete items: {0}'.format(e))
+        yield('Matching needs:')
         for need in needs:
-            print(u'- {summary}'.format(**need))
-            print('  primary key: {0}'.format(need.pk))
-            remove_need(need, dry_run=args.dry_run)
+            yield(u'- {summary}'.format(**need))
+            yield('  primary key: {0}'.format(need.pk))
+        if confirm('Delete these items'):
+            if not args.dry_run:
+                for need in needs:
+                    yield('Dropping {summary}…'.format(**need))
+                    remove_need(need, dry_run=args.dry_run)
+        else:
+            yield('Operation cancelled.')
     else:
-        print('Please specify either --primary-key or --query/--project.')
+        yield('Please specify either --primary-key or --query/--project.')
 
     if args.dry_run:
-        print('Simulation: nothing was actually changed in the database.')
+        yield('Simulation: nothing was actually changed in the database.')
+
+@alias('mark')
+@arg('-p', '--project')
+@arg('-q', '--query')
+@arg('-k', '--primary-key')
+@arg('--satisfied', default=False)#, help='mark the need as satisfied')
+@arg('--unsatisfied', default=False)#, help='mark the need as not satisfied')
+@arg('--important', default=False)
+@arg('--unimportant', default=False)
+@arg('-y', '--yes', default=False, help='answer "yes" instead of prompting')
+@arg('-d', '--dry-run', default=False)
+def mark_need(args):
+    "Marks matching needs as satisfied."
+    for func in flatten_nargs_plus, fix_unicode:
+        func(args, 'query', 'project')
+    assert not (args.important and args.unimportant)
+    assert not (args.satisfied and args.unsatisfied)
+    assert any([args.satisfied, args.unsatisfied,
+                args.important, args.unimportant]), 'A flag must be chosen'
+    if args.primary_key:
+        assert not (args.query or args.project), (
+            '--primary-key cannot be combined with --query/--project')
+        db = app.get_feature('document_storage').default_db
+        needs = [db.get(Need, args.primary_key)]
+    else:
+        needs = ensure_results(find_needs, project=args.project, query=args.query)
+    for need in needs:
+        satisfied = 'satisfied' if need.is_satisfied else 'unsatisfied'
+        important = 'important' if not need.is_discarded else 'unimportant'
+        yield(u'- {summary} ({satisfied}, {important})'.format(
+            satisfied=satisfied, important=important, **need))
+    yield('')
+    if confirm('Apply changes to these items', default=True, skip=args.yes):
+        if args.dry_run:
+            yield('Simulation: nothing was actually changed in the database.')
+        else:
+            for need in needs:
+                yield(u'Marking "{summary}"...'.format(**need))
+                if args.satisfied and not need.is_satisfied:
+                    yield(u'  + unsatisfied → satisfied')
+                    need.is_satisfied = True
+                if args.unsatisfied and need.is_satisfied:
+                    yield(u'  + satisfied → unsatisfied')
+                    need.is_satisfied = False
+                if args.important and need.is_discarded:
+                    need.is_discarded = False
+                    yield(u'  + discarded → important')
+                if args.unimportant and not need.is_discarded:
+                    yield(u'  + important → discarded')
+                    need.is_discarded = True
+                need.save()
+            yield('Changes have been applied.')
+    else:
+        yield('Operation cancelled.')

orgtool/ext/needs/schema.py

 # -*- coding: utf-8 -*-
 
-from docu import Field as f, Many
+from doqu import Field as f, Many
 
 from orgtool.ext.tracking import TrackedDocument
 #from orgtool.ext.events.schema import HierarchicalPlan as Plan, Event
-from orgtool.ext.contacts.schema import Actor
+from orgtool.ext.actors.schema import Actor
 from orgtool.ext.events.schema import HierarchicalPlan, Plan, Event
 
 
+__all__ = [
+    'SystemUnderDevelopment', 'Need', 'Feature',
+    'ReasonedPlan', 'ReasonedEvent',
+]
+
+
 class Need(TrackedDocument):  # потребность
     """
     Intention, goal, test (TDD), user story (XP), positive outome (GTD),

orgtool/ext/needs/templates/need_detail.html

 
 <p>
     {% for project in object.get_projects() %}
-        <a href="{{ url_for('devel_tasks.project', pk=project.pk) }}">{{ project }}</a>
+        <a href="{{ url_for('orgtool.ext.needs.views.project', pk=project.pk) }}">{{ project }}</a>
     {% endfor %}
 </p>
 
 </ul>
 {% for plan in object.get_plans() %}
     <li>
-        <a href="{{ url_for('devel_tasks.plan', pk=plan.pk) }}" 
+        <a href="{{ url_for('orgtool.ext.needs.views.plan', pk=plan.pk) }}" 
            class="plan {% if not plan.is_active() %}inactive{% endif %}">{{ plan }}</a>
     </li>
 {% endfor %}
 </ul>
 {% for event in object.get_events() %}
     <li>{{ event }}
-{#        <a href="{{ url_for('devel_tasks.plan', pk=plan.pk) }}" 
+{#        <a href="{{ url_for('needs.plan', pk=plan.pk) }}" 
            class="plan {% if not plan.is_active() %}inactive{% endif %}">{{ plan }}</a>
 #}
     </li>

orgtool/ext/needs/templates/plan_detail.html

 
     {% if outcome %}
         <p class="outcome">Outcome: 
-            <a href="{{ url_for('devel_tasks.need', pk=outcome.pk) }}">
+            <a href="{{ url_for('orgtool.ext.needs.views.need', pk=outcome.pk) }}">
                 {{ outcome }}</a></p>
     {% endif %}
 
 
     {% if object.unlocks_plan %}
         <p>Unlocks plan 
-            <a href="{{ url_for('devel_tasks.views.plan', 
+            <a href="{{ url_for('orgtool.ext.needs.views.plan', 
                                 pk=object.unlocks_plan.pk) }}">{{ object.unlocks_plan }}</a>.</p>
 
     {% endif %}
                 {% if dep.is_active() %}todo{% endif %}
                 {% if dep.is_accomplished %}accomplished{% endif %}
                 {% if dep.is_cancelled() %}cancelled{% endif %}">
-                <a href="{{ url_for('devel_tasks.views.plan', pk=dep.pk) }}">{{ dep }}</a></li>
+                <a href="{{ url_for('orgtool.ext.needs.views.plan', pk=dep.pk) }}">{{ dep }}</a></li>
         {% endfor %}
         </ul>
     {% endif %}

orgtool/ext/needs/templates/plan_index.html

         {% for plan in object_list.where_not(valid_until__in=[None]).order_by('valid_until') %}
             {% if plan.is_active() %}
                 <li><span class="date">{{ render_rel_delta(plan.valid_until) }}</span>
-                    <a href="{{ url_for('devel_tasks.plan', pk=plan.pk) }}">{{ plan }}</a>
+                    <a href="{{ url_for('orgtool.ext.needs.views.plan', pk=plan.pk) }}">{{ plan }}</a>
                 </li>
             {% endif %}
         {% endfor %}
         {% for plan in object_list.where_not(valid_since__in=[None]).order_by('valid_since') %}
             {% if plan.is_stalled() and not plan.skip_past_events %}
                 <li><span class="date">{{ render_rel_delta(plan.valid_since) }}</span>
-                    <a href="{{ url_for('devel_tasks.plan', pk=plan.pk) }}">{{ plan }}</a>
+                    <a href="{{ url_for('orgtool.ext.needs.views.plan', pk=plan.pk) }}">{{ plan }}</a>
                 </li>
             {% endif %}
         {% endfor %}
                     {% if plan.next_date_time.time() and is_date_within_a_day(plan.next_date_time) %}
                         <span class="date">{{ plan.next_date_time.strftime("%H:%M") }}</span>
                     {% endif %}
-                    <a href="{{ url_for('devel_tasks.plan', pk=plan.pk) }}">{{ plan }}</a>
+                    <a href="{{ url_for('orgtool.ext.needs.views.plan', pk=plan.pk) }}">{{ plan }}</a>
                 </li>
             {#% endif %#}
         {% endfor %}
 <ul style="overflow: hidden;">
 {% for plan in object_list.where(unlocks_plan__in=[None]) %}
     <li style="float: left; margin-left: 4ex;">
-        <a href="{{ url_for('devel_tasks.views.plan', pk=plan.pk) }}"
+        <a href="{{ url_for('orgtool.ext.needs.views.plan', pk=plan.pk) }}"
            class="{% if plan.is_active() %}active{% else %}inactive{% endif %}
                   {% if plan.valid_until %}has-deadline{% endif %}">
         {{ plan }} 
 <dl>
 {% for plan in object_list.where(unlocks_plan__in=[None]) %}
     <dt class="{% if not plan.is_active() %}outdated{% endif %}">
-        <h2><a href="{{ url_for('devel_tasks.views.plan', pk=plan.pk) }}">{{ plan }}</a></h2>
+        <h2><a href="{{ url_for('orgtool.ext.needs.views.plan', pk=plan.pk) }}">{{ plan }}</a></h2>
     </dt>
     {% if plan.is_active() %}
     <dd>
         requires: 
         <ul>
         {% for dep in object_list.where(unlocks_plan=plan.pk) %}
-            <li><a href="{{ url_for('devel_tasks.views.plan', pk=dep.pk)}}">{{ dep }}</a></li>
+            <li><a href="{{ url_for('orgtool.ext.needs.views.plan', pk=dep.pk)}}">{{ dep }}</a></li>
         {% endfor %}
         </ul>
     </dd>

orgtool/ext/needs/templates/project_detail.html

     <ul>
     {% for need in object.needs %}
         <li>
-            <a href="{{ url_for('devel_tasks.need', 
+            <a href="{{ url_for('orgtool.ext.needs.views.need', 
                                 pk=need.pk) }}"
                 class="need
                     {% if need.is_satisfied %}satisfied{% endif %}
     </ul>
     {% for plan in object.get_plans() %}
         <li>
-            <a href="{{ url_for('devel_tasks.plan', pk=plan.pk) }}" 
+            <a href="{{ url_for('orgtool.ext.needs.views.plan', pk=plan.pk) }}" 
                class="plan {% if not plan.is_active() %}inactive{% endif %}">{{ plan }}</a>
         </li>
     {% endfor %}

orgtool/ext/needs/templates/project_index.html

 <ul>
 {% for project in object_list %}
     <li>{% if not project.is_active() %}(?){% endif %}
-        <a href="{{ url_for('devel_tasks.project', pk=project.pk) }}">{{ project }}</a>
+        <a href="{{ url_for('orgtool.ext.needs.views.project', pk=project.pk) }}">{{ project }}</a>
         {% if project.is_active() %}
             {% if project.stakeholders %}
 {% for project in object_list %}
     <li>
         <h3>{% if not project.is_active() %}INACTIVE{% endif %}
-            <a href="{{ url_for('devel_tasks.project', pk=project.pk) }}">{{ project }}</a>
+            <a href="{{ url_for('orgtool.ext.needs.views.project', pk=project.pk) }}">{{ project }}</a>
         </h3>
         {% if project.is_active() %}
             {% if project.stakeholders %}
                 {XXX#
                 <p>Plans:
                 {% for plan in project.get_plans() %}
-                    <a href="{{ url_for('devel_tasks.plan', pk=plan.pk) }}"
+                    <a href="{{ url_for('orgtool.ext.needs.views.plan', pk=plan.pk) }}"
                        style="{% if not plan.is_active() %}text-decoration:
                        line-through{% endif %}">{{ plan }}</a>
                     {%- if not loop.last %},{% endif %}

orgtool/ext/needs/views.py

 
 import datetime
 
-from docu.validators import ValidationError
+from doqu.validators import ValidationError
 
 from tool.routing import url, redirect_to
 from tool.ext.who import requires_auth
 @url('/projects/')
 @requires_auth
 @entitled(u'Projects')
-@as_html('devel_tasks/project_index.html')
+@as_html('needs/project_index.html')
 def project_index(request):
     db = default_storage()
     projects = SystemUnderDevelopment.objects(db).order_by('summary')
 @url('/projects/<string:pk>')
 @requires_auth
 @entitled(lambda pk: u'{0}'.format(default_storage().get(SystemUnderDevelopment,pk)))
-@as_html('devel_tasks/project_detail.html')
+@as_html('needs/project_detail.html')
 def project(request, pk):
     db = default_storage()
     obj = db.get(SystemUnderDevelopment, pk)
 @url('/needs/')
 @requires_auth
 @entitled(u'Needs')
-@as_html('devel_tasks/need_index.html')
+@as_html('needs/need_index.html')
 def need_index(request):
     db = default_storage()
     needs = Need.objects(db).order_by('summary')
 @url('/needs/add')
 @requires_auth
 @entitled(u'Add a need')
-@as_html('devel_tasks/add_need.html')
+@as_html('needs/add_need.html')
 def add_need(request):
     db = default_storage()
 
     ###
     import wtforms
-#    from docu.ext.forms import document_form_factory
+#    from doqu.ext.forms import document_form_factory
 #    BaseNeedForm = document_form_factory(Need, storage=db)
     class NeedForm(wtforms.Form):
         summary = wtforms.TextField('summary')
             obj = Need()
             form.populate_obj(obj)
             obj.save(db)
-            return redirect_to('devel_tasks.need', pk=obj.pk)
+            return redirect_to('needs.need', pk=obj.pk)
     return {'form': form}
 
 @url('/needs/<string:pk>')
 @requires_auth
 @entitled(lambda pk: u'{0}'.format(default_storage().get(Need,pk)))
-@as_html('devel_tasks/need_detail.html')
+@as_html('needs/need_detail.html')
 def need(request, pk):
     db = default_storage()
     obj = db.get(Need, pk)
 @url('/plans/')
 @requires_auth
 @entitled(u'Plans')
-@as_html('devel_tasks/plan_index.html')
+@as_html('needs/plan_index.html')
 def plan_index(request):
     db = default_storage()
     plans = Plan.objects(db).order_by('valid_until', reverse=True)
 @url('/plans/<string:pk>')
 @requires_auth
 @entitled(lambda pk: u'{0}'.format(default_storage().get(Plan,pk)))
-@as_html('devel_tasks/plan_detail.html')
+@as_html('needs/plan_detail.html')
 def plan(request, pk):
     db = default_storage()
     # TODO: drop hierarchy, stick to semantics (reference to Need document)
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.