# GNU Enterprise Forms - GF Object Hierarchy - Block
#
# Copyright 2001-2009 Free Software Foundation
#
# This file is part of GNU Enterprise
#
# GNU Enterprise is free software; you can redistribute it
# and/or modify it under the terms of the GNU General Public
# License as published by the Free Software Foundation; either
# version 3, or (at your option) any later version.
#
# GNU Enterprise 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public
# License along with program; see the file COPYING. If not,
# write to the Free Software Foundation, Inc., 59 Temple Place
# - Suite 330, Boston, MA 02111-1307, USA.
#
# $Id: GFBlock.py 10016 2009-10-28 14:41:59Z reinhard $

"""
Classes making up the Block object.
"""

from gnue.common.apps import errors
from gnue.common.datasources import GConditions
from gnue.common.definitions import GParser
from gnue.common import events

from gnue.forms.GFObjects.GFTabStop import GFFieldBound
from gnue.forms.GFObjects.GFContainer import GFContainer
from gnue.forms.GFObjects.GFDataSource import GFDataSource

__all__ = ['GFBlock', 'DatasourceNotFoundError']


# =============================================================================
# <block>
# =============================================================================

class GFBlock(GFContainer, events.EventAware):
    """
    A block in a form definition.

    A block covers all aspects of a form's connection to a data source.

    Blocks can be filled with data by using the L{init_filter},
    L{change_filter}, and L{apply_filter} methods or in a single step with the
    L{set_filter} method.  The L{clear} method populates the block with a
    single empty record.

    Within the result set, blocks maintain a pointer to a current record which
    can be moved around with the L{first_record}, L{prev_record},
    L{next_record}, L{last_record}, L{goto_record}, L{jump_records}, and
    L{search_record} methods.

    Read and write access to the data of the current record (and the
    surrounding records) is possible with the L{get_value} and L{set_value}
    methods. New records can be inserted with the L{new_record} and
    L{duplicate_record} methods. Records can be marked for deletion at next
    commit with the L{delete_record} method and this mark can be undone with
    L{undelete_record}.

    The L{post} and L{requery} methods are available to write the block's
    changes back to the datasource.

    In case the block is connected to an active datasource (especially the GNUe
    AppServer), the L{update} method can be used to write the block's changes
    to the backend without committing them, in order to run trigger functions
    on the backend, while L{call} directly calls a backend function for the
    block's current record.

    The status of the current record can be determined by L{get_record_status},
    while L{is_pending} can be used to determine the status of the whole block.
    The method L{get_possible_operations} lists all operations that are
    possible for the block in its current status.

    The block keeps track of all user interface elements that are connected to
    it and keeps the user interface up to date on all its operations. It should
    always be kept in mind that this might result in bad performance for mass
    operations on the data, so the method L{get_data} is available to access
    the raw data behind the block for high speed operations.

    Whenever the user interface focus enters or leaves the block, it must be
    notified via the L{focus_in}, L{validate}, and L{focus_out} methods.
    """

    # -------------------------------------------------------------------------
    # Constructor
    # -------------------------------------------------------------------------

    def __init__(self, parent):
        """
        Create a new block instance.

        @param parent: Parent object.
        """

        GFContainer.__init__(self, parent, 'GFBlock')

        # Current mode of the block. Can be 'normal', 'commit' (during commit
        # triggers), 'init' (during record initialization triggers), or 'query'
        # (if the form is in filter mode).
        self.mode = 'normal'

        # The underlying resultset.
        self.__resultset = None

        # The attached data source.
        self._dataSourceLink = None     # FIXME: make private!

        # The current record number.
        self._currentRecord = 0         # FIXME: make private!

        # The total number of records.
        self.__record_count = 0

        # The default filter criteria, defined through the fields.
        self._queryDefaults = {}        # FIXME: make private!

        # The current filter criteria.
        self.__query_values = {}

        # The criteria of the last filter that was executed.
        self.__last_query_values = {}

        # Flag set to True while a query is running.
        self.__in_query = False

        # A list of all GFScrollbar objects bound to this block.
        self.__scrollbars = []

        # List of all entries bound to this block, populated by GFEntry's
        # initialize
        self._entryList = []            # FIXME: make private!

        # All fields of this block by name.
        self._fieldMap = {}             # FIXME: make private!

        # Current top of visible portion (only used for scrollbars, every entry
        # maintains its own visible portion because the number of visible rows
        # can differ per entry)
        self.__visible_start = 0

        # If this is true, notifying attached objects of current record changes
        # is temporarily blocked
        self.__scrolling_blocked = False

        # Trigger exposure
        self._validTriggers = {
                'ON-NEWRECORD':   'On-NewRecord',
                'ON-RECORDLOADED':'On-RecordLoaded',
                'PRE-COMMIT':     'Pre-Commit',
                'POST-COMMIT':    'Post-Commit',
                'PRE-QUERY':      'Pre-Query',
                'POST-QUERY':     'Post-Query',
                'PRE-MODIFY':     'Pre-Modify',
                'PRE-INSERT':     'Pre-Insert',
                'PRE-DELETE':     'Pre-Delete',
                'PRE-UPDATE':     'Pre-Update',
                'PRE-FOCUSIN':    'Pre-FocusIn',
                'POST-FOCUSIN':   'Post-FocusIn',
                'PRE-FOCUSOUT':   'Pre-FocusOut',
                'POST-FOCUSOUT':  'Post-FocusOut',
                'PRE-CHANGE':     'Pre-Change',
                'POST-CHANGE':    'Post-Change'}

        self._triggerGlobal = True
        self._triggerFunctions = {
                # Query
                'set_filter': {'function': self.set_filter},
                'query': {'function': self.set_filter},         # Deprecated!
                'clear': {'function': self.clear},

                # Record navigation
                'first_record': {'function': self.first_record},
                'prev_record': {'function': self.prev_record},
                'next_record': {'function': self.next_record},
                'last_record': {'function': self.last_record},
                'goto_record': {'function': self.goto_record},
                'jump_records': {'function': self.jump_records},
                'search_record': {'function': self.search_record},

                # Record status
                'get_record_status': {'function': self.get_record_status},
                'get_record_count': {'function': self.get_record_count},
                'is_pending': {'function': self.is_pending},
                'get_possible_operations':
                        {'function': self.get_possible_operations},

                # Record insertion and deletion
                'new_record': {'function': self.new_record},
                'duplicate_record': {'function': self.duplicate_record},
                'delete_record': {'function': self.delete_record},
                'undelete_record': {'function': self.undelete_record},

                # Other stuff
                'call': {'function': self.call},
                'update': {'function': self.update},
                'get_data': {'function': self.get_data},

                # Deprecated functions
                'isPending': {'function': self.is_pending},
                'isSaved': {'function': self.is_saved},
                'isEmpty': {'function': self.is_empty},
                'firstRecord': {'function': self.first_record},
                'prevRecord': {'function': self.prev_record},
                'nextRecord': {'function': self.next_record},
                'lastRecord': {'function': self.last_record},
                'gotoRecord': {'function': self.goto_record},
                'jumpRecords': {'function': self.jump_records},
                'newRecord': {'function': self.new_record},
                'duplicateRecord': {'function': self.duplicate_record},
                'deleteRecord': {'function': self.delete_record},
                'undeleteRecord': {'function': self.undelete_record},
                'getResultSet': {'function': self.get_resultset}}


    # -------------------------------------------------------------------------
    # Object construction from xml
    # -------------------------------------------------------------------------

    def _buildObject(self):

        self._rows = getattr(self, 'rows', self._rows)
        self._gap  = getattr(self, 'rowSpacer', self._gap)

        if hasattr(self, 'restrictDelete') and self.restrictDelete:
            self.deletable = False
            del self.__dict__['restrictDelete']

        if hasattr(self, 'restrictInsert') and self.restrictInsert:
            self.editable = 'update'
            del self.__dict__['restrictInsert']

        if hasattr(self,'datasource'):
            self.datasource = self.datasource.lower()

        # Build a list and a dictionary of all fields in this block
        for field in self.findChildrenOfType('GFField', includeSelf=False):
            self._fieldMap[field.name] = field

        return GFContainer._buildObject(self)


    # -------------------------------------------------------------------------
    # Implementation of virtual methods
    # -------------------------------------------------------------------------

    def _phase_1_init_(self):

        GFContainer._phase_1_init_(self)

        self._convertAsterisksToPercent = gConfigForms('AsteriskWildcard')

        self._logic = logic = self.findParentOfType('GFLogic')

        self._lastValues = {}

        logic._blockList.append(self)
        logic._blockMap[self.name] = self

        # Initialize our events system
        events.EventAware.__init__(self, self._form._instance.eventController)

        # Create a stub/non-bound datasource if we aren't bound to one
        if not hasattr(self, 'datasource') or not self.datasource:
            datasource = GFDataSource(self._form)
            datasource.type = 'unbound'
            self.datasource = datasource.name = "__dts_%s" % id(self)
            self._form._datasourceDictionary[datasource.name] = datasource
            datasource._buildObject()
            datasource.phaseInit()

        dsDict = self._form._datasourceDictionary
        self._dataSourceLink = dsDict.get(self.datasource)

        if self._dataSourceLink is None:
            raise DatasourceNotFoundError, (self.datasource, self)

        # Register event handling functions
        self._dataSourceLink.registerEventListeners({
                'dsResultSetActivated': self.__ds_resultset_activated,
                'dsResultSetChanged'  : self.__ds_resultset_activated, # sic!
                'dsCursorMoved'       : self.__ds_cursor_moved,
                'dsRecordInserted'    : self.__ds_record_inserted,
                'dsRecordLoaded'      : self.__ds_record_loaded,
                'dsCommitInsert'      : self.__ds_commit_insert,
                'dsCommitUpdate'      : self.__ds_commit_update,
                'dsCommitDelete'      : self.__ds_commit_delete})

        # Get min and max child rows, if applicable
        self._minChildRows = getattr(self._dataSourceLink, 'detailmin', 0)
        self._maxChildRows = getattr(self._dataSourceLink, 'detailmax', None)
        self.walk(self.__set_child_row_settings)

    # -------------------------------------------------------------------------

    def __set_child_row_settings(self, child):

        # If a child has no rows- or rowSpacer-attribute copy the blocks values
        # to the child 
        child._rows = getattr(child, 'rows', self._rows)
        child._gap  = getattr(child, 'rowSpacer', self._gap)


    # -------------------------------------------------------------------------
    # Get an ordered list of focus-controls
    # -------------------------------------------------------------------------

    def get_focus_order(self):

        ctrlList = []
        for field in self._children:
            ctrlList += getattr(field, '_entryList', [])

        return GFContainer.get_focus_order(self, ctrlList)


    # -------------------------------------------------------------------------
    # Register a scrollbar widget
    # -------------------------------------------------------------------------

    def register_scrollbar(self, widget):
        """
        Register a given scrollbar widget to the block. This widget will be
        notified on record movement. It has to implement a method
        'adjust_scrollbar', taking the current record and the number of records
        as arguments.
        """

        self.__scrollbars.append(widget)


    # -------------------------------------------------------------------------
    # Event handling functions for datasource events
    # -------------------------------------------------------------------------

    def __ds_resultset_activated(self, event):

        # Don't let the user interface follow while we iterating through the
        # detail resultsets for the commit triggers.
        # FIXME: This also means that detail blocks don't contain the correct
        # result set at PRE-INSERT/PRE-UPDATE/PRE-DELETE trigger execution of
        # the master block.
        if self.mode == 'commit':
            return

        # FIXME: If an exception appears here, we have a problem: it is probably
        # too late to cancel the operation.
        if not self.__in_query:
            self._focus_out()

        self.__resultset = event.resultSet

        if self.__resultset is not None:
            recno = self.__resultset.getRecordNumber()
            if recno == -1:
                self.__scrolling_blocked = True
                try:
                    if not self.__resultset.firstRecord():
                        if self.editable in ('Y', 'new'):
                            self.__resultset.insertRecord(self._lastValues)
                finally:
                    self.__scrolling_blocked = False

        self.__current_record_changed(True)

        if not self.__in_query:
            self._focus_in()

    # -------------------------------------------------------------------------

    def __ds_cursor_moved(self, event):

        # Don't let the user interface follow while we iterating through the
        # records for the commit triggers
        if self.mode == 'commit':
            return

        if self.__scrolling_blocked:
            return

        self.__current_record_changed(False)

    # -------------------------------------------------------------------------

    def __ds_record_inserted(self, event):
        oldmode = self.mode
        self.mode = 'init'
        self.__initializing_record = event.record
        self.processTrigger('ON-NEWRECORD')
        self.mode = oldmode
        del self.__initializing_record

    # -------------------------------------------------------------------------

    def __ds_record_loaded(self, event):
        oldmode = self.mode
        self.mode = 'init'
        self.__initializing_record = event.record
        self.processTrigger('ON-RECORDLOADED')
        self.mode = oldmode
        del self.__initializing_record

    # -------------------------------------------------------------------------

    def __ds_commit_insert(self, event):
        self.__fire_record_trigger('PRE-INSERT')
        self.__fire_record_trigger('PRE-COMMIT')

    # -------------------------------------------------------------------------

    def __ds_commit_update(self, event):
        self.__fire_record_trigger('PRE-UPDATE')
        self.__fire_record_trigger('PRE-COMMIT')

    # -------------------------------------------------------------------------

    def __ds_commit_delete(self, event):
        self.__fire_record_trigger('PRE-DELETE')
        self.__fire_record_trigger('PRE-COMMIT')

    # -------------------------------------------------------------------------

    def __fire_record_trigger(self, trigger):
        self.processTrigger(trigger, ignoreAbort=False)
        for field in self._fieldMap.itervalues():
            field.processTrigger(trigger, ignoreAbort=False)


    # -------------------------------------------------------------------------
    # Event handlers for UI events(scrollbar or mouse wheel)
    # -------------------------------------------------------------------------

    def _event_rows_changed(self, new_rows):
        """
        Notify the block that the number of rows displayed has changed.

        If the number of rows has decreased and the current record would end up
        outside the visible area, scroll the visible area to again include the
        current record.
        """

        self._rows = new_rows

        if self.__visible_start + self._rows < self._currentRecord + 1:
            self.__switch_record(None, False)
        else:
            self.__adjust_scrollbars()

    # -------------------------------------------------------------------------

    def _event_scroll_delta(self, adjustment):
        """
        Scroll the given number of records.

        @param adjustment: number of records to move relative to the current
            record.
        """
  
        if self.__visible_start == 0 and adjustment < 0:
            # Already at top: move database cursor instead of scrolling.
            self.jump_records(adjustment)

        if self.__visible_start + self._rows == self.__record_count \
                and adjustment > 0:
            # Already at bottom: move database cursor instead of scrolling.
            self.jump_records(adjustment)

        self._event_scroll_to_record(self.__visible_start + adjustment)

    # -------------------------------------------------------------------------

    def _event_scroll_to_record(self, position):
        """
        Scrolls the block to the given position.

        @param position: the record number to scroll to. This record will
            become the first visible record.
        """

        # Make sure we don't scroll outside the available records
        position = min(position, self.__resultset.getRecordCount() - self._rows)
        position = max(position, 0)

        # Got anything to do at all?
        if position == self.__visible_start:
            return

        # Jump to another record if necessary
        self.__scrolling_blocked = True
        jumped = False
        try:
            if self._currentRecord < position:
                self.goto_record(position)
                jumped = True
            elif self._currentRecord > position + self._rows -1:
                self.goto_record(position + self._rows - 1)
                jumped = True
        finally:
            self.__scrolling_blocked = False

        # And now scroll the entries
        self.__switch_record(position, False)
        if jumped:
            self.__new_current_record()


    # -------------------------------------------------------------------------
    # Queries
    # -------------------------------------------------------------------------

    def populate(self):
        """
        Populate the block with data at startup.

        Depending on the properties of the block, it is populated either with a
        single empty record or the result of a full query.
        """

        if self.__get_master_block() is not None:
            # Population will happen through the master
            return

        if self.startup == 'full':
            self.__query()
        elif self.startup == 'empty':
            self.clear()

    # -------------------------------------------------------------------------

    def init_filter(self):
        """
        Set the block into filter mode.

        From this point on, changes to fields within this block will be
        understood as filter criteria. Use L{apply_filter} to apply the filter
        and populate the block with all records matching the criteria.
        """

        self.mode = 'query'
        self.__query_values = {}
        self.__query_values.update(self._queryDefaults)
        self.__refresh_choices()
        self.__current_record_changed(True)
        if self._form.get_focus_block() is self:
            self.__update_record_status()

    # -------------------------------------------------------------------------

    def change_filter(self):
        """
        Set the block into filter mode and initialize the filter criteria with
        the last executed filter.

        From this point on, changes to fields within this block will be
        understood as filter criteria. Use L{apply_filter} to apply the filter
        and populate the block with all records matching the criteria.
        """

        self.mode = 'query'
        self.__query_values = {}
        self.__query_values.update(self.__last_query_values)
        self.__refresh_choices()
        self.__current_record_changed(True)
        if self._form.get_focus_block() is self:
            self.__update_record_status()

    # -------------------------------------------------------------------------

    def discard_filter(self):
        """
        Reset the block from filter mode back to data browsing mode without
        applying the new filter.

        The result set that was active before switching to filter mode remains
        active.
        """

        self.mode = 'normal'
        self.__refresh_choices()
        self.__current_record_changed(True)

    # -------------------------------------------------------------------------

    def apply_filter(self):
        """
        Apply the filter defined through the field values.

        The block switches back from filter mode to data browsing mode and is
        populated with all records that match the filter criteria.
        """

        # Store block states
        self.__last_query_values = self.__query_values.copy()

        if self.__get_master_block() is None:
            # Condition for the master block
            conditions = self.__generate_condition_tree()

            self.__in_query = True
            try:
                self._dataSourceLink.createResultSet(conditions)
            finally:
                self.__in_query = False

        # Update list of allowed values
        self.__refresh_choices()
        # This seems redundant at first sight, but we must make sure to update
        # the UI after we have changed the available choices, otherwise the UI
        # will get confused. Doing this here makes sure it even is done for
        # detail blocks that were queried before through the master.
        self.__current_record_changed(True)

    # -------------------------------------------------------------------------

    def set_filter(self, *args, **params):
        """
        Set new filter criteria for this block.

        @param args: zero, one or more condition trees, can be in dictionary
            format, in prefix notation, or GCondition object trees. Field names
            in these conditions are passed directly to the backend, i.e. they
            are database column names. This is useful to create queries of
            arbitary complexity.
        @param params: simple filter values in the notation C{fieldname=value}
            where the fieldname is the name of a GFField. This is useful to
            create straightforward simple filters where the database columns
            included in the condition have their GFField assigned. This also
            works for lookup fields.
        @returns: True if the filter was applied, False if the user aborted
            when being asked whether or not to save changes.
        """

        self._focus_out()
        # We only have to save if the queried block or one of its details has
        # pending changes, not if any other unrelated block is dirty.
        if self.is_pending() and not self._form._must_save():
            return False
        try:
            self.__query(*args, **params)
        finally:
            self._focus_in()
        return True

    # -------------------------------------------------------------------------

    def __query(self, *args, **params):

        # First, convert the fieldname/value pairs to column/value pairs.
        cond = {}
        for (fieldname, value) in params.iteritems():
            field = self._fieldMap[fieldname]
            cond[field.field] = field.reverse_lookup(value)

        # Then, mix in the conditions given in args.
        for arg in args:
            cond = GConditions.combineConditions(cond, arg)

        # Now, do the query.
        self.__in_query = True
        try:
            self._dataSourceLink.createResultSet(cond)
        finally:
            self.__in_query = False

    # -------------------------------------------------------------------------

    def clear(self):
        """
        Discard changes in this block and populate it with a single empty
        record.
        """

        # Detail blocks cannot be cleared - they follow their master blindly.
        if self.__get_master_block() is not None:
            return

        self._dataSourceLink.createEmptyResultSet()


    # -------------------------------------------------------------------------
    # Record Navigation
    # -------------------------------------------------------------------------

    def first_record(self):
        """
        Move the record pointer to the first record of the block.
        """

        if self.mode == 'query':
            return

        if self.__resultset is None:
            return

        if self.__resultset.isFirstRecord():
            return

        self._focus_out()

        self.__resultset.firstRecord()

        self._focus_in()

    # -------------------------------------------------------------------------

    def prev_record(self):
        """
        Move the record pointer to the previous record in the block.

        @returns: True if the record pointer was moved, False if it was already
            the first record.
        """

        if self.mode == 'query':
            return False

        if self.__resultset is None:
            return

        if self.__resultset.isFirstRecord():
            return False

        self._focus_out()

        self.__resultset.prevRecord()

        self._focus_in()

        return True

    # -------------------------------------------------------------------------

    def next_record(self):
        """
        Move the record pointer to the next record in the block.

        If the record is already the last one, a new record will be created if
        the "autoCreate" attribute of the block is set.

        @returns: True if the record pointer was moved, False if it was already
            the last record and no new record was inserted.
        """

        if self.mode == 'query':
            return False

        if self.__resultset is None:
            return

        if self.__resultset.isLastRecord():
            if self.autoCreate and self.get_record_status() != 'empty' and \
                    not self.editable in('update', 'N'):
                self.new_record()
                return True
            return False

        self._focus_out()

        self.__resultset.nextRecord()

        self._focus_in()

        return True

    # -------------------------------------------------------------------------

    def last_record(self):
        """
        Move the record pointer to the last record of the block.
        """

        if self.mode == 'query':
            return

        if self.__resultset is None:
            return

        if self.__resultset.isLastRecord():
            return

        self._focus_out()

        self.__resultset.lastRecord()

        self._focus_in()

    # -------------------------------------------------------------------------

    def goto_record(self, record_number):
        """
        Move the record pointer to a specific record number in the block.

        @param record_number: Record number to jump to. If this is a negative
            value, move relative to the last record.
        """

        if self.mode == 'query':
            return

        if self.__resultset is None:
            return

        # If record_number is negative, move relative to last record
        if record_number < 0:
            record_number += self.__resultset.getRecordCount()
        if record_number < 0:
            record_number = 0

        if record_number == self.__resultset.getRecordNumber():
            return

        self._focus_out()

        if not self.__resultset.setRecord(record_number):
            self.__resultset.lastRecord()

        self._focus_in()

    # -------------------------------------------------------------------------

    def jump_records(self, count):
        """
        Move the record pointer by a given adjustment relative to the current
        record.

        @param count: the number of records to move from the current record.
        """

        if self.__resultset is None:
            return

        record_number = self.__resultset.getRecordNumber() + count

        record_number = max(record_number, 0)
        record_number = min(record_number, self.__resultset.getRecordCount())

        self.goto_record(record_number)

    # -------------------------------------------------------------------------

    def search_record(self, **params):
        """
        Search for (and jump to) the first record matching a set of field
        values.

        @param params: search conditions in the notation C{fieldname=value}
            where the fieldname is the name of a GFField.
        @returns: True if a record was found, False otherwise.
        """

        if self.mode == 'query':
            return False

        if self.__resultset is None:
            return

        # First, convert the fieldname/value pairs to column/value pairs.
        cond = {}
        for (fieldname, value) in params.iteritems():
            field = self._fieldMap[fieldname]
            cond[field.field] = field.reverse_lookup(value)

        self._focus_out()

        result = (self.__resultset.findRecord(cond) is not None)

        self._focus_in()

        return result


    # -------------------------------------------------------------------------
    # Status information
    # -------------------------------------------------------------------------

    def get_record_status(self, offset=0):
        """
        Find out about the status of the record.

        The status can be one of 'empty', 'inserted', 'void', 'clean',
        'modified', or 'deleted', or C{None} if there is no current record.
        """

        if self._dataSourceLink.type == 'unbound':
            return None

        elif self.mode == 'query':
            return None

        elif self.mode == 'init':
            rec = self.__initializing_record

        else:
            if self.__resultset is None:
                return None

            record_number = self.__resultset.getRecordNumber() + offset
            if record_number < 0 or \
                    record_number >= self.__resultset.getRecordCount():
                return None
            else:
                rec = self.__resultset[record_number]

        # try functions that do not depend on detail records first, because
        # they are faster
        if rec.isVoid():
            return 'void'
        elif rec.isDeleted():
            return 'deleted'
        elif rec.isEmpty():
            return 'empty'
        elif rec.isInserted():
            return 'inserted'
        elif rec.isModified():
            return 'modified'
        else:
            return 'clean'

    # -------------------------------------------------------------------------

    def get_record_count(self):
        """
        Return the number of records in this block.
        """

        if self.__resultset is not None:
            return self.__resultset.getRecordCount()
        else:
            return 0

    # -------------------------------------------------------------------------

    def get_possible_operations(self):
        """
        Return a list of possible operations for this block.

        The form can use this function to enable or disable commanders (menu
        items or toolbar buttons) that are bound to the respective action.

        The return value is basically a list of method names that can be called
        for this block in the current state.
        """

        result = []

        if self.mode == 'query':
            result.append('discard_filter')
            result.append('apply_filter')
        else:
            result.append('init_filter')
            result.append('change_filter')

            if self._dataSourceLink.type != 'unbound':
                rs = self.__resultset
                if rs is not None:
                    rec = rs.current
                    status = self.get_record_status()

                    if rec is not None:
                        if not rs.isFirstRecord():
                            result.append('first_record')
                            result.append('prev_record')
                        if not rs.isLastRecord():
                            result.append('next_record')
                            result.append('last_record')
                        result.append('goto_record')

                    if not self._form.readonly:
                        if self.editable in ('Y', 'new') and status != 'empty':
                            result.append('new_record')
                            result.append('duplicate_record')
                            if self.autoCreate and rs.isLastRecord():
                                result.append('next_record')

                        if self.deletable:
                            if status not in ('void', 'deleted'):
                                result.append('delete_record')
                            else:
                                result.append('undelete_record')

        return result

    # -------------------------------------------------------------------------

    def is_saved(self):
        """
        Return True if the block is not pending any uncommited changes.

        This method is depreciated. Please use block.is_pending() instead !
        """

        assert gDebug(1, "DEPRECATED: <block>.isSaved trigger function")
        return not self.is_pending()

    # -------------------------------------------------------------------------

    def is_pending(self):
        """
        Return True if the block is pending any uncommited changes.
        """

        return self.__resultset is not None and self.__resultset.isPending()

    # -------------------------------------------------------------------------

    def is_empty(self):
        """
        Return True if the current record is empty.

        Empty means that it has been newly inserted, but neither has any field
        been changed nor has a detail for this record been inserted with a
        status other than empty.
        """

        assert gDebug(1, "DEPRECATED: <block>.isEmpty trigger function")
        return self.get_record_status() in [None, 'empty']

    # -------------------------------------------------------------------------

    def is_first_record(self):
        """
        Return True if the current record is the first one in the result set.
        """

        if self.__resultset is None:
            return True

        return self.__resultset.isFirstRecord()

    # -------------------------------------------------------------------------

    def is_last_record(self):
        """
        Return True if the current record is the last one in the result set.
        """

        if self.__resultset is None:
            return True

        return self.__resultset.isLastRecord()


    # -------------------------------------------------------------------------
    # Field access
    # -------------------------------------------------------------------------

    def get_value(self, field, offset):
        """
        Return the value of the given field, depending on the block's state.

        @param field: the GFField object.
        @param offset: the offset from the current record (to get data for
            records other than the current one).
        """

        if offset == 0:
            if self.mode == 'query':
                value = self.__query_values.get(field)

            elif self.mode == 'init':
                value = self.__initializing_record[field.field]

            else:
                if self.__resultset and self.__resultset.current:
                    value = self.__resultset.current[field.field]
                else:
                    value = None
        else:
            if self.mode in ['query', 'init'] or self.__resultset is None:
                value = None
            else:
                record_number = self.__resultset.getRecordNumber() + offset
                if record_number < 0 or \
                        record_number >= self.__resultset.getRecordCount():
                    value = None
                else:
                    value = self.__resultset[record_number][field.field]

        return value

    # -------------------------------------------------------------------------

    def set_value(self, field, value):
        """
        Set the value of the given field, depending on the block's state.

        @param field: the GFField object.
        """

        if self.mode == 'query':
            if value is None:
                if field in self.__query_values:
                    del self.__query_values[field]
            else:
                self.__query_values[field] = value
            field.value_changed(value)

        elif self.mode == 'init':
            self.__initializing_record[field.field] = value

        elif self.__resultset is None:
            raise NoDataInBlockError(self)
        else:
            self.processTrigger('Pre-Change')
            field.processTrigger('Pre-Change')

            self.__resultset.current[field.field] = value
            if field.defaultToLast:
                self._lastValues[field.field] = value
            field.value_changed(value)

            field.processTrigger('Post-Change')
            self.processTrigger('Post-Change')

            # Status could have changed from clean to modified
            if self._form.get_focus_block() is self:
                self.__update_record_status()


    # -------------------------------------------------------------------------
    # Insertion and Deletion of Records
    # -------------------------------------------------------------------------

    def new_record(self):
        """
        Add a new record to the block.
        """

        if self.mode == 'query':
            return

        if self.__resultset is None:
            raise NoDataInBlockError(self)

        self._focus_out()

        self.__resultset.insertRecord(self._lastValues)

        self._focus_in()

    # -------------------------------------------------------------------------

    def duplicate_record(self, exclude=(), include=()):
        """
        Create a new record and initialize it with field values from the
        current record.

        @param exclude: list of fields not to copy.
        @param include: list of fields to copy. An empty list means to copy all
            fields except primary key fields and rowid fields, which are never
            copied anyway.
        """

        if self.mode == 'query':
            return

        if self.__resultset is None:
            raise NoDataInBlockError(self)

        self._focus_out()

        self.__resultset.duplicateRecord(exclude=exclude, include=include)

        self._focus_in()

    # -------------------------------------------------------------------------

    def delete_record(self):
        """
        Mark the current record for deletion. The acutal deletion will be done
        on the next commit, call or update.
        """

        if self.mode == 'query':
            return

        if self.__resultset is None:
            raise NoDataInBlockError(self)

        self.__resultset.current.delete()

        if self._form.get_focus_block() is self:
            self.__update_record_status()

    # -------------------------------------------------------------------------

    def undelete_record(self):
        """
        Remove the deletion mark from the current record.
        """

        if self.mode == 'query':
            return

        if self.__resultset is None:
            raise NoDataInBlockError(self)

        self.__resultset.current.undelete()

        if self._form.get_focus_block() is self:
            self.__update_record_status()


    # -------------------------------------------------------------------------
    # Saving and Discarding
    # -------------------------------------------------------------------------

    def post(self):
        """
        Post all pending changes of the block and all its detail blocks to the
        database backend.

        If this function has been run successfully, L{requery} must be called.
        """

        assert gDebug(4, "processing commit on block %s" % self.name, 1)

        try:
            if self.__get_master_block() is None:
                self._dataSourceLink.postAll()
        except:
            # if an exception happened, the record pointer keeps sticking at
            # the offending record, so we must update the UI
            self.__current_record_changed(True)
            raise

    # -------------------------------------------------------------------------

    def requery(self, commit):
        """
        Finalize storing the values of the block in the database.

        This method must be called after L{post} has run successfully. It
        restores the block into a consistent state.

        @param commit: True if a commit has been run on the backend connection
            between post and requery.
        """

        if self.__get_master_block() is None:
            self._dataSourceLink.requeryAll(commit)


    # -------------------------------------------------------------------------
    # Function and Update
    # -------------------------------------------------------------------------

    def call(self, name, parameters):
        """
        Call a server side function.

        Currently, the only backend to support server side function calls is
        gnue-appserver.

        @param name: Function name.
        @param parameters: Function parameter dictionary.
        """

        if self.__resultset is None:
            raise NoDataInBlockError(self)

        # Remember the current record; the record pointer is not reliable
        # between postAll and requeryAll!
        current = self.__resultset.current
        self._dataSourceLink.postAll()

        try:
            res = current.call(name, parameters)
        finally:
            self._dataSourceLink.requeryAll(False)

        return res

    # -------------------------------------------------------------------------

    def update(self):
        """
        Update the backend with changes to this block without finally
        commmitting them.

        This can be useful to make the backend react to changes entered by the
        user, for example to make gnue-appserver recalculate calculated fields.
        """

        if self.__resultset is None:
            raise NoDataInBlockError(self)

        self._dataSourceLink.postAll()
        self._dataSourceLink.requeryAll(False)


    # -------------------------------------------------------------------------
    # Raw data access
    # -------------------------------------------------------------------------

    def get_resultset(self):
        """
        Return the current ResultSet of the block.
        """

        gDebug(1, "DEPRECATED: <block>.getResultSet trigger function")
        return self.__resultset

    # -------------------------------------------------------------------------

    def get_data(self, fieldnames=None):
        """
        Build a list of dictionaries of the current resultset using the fields
        defined by fieldnames.

        @param fieldnames: list of fieldnames to export per record
        @returns: list of dictionaries (one per record)
        """

        if self.__resultset is None:
            return []

        result = []
        if not fieldnames:
            fields = self._fieldMap.values()
        else:
            fields = [self._fieldMap[fld] for fld in fieldnames]

        for recno in xrange(0, self.__resultset.getRecordCount()):
            offset = recno - self.__resultset.getRecordNumber()
            add = {}
            for field in fields:
                add[field.name] = field.get_value(offset)
            result.append(add)

        return result


    # -------------------------------------------------------------------------
    # Shared code called whenever focus enters or leaves a record
    # -------------------------------------------------------------------------

    def _focus_in(self):

        focus_object = self._form.get_focus_object()
        if focus_object and focus_object.get_bound_block() is self:
            self.focus_in()
            if isinstance(focus_object, GFFieldBound):
                focus_object.get_field().focus_in()

            self._form.beginEditing()

        elif self._form.get_focus_block() is self:
            # Current object is not bound to this block, but still a "member"
            # of theis block, like an entry linked to this block via grid_link.
            # We want to update the record status.
            self.__update_record_status()

    # -------------------------------------------------------------------------

    def _focus_out(self):

        focus_object = self._form.get_focus_object()
        if focus_object and focus_object.get_bound_block() is self:

            self._form.endEditing()

            try:
                if isinstance(focus_object, GFFieldBound):
                    focus_object.get_field().validate()
                self.validate()
                if isinstance(focus_object, GFFieldBound):
                    focus_object.get_field().focus_out()
                self.focus_out()
            except Exception:
                self._form.beginEditing()
                raise


    # -------------------------------------------------------------------------
    # Focus handling
    # -------------------------------------------------------------------------

    def focus_in(self):
        """
        Notify the block that it has received the focus.
        """

        if self.mode == 'normal':
            self.processTrigger('PRE-FOCUSIN')
            self.processTrigger('POST-FOCUSIN')

        self.__update_record_status()

    # -------------------------------------------------------------------------

    def validate(self):
        """
        Validate the block to decide whether the focus can be moved away from
        it.

        This function can raise an exception, in which case the focus change
        will be prevented.
        """

        if self.mode == 'normal':
            self.processTrigger('PRE-FOCUSOUT', ignoreAbort=False)

            if self.autoCommit and self.is_pending():
                self._form.execute_commit()

    # -------------------------------------------------------------------------

    def focus_out(self):
        """
        Notify the block that it is going to lose the focus.

        The focus change is already decided at this moment, there is no way to
        stop the focus from changing now.
        """

        if self.mode == 'normal':
            self.processTrigger('POST-FOCUSOUT')


    # -------------------------------------------------------------------------
    # Current record has changed
    # -------------------------------------------------------------------------

    def __current_record_changed(self, refresh_all):

        self.__switch_record(None, refresh_all)
        self.__new_current_record()


    # -------------------------------------------------------------------------
    # Switch the proper record into editing position
    # -------------------------------------------------------------------------

    def __switch_record(self, new_visible_start, refresh_all):

        if self.mode == 'query':
            newRecord = 0
            newRecordCount = 1
        elif self.__resultset is None:
            newRecord = 0
            newRecordCount = 0
        else:
            newRecord = self.__resultset.getRecordNumber()
            newRecordCount = self.__resultset.getRecordCount()

        adjustment = newRecord - self._currentRecord

        if new_visible_start is None:
            new_visible_start = self.__visible_start
        new_visible_start = max(new_visible_start, newRecord - self._rows + 1)
        new_visible_start = max(new_visible_start, 0)
        new_visible_start = min(new_visible_start, newRecord)

        adjustment += self.__visible_start - new_visible_start
        self.__visible_start = new_visible_start

        # Find out which ui entries to refresh
        if refresh_all:
            # Result set completely changed, refresh everything
            refresh = 'all'
        elif newRecordCount != self.__record_count:
            # Newly inserted or deleted record, so all records starting from
            # this one have moved, so refresh them
            refresh = 'current'
        else:
            refresh = 'none'

        self._currentRecord = newRecord
        self.__record_count = newRecordCount

        # We iterate through the fields' entries here, since our own _entryList
        # also contains the entries that are bound to us via grid_link.
        for field in self._fieldMap.itervalues():
            for entry in field._entryList:
                entry.recalculate_visible(adjustment, self._currentRecord,
                        self.__record_count, refresh)

        self.__adjust_scrollbars()


    # -------------------------------------------------------------------------
    # Update the list of choices in all fields of the block
    # -------------------------------------------------------------------------

    def __refresh_choices(self):

        for field in self._fieldMap.itervalues():
            field.refresh_choices()


    # -------------------------------------------------------------------------
    # Things that have to be done if a new current record is activated
    # -------------------------------------------------------------------------

    def __new_current_record(self):

        for field in self._fieldMap.itervalues():
            field._event_new_current_record()


    # -------------------------------------------------------------------------
    # Update the record status
    # -------------------------------------------------------------------------

    def __update_record_status(self):

        if self.mode == 'query':
            record_number = 1
            record_count = 1
            record_status = 'QRY'
        elif self.__resultset is None:
            record_number = 0
            record_count = 0
            record_status = 'NUL'
        else:
            record_number = self.__resultset.getRecordNumber()+1
            record_count = self.__resultset.getRecordCount()
            record_status = {
                    None:       '',
                    'empty':    'NEW',
                    'inserted': 'MOD',
                    'void':     'DEL',
                    'clean':    'OK',
                    'modified': 'MOD',
                    'deleted':  'DEL'}[self.get_record_status()]

        self._form.update_record_counter(record_number=record_number,
                record_count=record_count)

        self._form.update_record_status(record_status)
        self._form.status_changed()


    # -------------------------------------------------------------------------
    # Adjust the scrollbars connected to this block
    # -------------------------------------------------------------------------

    def __adjust_scrollbars(self):

        for sb in self.__scrollbars:
            sb.adjust_scrollbar(self.__visible_start,
                    max(self.__record_count, self.__visible_start + self._rows))


    # -------------------------------------------------------------------------
    # Return the top level master block of this block
    # -------------------------------------------------------------------------

    def __get_top_master_block(self):

        result = self
        master = result.__get_master_block()
        while master is not None:
            result = master
            master = result.__get_master_block()
        return result


    # -------------------------------------------------------------------------
    # Return the master block of this block
    # -------------------------------------------------------------------------

    def __get_master_block(self):

        if self._dataSourceLink.hasMaster():
            ds = self._dataSourceLink.getMaster()
            for block in self._logic._blockList:
                if block._dataSourceLink == ds:
                    return block
            # return None in case our master is not bound to a block; e.g. if
            # our master is a dropdown
            return None
        else:
            return None


    # -------------------------------------------------------------------------
    # Create a condition tree
    # -------------------------------------------------------------------------

    def __generate_condition_tree(self):
        """
        Create a condition tree based upon the values currently stored in the
        form.

        @return: GCondition instance with the condition to use or an empty
            dictionary if no condition is needed.
        """

        # 'user input': [GCondition, pass value?]
        baseComparisons = {
                '>': ['gt', True],
                '>=': ['ge', True],
                '<': ['lt', True],
                '<=': ['le', True],
                '=': ['eq', True],
                '!=': ['ne', True],
                'like': ['like', True],
                'empty': ['null', False],
                'notempty': ['notnull', False]}
        comparisonDelimiter = ":"
  
        condLike = {}
        condEq   = {}
        conditions = []
        # Get all the user-supplied parameters from the entry widgets
        for entry, val in self.__query_values.items():
            if entry._bound and len(("%s" % val)):
      
                # New : operator support
                match = False
                for comparison in baseComparisons.keys():
                    if isinstance(val, basestring) and \
                            val[:2+len(comparison)].lower() == "%s%s%s" % \
                        (comparisonDelimiter, comparison, comparisonDelimiter):
                        value = val[2 + len(comparison):]

                        if baseComparisons[comparison][1]:
                            field = ['field', entry.field]
                            const = ['const', value]

                            if not entry.query_casesensitive:
                                field = ['upper', field]
                                const = ['upper', const]

                            conditions.append([
                                baseComparisons[comparison][0],
                                field,
                                const])
                        else:
                            conditions.append([
                                baseComparisons[comparison][0],
                                ['field', entry.field]])
                        match = True
                        break

                if not match and isinstance(val, bool) and not val:
                    conditions.append(['or', \
                            ['eq', ['field', entry.field], ['const', val]],
                            ['null', ['field', entry.field]]])
                    match = True

                # Falls through to old behaviour if no : condition given or 
                # the : condition is unknown
                if not match:
                    if isinstance(val, basestring):
                        if self._convertAsterisksToPercent:
                            try:
                                val = ("%s" % val).replace('*', '%')
                            except ValueError:
                                pass

                        val = cond_value(val)
                        # a Null-Character means a dropdown with '(empty)'
                        # selected.
                        if val == chr(0):
                            conditions.append(['null', ['field', entry.field]])
  
                        else:
                            if (val.find('%') >= 0 or val.find('_') >= 0):
                                condLike[entry] = val
                            else:
                                condEq[entry] = val
                    else:
                        condEq[entry] = val

        epf = []
        for (entry, value) in condEq.items():
            field = ['field', entry.field]
            const = ['const', value]

            if not entry.query_casesensitive and isinstance(value, basestring):
                field = ['upper', field]
                const = ['upper', const]

            epf.append(['eq', field, const])

        lpf = []
        for (entry, value) in condLike.items():
            field = ['field', entry.field]
            const = ['const', value]

            if not entry.query_casesensitive and isinstance(value, basestring):
                field = ['upper', field]
                const = ['upper', const]

            epf.append(['like', field, const])

        if epf or lpf or conditions:
            result = GConditions.buildConditionFromPrefix(
                    ['and'] + epf + lpf + conditions)
        else:
            result = {}

        if result and self.__get_master_block() is not None:
            exist = GConditions.GCexist()
            exist.table = self._dataSourceLink.table
            exist.masterlink = self._dataSourceLink.masterlink
            exist.detaillink = self._dataSourceLink.detaillink
            exist._children = [result]
            result = exist

        for detail in self._logic._blockList:
            if detail.__get_master_block() != self:
                continue
            result = GConditions.combineConditions(result,
                    detail.__generate_condition_tree())

        return result


# -----------------------------------------------------------------------------
# Change a condition value as needed
# -----------------------------------------------------------------------------

def cond_value(value):
    """
    Change a given condition value as needed.  If it is a string and the option
    'fake_ascii_query' is set, all characters above 127 are changed into an
    underscore.
    """

    if isinstance(value, basestring) and gConfigForms('fake_ascii_query'):
        result = ''
        for char in value:
            if ord(char) > 127:
                char = '_'
            result += char

        value = result

    return value


# =============================================================================
# Exceptions
# =============================================================================

class DatasourceNotFoundError(GParser.MarkupError):
    """
    This block references a non-existant datasource.
    """
    def __init__(self, source, block):
        GParser.MarkupError.__init__(self, u_(
                    "Datasource '%(datasource)s' in block '%(block)s' not "
                    "found"
                ) % {
                    'datasource': source,
                    'block': block.name},
                block._url, block._lineNumber)

# =============================================================================

class NoDataInBlockError(errors.ApplicationError):
    """
    There is no data in this block loaded.
    """
    def __init__(self, source, block):
        message = u_("There is no data in block '%(block)s'")
        errors.ApplicationError.__init__(self, message % {
                    'block': block.name})
