diff sat_frontends/jp/cmd_event.py @ 2562:26edcf3a30eb

core, setup: huge cleaning: - moved directories from src and frontends/src to sat and sat_frontends, which is the recommanded naming convention - move twisted directory to root - removed all hacks from setup.py, and added missing dependencies, it is now clean - use https URL for website in setup.py - removed "Environment :: X11 Applications :: GTK", as wix is deprecated and removed - renamed sat.sh to sat and fixed its installation - added python_requires to specify Python version needed - replaced glib2reactor which use deprecated code by gtk3reactor sat can now be installed directly from virtualenv without using --system-site-packages anymore \o/
author Goffi <goffi@goffi.org>
date Mon, 02 Apr 2018 19:44:50 +0200
parents frontends/src/jp/cmd_event.py@772447ec070f
children a5b96950b81a
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sat_frontends/jp/cmd_event.py	Mon Apr 02 19:44:50 2018 +0200
@@ -0,0 +1,416 @@
+#!/usr/bin/env python2
+# -*- coding: utf-8 -*-
+
+# jp: a SàT command line tool
+# Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org)
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+
+import base
+from sat.core.i18n import _
+from sat.tools.common.ansi import ANSI as A
+from sat_frontends.jp.constants import Const as C
+from sat_frontends.jp import common
+from functools import partial
+from dateutil import parser as du_parser
+import calendar
+import time
+
+__commands__ = ["Event"]
+
+OUTPUT_OPT_TABLE = u'table'
+
+# TODO: move date parsing to base, it may be useful for other commands
+
+
+class Get(base.CommandBase):
+
+    def __init__(self, host):
+        base.CommandBase.__init__(self,
+                                  host,
+                                  'get',
+                                  use_output=C.OUTPUT_DICT,
+                                  use_pubsub=True, pubsub_flags={C.NODE, C.SINGLE_ITEM},
+                                  use_verbose=True,
+                                  help=_(u'get event data'))
+        self.need_loop=True
+
+    def add_parser_options(self):
+        pass
+
+    def eventInviteeGetCb(self, result):
+        event_date, event_data = result
+        event_data['date'] = event_date
+        self.output(event_data)
+        self.host.quit()
+
+    def start(self):
+        self.host.bridge.eventGet(
+            self.args.service,
+            self.args.node,
+            self.args.item,
+            self.profile,
+            callback=self.eventInviteeGetCb,
+            errback=partial(self.errback,
+                            msg=_(u"can't get event data: {}"),
+                            exit_code=C.EXIT_BRIDGE_ERRBACK))
+
+
+class EventBase(object):
+
+    def add_parser_options(self):
+        self.parser.add_argument("-i", "--id", type=base.unicode_decoder, default=u'', help=_(u"ID of the PubSub Item"))
+        self.parser.add_argument("-d", "--date", type=unicode, help=_(u"date of the event"))
+        self.parser.add_argument("-f", "--field", type=base.unicode_decoder, action='append', nargs=2, dest='fields',
+                                 metavar=(u"KEY", u"VALUE"), help=_(u"configuration field to set"))
+
+    def parseFields(self):
+        return dict(self.args.fields) if self.args.fields else {}
+
+    def parseDate(self):
+        if self.args.date:
+            try:
+                date = int(self.args.date)
+            except ValueError:
+                try:
+                    date_time = du_parser.parse(self.args.date, dayfirst=not (u'-' in self.args.date))
+                except ValueError as e:
+                    self.parser.error(_(u"Can't parse date: {msg}").format(msg=e))
+                if date_time.tzinfo is None:
+                    date = calendar.timegm(date_time.timetuple())
+                else:
+                    date = time.mktime(date_time.timetuple())
+        else:
+            date = -1
+        return date
+
+
+class Create(EventBase, base.CommandBase):
+    def __init__(self, host):
+        super(Create, self).__init__(host, 'create', use_pubsub=True, pubsub_flags={C.NODE}, help=_('create or replace event'))
+        EventBase.__init__(self)
+        self.need_loop=True
+
+    def eventCreateCb(self, node):
+        self.disp(_(u'Event created successfuly on node {node}').format(node=node))
+        self.host.quit()
+
+    def start(self):
+        fields = self.parseFields()
+        date = self.parseDate()
+        self.host.bridge.eventCreate(
+            date,
+            fields,
+            self.args.service,
+            self.args.node,
+            self.args.id,
+            self.profile,
+            callback=self.eventCreateCb,
+            errback=partial(self.errback,
+                            msg=_(u"can't create event: {}"),
+                            exit_code=C.EXIT_BRIDGE_ERRBACK))
+
+
+class Modify(EventBase, base.CommandBase):
+    def __init__(self, host):
+        super(Modify, self).__init__(host, 'modify', use_pubsub=True, pubsub_flags={C.NODE}, help=_('modify an existing event'))
+        EventBase.__init__(self)
+        self.need_loop=True
+
+    def start(self):
+        fields = self.parseFields()
+        date = 0 if not self.args.date else self.parseDate()
+        self.host.bridge.eventModify(
+            self.args.service,
+            self.args.node,
+            self.args.id,
+            date,
+            fields,
+            self.profile,
+            callback=self.host.quit,
+            errback=partial(self.errback,
+                            msg=_(u"can't update event data: {}"),
+                            exit_code=C.EXIT_BRIDGE_ERRBACK))
+
+
+class InviteeGet(base.CommandBase):
+
+    def __init__(self, host):
+        base.CommandBase.__init__(self,
+                                  host,
+                                  'get',
+                                  use_output=C.OUTPUT_DICT,
+                                  use_pubsub=True, pubsub_flags={C.NODE},
+                                  use_verbose=True,
+                                  help=_(u'get event attendance'))
+        self.need_loop=True
+
+    def add_parser_options(self):
+        pass
+
+    def eventInviteeGetCb(self, event_data):
+        self.output(event_data)
+        self.host.quit()
+
+    def start(self):
+        self.host.bridge.eventInviteeGet(
+            self.args.service,
+            self.args.node,
+            self.profile,
+            callback=self.eventInviteeGetCb,
+            errback=partial(self.errback,
+                            msg=_(u"can't get event data: {}"),
+                            exit_code=C.EXIT_BRIDGE_ERRBACK))
+
+
+class InviteeSet(base.CommandBase):
+    def __init__(self, host):
+        super(InviteeSet, self).__init__(host, 'set', use_output=C.OUTPUT_DICT, use_pubsub=True, pubsub_flags={C.NODE}, help=_('set event attendance'))
+        self.need_loop=True
+
+    def add_parser_options(self):
+        self.parser.add_argument("-f", "--field", type=base.unicode_decoder, action='append', nargs=2, dest='fields',
+                                 metavar=(u"KEY", u"VALUE"), help=_(u"configuration field to set"))
+
+    def start(self):
+        fields = dict(self.args.fields) if self.args.fields else {}
+        self.host.bridge.eventInviteeSet(
+            self.args.service,
+            self.args.node,
+            fields,
+            self.profile,
+            callback=self.host.quit,
+            errback=partial(self.errback,
+                            msg=_(u"can't set event data: {}"),
+                            exit_code=C.EXIT_BRIDGE_ERRBACK))
+
+
+class InviteesList(base.CommandBase):
+
+    def __init__(self, host):
+        extra_outputs = {'default': self.default_output}
+        base.CommandBase.__init__(self,
+                                  host,
+                                  'list',
+                                  use_output=C.OUTPUT_DICT_DICT,
+                                  extra_outputs=extra_outputs,
+                                  use_pubsub=True, pubsub_flags={C.NODE},
+                                  use_verbose=True,
+                                  help=_(u'get event attendance'))
+        self.need_loop=True
+
+    def add_parser_options(self):
+        self.parser.add_argument('-m', '--missing', action='store_true', help=_(u'show missing people (invited but no R.S.V.P. so far)'))
+        self.parser.add_argument('-R', '--no-rsvp', action='store_true', help=_(u"don't show people which gave R.S.V.P."))
+
+    def _attend_filter(self, attend, row):
+        if attend == u'yes':
+            attend_color = C.A_SUCCESS
+        elif attend == u'no':
+            attend_color = C.A_FAILURE
+        else:
+            attend_color = A.FG_WHITE
+        return A.color(attend_color, attend)
+
+    def _guests_filter(self, guests):
+        return u'(' + unicode(guests) + ')' if guests else u''
+
+    def default_output(self, event_data):
+        data = []
+        attendees_yes = 0
+        attendees_maybe = 0
+        attendees_no = 0
+        attendees_missing = 0
+        guests = 0
+        guests_maybe = 0
+        for jid_, jid_data in event_data.iteritems():
+            jid_data[u'jid'] = jid_
+            try:
+                guests_int = int(jid_data['guests'])
+            except (ValueError, KeyError):
+                pass
+            attend = jid_data.get(u'attend',u'')
+            if attend == 'yes':
+                attendees_yes += 1
+                guests += guests_int
+            elif attend == 'maybe':
+                attendees_maybe += 1
+                guests_maybe += guests_int
+            elif attend == 'no':
+                attendees_no += 1
+                jid_data[u'guests'] = ''
+            else:
+                attendees_missing += 1
+                jid_data[u'guests'] = ''
+            data.append(jid_data)
+
+        show_table = OUTPUT_OPT_TABLE in self.args.output_opts
+
+        table = common.Table.fromDict(self.host,
+            data,
+            (u'nick',) + ((u'jid',) if self.host.verbosity else ()) +  (u'attend', 'guests'),
+            headers=None,
+            filters = { u'nick': A.color(C.A_HEADER, u'{}' if show_table else u'{} '),
+                        u'jid': u'{}' if show_table else u'{} ',
+                        u'attend': self._attend_filter,
+                        u'guests': u'{}' if show_table else self._guests_filter,
+                      },
+            defaults = { u'nick': u'',
+                         u'attend': u'',
+                         u'guests': 1
+                       }
+            )
+        if show_table:
+            table.display()
+        else:
+            table.display_blank(show_header=False, col_sep=u'')
+
+        if not self.args.no_rsvp:
+            self.disp(u'')
+            self.disp(A.color(
+                C.A_SUBHEADER,
+                _(u'Attendees: '),
+                A.RESET,
+                unicode(len(data)),
+                _(u' ('),
+                C.A_SUCCESS,
+                _(u'yes: '),
+                unicode(attendees_yes),
+                A.FG_WHITE,
+                _(u', maybe: '),
+                unicode(attendees_maybe),
+                u', ',
+                C.A_FAILURE,
+                _(u'no: '),
+                unicode(attendees_no),
+                A.RESET,
+                u')'
+                ))
+            self.disp(A.color(C.A_SUBHEADER, _(u'confirmed guests: '), A.RESET, unicode(guests)))
+            self.disp(A.color(C.A_SUBHEADER, _(u'unconfirmed guests: '), A.RESET, unicode(guests_maybe)))
+            self.disp(A.color(C.A_SUBHEADER, _(u'total: '), A.RESET, unicode(guests+guests_maybe)))
+        if attendees_missing:
+            self.disp('')
+            self.disp(A.color(C.A_SUBHEADER, _(u'missing people (no reply): '), A.RESET, unicode(attendees_missing)))
+
+    def eventInviteesListCb(self, event_data, prefilled_data):
+        """fill nicknames and keep only requested people
+
+        @param event_data(dict): R.S.V.P. answers
+        @param prefilled_data(dict): prefilled data with all people
+            only filled if --missing is used
+        """
+        if self.args.no_rsvp:
+            for jid_ in event_data:
+                # if there is a jid in event_data
+                # it must be there in prefilled_data too
+                # so no need to check for KeyError
+                del prefilled_data[jid_]
+        else:
+            # we replace empty dicts for existing people with R.S.V.P. data
+            prefilled_data.update(event_data)
+
+        # we get nicknames for everybody, make it easier for organisers
+        for jid_, data in prefilled_data.iteritems():
+            id_data = self.host.bridge.identityGet(jid_, self.profile)
+            data[u'nick'] = id_data.get(u'nick', u'')
+
+        self.output(prefilled_data)
+        self.host.quit()
+
+    def getList(self, prefilled_data={}):
+        self.host.bridge.eventInviteesList(
+            self.args.service,
+            self.args.node,
+            self.profile,
+            callback=partial(self.eventInviteesListCb,
+                             prefilled_data=prefilled_data),
+            errback=partial(self.errback,
+                            msg=_(u"can't get event data: {}"),
+                            exit_code=C.EXIT_BRIDGE_ERRBACK))
+
+    def psNodeAffiliationsGetCb(self, affiliations):
+        # we fill all affiliations with empty data
+        # answered one will be filled in eventInviteesListCb
+        # we only consider people with "publisher" affiliation as invited, creators are not, and members can just observe
+        prefilled = {jid_: {} for jid_, affiliation in affiliations.iteritems() if affiliation in (u'publisher',)}
+        self.getList(prefilled)
+
+    def start(self):
+        if self.args.no_rsvp and not self.args.missing:
+            self.parser.error(_(u"you need to use --missing if you use --no-rsvp"))
+        if self.args.missing:
+            self.host.bridge.psNodeAffiliationsGet(
+                self.args.service,
+                self.args.node,
+                self.profile,
+                callback=self.psNodeAffiliationsGetCb,
+                errback=partial(self.errback,
+                                msg=_(u"can't get event data: {}"),
+                                exit_code=C.EXIT_BRIDGE_ERRBACK))
+        else:
+            self.getList()
+
+
+class InviteeInvite(base.CommandBase):
+
+    def __init__(self, host):
+        base.CommandBase.__init__(self, host, 'invite', use_pubsub=True, pubsub_flags={C.NODE, C.SINGLE_ITEM}, help=_(u'invite someone to the event through email'))
+        self.need_loop=True
+
+    def add_parser_options(self):
+        self.parser.add_argument("-e", "--email", action="append", type=base.unicode_decoder, default=[], help='email(s) to send the invitation to')
+        self.parser.add_argument("-N", "--name", type=base.unicode_decoder, default='', help='name of the invitee')
+        self.parser.add_argument("-H", "--host-name", type=base.unicode_decoder, default='', help='name of the host')
+        self.parser.add_argument("-l", "--lang", type=base.unicode_decoder, default='', help='main language spoken by the invitee')
+        self.parser.add_argument("-U", "--url-template", type=base.unicode_decoder, default='', help='template to construct the URL')
+        self.parser.add_argument("-S", "--subject", type=base.unicode_decoder, default='', help='subject of the invitation email (default: generic subject)')
+        self.parser.add_argument("-b", "--body", type=base.unicode_decoder, default='', help='body of the invitation email (default: generic body)')
+
+    def start(self):
+        email = self.args.email[0] if self.args.email else None
+        emails_extra = self.args.email[1:]
+
+        self.host.bridge.eventInvite(
+            self.args.service,
+            self.args.node,
+            self.args.item,
+            email,
+            emails_extra,
+            self.args.name,
+            self.args.host_name,
+            self.args.lang,
+            self.args.url_template,
+            self.args.subject,
+            self.args.body,
+            self.args.profile,
+            callback=self.host.quit,
+            errback=partial(self.errback,
+                            msg=_(u"can't create invitation: {}"),
+                            exit_code=C.EXIT_BRIDGE_ERRBACK))
+
+
+class Invitee(base.CommandBase):
+    subcommands = (InviteeGet, InviteeSet, InviteesList, InviteeInvite)
+
+    def __init__(self, host):
+        super(Invitee, self).__init__(host, 'invitee', use_profile=False, help=_(u'manage invities'))
+
+
+class Event(base.CommandBase):
+    subcommands = (Get, Create, Modify, Invitee)
+
+    def __init__(self, host):
+        super(Event, self).__init__(host, 'event', use_profile=False, help=_('event management'))