### Navigation

- index
- modules |
- Camelot documentation »
- Module code »
- camelot.view.proxy »

# Source code for camelot.view.proxy.queryproxy

    #  ============================================================================
    #
    #  Copyright (C) 2007-2011 Conceptive Engineering bvba. All rights reserved.
    #  www.conceptive.be / project-camelot@conceptive.be
    #
    #  This file is part of the Camelot Library.
    #
    #  This file may be used under the terms of the GNU General Public
    #  License version 2.0 as published by the Free Software Foundation
    #  and appearing in the file license.txt included in the packaging of
    #  this file.  Please review this information to ensure GNU
    #  General Public Licensing requirements will be met.
    #
    #  If you are unsure which license is appropriate for your use, please
    #  visit www.python-camelot.com or contact project-camelot@conceptive.be
    #
    #  This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
    #  WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
    #
    #  For use of this library in commercial applications, please contact
    #  project-camelot@conceptive.be
    #
    #  ============================================================================

    """Proxies representing the results of a query"""

    import functools
    import logging
    logger = logging.getLogger('camelot.view.proxy.queryproxy')

    from collection_proxy import CollectionProxy, strip_data_from_object
    from camelot.view.model_thread import model_function, gui_function, post

    class QueryTableProxy(CollectionProxy):
        """The QueryTableProxy contains a limited copy of the data in the SQLAlchemy
        model, which is fetched from the database to be used as the model for a
        QTableView
        """

        def __init__(self, admin, query_getter, columns_getter,
                     max_number_of_rows=10, edits=None,
                     cache_collection_proxy=None):
            """@param query_getter: a model_thread function that returns a query, can be None at construction time and set later"""
            logger.debug('initialize query table')
            self._query_getter = query_getter
            self._sort_decorator = None
            self._mapper = admin.mapper
            #rows appended to the table which have not yet been flushed to the
            #database, and as such cannot be a result of the query
            self._appended_rows = []
            super(QueryTableProxy, self).__init__(admin, lambda: [],
                                                  columns_getter,
                                                  max_number_of_rows=max_number_of_rows,
                                                  edits=None,
                                                  cache_collection_proxy=cache_collection_proxy)

        def get_query_getter(self):
            if self._query_getter == None:
                return None
            if self._sort_decorator == None:
                self._set_sort_decorator()

            def sorted_query_getter( query_getter, sort_decorator ):
                return sort_decorator( query_getter() )

            return functools.partial( sorted_query_getter,
                                      self._query_getter,
                                      self._sort_decorator )

        def _update_unflushed_rows( self ):
            """Does nothing since all rows returned by a query are flushed"""
            pass

        @model_function
        def _clean_appended_rows(self):
            """Remove those rows from appended rows that have been flushed"""
            flushed_rows = []
            for o in self._appended_rows:
                primary_key = self._mapper.primary_key_from_instance(o)
                if None not in primary_key:
                    flushed_rows.append(o)
            for o in flushed_rows:
                self._appended_rows.remove(o)

        @model_function
        def getRowCount(self):
            self._clean_appended_rows()
            if not self._query_getter:
                return 0
            return self.get_query_getter()().count() + len(self._appended_rows)

        @gui_function
        def setQuery(self, query_getter):
            """Set the query and refresh the view"""
            self._query_getter = query_getter
            self.refresh()

        def get_collection(self):
            """In case the collection is requested of a QueryProxy, we will return
            a collection getter for a collection that reuses the data already queried by
            the collection proxy, and available in the cache.

            We do this to :

            1. Prevent an unneeded query when the collection is used to fetch an object already
               fetched by the query proxy (eg when a form is opened on a table view)

            2. To make sure the index of an object in the query proxy is the same as the index
               in the returned collection.  Should we do the same query twice (once to fill the
               query proxy, and once to fill the returned collection), the same object might appear
               in a different row.  eg when a form is opened in a table view, the form contains
               another record than the selected row in the table.
            """

            if not self._query_getter:
                return []
            return self.get_query_getter()().all()

        @model_function
        def _set_sort_decorator( self, column=None, order=None ):
            """set the sort decorator attribute of this model to a function that
            sorts a query by the given column using the given order.  When no
            arguments are given, use the default sorting, which is according to
            the primary keys of the model.  This to impose a string ordening of
            the rows in the model.
            """
            from sqlalchemy import orm
            from sqlalchemy.exc import InvalidRequestError

            class_attributes_to_sort_by, join = [], None
            mapper = orm.class_mapper(self.admin.entity)
            #
            # First sort according the requested column
            #
            if column != None and order != None:
                property = None
                field_name = self._columns[column][0]
                class_attribute = getattr(self.admin.entity, field_name)
                try:
                    property = mapper.get_property(
                        field_name,
                        resolve_synonyms=True
                    )
                except InvalidRequestError:
                    #
                    # If the field name is not a property of the mapper, we cannot
                    # sort it using sql
                    #
                    pass

                # If the field is a relation:
                #  If it specifies an order_by option we have to join the related table,
                #  else we use the foreing key as sort field, without joining
                if property and isinstance(property, orm.properties.PropertyLoader):
                    target = property._get_target()
                    if target:
                        if target.order_by:
                            join = field_name
                            class_attribute = target.order_by[0]
                        else:
                            #
                            # _foreign_keys is for sqla pre 0.6.4
                            #
                            if hasattr(property, '_foreign_keys'):
                                class_attribute = list(property._foreign_keys)[0]
                            else:
                                class_attribute = list(property._calculated_foreign_keys)[0]
                if property:
                    if order:
                        class_attributes_to_sort_by.append( class_attribute.desc() )
                    else:
                        class_attributes_to_sort_by.append( class_attribute )

            #
            # Next sort according to default sort column if any
            #
            if mapper.order_by:
                class_attributes_to_sort_by.extend( mapper.order_by )

            #
            # In the end, sort according to the primary keys of the model, to enforce
            # a unique order in any case
            #
            class_attributes_to_sort_by.extend( mapper.primary_key )

            def sort_decorator(class_attributes_to_sort_by, join, query):
                if join:
                    query = query.join(join)
                if class_attributes_to_sort_by:
                    return query.order_by( *class_attributes_to_sort_by )
                else:
                    return query

            self._sort_decorator = functools.partial( sort_decorator,
                                                      class_attributes_to_sort_by,
                                                      join )
            return self._rows

        @gui_function
        def sort( self, column, order ):
            """Overwrites the :meth:`QAbstractItemModel.sort` method
            """
            post( functools.update_wrapper( functools.partial( self._set_sort_decorator, column, order ), self._set_sort_decorator ),
                  self._refresh_content )

        def append(self, o):
            """Add an object to this collection, used when inserting a new
            row, overwrite this method for specific behaviour in subclasses"""
            primary_key = self._mapper.primary_key_from_instance(o)
            if None in primary_key:
                self._appended_rows.append(o)

        def remove(self, o):
            if o in self._appended_rows:
                self._appended_rows.remove(o)
            self._rows = self._rows - 1

        @model_function
        def getData(self):
            """Generator for all the data queried by this proxy"""
            if self._query_getter:
                for _i,o in enumerate(self.get_query_getter()().all()):
                    yield strip_data_from_object(o, self.getColumns())

        @model_function
        def _get_collection_range( self, offset, limit ):
            """Get the objects in a certain range of the collection
            :return: an iterator over the objects in the collection, starting at
            offset, until limit
            """
            query = self.get_query_getter()().offset(offset).limit(limit)
            return query.all()

        @model_function
        def _extend_cache(self):
            """Extend the cache around the rows under request"""
            if self._query_getter:
                offset, limit = self._offset_and_limit_rows_to_get()
                if limit:
                    columns = self.getColumns()
                    #
                    # try to move the offset further by looking if the
                    # objects are already in the cache.
                    #
                    # this has the advantage that we might not need a query,
                    # and more important, that objects remain at the same row
                    # while their position in the query might have been changed
                    # since the previous query.
                    #
                    rows_in_cache = 0
                    for row in range(offset, limit + 1):
                        try:
                            cached_obj =  self.edit_cache.get_entity_at_row(row)
                            self._add_data( columns, row, cached_obj)
                            rows_in_cache += 1
                        except KeyError:
                            break
                    #
                    # query the remaining rows
                    #
                    query_offset = offset + rows_in_cache
                    query_limit = limit - rows_in_cache
                    if query_limit > 0:
                        for i, obj in enumerate( self._get_collection_range(query_offset,
                                                                            query_limit) ):
                            row = i + query_offset
                            try:
                                previous_obj = self.edit_cache.get_entity_at_row(row)
                                if previous_obj != obj:
                                    continue
                            except KeyError:
                                pass
                            self._add_data(columns, row, obj)
                    rows_in_query = (self._rows - len(self._appended_rows))
                    # Verify if rows that have not yet been flushed have been
                    # requested
                    if offset+limit >= rows_in_query:
                        for row in range(max(rows_in_query, offset), min(offset+limit, self._rows)):
                            obj = self._get_object(row)
                            self._add_data(columns, row, obj)
                return (offset, limit)

        @model_function
        def _get_object(self, row):
            """Get the object corresponding to row.  If row is smaller than 0
            (in case of an invalid widget mapper), None is returned as an object"""
            if self._rows > 0 and row >= 0:
                self._clean_appended_rows()
                rows_in_query = (self._rows - len(self._appended_rows))
                if row >= rows_in_query:
                    return self._appended_rows[row - rows_in_query]
                # first try to get the primary key out of the cache, if it's not
                # there, query the collection_getter
                try:
                    return self.edit_cache.get_entity_at_row(row)
                except KeyError:
                    pass
                # momentary hack for list error that prevents forms to be closed
                if self._query_getter:
                    res = self.get_query_getter()().offset(row)
                    if isinstance(res, list):
                        res = res[0]
                    # @todo: remove this try catch and find out why it
                    # sometimes fails
                    try:
                        return res.limit(1).first()
                    except:
                        pass

### Navigation

- index
- modules |
- Camelot documentation »
- Module code »
- camelot.view.proxy »

Comments
blog comments powered by Disqus