Andy Mikhailenko avatar Andy Mikhailenko committed fd58679

Extracted a part of "contacts" extension to extension "actors". Added CLI for contacts. Upgraded to the new Tool API. (Bulk commit, unsure about dates.)

Comments (0)

Files changed (8)

orgtool/ext/actors/__init__.py

+"""
+Actors management
+=================
+
+An *actor* in the UML_ "specifies a role played by a user or any other system
+that interacts with the subject". In *OrgTool* this concept is used to
+represent people, organizations, stakeholders, etc.
+
+.. _UML: http://en.wikipedia.org/wiki/Actor_(UML)
+"""
+from .admin import *

orgtool/ext/actors/admin.py

+from tool.ext import admin
+from orgtool.ext.tracking.admin import TrackedAdmin
+from schema import Actor, Organization, Person
+
+
+NAMESPACE = 'actors'
+
+
+@admin.register_for(Actor)
+class ActorAdmin(TrackedAdmin):
+    namespace = NAMESPACE
+    list_names = 'name',
+
+
+@admin.register_for(Organization)
+class OrganizationAdmin(ActorAdmin):
+    namespace = NAMESPACE
+
+
+@admin.register_for(Person)
+class PersonAdmin(ActorAdmin):
+    namespace = NAMESPACE
+    list_names = 'name', 'first_name', 'last_name'

orgtool/ext/actors/schema.py

+# -*- coding: utf-8 -*-
+
+from doqu import Document, Field as f
+from werkzeug import cached_property
+from orgtool.ext.tracking import TrackedDocument
+
+
+__all__ = ['Actor', 'Organization', 'Person']
+
+
+class Actor(TrackedDocument):
+    name = f(unicode, required=True)
+    is_actor = f(bool, choices=[True], default=True)
+
+    def __unicode__(self):
+        return u'{name}'.format(**self)
+
+#    @cached_property
+#    def contacts(self):
+#        db = self._saved_state.storage
+#        return Contact.objects(db).where(actor=self.pk)
+
+
+class Organization(Actor):
+    is_organization = f(bool, choices=[True], default=True)
+
+
+class Person(Actor):
+    name = f(unicode, required=True,
+             default=lambda d: ' '.join([d.first_name or '',
+                                         d.last_name or '']).strip())
+    first_name = f(unicode, essential=True)
+    second_name = f(unicode, essential=True)
+    last_name = f(unicode, essential=True)
+    details = f(unicode)
+#    birth_date =

orgtool/ext/contacts/__init__.py

-from tool.ext.templating import register_templates
+# -*- coding: utf-8 -*-
+"""
+Contacts
+========
+"""
+from tool.plugins import BasePlugin
+#from tool.ext.templating import register_templates
 
 from schema import *
 import admin
-from views import *
+#from views import *
+from commands import (
+    list_contacts, open_urls, add_contact, move_contact
+)
 
 
-register_templates(__name__)
+class ContactsCLI(BasePlugin):
+    """A stripped-down CLI-only contacts management plugin.
+    """
+    features = 'contacts'
+    requires = ('{document_storage}',)
+    commands = [list_contacts, open_urls, add_contact, move_contact]
+
+
+class ContactsWeb(ContactsCLI):
+    """A CLI- and web-enabled contacts management plugin. Integrates with
+    WebAdmin.
+    """
+    requires = ContactsCLI.requires + (
+        '{routing}', '{templating}',
+        'tool.ext.admin.AdminWeb',
+    )
+
+    def make_env(self):
+        templating = self.app.get_feature('templating')
+        templating.register_templates(__name__)

orgtool/ext/contacts/admin.py

 from tool.ext import admin
 from orgtool.ext.tracking.admin import TrackedAdmin
-from schema import Actor, Organization, Person, Contact
+from schema import Contact
 
 
 NAMESPACE = 'contacts'
 
 
-@admin.register_for(Actor)
-class ActorAdmin(TrackedAdmin):
-    namespace = NAMESPACE
-    list_names = 'name',
-
-
-@admin.register_for(Organization)
-class OrganizationAdmin(ActorAdmin):
-    namespace = NAMESPACE
-
-
-@admin.register_for(Person)
-class PersonAdmin(ActorAdmin):
-    namespace = NAMESPACE
-    list_names = 'name', 'first_name', 'last_name'
-
-
 @admin.register_for(Contact)
 class ContactAdmin(TrackedAdmin):
     namespace = NAMESPACE

