Source code for pybot.minitel.forms

# -*- coding: utf-8 -*-

""" A simple Minitel forms management toolkit.
"""

from collections import namedtuple
import json
import time

from .core import Minitel
from .constants import *

__author__ = 'Eric Pascual'


[docs]class Form(object): """ Defines the forms in terms of layout and field connection with the application. A form is made of static prompts and entry fields. The class handles its rendering and the management of user interactions when modifying its content. The definition of the form can be provided by calls to :py:meth:`add_prompt` and :py:meth:`add_field`. This can also be done by loading the equivalent JSON data structure, using :py:meth:`load_definition`. For convenience, the :py:meth:`dump_definition` does the reverse operation, which allows producing the JSON data directly from the current definition of the form. Warning: The class is intended to be used in Videotex mode. Maybe it works fine in Teleinfo mode too, but it has not been tested. By the way, to ensure all goes well, the Minitel is switched in Videotex mode when rendering forms. """ def __init__(self, mt): """ Parameters: mt (:py:class:`Minitel`): the Minitel instance """ if not (mt and isinstance(mt, Minitel)): raise ValueError('missing or invalid mt parameter') self._mt = mt self._width = 80 if mt.is_w80() else 40 self._prompts = [] self._fields = {} self._fields_sequence = [] self._prepared = False
[docs] def add_prompt(self, x, y, text): """ Adds a fixed text to the form, at a given position. Parameters: x (int): X coordinate of the prompt start position y (int): Y coordinate of the prompt start position text (str): the prompt text (can include an attributes sequence) """ self._prompts.append(PromptDefinition(x, y, text)) self._prepared = False
[docs] def add_field(self, name, x, y, size, marker='.'): """ Adds a field to the form, at a given position and with a given size. Parameters: name (str): the field name x (int): X coordinate of the prompt start position y (int): Y coordinate of the prompt start position size (int): the field size marker (char): the character used to mark the fields area (default: '.') """ self._fields[name] = FieldDefinition(x, y, size, marker or '.') self._prepared = False
def _screen_pos(self, o): return o.y * self._width + o.x def _cmp_prompt(self, a, b): return cmp(self._screen_pos(a), self._screen_pos(b)) def _cmp_field(self, a, b): return cmp(self._screen_pos(self._fields[a]), self._screen_pos(self._fields[b]))
[docs] def prepare(self): """ Prepares the form by sorting the prompts and fields according to their position. Is automatically invoked by :py:meth:`render`. """ if self._prepared: return self._prompts.sort(self._cmp_prompt) self._fields_sequence = (sorted(self._fields.keys(), cmp=self._cmp_field)) self._prepared = True
[docs] def render(self, content=None): """ Renders the form on the screen. Should normally be invoked before calling :py:meth:`input`. Warning: The Minitel is switched in Videotex mode before processing. Parameters: content (dict): optional dictionary containing the initial field values """ self._mt.set_mode(Minitel.VIDEOTEX) content = content or {} self.prepare() self._mt.clear_screen() for prompt in self._prompts: self._mt.display_text(prompt.text, prompt.x, prompt.y) for field_name in self._fields_sequence: field = self._fields[field_name] value = content.get(field_name, '') self._mt.display_text(value.ljust(field.size, field.marker), field.x, field.y)
[docs] def input(self, content=None, max_wait=None): """ Handles user interactions and return the fields content if the form is submitted. The cursor is made visible on start, and hidden back when exiting. Special keys are interpreted as follows : ``ENVOI`` (SEND) submits the form, returning the fields content as a dictionary ``SOMMAIRE`` (CONTENT) cancels and returns None ``RETOUR`` (BACK) jumps to the previous field (or to the last one if current on the first one) ``SUITE`` (NEXT) jumps to the next field (or to the first one if current on the last one) ``CORRECTION`` backspaces one character in the field ``ANNULATION`` (CANCEL) clears the field Parameters: content (dict): optional dictionary containing the initial field values max_wait (int): maximum wait time in seconds for filling and validating the form (if None, waits indefinitely) Returns: dict: the fields content if the form has been submitted, None otherwise. """ content = content or {} field_num = 0 field_count = len(self._fields_sequence) self._mt.show_cursor() try: limit = time.time() + (max_wait if max_wait else float('inf')) while time.time() < limit: remain = limit - time.time() field_name = self._fields_sequence[field_num] field = self._fields[field_name] value, key = self._mt.rlinput( field.size, field.marker, (field.x, field.y), content.get(field_name, ''), max_wait=remain ) if key in (None, KeyCode.CONTENT): return None content[field_name] = value if key == KeyCode.SEND: return content if key in (KeyCode.NEXT, CR): field_num = (field_num + 1) % field_count elif key == KeyCode.PREV: field_num = (field_num - 1) % field_count else: self._mt.beep() finally: self._mt.show_cursor(False)
[docs] def render_and_input(self, content=None): """ A shortcut for the render / input sequence. Refer to :py:meth:`render` and :py:meth:`input` for documentation of the parameters and return value. """ self.render(content) return self.input(content)
[docs] def load_definition(self, data): """ Loads the form definition from the provided JSON structure. Refer to :py:meth:`dump_definition` documentation for the structure specifications. Parameters: data (str): the form definition in JSON format Raises: ValueError: if no data or invalid JSON data provided """ if not data: raise ValueError('no definition provided') try: defs = json.loads(data) self._fields = {} self._prompts = [] for prompt_def in defs['prompts']: x, y, text = prompt_def self.add_prompt(int(x), int(y), text) for field_name, field_def in defs['fields'].iteritems(): x, y, size, marker = (field_def + ['.'])[:4] self.add_field(str(field_name), int(x), int(y), int(size), str(marker)) except ValueError as e: raise ValueError('invalid form definition data (%s)' % e)
[docs] def dump_definition(self): """ Returns the form current definition as a JSON formatted structure. The returned structure is a dictionary, containing the two top-level entries ``prompts`` and ``fields``. The value of the ``prompts`` entry is a list of tuples, composed of : - the X position - the Y position - the prompt text The value of the ``fields`` entry is a sub-dictionary, keyed by the field names, and which values are tuples, composed of : - the X position - the Y position - the size (in characters) - the marker character (defaulted to ``.`` if not included) Example: :: { "prompts": [ [0, 2, "First name"], [0, 4, "Last name"], [30, 23, "ENVOI"] ], "fields": { "lname": [15, 4, 20, "."], "fname": [15, 2, 20, "."] } } Returns: str: JSON form definition """ data = { 'prompts': self._prompts, 'fields': self._fields } return json.dumps(data)
PromptDefinition = namedtuple('PromptDefinition', 'x y text') class FieldDefinition(namedtuple('FieldDefinition', 'x y size marker')): __slots__ = () def __new__(cls, x, y, size, marker='.'): if not 0 <= x < 40: raise ValueError('invalid x position : %s' % x) if not 0 <= y <= 23: raise ValueError('invalid y position : %s' % y) if not 0 <= size < 40: raise ValueError('invalid field size : %s' % size) return super(FieldDefinition, cls).__new__(cls, x, y, size, (marker or '.')[0])