class Menu(Interaction): """IVR-based menu, returns options selected by the user and keypresses The Menu holds a collection of Option instances along with a prompt which presents those options to the user. The menu will attempt to collect the user's selected option up to maxRepetitions times, playing the prompt each time. If tellInvalid is true, will allow any character being pressed to stop the playback, and will tell the user if the pressed character is not recognised. Otherwise will simply ignore a pressed character which isn't part of an Option object's 'option' property. The menu will chain into callable Options, so that SubMenu and ExitOn can be used to produce effects such as multi-level menus with options to return to the parent menu level. Returns [(option,char(pressedKey))...] for each level of menu explored """ INVALID_OPTION_FILE = 'pm-invalid-option' prompt = common.ListProperty( "prompt", """(Set of) prompts to run, can be Prompt instances or filenames Used by the PromptRunner to produce prompt selections """, ) textPrompt = common.StringProperty( "textPrompt", """Textual prompt describing the option""", ) options = common.ListProperty( "options", """Set of options the user may select""", ) tellInvalid = common.IntegerProperty( "tellInvalid", """Whether to tell the user that their selection is unrecognised""", defaultValue=True, ) runnerClass = MenuRunner
class Tag(propertied.Propertied): """Represents a particular tag within a document""" name = common.StringProperty( "name", "The name of the tag", defaultValue="", ) attributes = common.DictionaryProperty( "attributes", """The in-tag attributes of the tag""", defaultFunction=lambda x, y: {}, ) content = common.ListProperty( "content", """The content (children) of the tag""", setDefaultOnGet=1, defaultFunction=lambda x, y: [], ) def __cmp__(self, other): """Compare this tag to another""" if not isinstance(other, Tag): return -1 if other.name != self.name: return cmp(self.name, other.name) if other.attributes != self.attributes: return cmp(self.attributes, other.attributes) if other.content != self.content: return cmp(self.content, other.content) return 0 def __repr__(self): """Create a decent representation of this tag""" fragments = [] name = self.name.decode().encode('utf-8') fragments.append("<" + name) for key, value in self.attributes.items(): fragments.append("""%s=%r""" % (key, value)) fragments = [" ".join(fragments)] if self.content: fragments.append(">") for item in self.content: if isinstance(item, str): fragments.append(item) else: fragments.append(repr(item)) fragments.append("</%s>" % (name)) else: fragments.append("/>") return "".join(fragments)
class CollectAudio(Interaction): """Collects audio file from the user""" prompt = common.ListProperty( "prompt", """(Set of) prompts to run, can be Prompt instances or filenames Used by the PromptRunner to produce prompt selections """, ) textPrompt = common.StringProperty( "textPrompt", """Textual prompt describing the option""", ) temporaryFile = common.StringLocaleProperty( "temporaryFile", """Temporary file into which to record the audio before moving to filename""", ) filename = common.StringLocaleProperty( "filename", """Final filename into which to record the file...""", ) deleteOnFail = common.BooleanProperty( "deleteOnFail", """Whether to delete failed attempts to record a file""", defaultValue=True) escapeDigits = common.StringLocaleProperty( "escapeDigits", """Set of digits which escape from recording the file""", defaultValue='#*0123456789', ) timeout = common.FloatProperty( "timeout", """Duration to wait for recording (maximum record time)""", defaultValue=60, ) silence = common.FloatProperty( "silence", """Duration to wait for recording (maximum record time)""", defaultValue=5, ) beep = common.BooleanProperty( "beep", """Whether to play a "beep" sound at beginning of recording""", defaultValue=True, ) runnerClass = CollectAudioRunner
class PromptRunner(propertied.Propertied): """Prompt formed from list of sub-prompts """ elements = common.ListProperty( "elements", """Sub-elements of the prompt to be presented""", ) agi = basic.BasicProperty( "agi", """The FastAGI instance we're controlling""", ) escapeDigits = common.StringLocaleProperty( "escapeDigits", """Set of digits which escape from playing the prompt""", ) timeout = common.FloatProperty( "timeout", """Timeout on data-entry after completed reading""", ) def __call__(self): """Return a deferred that chains all of the sub-prompts in order Returns from the first of the sub-prompts that recevies a selection returns str(digit) for the key the user pressed """ return self.onNext(None) def onNext(self, result, index=0): """Process the next operation""" if result is not None: return result try: element = self.elements[index] except IndexError, err: # okay, do a waitForDigit from timeout seconds... return self.agi.waitForDigit(self.timeout).addCallback( self.processKey).addCallback(self.processLast) else:
class Survey(propertied.Propertied): """Models a single survey to be completed""" surveyId = common.IntegerProperty( "surveyId", """Unique identifier for this survey""", ) owner = basic.BasicProperty( "owner", """Owner's phone number to which to connect""", ) questions = common.ListProperty( "questions", """Set of questions which make up the survey""", ) YOU_CURRENTLY_HAVE = 'vm-youhave' QUESTIONS_IN_YOUR_SURVEY = 'vm-messages' QUESTION_IN_YOUR_SURVEY = 'vm-message' TO_LISTEN_TO_SURVEY_QUESTION = 'to-listen-to-it' TO_RECORD_A_NEW_SURVEY_QUESTION = 'to-rerecord-it' TO_FINISH_SURVEY_SETUP = 'vm-helpexit' def setupSurvey(self, agi): """AGI application to allow the user to set up the survey Screen 1: You have # questions. To listen to a question, press the number of the question. To record a new question, press pound. To finish setup, press star. """ seq = fastagi.InSequence() seq.append(agi.wait, 2) base = """You currently have %s question%s. To listen to a question press the number of the question. To record a new question, press pound. To finish survey setup, press star. """ % ( len(self.questions), ['', 's'][len(self.questions) == 1], ) if len(base) != 1: base += 's' base = " ".join(base.split()) seq.append(agi.execute, 'Festival', base) seq.append(agi.finish, ) return seq() seq.append(agi.streamFile, self.YOU_CURRENTLY_HAVE) seq.append(agi.sayNumber, len(self.questions)) if len(self.questions) == 1: seq.append(agi.streamFile, self.QUESTION_IN_YOUR_SURVEY) else: seq.append(agi.streamFile, self.QUESTIONS_IN_YOUR_SURVEY) seq.append(agi.streamFile, self.TO_LISTEN_TO_SURVEY_QUESTION) seq.append(agi.streamFile, self.TO_RECORD_A_NEW_SURVEY_QUESTION) seq.append(agi.streamFile, self.TO_FINISH_SURVEY_SETUP) seq.append(agi.finish, ) return seq() def newQuestionId(self): """Return a new, unique, question id""" bad = True while bad: bad = False id = random.randint(0, sys.maxint) for question in self.questions: if id == question.__dict__.get('questionId'): bad = True return id
class Callable(propertied.Propertied): """Modelling of a callable Python object""" name = common.StringProperty( 'name', """The callable object's-name (may be different from underlying object)""", ) implementation = basic.BasicProperty( "implementation", """The underlying implementation (callable Python object)""", ) arguments = common.ListProperty( 'arguments', """Argument-list for the callable object""", baseType=listof_Arguments, ) shortHelp = common.StringProperty( 'shortHelp', """Short help-string suitable for tooltips/status-bars""", ) longHelp = common.StringProperty( 'longHelp', """Longer help-string suitable for context-sensitive help""", ) coerce = common.BooleanProperty( "coerce", """Whether to coerce arguments if possible""", defaultValue=0, ) def __init__(self, implementation, name=__NULL__, arguments=__NULL__, shortHelp=__NULL__, longHelp=__NULL__, **named): """Initialize the Callable object implementation -- a callable python object name -- if provided, will override the given name arguments -- if provided, will override calculated arguments shortHelp -- short help-string, first line of __doc__ if not given longHelp -- long help-string, entire __doc__ string if not given """ if name is __NULL__: name = self._name(implementation) if arguments is __NULL__: arguments = self._arguments(implementation) if shortHelp is __NULL__: shortHelp = self._shortHelp(implementation) if longHelp is __NULL__: longHelp = self._longHelp(implementation) super(Callable, self).__init__(implementation=implementation, name=name, arguments=arguments, **named) def __str__(self): """Return a friendly string representation""" return """%s( %s )""" % (self.__class__.__name__, self.implementation) def __call__(self, *arguments, **named): """Do the actual calling of the callable object""" set = {} for argument, value in zip(arguments, self.arguments): set[argument.name] = (argument, value) # XXX potentially there are missing positional arguments! if named: nameSet = dict([(arg.name, arg) for arg in self.arguments]) for key, value in named.items(): if set.has_key(key): raise ValueError( """Redefinition of argument order for argument %s""" % (set.get(key))) else: # note that argument may be None set[key] = nameSet.get(key), value for key, (argument, value) in set.items(): if self.coerce and argument and argument.baseType and hasattr( argument.baseType, "coerce"): value = argument.baseType.coerce(argument) set[key] = value # XXX Should keep arguments in order to allow for *args set :( return self.implementation(**set) def getArgument(self, name): """Retieve an argument by name""" for argument in self.arguments: if argument.name == name: return argument raise KeyError("""%r object doesn't have a %s argument""" % (self, name)) def _name(self, value): """Try to find a decent name for a callable object""" name = "<unknown>" for attribute in [ '__name__', 'name', 'func_name', 'co_name', '__file__', "friendlyName" ]: if hasattr(value, attribute): v = getattr(value, attribute) if isinstance(v, (str, unicode)): name = v if '.' in name: return name.split('.')[-1] return name def _shortHelp(self, value): """Try to find the short-docstring for an object""" if hasattr(value, '__doc__') and value.__doc__: return value.__doc__.split('\n')[0] else: return "" def _longHelp(self, value): """Try to find the short-docstring for an object""" if hasattr(value, '__doc__') and value.__doc__: return value.__doc__ else: return "" def _useCall(self, value): """Can we use __call__ to call this object? returns true if we should be able to use it """ return ( # must have __call__ hasattr(value, '__call__') and ( # call should be a function or method... hasattr(value.__call__, 'im_func') or hasattr(value.__call__, 'im_code'))) def _arguments(self, value): """Get a list of arguments for a callable object""" if self._useCall(value): value = value.__call__ if hasattr(value, 'im_func'): # receiver is a method. Drop the first argument, usually 'self'. func = value.im_func arguments = inspect.getargspec(func) if value.im_self is not None: # a bound instance or class method arguments = inspect.getargspec(func) del arguments[0][0] else: # an un-bound method pass elif hasattr(value, 'func_code') or hasattr(value, 'im_code'): # receiver is a function. func = value arguments = inspect.getargspec(func) else: raise ValueError('unknown reciever type %s %s' % (receiver, type(receiver))) names, vararg, varnamed, defaults = arguments defaults = defaults or () result = [Argument(name=name) for name in names] for name, default in zip(names[-len(defaults):], defaults): for item in result: if item.name == name: item.default = default return result def check(cls, value): """Strict check to see if value is an instance of cls""" return isinstance(value, cls) check = classmethod(check) def coerce(cls, value): """Coerce value to a Callable-object""" if cls.check(value): return value if callable(value): return cls(implementation=value, ) else: raise TypeError("Don't know how to convert %r to a %s object" % ( value, cls.__name__, )) coerce = classmethod(coerce) def __eq__(self, other): """Determine whether other is our equivalent returns true if other is of the same class, with the same primary attributes """ if self.__class__ is not other.__class__: return 0 NULL = [] for nm in ['name', 'implementation', 'arguments']: if hasattr(self, nm) and not hasattr(other, nm): return 0 elif not hasattr(self, nm) and hasattr(other, nm): return 0 elif hasattr(self, nm): if getattr(self, nm) != getattr(other, nm): return 0 return 1
class PromptRunner(propertied.Propertied): """Prompt formed from list of sub-prompts """ elements = common.ListProperty( "elements", """Sub-elements of the prompt to be presented""", ) agi = basic.BasicProperty( "agi", """The FastAGI instance we're controlling""", ) escapeDigits = common.StringLocaleProperty( "escapeDigits", """Set of digits which escape from playing the prompt""", ) timeout = common.FloatProperty( "timeout", """Timeout on data-entry after completed reading""", ) def __call__(self): """Return a deferred that chains all of the sub-prompts in order Returns from the first of the sub-prompts that recevies a selection returns str(digit) for the key the user pressed """ return self.onNext(None) def onNext(self, result, index=0): """Process the next operation""" if result is not None: return result try: element = self.elements[index] except IndexError as err: # okay, do a waitForDigit from timeout seconds... return self.agi.waitForDigit(self.timeout).addCallback( self.processKey).addCallback(self.processLast) else: df = element.read(self.agi, self.escapeDigits) df.addCallback(self.processKey) df.addCallback(self.onNext, index=index + 1) return df def processKey(self, result): """Does the pressed key belong to escapeDigits?""" if isinstance(result, tuple): # getOption result... if result[1] == 0: # failure during load of the file... log.warn("Apparent failure during load of audio file: %s", self.value) result = 0 else: result = result[0] if isinstance(result, str): if result: result = ord(result) else: result = 0 if result: # None or 0 # User pressed a key during the reading... key = chr(result) if key in self.escapeDigits: log.info('Exiting early due to user press of: %r', key) return key else: # we don't warn user in this menu if they press an unrecognised key! log.info( 'Ignoring user keypress because not in escapeDigits: %r', key) # completed reading without any escape digits, continue reading return None def processLast(self, result): if result is None: result = '' return result