orgtool/ext/contacts/commands.py

+# -*- coding: utf-8 -*-
+
+import webbrowser
+
+from tool.cli import arg, alias, CommandError, confirm, Fore, Style
+from tool.ext.documents import default_storage
+from orgtool.ext.actors.schema import Actor
+from orgtool.ext.needs.helpers import (
+    MultipleMatches, NotFound, get_single, fix_unicode
+)
+
+from .schema import Contact, CONTACT_TYPES
+
+
+def find_actors(query):
+    db = default_storage()
+    return Actor.objects(db).where(name__matches_caseless=query)
+
+def bright(string):
+    return Style.BRIGHT + unicode(string) + Style.NORMAL
+
+
+@alias('ls')
+@arg('actor')
+@arg('--type', nargs='+', help='Only show these types of contacts')
+def list_contacts(args):
+    "Lists contacts for matching actors."
+    fix_unicode(args, 'actor', 'type')
+    for actor in find_actors(args.actor):
+        contacts = actor.contacts.order_by(['kind', 'scope'])
+        if args.type:
+            contacts = contacts.where(kind__in=args.type)
+        if not contacts:
+            continue
+        yield(Style.BRIGHT + Fore.YELLOW +
+              u'{0}:'.format(actor) +
+              Fore.RESET + Style.NORMAL)
+        yield('')
+        for contact in contacts:
+            summary = '' if contact.summary == actor.name else contact.summary
+            scope = '' if contact.scope == 'primary' else contact.scope
+            value = contact.value
+            if contact.kind == 'text':
+                value = contact.value.replace('\n', '\n   ')
+                template = u'\n {style}{scope}{summary}{endstyle}\n   {value}\n'
+            else:
+                template = u' {value} {style}{scope}{summary}{endstyle}'
+            scope = u'({0}) '.format(scope) if scope else ''
+            style = Style.DIM
+            endstyle = Style.NORMAL
+            yield(template.format(**locals()))
+        yield('')
+
+@alias('add')
+@arg('actor')
+@arg('value')
+@arg('-t', '--type', default='text', choices=CONTACT_TYPES)
+def add_contact(args):
+    "Adds a contact to given actor"
+    fix_unicode(args, 'actor', 'value', 'type')
+    actor = None
+    try:
+        actor = get_single(find_actors, args.actor)
+    except MultipleMatches as e:
+        raise CommandError(e)
+    except NotFound as e:
+        raise CommandError(u'No actor matches "{0}".'.format(args.actor))
+
+    db = default_storage()
+    qs = Contact.objects(db)
+    dupes = qs.where(actor=actor, kind=args.type, value=args.value)
+    if dupes.count():
+        raise CommandError('Such contact already exists.')
+
+    # FIXME Docu doesn't properly assign values from Contact(foo=bar)
+    contact = Contact()
+    contact.actor = actor
+    contact.kind = args.type
+    contact.value = args.value
+    contact.save(db)
+
+@alias('mv')
+@arg('-f', '--from-actor')
+@arg('-q', '--query')
+@arg('-t', '--type', nargs='+')
+@arg('new_actor')
+def move_contact(args):
+    "Moves matching contacts to given actor"
+    fix_unicode(args, 'from_actor', 'query', 'type', 'new_actor')
+    new_actor = None
+    try:
+        new_actor = get_single(find_actors, args.new_actor)
+    except MultipleMatches as e:
+        raise CommandError(e)
+    except NotFound as e:
+        raise CommandError(u'No actor matches "{0}".'.format(args.actor))
+
+    assert args.from_actor or args.query, 'specify actor or auery'
+
+    db = default_storage()
+    contacts = Contact.objects(db).where_not(actor=new_actor.pk)
+    if args.type:
+        contacts = contacts.where(type__in=args.type)
+    if args.query:
+        contacts = contacts.where(value__matches_caseless=args.query)
+    if args.from_actor:
+        try:
+            from_actor = get_single(find_actors, args.from_actor)
+        except MultipleMatches as e:
+            raise CommandError(e)
+        except NotFound(e):
+            raise CommandError('Bad --from-actor: no match for "{0}"'.format(
+                args.from_actor))
+        contacts = contacts.where(actor=from_actor.pk)
+
+    if not len(contacts):
+        raise CommandError('No suitable contacts were found.')
+
+    yield('About to move these contacts:\n')
+    for c in contacts:
+        yield(u'- {actor}: {kind} {v}'.format(v=bright(c.value), **c))
+    yield('')
+
+    msg = u'Move these contacts to {0}'.format(bright(new_actor))
+    if confirm(msg, default=True):
+        for c in contacts:
+            c.actor = new_actor
+            c.save(db)
+    else:
+        yield('\nCancelled.')
+
+
+@alias('go')
+@arg('actor')
+def open_urls(args):
+    "Gathers URLs for matching actors and opens them is a web browser."
+    actors = find_actors(args.actor)
+    def _contacts():
+        for a in actors:
+            for c in a.contacts.where(kind='url'):
+                yield c
+    contacts = list(_contacts())
+    yield('Found:\n')
+    for c in contacts:
+        yield(u'  {url} {actor}\n'.format(
+            url=c.value, actor=Style.DIM+unicode(c.actor)+Style.NORMAL))
+    if confirm('Open these URLs', default=True):
+        for c in contacts:
+            webbrowser.open_new_tab(c.value)
+        yield('URLs were open in new tabs.')
+    else:
+        yield('\nCancelled.')

orgtool/ext/contacts/schema.py

 # -*- coding: utf-8 -*-
 
-from docu import Document, Field as f
+from doqu import Document, Field as f
 from werkzeug import cached_property
 from orgtool.ext.tracking import TrackedDocument
+from orgtool.ext.actors.schema import Actor
 
 
-__all__ = ['Actor', 'Organization', 'Person', 'Contact']
+__all__ = ['Contact']
 
 
 CONTACT_TYPES = (
 )
 
 
-class Actor(TrackedDocument):
-    name = f(unicode, required=True)
-    is_actor = f(bool, choices=[True])
-
-    def __unicode__(self):
-        return u'{name}'.format(**self)
-
-    @cached_property
-    def contacts(self):
-        db = self._saved_state.storage
-        return Contact.objects(db).where(actor=self.pk)
-
-
-class Organization(Actor):
-    is_organization = f(bool, choices=[True])
-
-
-class Person(Actor):
-    name = f(unicode, required=True,
-             default=lambda d: ' '.join([d.first_name or '',
-                                         d.last_name or '']).strip())
-    first_name = f(unicode, essential=True)
-    second_name = f(unicode, essential=True)
-    last_name = f(unicode, essential=True)
-    details = f(unicode)
-#    birth_date =
-
-
 class Contact(TrackedDocument):
     summary = f(unicode, required=True,
-                default=lambda d: d.person.name if d.person else None)
+                default=lambda d: unicode(d.actor) or None)
     actor = f(Actor)
 
-    kind = f(unicode, required=True, choices=CONTACT_TYPES)
-    scope = f(unicode, choices=CONTACT_SCOPES, default=CONTACT_SCOPES[0][0])
+    kind = f(unicode, required=True, choices=CONTACT_TYPES, default='text')
+    scope = f(unicode, choices=CONTACT_SCOPES, default=CONTACT_SCOPES[0])
     value = f(unicode, required=True)
 
     # XXX validators?
     def __unicode__(self):
         return u'{summary} {kind} {value} ({scope})'.format(**self)
 
+# class Actor(...):
+#    @cached_property
+#    def contacts(self):
+#        db = self._saved_state.storage
+#        return Contact.objects(db).where(actor=self.pk)
+
+# TODO use Docu's reverse relationship API when it's ready
+Actor.contacts = cached_property(lambda self: Contact.objects(self._saved_state.storage).where(actor=self.pk))
+

orgtool/ext/contacts/views.py

 # -*- coding: utf-8 -*-
 
 from tool.routing import url
-from tool.ext.documents import db
+from tool.ext.documents import default_storage
 from tool.ext.templating import as_html
 from tool.ext.breadcrumbs import entitled
 
 @entitled(u'People and Organizations')
 @as_html('contacts/index.html')
 def index(request):
+    db = default_storage()
     people = Actor.objects(db).order_by('name')
     return {'object_list': people}
 
 @url('/<string:pk>')
-@entitled(lambda pk: u'{0}'.format(db.get(Actor,pk)))
+@entitled(lambda pk: u'{0}'.format(default_storage().get(Actor,pk)))
 @as_html('contacts/person.html')
 def person(request, pk):
-    person = db.get(Actor, pk)
+    person = default_storage().get(Actor, pk)
     return {'object': person}
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.