Exemplo n.º 1
0
 def _BTreeContainer__len(self):
     l = Length()
     ol = len(self._SampleContainer__data)
     if ol > 0:
         l.change(ol)
     self._p_changed = True
     return l
Exemplo n.º 2
0
 def _BTreeContainer__len(self):
     l = Length()
     ol = len(self.__data)
     if ol > 0:
         l.change(ol)
     self._p_changed = True
     return l
Exemplo n.º 3
0
class DateBookingList(Persistent):
    """ Simple set of booking objects with a count attribute.
    """
    def __init__(self):
        self._bookings = OOBTree.OOTreeSet()
        self._count = Length(0)

    def addBooking(self, booking):
        self._bookings.insert(booking)
        self._count.change(1)

    def removeBooking(self, booking):
        self._bookings.remove(booking)
        self._count.change(-1)

    def getCount(self):
        return self._count()

    def iterbookings(self):
        """ Iterator over the bookings
        """
        return self._bookings.__iter__()

    def getBookingsPerConf(self):
        """ Returns a dictionary where the keys are Conference objects
            and the values are the number of Vidyo bookings of that conference.
        """
        result = {}
        for b in self._bookings:
            result[b.getConference()] = result.setdefault(
                b.getConference(), 0) + 1
        return result
Exemplo n.º 4
0
 def _Folder__len(self):
     l = Length()
     ol = len(self.__data)
     if ol > 0:
         l.change(ol)
     self._p_changed = True
     return l
Exemplo n.º 5
0
class DateBookingList(Persistent):
    """ Simple set of booking objects with a count attribute.
    """

    def __init__(self):
        self._bookings = OOBTree.OOTreeSet()
        self._count = Length(0)

    def addBooking(self, booking):
        self._bookings.insert(booking)
        self._count.change(1)

    def removeBooking(self, booking):
        self._bookings.remove(booking)
        self._count.change(-1)

    def getCount(self):
        return self._count()

    def iterbookings(self):
        """ Iterator over the bookings
        """
        return self._bookings.__iter__()

    def getBookingsPerConf(self):
        """ Returns a dictionary where the keys are Conference objects
            and the values are the number of Vidyo bookings of that conference.
        """
        result = {}
        for b in self._bookings:
            result[b.getConference()] = result.setdefault(b.getConference(), 0) + 1
        return result
Exemplo n.º 6
0
 def _QuestionRecord__len(self):
     l=Length()
     ol = len(self._tree)
     if ol>0:
         l.change(ol)
     self._p_changed=True
     return l
Exemplo n.º 7
0
 def _Registry__len(self):
     l = Length()
     ol = len(self.__data)
     if ol > 0:
         l.change(ol)
     self._p_changed = True
     return l
Exemplo n.º 8
0
 def _PersitentOOBTree__len(self):
     l = Length()
     ol = len(self._data)
     if ol > 0:
         l.change(ol)
     self._p_changed = True
     return l
Exemplo n.º 9
0
 def _get_next_number(self, prefix):
     last = getattr(self, '_autoname_last_' + prefix, None)
     if last is None:
         last = Length()
         setattr(self, '_autoname_last_' + prefix, last)
     number = last.value
     last.change(1)
     return number
Exemplo n.º 10
0
 def _get_next_number(self, prefix):
     last = getattr(self, '_autoname_last_' + prefix, None)
     if last is None:
         last = Length()
         setattr(self, '_autoname_last_' + prefix, last)
     number = last.value
     last.change(1)
     return number
Exemplo n.º 11
0
class MailDataStorage(PersistentItem):
    interface.implements(IMailDataStorage)

    def __init__(self, **kw):
        self.count = Length(0)
        super(MailDataStorage, self).__init__(**kw)

    def append(self, form, record, request):
        mail = getMultiAdapter((form, request), IMailTemplate)
        mail.send((self.emailto,), record=record, storage=self)
        self.count.change(1)
Exemplo n.º 12
0
class MessageService(Persistent, Location):
    interface.implements(IMessageService)

    def __init__(self, storage):
        self.__parent__ = storage

        self.index = OIBTree()
        self.unread = Length(0)

    def __len__(self):
        return len(self.index)

    def __iter__(self):
        return iter(self.index.values())

    def __contains__(self, key):
        msg = self.__parent__.getMessage(key)
        if msg is not None:
            return True
        else:
            return False

    def get(self, msgId, default=None):
        msg = self.__parent__.getMessage(msgId)
        if msg is not None:
            if msg.__date__ in self.index:
                return msg

        return default

    def append(self, message):
        message.__parent__ = self

        if self.__parent__.readStatus(message):
            self.unread.change(1)

        self.index[message.__date__] = message.__id__

    def remove(self, message):
        id = message.__date__

        if id in self.index:
            del self.index[id]

            if self.__parent__.readStatus(message) and self.unread() > 0:
                self.unread.change(-1)

    def create(self, **data):
        raise NotImplemented('create')
Exemplo n.º 13
0
class MessageQueues(persistent.dict.PersistentDict):
    interface.implements(interfaces.IMessageQueues)

    def __init__(self, *args, **kwargs):
        super(MessageQueues, self).__init__(*args, **kwargs)
        for status in interfaces.MESSAGE_STATES:
            self[status] = queue.CompositeQueue()
        self._messages_sent = Length()

    @property
    def messages_sent(self):
        return self._messages_sent()

    def dispatch(self):
        try:
            lock = zc.lockfile.LockFile(LOCKFILE_NAME)
        except zc.lockfile.LockError:
            logger.info("Dispatching is locked by another process.")
            return (0, 0)

        try:
            return self._dispatch()
        finally:
            lock.close()

    def _dispatch(self):
        sent = 0
        failed = 0

        for name in 'new', 'retry':
            queue = self[name]
            while True:
                try:
                    message = queue.pull()
                except IndexError:
                    break
                else:
                    status, message = dispatch(message)
                    if status == 'sent':
                        sent += 1
                    else:
                        failed += 1

        self._messages_sent.change(sent)
        return sent, failed

    def clear(self, queue_names=('error', 'sent')):
        for name in queue_names:
            self[name] = self[name].__class__()
Exemplo n.º 14
0
class MessageQueues(persistent.dict.PersistentDict):
    interface.implements(interfaces.IMessageQueues)

    def __init__(self, *args, **kwargs):
        super(MessageQueues, self).__init__(*args, **kwargs)
        for status in interfaces.MESSAGE_STATES:
            self[status] = queue.CompositeQueue()
        self._messages_sent = Length()

    @property
    def messages_sent(self):
        return self._messages_sent()

    def dispatch(self):
        try:
            lock = zc.lockfile.LockFile(LOCKFILE_NAME)
        except zc.lockfile.LockError:
            logger.info("Dispatching is locked by another process.")
            return (0, 0)

        try:
            return self._dispatch()
        finally:
            lock.close()

    def _dispatch(self):
        sent = 0
        failed = 0

        for name in 'new', 'retry':
            queue = self[name]
            while True:
                try:
                    message = queue.pull()
                except IndexError:
                    break
                else:
                    status, message = dispatch(message)
                    if status == 'sent':
                        sent += 1
                    else:
                        failed += 1

        self._messages_sent.change(sent)
        return sent, failed

    def clear(self, queue_names=('error', 'sent')):
        for name in queue_names:
            self[name] = self[name].__class__()
Exemplo n.º 15
0
class PollRecord(BTreeContainer):
    implements(IPollRecord, IContentContainer)

    voteCount = None

    firstVote = None

    lastVote = None

    def __init__(self, *kv, **kw):
        super(PollRecord, self).__init__(*kv, **kw)
        self._results = OOBTree()
        self.voteCount = Length()

    def add(self, record):
        polling = getUtility(IPolling)
        for key, value in record.choices.items():
            item = self._results.get(key)
            if item is None:
                item = QuestionRecord()
                notify(ObjectCreatedEvent(item))
                self._results[key] = item
            for id in value:
                self.voteCount.change(1)
                polling.voteCount.change(1)
                item.voteCount.change(1)
                if item.firstVote is None:
                    item.firstVote = record
                item.lastVote = record
                answer = item.get(id)
                if answer:
                    answer.change(1)
                else:
                    item[id] = Length(1)
        if self.firstVote is None:
            self.firstVote = record
        self.lastVote = record
        self._p_changed = 1

    def getResults(self):
        res = {}
        for question, answers in self._results.items():
            res[question] = {}
            size = float(answers.voteCount.value)
            for answer, votes in answers.items():
                res[question][answer] = (votes.value, votes.value/size)
        return res, self
Exemplo n.º 16
0
class UniqueIdGeneratorTool(UniqueObject, SimpleItem, ActionProviderBase):
    """Generator of unique ids.
    """

    __implements__ = (
        SimpleItem.__implements__,
        IUniqueIdGenerator,
    )

    id = 'portal_uidgenerator'
    alternative_id = 'portal_standard_uidgenerator'
    meta_type = 'Unique Id Generator Tool'

    # make AnnotatedUniqueId class available through the tool
    # not meant to be altered on runtime !!!
    _uid_implementation = AnnotatedUniqueId

    security = ClassSecurityInfo()

    security.declarePrivate('__init__')
    def __init__(self):
        """Initialize the generator
        """
        # Using the Length implementation of the BTree.Length module as 
        # counter handles zodb conflicts for us.
        self._uid_counter = Length(0)
    
    def reinitialize(self):
        """Reinitialze the uid generator.
        
        Avoids already existing unique ids beeing generated again.
        To be called e.g. after having imported content objects.
        """
        # XXX to be implemented by searching the max value in the catalog
        raise NotImplementedError
    
    security.declarePrivate('__call__')
    def __call__(self):
        """See IUniqueIdGenerator.
        """
        self._uid_counter.change(+1)
        return self._uid_implementation(self._uid_counter())
Exemplo n.º 17
0
class DateBookingList(Persistent):
    """ Simple set of booking objects with a count attribute.
    """
    def __init__(self):
        self._bookings = OOBTree.OOTreeSet()
        self._count = Length(0)

    def addBooking(self, booking):
        self._bookings.insert(booking)
        self._count.change(1)

    def removeBooking(self, booking):
        self._bookings.remove(booking)
        self._count.change(-1)

    def getCount(self):
        return self._count()

    def iterbookings(self):
        """ Iterator over the bookings
        """
        return self._bookings.__iter__()
Exemplo n.º 18
0
class UniqueIdGeneratorTool(UniqueObject, SimpleItem, ActionProviderBase):
    """Generator of unique ids.
    """

    __implements__ = (
        IUniqueIdGenerator,
        ActionProviderBase.__implements__,
        SimpleItem.__implements__,
    )

    id = 'portal_uidgenerator'
    alternative_id = 'portal_standard_uidgenerator'
    meta_type = 'Unique Id Generator Tool'

    security = ClassSecurityInfo()

    security.declarePrivate('__init__')

    def __init__(self):
        """Initialize the generator
        """
        # Using the Length implementation of the BTree.Length module as
        # counter handles zodb conflicts for us.
        self._uid_counter = Length(0)

    security.declarePrivate('__call__')

    def __call__(self):
        """See IUniqueIdGenerator.
        """
        self._uid_counter.change(+1)
        return self._uid_counter()

    security.declarePrivate('convert')

    def convert(self, uid):
        """See IUniqueIdGenerator.
        """
        return int(uid)
Exemplo n.º 19
0
class DateBookingList(Persistent):
    """ Simple set of booking objects with a count attribute.
    """

    def __init__(self):
        self._bookings = OOBTree.OOTreeSet()
        self._count = Length(0)

    def addBooking(self, booking):
        self._bookings.insert(booking)
        self._count.change(1)

    def removeBooking(self, booking):
        self._bookings.remove(booking)
        self._count.change(-1)

    def getCount(self):
        return self._count()

    def iterbookings(self):
        """ Iterator over the bookings
        """
        return self._bookings.__iter__()
Exemplo n.º 20
0
class UniqueIdGeneratorTool(UniqueObject, SimpleItem, ActionProviderBase):
    """Generator of unique ids.
    """

    __implements__ = (
        IUniqueIdGenerator,
        ActionProviderBase.__implements__,
        SimpleItem.__implements__,
    )

    id = 'portal_uidgenerator'
    alternative_id = 'portal_standard_uidgenerator'
    meta_type = 'Unique Id Generator Tool'
    
    security = ClassSecurityInfo()
    
    security.declarePrivate('__init__')
    def __init__(self):
        """Initialize the generator
        """
        # Using the Length implementation of the BTree.Length module as 
        # counter handles zodb conflicts for us.
        self._uid_counter = Length(0)
    
    security.declarePrivate('__call__')
    def __call__(self):
        """See IUniqueIdGenerator.
        """
        self._uid_counter.change(+1)
        return self._uid_counter()
        
    security.declarePrivate('convert')
    def convert(self, uid):
        """See IUniqueIdGenerator.
        """
        return int(uid)
Exemplo n.º 21
0
class KeywordIndex(Persistent):
    """
    Keyword index.

    Implements :class:`zope.index.interfaces.IInjection`,
    :class:`zope.index.interfaces.IStatistics`,
    :class:`zope.index.interfaces.IIndexSearch` and
    :class:`zope.index.keyword.interfaces.IKeywordQuerying`.
    """

    family = BTrees.family32

    # If a word is referenced by at least tree_threshold docids,
    # use a TreeSet for that word instead of a Set.
    tree_threshold = 64

    def __init__(self, family=None):
        if family is not None:
            self.family = family
        self.clear()

    def clear(self):
        """Initialize forward and reverse mappings."""

        # The forward index maps index keywords to a sequence of docids
        self._fwd_index = self.family.OO.BTree()

        # The reverse index maps a docid to its keywords
        # TODO: Using a vocabulary might be the better choice to store
        # keywords since it would allow use to use integers instead of strings
        self._rev_index = self.family.IO.BTree()
        self._num_docs = Length(0)

    def documentCount(self):
        """Return the number of documents in the index."""
        return self._num_docs()

    def wordCount(self):
        """Return the number of indexed words"""
        return len(self._fwd_index)

    def has_doc(self, docid):
        return bool(docid in self._rev_index)

    def normalize(self, seq):
        """Perform normalization on sequence of keywords.

        Return normalized sequence. This method may be
        overriden by subclasses.

        """
        return seq

    def index_doc(self, docid, seq):
        if isinstance(seq, six.string_types):
            raise TypeError('seq argument must be a list/tuple of strings')

        old_kw = self._rev_index.get(docid, None)
        if not seq:
            if old_kw:
                self.unindex_doc(docid)
            return

        seq = self.normalize(seq)

        new_kw = self.family.OO.Set(seq)

        if old_kw is None:
            self._insert_forward(docid, new_kw)
            self._insert_reverse(docid, new_kw)
            self._num_docs.change(1)
        else:

            # determine added and removed keywords
            kw_added = self.family.OO.difference(new_kw, old_kw)
            kw_removed = self.family.OO.difference(old_kw, new_kw)

            # removed keywords are removed from the forward index
            for word in kw_removed:
                fwd = self._fwd_index[word]
                fwd.remove(docid)
                if not fwd:
                    del self._fwd_index[word]

            # now update reverse and forward indexes
            self._insert_forward(docid, kw_added)
            self._insert_reverse(docid, new_kw)

    def unindex_doc(self, docid):
        idx = self._fwd_index

        try:
            for word in self._rev_index[docid]:
                idx[word].remove(docid)
                if not idx[word]:
                    del idx[word]
        except KeyError:
            # 'WAAA!  Inconsistent'
            return

        try:
            del self._rev_index[docid]
        except KeyError:  # pragma: no cover
            # 'WAAA!  Inconsistent'
            pass

        self._num_docs.change(-1)

    def _insert_forward(self, docid, words):
        """insert a sequence of words into the forward index """

        idx = self._fwd_index
        get_word_idx = idx.get
        IF = self.family.IF
        Set = IF.Set
        TreeSet = IF.TreeSet
        for word in words:
            word_idx = get_word_idx(word)
            if word_idx is None:
                idx[word] = word_idx = Set()
            word_idx.insert(docid)
            if (not isinstance(word_idx, TreeSet)
                    and len(word_idx) >= self.tree_threshold):
                # Convert to a TreeSet.
                idx[word] = TreeSet(word_idx)

    def _insert_reverse(self, docid, words):
        """ add words to forward index """

        if words:
            self._rev_index[docid] = words

    def search(self, query, operator='and'):
        """Execute a search given by 'query'."""
        if isinstance(query, six.string_types):
            query = [query]

        query = self.normalize(query)

        sets = []
        for word in query:
            docids = self._fwd_index.get(word, self.family.IF.Set())
            sets.append(docids)

        if operator == 'or':
            rs = self.family.IF.multiunion(sets)
        elif operator == 'and':
            # sort smallest to largest set so we intersect the smallest
            # number of document identifiers possible
            sets.sort(key=len)
            rs = None
            for set in sets:
                rs = self.family.IF.intersection(rs, set)
                if not rs:
                    break
        else:
            raise TypeError('Keyword index only supports `and` and `or` '
                            'operators, not `%s`.' % operator)

        if rs:
            return rs
        return self.family.IF.Set()

    def apply(self, query):
        operator = 'and'
        if isinstance(query, dict):
            if 'operator' in query:
                operator = query['operator']
            query = query['query']
        return self.search(query, operator=operator)

    def optimize(self):
        """Optimize the index. Call this after changing tree_threshold.

        This converts internal data structures between
        Sets and TreeSets based on tree_threshold.
        """
        idx = self._fwd_index
        IF = self.family.IF
        Set = IF.Set
        TreeSet = IF.TreeSet
        items = list(self._fwd_index.items())
        for word, word_idx in items:
            if len(word_idx) >= self.tree_threshold:
                if not isinstance(word_idx, TreeSet):
                    # Convert to a TreeSet.
                    idx[word] = TreeSet(word_idx)
            else:
                if isinstance(word_idx, TreeSet):
                    # Convert to a Set.
                    idx[word] = Set(word_idx)
class FormSaveDataAdapter(FormActionAdapter):
    """A form action adapter that will save form input data and
       return it in csv- or tab-delimited format."""

    schema = FormAdapterSchema.copy() + Schema((
        LinesField('showFields',
            required=0,
            searchable=0,
            vocabulary='allFieldDisplayList',
            widget=PicklistWidget(
                label=_(u'label_savefields_text', default=u"Saved Fields"),
                description=_(u'help_savefields_text', default=u"""
                    Pick the fields whose inputs you'd like to include in
                    the saved data. If empty, all fields will be saved.
                    """),
                ),
            ),
        LinesField('ExtraData',
            widget=MultiSelectionWidget(
                label=_(u'label_savedataextra_text', default='Extra Data'),
                description=_(u'help_savedataextra_text', default=u"""
                    Pick any extra data you'd like saved with the form input.
                    """),
                format='checkbox',
                ),
            vocabulary='vocabExtraDataDL',
            ),
        StringField('DownloadFormat',
            searchable=0,
            required=1,
            default='csv',
            vocabulary='vocabFormatDL',
            widget=SelectionWidget(
                label=_(u'label_downloadformat_text', default=u'Download Format'),
                ),
            ),
        BooleanField("UseColumnNames",
            required=False,
            searchable=False,
            widget=BooleanWidget(
                label=_(u'label_usecolumnnames_text', default=u"Include Column Names"),
                description=_(u'help_usecolumnnames_text', default=u"Do you wish to have column names on the first line of downloaded input?"),
                ),
            ),
        ExLinesField('SavedFormInput',
            edit_accessor='getSavedFormInputForEdit',
            mutator='setSavedFormInput',
            searchable=0,
            required=0,
            primary=1,
            schemata="saved data",
            read_permission=DOWNLOAD_SAVED_PERMISSION,
            widget=TextAreaWidget(
                label=_(u'label_savedatainput_text', default=u"Saved Form Input"),
                description=_(u'help_savedatainput_text'),
                ),
            ),
    ))

    schema.moveField('execCondition', pos='bottom')

    meta_type      = 'FormSaveDataAdapter'
    portal_type    = 'FormSaveDataAdapter'
    archetype_name = 'Save Data Adapter'

    immediate_view = 'fg_savedata_view_p3'
    default_view   = 'fg_savedata_view_p3'
    suppl_views    = ('fg_savedata_tabview_p3', 'fg_savedata_recview_p3',)

    security       = ClassSecurityInfo()


    def _migrateStorage(self):
        # we're going to use an LOBTree for storage. we need to
        # consider the possibility that self is from an
        # older version that uses the native Archetypes storage
        # or the former IOBTree (<= 1.6.0b2 )
        # in the SavedFormInput field.
        updated = base_hasattr(self, '_inputStorage') and \
                  base_hasattr(self, '_inputItems') and \
                  base_hasattr(self, '_length')

        if not updated:
            try:
                saved_input = self.getSavedFormInput()
            except AttributeError:
                saved_input = []

            self._inputStorage = SavedDataBTree()
            i = 0
            self._inputItems = 0
            self._length = Length()

            if len(saved_input):
                for row in saved_input:
                    self._inputStorage[i] = row
                    i += 1
                self.SavedFormInput = []
                self._inputItems = i
                self._length.set(i)


    security.declareProtected(DOWNLOAD_SAVED_PERMISSION, 'getSavedFormInput')
    def getSavedFormInput(self):
        """ returns saved input as an iterable;
            each row is a sequence of fields.
        """

        if base_hasattr(self, '_inputStorage'):
            return self._inputStorage.values()
        else:
            return self.SavedFormInput


    security.declareProtected(DOWNLOAD_SAVED_PERMISSION, 'getSavedFormInputItems')
    def getSavedFormInputItems(self):
        """ returns saved input as an iterable;
            each row is an (id, sequence of fields) tuple
        """
        if base_hasattr(self, '_inputStorage'):
            return self._inputStorage.items()
        else:
            return enumerate(self.SavedFormInput)


    security.declareProtected(ModifyPortalContent, 'getSavedFormInputForEdit')
    def getSavedFormInputForEdit(self, **kwargs):
        """ returns saved as CSV text """
        delimiter = self.csvDelimiter()
        sbuf = StringIO()
        writer = csv.writer(sbuf, delimiter=delimiter)
        for row in self.getSavedFormInput():
            writer.writerow(row)
        res = sbuf.getvalue()
        sbuf.close()

        return res


    security.declareProtected(ModifyPortalContent, 'setSavedFormInput')
    def setSavedFormInput(self, value, **kwargs):
        """ expects value as csv text string, stores as list of lists """

        self._migrateStorage()

        self._inputStorage.clear()
        i = 0
        self._inputItems = 0
        self._length.set(0)

        if len(value):
            delimiter = self.csvDelimiter()
            sbuf = StringIO(value)
            reader = csv.reader(sbuf, delimiter=delimiter)
            for row in reader:
                if row:
                    self._inputStorage[i] = row
                    i += 1
                self._inputItems = i
                self._length.set(i)
            sbuf.close()

        # logger.debug("setSavedFormInput: %s items" % self._inputItems)

    def _clearSavedFormInput(self):
        # convenience method to clear input buffer

        self._migrateStorage()

        self._inputStorage.clear()
        self._inputItems = 0
        self._length.set(0)

    security.declareProtected(ModifyPortalContent, 'clearSavedFormInput')
    def clearSavedFormInput(self, **kwargs):
        """ clear input buffer TTW """

        plone.protect.CheckAuthenticator(self.REQUEST)
        plone.protect.PostOnly(self.REQUEST)
        self._clearSavedFormInput()
        self.REQUEST.response.redirect(self.absolute_url())

    security.declareProtected(DOWNLOAD_SAVED_PERMISSION, 'getSavedFormInputById')
    def getSavedFormInputById(self, id):
        """ Return the data stored for record with 'id' """
        lst = [field.replace('\r', '').replace('\n', r'\n') for field in self._inputStorage[id]]
        return lst

    security.declareProtected(ModifyPortalContent, 'manage_saveData')
    def manage_saveData(self, id,  data):
        """ Save the data for record with 'id' """

        plone.protect.CheckAuthenticator(self.REQUEST)
        plone.protect.PostOnly(self.REQUEST)

        self._migrateStorage()

        lst = list()
        for i in range(0, len(self.getColumnNames())):
            lst.append(getattr(data, 'item-%d' % i, '').replace(r'\n', '\n'))

        self._inputStorage[id] = lst
        self.REQUEST.RESPONSE.redirect(self.absolute_url() + '/view')


    security.declareProtected(ModifyPortalContent, 'manage_deleteData')
    def manage_deleteData(self, id):
        """ Delete the data for record with 'id' """

        self._migrateStorage()

        del self._inputStorage[id]
        self._inputItems -= 1
        self._length.change(-1)

        self.REQUEST.RESPONSE.redirect(self.absolute_url() + '/view')


    def _addDataRow(self, value):

        self._migrateStorage()

        if isinstance(self._inputStorage, IOBTree):
            # 32-bit IOBTree; use a key which is more likely to conflict
            # but which won't overflow the key's bits
            id = self._inputItems
            self._inputItems += 1
        else:
            # 64-bit LOBTree
            id = int(time.time() * 1000)
            while id in self._inputStorage:  # avoid collisions during testing
                id += 1
        self._inputStorage[id] = value
        self._length.change(1)


    security.declareProtected(ModifyPortalContent, 'addDataRow')
    def addDataRow(self, value):
        # """ a wrapper for the _addDataRow method """

        self._addDataRow(value)


    security.declarePrivate('onSuccess')
    def onSuccess(self, fields, REQUEST=None, loopstop=False):
        # """
        # saves data.
        # """

        if LP_SAVE_TO_CANONICAL and not loopstop:
            # LinguaPlone functionality:
            # check to see if we're in a translated
            # form folder, but not the canonical version.
            parent = self.aq_parent
            if safe_hasattr(parent, 'isTranslation') and \
               parent.isTranslation() and not parent.isCanonical():
                # look in the canonical version to see if there is
                # a matching (by id) save-data adapter.
                # If so, call its onSuccess method
                cf = parent.getCanonical()
                target = cf.get(self.getId())
                if target is not None and target.meta_type == 'FormSaveDataAdapter':
                    target.onSuccess(fields, REQUEST, loopstop=True)
                    return

        from ZPublisher.HTTPRequest import FileUpload

        data = []
        for f in fields:
            showFields = getattr(self, 'showFields', [])
            if showFields and f.id not in showFields:
                continue
            if f.isFileField():
                file = REQUEST.form.get('%s_file' % f.fgField.getName())
                if isinstance(file, FileUpload) and file.filename != '':
                    file.seek(0)
                    fdata = file.read()
                    filename = file.filename
                    mimetype, enc = guess_content_type(filename, fdata, None)
                    if mimetype.find('text/') >= 0:
                        # convert to native eols
                        fdata = fdata.replace('\x0d\x0a', '\n').replace('\x0a', '\n').replace('\x0d', '\n')
                        data.append('%s:%s:%s:%s' % (filename, mimetype, enc, fdata))
                    else:
                        data.append('%s:%s:%s:Binary upload discarded' %  (filename, mimetype, enc))
                else:
                    data.append('NO UPLOAD')
            elif not f.isLabel():
                val = REQUEST.form.get(f.fgField.getName(), '')
                if not type(val) in StringTypes:
                    # Zope has marshalled the field into
                    # something other than a string
                    val = str(val)
                data.append(val)

        if self.ExtraData:
            for f in self.ExtraData:
                if f == 'dt':
                    data.append(str(DateTime()))
                else:
                    data.append(getattr(REQUEST, f, ''))


        self._addDataRow(data)


    security.declareProtected(ModifyPortalContent, 'allFieldDisplayList')
    def allFieldDisplayList(self):
        # """ returns a DisplayList of all fields """
        return self.fgFieldsDisplayList()


    security.declareProtected(DOWNLOAD_SAVED_PERMISSION, 'getColumnNames')
    def getColumnNames(self, excludeServerSide=True):
        # """Returns a list of column names"""

        showFields = getattr(self, 'showFields', [])
        names = [field.getName() for field
                 in self.fgFields(displayOnly=True, excludeServerSide=excludeServerSide)
                 if not showFields or field.getName() in showFields]
        for f in self.ExtraData:
            names.append(f)

        return names


    security.declareProtected(DOWNLOAD_SAVED_PERMISSION, 'getColumnTitles')
    def getColumnTitles(self, excludeServerSide=True):
        # """Returns a list of column titles"""

        names = [field.widget.label for field
                 in self.fgFields(displayOnly=True, excludeServerSide=excludeServerSide)]
        for f in self.ExtraData:
            names.append(self.vocabExtraDataDL().getValue(f, ''))

        return names


    def _cleanInputForTSV(self, value):
        # make data safe to store in tab-delimited format

        return  str(value).replace('\x0d\x0a', r'\n').replace('\x0a', r'\n').replace('\x0d', r'\n').replace('\t', r'\t')


    security.declareProtected(DOWNLOAD_SAVED_PERMISSION, 'download_tsv')
    def download_tsv(self, REQUEST=None, RESPONSE=None):
        # """Download the saved data
        # """

        filename = self.id
        if filename.find('.') < 0:
            filename = '%s.tsv' % filename
        header_value = contentDispositionHeader('attachment', self.getCharset(), filename=filename)
        RESPONSE.setHeader("Content-Disposition", header_value)
        RESPONSE.setHeader("Content-Type", 'text/tab-separated-values;charset=%s' % self.getCharset())

        if getattr(self, 'UseColumnNames', False):
            res = "%s\n" % '\t'.join(self.getColumnNames(excludeServerSide=False))
            if isinstance(res, unicode):
                res = res.encode(self.getCharset())
        else:
            res = ''

        for row in self.getSavedFormInput():
            res = '%s%s\n' % (res, '\t'.join([self._cleanInputForTSV(col) for col in row]))

        return res


    security.declareProtected(DOWNLOAD_SAVED_PERMISSION, 'download_csv')
    def download_csv(self, REQUEST=None, RESPONSE=None):
        # """Download the saved data
        # """

        filename = self.id
        if filename.find('.') < 0:
            filename = '%s.csv' % filename
        header_value = contentDispositionHeader('attachment', self.getCharset(), filename=filename)
        RESPONSE.setHeader("Content-Disposition", header_value)
        RESPONSE.setHeader("Content-Type", 'text/comma-separated-values;charset=%s' % self.getCharset())

        if getattr(self, 'UseColumnNames', False):
            delimiter = self.csvDelimiter()
            res = "%s\n" % delimiter.join(self.getColumnNames(excludeServerSide=False))
            if isinstance(res, unicode):
                res = res.encode(self.getCharset())
        else:
            res = ''

        return '%s%s' % (res, self.getSavedFormInputForEdit())

    security.declareProtected(DOWNLOAD_SAVED_PERMISSION, 'download_xls')
    def download_xls(self, REQUEST=None, RESPONSE=None):
        # """Download the saved data
        # """
        filename = self.id
        if filename.find('.') < 0:
            filename = '%s.xls' % filename
        header_value = contentDispositionHeader('attachment', self.getCharset(), filename=filename)
        RESPONSE.setHeader("Content-Disposition", header_value)
        RESPONSE.setHeader("Content-Type", 'application/vnd.ms-excel')

        xldoc = xlwt.Workbook(encoding=self.getCharset())
        sheet = xldoc.add_sheet(self.Title())

        row_num = 0

        if getattr(self, 'UseColumnNames', False):
            col_names = self.getColumnNames(excludeServerSide=False)
            for idx, label in enumerate(col_names):
                sheet.write(0, idx, label.encode(self.getCharset()))
            row_num += 1

        for row in self.getSavedFormInput():
            for col_num, col in enumerate(row):
                if type(col) is unicode:
                    col = col.encode(self.getCharset())

                if urlparse(col).scheme in ('http', 'https'):
                    col = xlwt.Formula('HYPERLINK("%(url)s")' % dict(url=col))
                else:
                    for format in (int, float):
                        try:
                            col = format(col)
                            break
                        except ValueError:
                            pass

                sheet.write(row_num, col_num, col)
            row_num += 1

        string_buffer = StringIO()
        xldoc.save(string_buffer)
        return string_buffer.getvalue()

    security.declareProtected(DOWNLOAD_SAVED_PERMISSION, 'download')
    def download(self, REQUEST=None, RESPONSE=None):
        """Download the saved data
        """

        format = getattr(self, 'DownloadFormat', 'tsv')
        if format == 'tsv':
            return self.download_tsv(REQUEST, RESPONSE)
        if format == 'xls':
            assert has_xls, 'xls download not available'
            return self.download_xls(REQUEST, RESPONSE)
        else:
            assert format == 'csv', 'Unknown download format'
            return self.download_csv(REQUEST, RESPONSE)


    security.declareProtected(DOWNLOAD_SAVED_PERMISSION, 'rowAsColDict')
    def rowAsColDict(self, row, cols):
        # """ Where row is a data sequence and cols is a column name sequence,
        #     returns a dict of colname:column. This is a convenience method
        #     used in the record view.
        # """

        colcount = len(cols)

        rdict = {}
        for i in range(0, len(row)):
            if i < colcount:
                rdict[cols[i]] = row[i]
            else:
                rdict['column-%s' % i] = row[i]
        return rdict


    security.declareProtected(DOWNLOAD_SAVED_PERMISSION, 'inputAsDictionaries')
    def inputAsDictionaries(self):
        # """returns saved data as an iterable of dictionaries
        # """

        cols = self.getColumnNames()

        for row in self.getSavedFormInput():
            yield self.rowAsColDict(row, cols)


    # alias for old mis-naming
    security.declareProtected(DOWNLOAD_SAVED_PERMISSION, 'InputAsDictionaries')
    InputAsDictionaries = inputAsDictionaries


    security.declareProtected(DOWNLOAD_SAVED_PERMISSION, 'formatMIME')
    def formatMIME(self):
        # """MIME format selected for download
        # """

        format = getattr(self, 'DownloadFormat', 'tsv')
        if format == 'tsv':
            return 'text/tab-separated-values'
        if format == 'xls':
            return 'application/vnd.ms-excel'
        else:
            assert format == 'csv', 'Unknown download format'
            return 'text/comma-separated-values'

    security.declarePrivate('csvDelimiter')
    def csvDelimiter(self):
        # """Delimiter character for CSV downloads
        # """
        fgt = getToolByName(self, 'formgen_tool')
        return fgt.getCSVDelimiter()

    security.declareProtected(DOWNLOAD_SAVED_PERMISSION, 'itemsSaved')
    def itemsSaved(self):
        # """Download the saved data
        # """

        if base_hasattr(self, '_length'):
            return self._length()
        elif base_hasattr(self, '_inputItems'):
            return self._inputItems
        else:
            return len(self.SavedFormInput)

    def vocabExtraDataDL(self):
        # """ returns vocabulary for extra data """

        return DisplayList((
                ('dt',
                    self.translate(msgid='vocabulary_postingdt_text',
                    domain='ploneformgen',
                    default='Posting Date/Time')
                    ),
                ('HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED_FOR',),
                ('REMOTE_ADDR', 'REMOTE_ADDR',),
                ('HTTP_USER_AGENT', 'HTTP_USER_AGENT',),
                ))


    def vocabFormatDL(self):
        # """ returns vocabulary for format """
        formats = [
            ('tsv',
                self.translate(msgid='vocabulary_tsv_text',
                domain='ploneformgen',
                default='Tab-Separated Values')
                ),
            ('csv',
                self.translate(msgid='vocabulary_csv_text',
                domain='ploneformgen',
                default='Comma-Separated Values')
                ),
        ]
        if has_xls:
            formats.append(
                ('xls',
                    self.translate(msgid='vocabulary_xls_doc',
                    domain='ploneformgen',
                    default='Excel document')
                    ),
            )
        return DisplayList(formats)
Exemplo n.º 23
0
class OkapiIndex(BaseIndex):
    """
    Full text index with relevance ranking, using an Okapi BM25 rank.
    """

    # BM25 free parameters.
    K1 = 1.2
    B = 0.75
    assert K1 >= 0.0
    assert 0.0 <= B <= 1.0

    def __init__(self, lexicon, family=None):
        BaseIndex.__init__(self, lexicon, family=family)

        # ._wordinfo for Okapi is
        # wid -> {docid -> frequency}; t -> D -> f(D, t)

        # ._docweight for Okapi is
        # docid -> # of words in the doc
        # This is just len(self._docwords[docid]), but _docwords is stored
        # in compressed form, so uncompressing it just to count the list
        # length would be ridiculously expensive.

        # sum(self._docweight.values()), the total # of words in all docs
        # This is a long for "better safe than sorry" reasons.  It isn't
        # used often enough that speed should matter.
        self._totaldoclen = Length(0)

    def index_doc(self, docid, text):
        count = BaseIndex.index_doc(self, docid, text)
        self._change_doc_len(count)
        return count

    def _reindex_doc(self, docid, text):
        self._change_doc_len(-self._docweight[docid])
        return BaseIndex._reindex_doc(self, docid, text)

    def unindex_doc(self, docid):
        if docid not in self._docwords:
            return
        self._change_doc_len(-self._docweight[docid])
        BaseIndex.unindex_doc(self, docid)

    def _change_doc_len(self, delta):
        # Change total doc length used for scoring
        delta = int(delta)
        try:
            self._totaldoclen.change(delta)
        except AttributeError:
            # Opportunistically upgrade _totaldoclen attribute to Length object
            self._totaldoclen = Length(int(self._totaldoclen + delta))

    # The workhorse.  Return a list of (IFBucket, weight) pairs, one pair
    # for each wid t in wids.  The IFBucket, times the weight, maps D to
    # TF(D,t) * IDF(t) for every docid D containing t.
    # As currently written, the weights are always 1, and the IFBucket maps
    # D to TF(D,t)*IDF(t) directly, where the product is computed as a float.
    # NOTE:  This may be overridden below, by a function that computes the
    # same thing but with the inner scoring loop in C.
    def _python_search_wids(self, wids):
        if not wids:
            return []
        N = float(self.documentCount())  # total # of docs
        try:
            doclen = self._totaldoclen()
        except TypeError:
            # _totaldoclen has not yet been upgraded
            doclen = self._totaldoclen
        meandoclen = doclen / N
        K1 = self.K1
        B = self.B
        K1_plus1 = K1 + 1.0
        B_from1 = 1.0 - B

        #                           f(D, t) * (k1 + 1)
        #   TF(D, t) =  -------------------------------------------
        #               f(D, t) + k1 * ((1-b) + b*len(D)/E(len(D)))

        L = []
        docid2len = self._docweight
        for t in wids:
            d2f = self._wordinfo[t]  # map {docid -> f(docid, t)}
            idf = inverse_doc_frequency(len(d2f), N)  # an unscaled float
            result = self.family.IF.Bucket()
            for docid, f in d2f.items():
                lenweight = B_from1 + B * docid2len[docid] / meandoclen
                tf = f * K1_plus1 / (f + K1 * lenweight)
                result[docid] = tf * idf
            L.append((result, 1))
        return L

        # Note about the above: the result is tf * idf.  tf is
        # small -- it can't be larger than k1+1 = 2.2.  idf is
        # formally unbounded, but is less than 14 for a term that
        # appears in only 1 of a million documents.  So the
        # product is probably less than 32, or 5 bits before the
        # radix point.  If we did the scaled-int business on both
        # of them, we'd be up to 25 bits.  Add 64 of those and
        # we'd be in overflow territory.  That's pretty unlikely,
        # so we *could* just store scaled_int(tf) in
        # result[docid], and use scaled_int(idf) as an invariant
        # weight across the whole result.  But besides skating
        # near the edge, it's not a speed cure, since the
        # computation of tf would still be done at Python speed,
        # and it's a lot more work than just multiplying by idf.

    # The same function as _search_wids above, but with the inner scoring
    # loop written in C (module okascore, function score()).
    # Cautions:  okascore hardcodes the values of K, B1, and the scaled_int
    # function.
    def _c_search_wids(self, wids):
        if not wids:
            return []
        N = float(self.documentCount())  # total # of docs
        try:
            doclen = self._totaldoclen()
        except TypeError:
            # _totaldoclen has not yet been upgraded
            doclen = self._totaldoclen
        meandoclen = doclen / N
        #K1 = self.K1
        #B = self.B
        #K1_plus1 = K1 + 1.0
        #B_from1 = 1.0 - B

        #                           f(D, t) * (k1 + 1)
        #   TF(D, t) =  -------------------------------------------
        #               f(D, t) + k1 * ((1-b) + b*len(D)/E(len(D)))

        L = []
        docid2len = self._docweight
        for t in wids:
            d2f = self._wordinfo[t]  # map {docid -> f(docid, t)}
            idf = inverse_doc_frequency(len(d2f), N)  # an unscaled float
            result = self.family.IF.Bucket()
            items = d2f.items() if PY2 else list(d2f.items())
            score(result, items, docid2len, idf, meandoclen)
            L.append((result, 1))
        return L

    _search_wids = _python_search_wids if score is None else _c_search_wids

    def query_weight(self, terms):
        # Get the wids.
        wids = []
        for term in terms:
            termwids = self._lexicon.termToWordIds(term)
            wids.extend(termwids)
        # The max score for term t is the maximum value of
        #     TF(D, t) * IDF(Q, t)
        # We can compute IDF directly, and as noted in the comments below
        # TF(D, t) is bounded above by 1+K1.
        N = float(len(self._docweight))
        tfmax = 1.0 + self.K1
        sum = 0
        for t in self._remove_oov_wids(wids):
            idf = inverse_doc_frequency(len(self._wordinfo[t]), N)
            sum += idf * tfmax
        return sum

    def _get_frequencies(self, wids):
        d = {}
        dget = d.get
        for wid in wids:
            d[wid] = dget(wid, 0) + 1
        return d, len(wids)
Exemplo n.º 24
0
class UnIndex(SimpleItem):
    """Simple forward and reverse index.
    """
    implements(ILimitedResultIndex, IUniqueValueIndex, ISortIndex)

    def __init__(self,
                 id,
                 ignore_ex=None,
                 call_methods=None,
                 extra=None,
                 caller=None):
        """Create an unindex

        UnIndexes are indexes that contain two index components, the
        forward index (like plain index objects) and an inverted
        index.  The inverted index is so that objects can be unindexed
        even when the old value of the object is not known.

        e.g.

        self._index = {datum:[documentId1, documentId2]}
        self._unindex = {documentId:datum}

        The arguments are:

          'id' -- the name of the item attribute to index.  This is
          either an attribute name or a record key.

          'ignore_ex' -- should be set to true if you want the index
          to ignore exceptions raised while indexing instead of
          propagating them.

          'call_methods' -- should be set to true if you want the index
          to call the attribute 'id' (note: 'id' should be callable!)
          You will also need to pass in an object in the index and
          uninded methods for this to work.

          'extra' -- a mapping object that keeps additional
          index-related parameters - subitem 'indexed_attrs'
          can be string with comma separated attribute names or
          a list

          'caller' -- reference to the calling object (usually
          a (Z)Catalog instance
        """
        def _get(o, k, default):
            """ return a value for a given key of a dict/record 'o' """
            if isinstance(o, dict):
                return o.get(k, default)
            else:
                return getattr(o, k, default)

        self.id = id
        self.ignore_ex = ignore_ex  # currently unimplemented
        self.call_methods = call_methods

        self.operators = ('or', 'and')
        self.useOperator = 'or'

        # allow index to index multiple attributes
        ia = _get(extra, 'indexed_attrs', id)
        if isinstance(ia, str):
            self.indexed_attrs = ia.split(',')
        else:
            self.indexed_attrs = list(ia)
        self.indexed_attrs = [
            attr.strip() for attr in self.indexed_attrs if attr
        ]
        if not self.indexed_attrs:
            self.indexed_attrs = [id]

        self.clear()

    def __len__(self):
        return self._length()

    def getId(self):
        return self.id

    def clear(self):
        self._length = Length()
        self._index = OOBTree()
        self._unindex = IOBTree()

    def __nonzero__(self):
        return not not self._unindex

    def histogram(self):
        """Return a mapping which provides a histogram of the number of
        elements found at each point in the index.
        """
        histogram = {}
        for item in self._index.items():
            if isinstance(item, int):
                entry = 1  # "set" length is 1
            else:
                key, value = item
                entry = len(value)
            histogram[entry] = histogram.get(entry, 0) + 1
        return histogram

    def referencedObjects(self):
        """Generate a list of IDs for which we have referenced objects."""
        return self._unindex.keys()

    def getEntryForObject(self, documentId, default=_marker):
        """Takes a document ID and returns all the information we have
        on that specific object.
        """
        if default is _marker:
            return self._unindex.get(documentId)
        return self._unindex.get(documentId, default)

    def removeForwardIndexEntry(self, entry, documentId):
        """Take the entry provided and remove any reference to documentId
        in its entry in the index.
        """
        indexRow = self._index.get(entry, _marker)
        if indexRow is not _marker:
            try:
                indexRow.remove(documentId)
                if not indexRow:
                    del self._index[entry]
                    self._length.change(-1)
            except ConflictError:
                raise
            except AttributeError:
                # index row is an int
                try:
                    del self._index[entry]
                except KeyError:
                    # XXX swallow KeyError because it was probably
                    # removed and then _length AttributeError raised
                    pass
                if isinstance(self.__len__, Length):
                    self._length = self.__len__
                    del self.__len__
                self._length.change(-1)
            except Exception:
                LOG.error(
                    '%s: unindex_object could not remove '
                    'documentId %s from index %s.  This '
                    'should not happen.' %
                    (self.__class__.__name__, str(documentId), str(self.id)),
                    exc_info=sys.exc_info())
        else:
            LOG.error('%s: unindex_object tried to retrieve set %s '
                      'from index %s but couldn\'t.  This '
                      'should not happen.' %
                      (self.__class__.__name__, repr(entry), str(self.id)))

    def insertForwardIndexEntry(self, entry, documentId):
        """Take the entry provided and put it in the correct place
        in the forward index.

        This will also deal with creating the entire row if necessary.
        """
        indexRow = self._index.get(entry, _marker)

        # Make sure there's actually a row there already. If not, create
        # a set and stuff it in first.
        if indexRow is _marker:
            # We always use a set to avoid getting conflict errors on
            # multiple threads adding a new row at the same time
            self._index[entry] = IITreeSet((documentId, ))
            self._length.change(1)
        else:
            try:
                indexRow.insert(documentId)
            except AttributeError:
                # Inline migration: index row with one element was an int at
                # first (before Zope 2.13).
                indexRow = IITreeSet((indexRow, documentId))
                self._index[entry] = indexRow

    def index_object(self, documentId, obj, threshold=None):
        """ wrapper to handle indexing of multiple attributes """
        fields = self.getIndexSourceNames()
        res = 0
        for attr in fields:
            res += self._index_object(documentId, obj, threshold, attr)
        return res > 0

    def _index_object(self, documentId, obj, threshold=None, attr=''):
        """ index and object 'obj' with integer id 'documentId'"""
        returnStatus = 0

        # First we need to see if there's anything interesting to look at
        datum = self._get_object_datum(obj, attr)

        # We don't want to do anything that we don't have to here, so we'll
        # check to see if the new and existing information is the same.
        oldDatum = self._unindex.get(documentId, _marker)
        if datum != oldDatum:
            if oldDatum is not _marker:
                self.removeForwardIndexEntry(oldDatum, documentId)
                if datum is _marker:
                    try:
                        del self._unindex[documentId]
                    except ConflictError:
                        raise
                    except Exception:
                        LOG.error('Should not happen: oldDatum was there, '
                                  'now its not, for document: %s' % documentId)

            if datum is not _marker:
                self.insertForwardIndexEntry(datum, documentId)
                self._unindex[documentId] = datum

            returnStatus = 1

        return returnStatus

    def _get_object_datum(self, obj, attr):
        # self.id is the name of the index, which is also the name of the
        # attribute we're interested in.  If the attribute is callable,
        # we'll do so.
        try:
            datum = getattr(obj, attr)
            if safe_callable(datum):
                datum = datum()
        except (AttributeError, TypeError):
            datum = _marker
        return datum

    def numObjects(self):
        """Return the number of indexed objects."""
        return len(self._unindex)

    def indexSize(self):
        """Return the size of the index in terms of distinct values."""
        return len(self)

    def unindex_object(self, documentId):
        """ Unindex the object with integer id 'documentId' and don't
        raise an exception if we fail
        """
        unindexRecord = self._unindex.get(documentId, _marker)
        if unindexRecord is _marker:
            return None

        self.removeForwardIndexEntry(unindexRecord, documentId)
        try:
            del self._unindex[documentId]
        except ConflictError:
            raise
        except Exception:
            LOG.debug('Attempt to unindex nonexistent document'
                      ' with id %s' % documentId,
                      exc_info=True)

    def _apply_not(self, not_parm, resultset=None):
        index = self._index
        setlist = []
        for k in not_parm:
            s = index.get(k, None)
            if s is None:
                continue
            elif isinstance(s, int):
                s = IISet((s, ))
            setlist.append(s)
        return multiunion(setlist)

    def _convert(self, value, default=None):
        return value

    def _apply_index(self, request, resultset=None):
        """Apply the index to query parameters given in the request arg.

        The request argument should be a mapping object.

        If the request does not have a key which matches the "id" of
        the index instance, then None is returned.

        If the request *does* have a key which matches the "id" of
        the index instance, one of a few things can happen:

          - if the value is a blank string, None is returned (in
            order to support requests from web forms where
            you can't tell a blank string from empty).

          - if the value is a nonblank string, turn the value into
            a single-element sequence, and proceed.

          - if the value is a sequence, return a union search.

          - If the value is a dict and contains a key of the form
            '<index>_operator' this overrides the default method
            ('or') to combine search results. Valid values are "or"
            and "and".

        If None is not returned as a result of the abovementioned
        constraints, two objects are returned.  The first object is a
        ResultSet containing the record numbers of the matching
        records.  The second object is a tuple containing the names of
        all data fields used.

        FAQ answer:  to search a Field Index for documents that
        have a blank string as their value, wrap the request value
        up in a tuple ala: request = {'id':('',)}
        """
        record = parseIndexRequest(request, self.id, self.query_options)
        if record.keys is None:
            return None

        index = self._index
        r = None
        opr = None

        # not / exclude parameter
        not_parm = record.get('not', None)
        if not record.keys and not_parm:
            # convert into indexed format
            not_parm = map(self._convert, not_parm)
            # we have only a 'not' query
            record.keys = [k for k in index.keys() if k not in not_parm]
        else:
            # convert query arguments into indexed format
            record.keys = map(self._convert, record.keys)

        # experimental code for specifing the operator
        operator = record.get('operator', self.useOperator)
        if not operator in self.operators:
            raise RuntimeError("operator not valid: %s" % escape(operator))

        # Range parameter
        range_parm = record.get('range', None)
        if range_parm:
            opr = "range"
            opr_args = []
            if range_parm.find("min") > -1:
                opr_args.append("min")
            if range_parm.find("max") > -1:
                opr_args.append("max")

        if record.get('usage', None):
            # see if any usage params are sent to field
            opr = record.usage.lower().split(':')
            opr, opr_args = opr[0], opr[1:]

        if opr == "range":  # range search
            if 'min' in opr_args:
                lo = min(record.keys)
            else:
                lo = None
            if 'max' in opr_args:
                hi = max(record.keys)
            else:
                hi = None
            if hi:
                setlist = index.values(lo, hi)
            else:
                setlist = index.values(lo)

            # If we only use one key, intersect and return immediately
            if len(setlist) == 1:
                result = setlist[0]
                if isinstance(result, int):
                    result = IISet((result, ))
                if not_parm:
                    exclude = self._apply_not(not_parm, resultset)
                    result = difference(result, exclude)
                return result, (self.id, )

            if operator == 'or':
                tmp = []
                for s in setlist:
                    if isinstance(s, int):
                        s = IISet((s, ))
                    tmp.append(s)
                r = multiunion(tmp)
            else:
                # For intersection, sort with smallest data set first
                tmp = []
                for s in setlist:
                    if isinstance(s, int):
                        s = IISet((s, ))
                    tmp.append(s)
                if len(tmp) > 2:
                    setlist = sorted(tmp, key=len)
                else:
                    setlist = tmp
                r = resultset
                for s in setlist:
                    # the result is bound by the resultset
                    r = intersection(r, s)

        else:  # not a range search
            # Filter duplicates
            setlist = []
            for k in record.keys:
                s = index.get(k, None)
                # If None, try to bail early
                if s is None:
                    if operator == 'or':
                        # If union, we can't possibly get a bigger result
                        continue
                    # If intersection, we can't possibly get a smaller result
                    return IISet(), (self.id, )
                elif isinstance(s, int):
                    s = IISet((s, ))
                setlist.append(s)

            # If we only use one key return immediately
            if len(setlist) == 1:
                result = setlist[0]
                if isinstance(result, int):
                    result = IISet((result, ))
                if not_parm:
                    exclude = self._apply_not(not_parm, resultset)
                    result = difference(result, exclude)
                return result, (self.id, )

            if operator == 'or':
                # If we already get a small result set passed in, intersecting
                # the various indexes with it and doing the union later is
                # faster than creating a multiunion first.
                if resultset is not None and len(resultset) < 200:
                    smalllist = []
                    for s in setlist:
                        smalllist.append(intersection(resultset, s))
                    r = multiunion(smalllist)
                else:
                    r = multiunion(setlist)
            else:
                # For intersection, sort with smallest data set first
                if len(setlist) > 2:
                    setlist = sorted(setlist, key=len)
                r = resultset
                for s in setlist:
                    r = intersection(r, s)

        if isinstance(r, int):
            r = IISet((r, ))
        if r is None:
            return IISet(), (self.id, )
        if not_parm:
            exclude = self._apply_not(not_parm, resultset)
            r = difference(r, exclude)
        return r, (self.id, )

    def hasUniqueValuesFor(self, name):
        """has unique values for column name"""
        if name == self.id:
            return 1
        return 0

    def getIndexSourceNames(self):
        """ return sequence of indexed attributes """
        # BBB:  older indexes didn't have 'indexed_attrs'
        return getattr(self, 'indexed_attrs', [self.id])

    def getIndexQueryNames(self):
        """Indicate that this index applies to queries for the index's name."""
        return (self.id, )

    def uniqueValues(self, name=None, withLengths=0):
        """returns the unique values for name

        if withLengths is true, returns a sequence of
        tuples of (value, length)
        """
        if name is None:
            name = self.id
        elif name != self.id:
            raise StopIteration

        if not withLengths:
            for key in self._index.keys():
                yield key
        else:
            for key, value in self._index.items():
                if isinstance(value, int):
                    yield (key, 1)
                else:
                    yield (key, len(value))

    def keyForDocument(self, id):
        # This method is superseded by documentToKeyMap
        return self._unindex[id]

    def documentToKeyMap(self):
        return self._unindex

    def items(self):
        items = []
        for k, v in self._index.items():
            if isinstance(v, int):
                v = IISet((v, ))
            items.append((k, v))
        return items
Exemplo n.º 25
0
class UUIDIndex(UnIndex):
    """Index for uuid fields with an unique value per key.

    The internal structure is:

    self._index = {datum:documentId]}
    self._unindex = {documentId:datum}

    For each datum only one documentId can exist.
    """

    meta_type = "UUIDIndex"

    manage_options = (
        {
            'label': 'Settings',
            'action': 'manage_main'
        },
        {
            'label': 'Browse',
            'action': 'manage_browse'
        },
    )

    query_options = ["query", "range"]

    manage = manage_main = DTMLFile('dtml/manageUUIDIndex', globals())
    manage_main._setName('manage_main')
    manage_browse = DTMLFile('../dtml/browseIndex', globals())

    def clear(self):
        self._length = Length()
        self._index = OIBTree()
        self._unindex = IOBTree()

    def numObjects(self):
        """Return the number of indexed objects. Since we have a 1:1 mapping
        from documents to values, we can reuse the stored length.
        """
        return self.indexSize()

    def uniqueValues(self, name=None, withLengths=0):
        """returns the unique values for name

        if withLengths is true, returns a sequence of
        tuples of (value, length)
        """
        if name is None:
            name = self.id
        elif name != self.id:
            return []

        if not withLengths:
            return tuple(self._index.keys())
        # We know the length for each value is one
        return [(k, 1) for k in self._index.keys()]

    def insertForwardIndexEntry(self, entry, documentId):
        """Take the entry provided and put it in the correct place
        in the forward index.
        """
        if entry is None:
            return

        old_docid = self._index.get(entry, _marker)
        if old_docid is _marker:
            self._index[entry] = documentId
            self._length.change(1)
        elif old_docid != documentId:
            logger.error("A different document with value '%s' already "
                         "exists in the index.'" % entry)

    def removeForwardIndexEntry(self, entry, documentId):
        """Take the entry provided and remove any reference to documentId
        in its entry in the index.
        """
        old_docid = self._index.get(entry, _marker)
        if old_docid is not _marker:
            del self._index[entry]
            self._length.change(-1)

    def _get_object_datum(self, obj, attr):
        # for a uuid it never makes sense to acquire a parent value via
        # Acquisition
        has_attr = getattr(aq_base(obj), attr, _marker)
        if has_attr is _marker:
            return _marker
        return super(UUIDIndex, self)._get_object_datum(obj, attr)
Exemplo n.º 26
0
class FieldIndex(SortingIndexMixin, persistent.Persistent):
    """
    A field index.

    Implements :class:`zope.index.interfaces.IInjection`,
    :class:`zope.index.interfaces.IStatistics` and
    :class:`zope.index.interfaces.IIndexSearch`.
    """

    family = BTrees.family32

    def __init__(self, family=None):
        if family is not None:
            self.family = family
        self.clear()

    def clear(self):
        """Initialize forward and reverse mappings."""
        # The forward index maps indexed values to a sequence of docids
        self._fwd_index = self.family.OO.BTree()
        # The reverse index maps a docid to its index value
        self._rev_index = self.family.IO.BTree()
        self._num_docs = Length(0)

    def documentCount(self):
        """See interface IStatistics"""
        return self._num_docs()

    def wordCount(self):
        """See interface IStatistics"""
        return len(self._fwd_index)

    def index_doc(self, docid, value):
        """See interface IInjection"""
        rev_index = self._rev_index
        if docid in rev_index:
            if docid in self._fwd_index.get(value, ()):
                # no need to index the doc, its already up to date
                return
            self.unindex_doc(docid)
        # Insert into forward index.
        set = self._fwd_index.get(value)
        if set is None:
            set = self.family.IF.TreeSet()
            self._fwd_index[value] = set
        set.insert(docid)

        # increment doc count
        self._num_docs.change(1)

        # Insert into reverse index.
        rev_index[docid] = value

    def unindex_doc(self, docid):
        """See interface IInjection"""
        rev_index = self._rev_index
        value = rev_index.get(docid, _MARKER)
        if value is _MARKER:
            return # not in index

        del rev_index[docid]

        try:
            set = self._fwd_index[value]
            set.remove(docid)
        except KeyError: #pragma NO COVERAGE
            # This is fishy, but we don't want to raise an error.
            # We should probably log something.
            # but keep it from throwing a dirty exception
            set = 1

        if not set:
            del self._fwd_index[value]

        self._num_docs.change(-1)

    def apply(self, query):
        if len(query) != 2 or not isinstance(query, tuple):
            raise TypeError("two-length tuple expected", query)
        return self.family.IF.multiunion(
            self._fwd_index.values(*query))
Exemplo n.º 27
0
class CachingCatalog(Catalog):
    implements(ICatalog)

    os = os # for unit tests
    generation = None # b/c

    def __init__(self):
        super(CachingCatalog, self).__init__()
        self.generation = Length(0)

    def clear(self):
        self.invalidate()
        super(CachingCatalog, self).clear()

    def index_doc(self, *arg, **kw):
        self.invalidate()
        super(CachingCatalog, self).index_doc(*arg, **kw)

    def unindex_doc(self, *arg, **kw):
        self.invalidate()
        super(CachingCatalog, self).unindex_doc(*arg, **kw)

    def reindex_doc(self, *arg, **kw):
        self.invalidate()
        super(CachingCatalog, self).reindex_doc(*arg, **kw)

    def __setitem__(self, *arg, **kw):
        self.invalidate()
        super(CachingCatalog, self).__setitem__(*arg, **kw)

    def search(self, *arg, **kw):
        use_cache = True

        if 'use_cache' in kw:
            use_cache = kw.pop('use_cache')

        if 'NO_CATALOG_CACHE' in self.os.environ:
            use_cache = False

        if 'tags' in kw:
            # The tags index changes without invalidating the catalog,
            # so don't cache any query involving the tags index.
            use_cache = False

        if not use_cache:
            return self._search(*arg, **kw)

        cache = queryUtility(ICatalogSearchCache)

        if cache is None:
            return self._search(*arg, **kw)

        key = cPickle.dumps((arg, kw))

        generation = self.generation

        if generation is None:
            generation = Length(0)

        genval = generation.value

        if (genval == 0) or (genval > cache.generation):
            # an update in another process requires that the local cache be
            # invalidated
            cache.clear()
            cache.generation = genval

        if cache.get(key) is None:
            num, docids = self._search(*arg, **kw)

            # We don't cache large result sets because the time it takes to
            # unroll the result set turns out to be far more time than it
            # takes to run the search. In a particular instance using OSI's
            # catalog a search that took 0.015s but returned nearly 35,295
            # results took over 50s to unroll the result set for caching,
            # significantly slowing search performance.
            if num > LARGE_RESULT_SET:
                return num, docids

            # we need to unroll here; a btree-based structure may have
            # a reference to its connection
            docids = list(docids)
            cache[key] = (num, docids)

        return cache.get(key)

    def _search(self, *arg, **kw):
        start = time.time()
        res = super(CachingCatalog, self).search(*arg, **kw)
        duration = time.time() - start
        notify(CatalogQueryEvent(self, kw, duration, res))
        return res

    def invalidate(self):
        # Increment the generation; this tells *another process* that
        # its catalog cache needs to be cleared
        generation = self.generation

        if generation is None:
            generation = self.generation = Length(0)

        if generation.value >= sys.maxint:
            # don't keep growing the generation integer; wrap at sys.maxint
            self.generation.set(0)
        else:
            self.generation.change(1)

        # Clear the cache for *this process*
        cache = queryUtility(ICatalogSearchCache)
        if cache is not None:
            cache.clear()
            cache.generation = self.generation.value
Exemplo n.º 28
0
class UnIndex(SimpleItem):

    """Simple forward and reverse index.
    """
    implements(ILimitedResultIndex, IUniqueValueIndex, ISortIndex)
    _counter = None

    def __init__(self, id, ignore_ex=None, call_methods=None,
                 extra=None, caller=None):
        """Create an unindex

        UnIndexes are indexes that contain two index components, the
        forward index (like plain index objects) and an inverted
        index.  The inverted index is so that objects can be unindexed
        even when the old value of the object is not known.

        e.g.

        self._index = {datum:[documentId1, documentId2]}
        self._unindex = {documentId:datum}

        The arguments are:

          'id' -- the name of the item attribute to index.  This is
          either an attribute name or a record key.

          'ignore_ex' -- should be set to true if you want the index
          to ignore exceptions raised while indexing instead of
          propagating them.

          'call_methods' -- should be set to true if you want the index
          to call the attribute 'id' (note: 'id' should be callable!)
          You will also need to pass in an object in the index and
          uninded methods for this to work.

          'extra' -- a mapping object that keeps additional
          index-related parameters - subitem 'indexed_attrs'
          can be string with comma separated attribute names or
          a list

          'caller' -- reference to the calling object (usually
          a (Z)Catalog instance
        """

        def _get(o, k, default):
            """ return a value for a given key of a dict/record 'o' """
            if isinstance(o, dict):
                return o.get(k, default)
            else:
                return getattr(o, k, default)

        self.id = id
        self.ignore_ex = ignore_ex  # currently unimplemented
        self.call_methods = call_methods

        self.operators = ('or', 'and')
        self.useOperator = 'or'

        # allow index to index multiple attributes
        ia = _get(extra, 'indexed_attrs', id)
        if isinstance(ia, str):
            self.indexed_attrs = ia.split(',')
        else:
            self.indexed_attrs = list(ia)
        self.indexed_attrs = [
            attr.strip() for attr in self.indexed_attrs if attr]
        if not self.indexed_attrs:
            self.indexed_attrs = [id]

        self.clear()

    def __len__(self):
        return self._length()

    def getId(self):
        return self.id

    def clear(self):
        self._length = Length()
        self._index = OOBTree()
        self._unindex = IOBTree()
        self._counter = Length()

    def __nonzero__(self):
        return not not self._unindex

    def histogram(self):
        """Return a mapping which provides a histogram of the number of
        elements found at each point in the index.
        """
        histogram = {}
        for item in self._index.items():
            if isinstance(item, int):
                entry = 1  # "set" length is 1
            else:
                key, value = item
                entry = len(value)
            histogram[entry] = histogram.get(entry, 0) + 1
        return histogram

    def referencedObjects(self):
        """Generate a list of IDs for which we have referenced objects."""
        return self._unindex.keys()

    def getEntryForObject(self, documentId, default=_marker):
        """Takes a document ID and returns all the information we have
        on that specific object.
        """
        if default is _marker:
            return self._unindex.get(documentId)
        return self._unindex.get(documentId, default)

    def removeForwardIndexEntry(self, entry, documentId):
        """Take the entry provided and remove any reference to documentId
        in its entry in the index.
        """
        indexRow = self._index.get(entry, _marker)
        if indexRow is not _marker:
            try:
                indexRow.remove(documentId)
                if not indexRow:
                    del self._index[entry]
                    self._length.change(-1)
            except ConflictError:
                raise
            except AttributeError:
                # index row is an int
                try:
                    del self._index[entry]
                except KeyError:
                    # XXX swallow KeyError because it was probably
                    # removed and then _length AttributeError raised
                    pass
                if isinstance(self.__len__, Length):
                    self._length = self.__len__
                    del self.__len__
                self._length.change(-1)
            except Exception:
                LOG.error('%s: unindex_object could not remove '
                          'documentId %s from index %s.  This '
                          'should not happen.' %
                          (self.__class__.__name__,
                           str(documentId), str(self.id)),
                          exc_info=sys.exc_info())
        else:
            LOG.error('%s: unindex_object tried to retrieve set %s '
                      'from index %s but couldn\'t.  This '
                      'should not happen.' %
                      (self.__class__.__name__,
                       repr(entry), str(self.id)))

    def insertForwardIndexEntry(self, entry, documentId):
        """Take the entry provided and put it in the correct place
        in the forward index.

        This will also deal with creating the entire row if necessary.
        """
        indexRow = self._index.get(entry, _marker)

        # Make sure there's actually a row there already. If not, create
        # a set and stuff it in first.
        if indexRow is _marker:
            # We always use a set to avoid getting conflict errors on
            # multiple threads adding a new row at the same time
            self._index[entry] = IITreeSet((documentId, ))
            self._length.change(1)
        else:
            try:
                indexRow.insert(documentId)
            except AttributeError:
                # Inline migration: index row with one element was an int at
                # first (before Zope 2.13).
                indexRow = IITreeSet((indexRow, documentId))
                self._index[entry] = indexRow

    def index_object(self, documentId, obj, threshold=None):
        """ wrapper to handle indexing of multiple attributes """

        fields = self.getIndexSourceNames()
        res = 0
        for attr in fields:
            res += self._index_object(documentId, obj, threshold, attr)

        if res > 0:
            self._increment_counter()

        return res > 0

    def _index_object(self, documentId, obj, threshold=None, attr=''):
        """ index and object 'obj' with integer id 'documentId'"""
        returnStatus = 0

        # First we need to see if there's anything interesting to look at
        datum = self._get_object_datum(obj, attr)
        if datum is None:
            # Prevent None from being indexed. None doesn't have a valid
            # ordering definition compared to any other object.
            # BTrees 4.0+ will throw a TypeError
            # "object has default comparison" and won't let it be indexed.
            raise TypeError('None cannot be indexed.')

        # We don't want to do anything that we don't have to here, so we'll
        # check to see if the new and existing information is the same.
        oldDatum = self._unindex.get(documentId, _marker)
        if datum != oldDatum:
            if oldDatum is not _marker:
                self.removeForwardIndexEntry(oldDatum, documentId)
                if datum is _marker:
                    try:
                        del self._unindex[documentId]
                    except ConflictError:
                        raise
                    except Exception:
                        LOG.error('Should not happen: oldDatum was there, '
                                  'now its not, for document: %s' % documentId)

            if datum is not _marker:
                self.insertForwardIndexEntry(datum, documentId)
                self._unindex[documentId] = datum

            returnStatus = 1

        return returnStatus

    def _get_object_datum(self, obj, attr):
        # self.id is the name of the index, which is also the name of the
        # attribute we're interested in.  If the attribute is callable,
        # we'll do so.
        try:
            datum = getattr(obj, attr)
            if safe_callable(datum):
                datum = datum()
        except (AttributeError, TypeError):
            datum = _marker
        return datum

    def _increment_counter(self):
        if self._counter is None:
            self._counter = Length()
        self._counter.change(1)

    def getCounter(self):
        """Return a counter which is increased on index changes"""
        return self._counter is not None and self._counter() or 0

    def numObjects(self):
        """Return the number of indexed objects."""
        return len(self._unindex)

    def indexSize(self):
        """Return the size of the index in terms of distinct values."""
        return len(self)

    def unindex_object(self, documentId):
        """ Unindex the object with integer id 'documentId' and don't
        raise an exception if we fail
        """
        unindexRecord = self._unindex.get(documentId, _marker)
        if unindexRecord is _marker:
            return None

        self._increment_counter()

        self.removeForwardIndexEntry(unindexRecord, documentId)
        try:
            del self._unindex[documentId]
        except ConflictError:
            raise
        except Exception:
            LOG.debug('Attempt to unindex nonexistent document'
                      ' with id %s' % documentId, exc_info=True)

    def _apply_not(self, not_parm, resultset=None):
        index = self._index
        setlist = []
        for k in not_parm:
            s = index.get(k, None)
            if s is None:
                continue
            elif isinstance(s, int):
                s = IISet((s, ))
            setlist.append(s)
        return multiunion(setlist)

    def _convert(self, value, default=None):
        return value

    def _apply_index(self, request, resultset=None):
        """Apply the index to query parameters given in the request arg.

        The request argument should be a mapping object.

        If the request does not have a key which matches the "id" of
        the index instance, then None is returned.

        If the request *does* have a key which matches the "id" of
        the index instance, one of a few things can happen:

          - if the value is a blank string, None is returned (in
            order to support requests from web forms where
            you can't tell a blank string from empty).

          - if the value is a nonblank string, turn the value into
            a single-element sequence, and proceed.

          - if the value is a sequence, return a union search.

          - If the value is a dict and contains a key of the form
            '<index>_operator' this overrides the default method
            ('or') to combine search results. Valid values are "or"
            and "and".

        If None is not returned as a result of the abovementioned
        constraints, two objects are returned.  The first object is a
        ResultSet containing the record numbers of the matching
        records.  The second object is a tuple containing the names of
        all data fields used.

        FAQ answer:  to search a Field Index for documents that
        have a blank string as their value, wrap the request value
        up in a tuple ala: request = {'id':('',)}
        """
        record = parseIndexRequest(request, self.id, self.query_options)
        if record.keys is None:
            return None

        index = self._index
        r = None
        opr = None

        # not / exclude parameter
        not_parm = record.get('not', None)
        if not record.keys and not_parm:
            # convert into indexed format
            not_parm = map(self._convert, not_parm)
            # we have only a 'not' query
            record.keys = [k for k in index.keys() if k not in not_parm]
        else:
            # convert query arguments into indexed format
            record.keys = map(self._convert, record.keys)

        # experimental code for specifing the operator
        operator = record.get('operator', self.useOperator)
        if not operator in self.operators:
            raise RuntimeError("operator not valid: %s" % escape(operator))

        # Range parameter
        range_parm = record.get('range', None)
        if range_parm:
            opr = "range"
            opr_args = []
            if range_parm.find("min") > -1:
                opr_args.append("min")
            if range_parm.find("max") > -1:
                opr_args.append("max")

        if record.get('usage', None):
            # see if any usage params are sent to field
            opr = record.usage.lower().split(':')
            opr, opr_args = opr[0], opr[1:]

        if opr == "range":  # range search
            if 'min' in opr_args:
                lo = min(record.keys)
            else:
                lo = None
            if 'max' in opr_args:
                hi = max(record.keys)
            else:
                hi = None
            if hi:
                setlist = index.values(lo, hi)
            else:
                setlist = index.values(lo)

            # If we only use one key, intersect and return immediately
            if len(setlist) == 1:
                result = setlist[0]
                if isinstance(result, int):
                    result = IISet((result,))
                if not_parm:
                    exclude = self._apply_not(not_parm, resultset)
                    result = difference(result, exclude)
                return result, (self.id,)

            if operator == 'or':
                tmp = []
                for s in setlist:
                    if isinstance(s, int):
                        s = IISet((s,))
                    tmp.append(s)
                r = multiunion(tmp)
            else:
                # For intersection, sort with smallest data set first
                tmp = []
                for s in setlist:
                    if isinstance(s, int):
                        s = IISet((s,))
                    tmp.append(s)
                if len(tmp) > 2:
                    setlist = sorted(tmp, key=len)
                else:
                    setlist = tmp
                r = resultset
                for s in setlist:
                    # the result is bound by the resultset
                    r = intersection(r, s)

        else:  # not a range search
            # Filter duplicates
            setlist = []
            for k in record.keys:
                if k is None:
                    raise TypeError('None cannot be in an index.')
                s = index.get(k, None)
                # If None, try to bail early
                if s is None:
                    if operator == 'or':
                        # If union, we can't possibly get a bigger result
                        continue
                    # If intersection, we can't possibly get a smaller result
                    return IISet(), (self.id,)
                elif isinstance(s, int):
                    s = IISet((s,))
                setlist.append(s)

            # If we only use one key return immediately
            if len(setlist) == 1:
                result = setlist[0]
                if isinstance(result, int):
                    result = IISet((result,))
                if not_parm:
                    exclude = self._apply_not(not_parm, resultset)
                    result = difference(result, exclude)
                return result, (self.id,)

            if operator == 'or':
                # If we already get a small result set passed in, intersecting
                # the various indexes with it and doing the union later is
                # faster than creating a multiunion first.
                if resultset is not None and len(resultset) < 200:
                    smalllist = []
                    for s in setlist:
                        smalllist.append(intersection(resultset, s))
                    r = multiunion(smalllist)
                else:
                    r = multiunion(setlist)
            else:
                # For intersection, sort with smallest data set first
                if len(setlist) > 2:
                    setlist = sorted(setlist, key=len)
                r = resultset
                for s in setlist:
                    r = intersection(r, s)

        if isinstance(r, int):
            r = IISet((r, ))
        if r is None:
            return IISet(), (self.id,)
        if not_parm:
            exclude = self._apply_not(not_parm, resultset)
            r = difference(r, exclude)
        return r, (self.id,)

    def hasUniqueValuesFor(self, name):
        """has unique values for column name"""
        if name == self.id:
            return 1
        return 0

    def getIndexSourceNames(self):
        """ return sequence of indexed attributes """
        # BBB:  older indexes didn't have 'indexed_attrs'
        return getattr(self, 'indexed_attrs', [self.id])

    def getIndexQueryNames(self):
        """Indicate that this index applies to queries for the index's name."""
        return (self.id,)

    def uniqueValues(self, name=None, withLengths=0):
        """returns the unique values for name

        if withLengths is true, returns a sequence of
        tuples of (value, length)
        """
        if name is None:
            name = self.id
        elif name != self.id:
            raise StopIteration

        if not withLengths:
            for key in self._index.keys():
                yield key
        else:
            for key, value in self._index.items():
                if isinstance(value, int):
                    yield (key, 1)
                else:
                    yield (key, len(value))

    def keyForDocument(self, id):
        # This method is superseded by documentToKeyMap
        return self._unindex[id]

    def documentToKeyMap(self):
        return self._unindex

    def items(self):
        items = []
        for k, v in self._index.items():
            if isinstance(v, int):
                v = IISet((v,))
            items.append((k, v))
        return items
Exemplo n.º 29
0
class BTreeFolder2Base(Persistent):
    """Base for BTree-based folders.
    """

    security = ClassSecurityInfo()

    manage_options = (({
        'label': 'Contents',
        'action': 'manage_main',
    }, ) + Folder.manage_options[1:])

    security.declareProtected(view_management_screens, 'manage_main')
    manage_main = DTMLFile('contents', globals())

    _tree = None  # OOBTree: { id -> object }
    _count = None  # A BTrees.Length
    _v_nextid = 0  # The integer component of the next generated ID
    _mt_index = None  # OOBTree: { meta_type -> OIBTree: { id -> 1 } }
    title = ''

    def __init__(self, id=None):
        if id is not None:
            self.id = id
        self._initBTrees()

    def _initBTrees(self):
        self._tree = OOBTree()
        self._count = Length()
        self._mt_index = OOBTree()

    def _populateFromFolder(self, source):
        """Fill this folder with the contents of another folder.
        """
        for name in source.objectIds():
            value = source._getOb(name, None)
            if value is not None:
                self._setOb(name, aq_base(value))

    security.declareProtected(view_management_screens, 'manage_fixCount')

    def manage_fixCount(self):
        """Calls self._fixCount() and reports the result as text.
        """
        old, new = self._fixCount()
        path = '/'.join(self.getPhysicalPath())
        if old == new:
            return "No count mismatch detected in BTreeFolder2 at %s." % path
        else:
            return ("Fixed count mismatch in BTreeFolder2 at %s. "
                    "Count was %d; corrected to %d" % (path, old, new))

    def _fixCount(self):
        """Checks if the value of self._count disagrees with
        len(self.objectIds()). If so, corrects self._count. Returns the
        old and new count values. If old==new, no correction was
        performed.
        """
        old = self._count()
        new = len(self.objectIds())
        if old != new:
            self._count.set(new)
        return old, new

    security.declareProtected(view_management_screens, 'manage_cleanup')

    def manage_cleanup(self):
        """Calls self._cleanup() and reports the result as text.
        """
        v = self._cleanup()
        path = '/'.join(self.getPhysicalPath())
        if v:
            return "No damage detected in BTreeFolder2 at %s." % path
        else:
            return ("Fixed BTreeFolder2 at %s.  "
                    "See the log for more details." % path)

    def _cleanup(self):
        """Cleans up errors in the BTrees.

        Certain ZODB bugs have caused BTrees to become slightly insane.
        Fortunately, there is a way to clean up damaged BTrees that
        always seems to work: make a new BTree containing the items()
        of the old one.

        Returns 1 if no damage was detected, or 0 if damage was
        detected and fixed.
        """
        from BTrees.check import check
        path = '/'.join(self.getPhysicalPath())
        try:
            check(self._tree)
            for key in self._tree.keys():
                if not self._tree.has_key(key):
                    raise AssertionError("Missing value for key: %s" %
                                         repr(key))
            check(self._mt_index)
            for key, value in self._mt_index.items():
                if (not self._mt_index.has_key(key)
                        or self._mt_index[key] is not value):
                    raise AssertionError(
                        "Missing or incorrect meta_type index: %s" % repr(key))
                check(value)
                for k in value.keys():
                    if not value.has_key(k):
                        raise AssertionError(
                            "Missing values for meta_type index: %s" %
                            repr(key))
            return 1
        except AssertionError:
            LOG.warn('Detected damage to %s. Fixing now.' % path,
                     exc_info=sys.exc_info())
            try:
                self._tree = OOBTree(self._tree)
                mt_index = OOBTree()
                for key, value in self._mt_index.items():
                    mt_index[key] = OIBTree(value)
                self._mt_index = mt_index
            except:
                LOG.error('Failed to fix %s.' % path, exc_info=sys.exc_info())
                raise
            else:
                LOG.info('Fixed %s.' % path)
            return 0

    def _getOb(self, id, default=_marker):
        """Return the named object from the folder.
        """
        tree = self._tree
        if default is _marker:
            ob = tree[id]
            return ob.__of__(self)
        else:
            ob = tree.get(id, _marker)
            if ob is _marker:
                return default
            else:
                return ob.__of__(self)

    def _setOb(self, id, object):
        """Store the named object in the folder.
        """
        tree = self._tree
        if tree.has_key(id):
            raise KeyError('There is already an item named "%s".' % id)
        tree[id] = object
        self._count.change(1)
        # Update the meta type index.
        mti = self._mt_index
        meta_type = getattr(object, 'meta_type', None)
        if meta_type is not None:
            ids = mti.get(meta_type, None)
            if ids is None:
                ids = OIBTree()
                mti[meta_type] = ids
            ids[id] = 1

    def _delOb(self, id):
        """Remove the named object from the folder.
        """
        tree = self._tree
        meta_type = getattr(tree[id], 'meta_type', None)
        del tree[id]
        self._count.change(-1)
        # Update the meta type index.
        if meta_type is not None:
            mti = self._mt_index
            ids = mti.get(meta_type, None)
            if ids is not None and ids.has_key(id):
                del ids[id]
                if not ids:
                    # Removed the last object of this meta_type.
                    # Prune the index.
                    del mti[meta_type]

    security.declareProtected(view_management_screens, 'getBatchObjectListing')

    def getBatchObjectListing(self, REQUEST=None):
        """Return a structure for a page template to show the list of objects.
        """
        if REQUEST is None:
            REQUEST = {}
        pref_rows = int(REQUEST.get('dtpref_rows', 20))
        b_start = int(REQUEST.get('b_start', 1))
        b_count = int(REQUEST.get('b_count', 1000))
        b_end = b_start + b_count - 1
        url = self.absolute_url() + '/manage_main'
        idlist = self.objectIds()  # Pre-sorted.
        count = self.objectCount()

        if b_end < count:
            next_url = url + '?b_start=%d' % (b_start + b_count)
        else:
            b_end = count
            next_url = ''

        if b_start > 1:
            prev_url = url + '?b_start=%d' % max(b_start - b_count, 1)
        else:
            prev_url = ''

        formatted = []
        formatted.append(listtext0 % pref_rows)
        for i in range(b_start - 1, b_end):
            optID = escape(idlist[i])
            formatted.append(listtext1 % (escape(optID, quote=1), optID))
        formatted.append(listtext2)
        return {
            'b_start': b_start,
            'b_end': b_end,
            'prev_batch_url': prev_url,
            'next_batch_url': next_url,
            'formatted_list': ''.join(formatted)
        }

    security.declareProtected(view_management_screens,
                              'manage_object_workspace')

    def manage_object_workspace(self, ids=(), REQUEST=None):
        '''Redirects to the workspace of the first object in
        the list.'''
        if ids and REQUEST is not None:
            REQUEST.RESPONSE.redirect('%s/%s/manage_workspace' %
                                      (self.absolute_url(), quote(ids[0])))
        else:
            return self.manage_main(self, REQUEST)

    security.declareProtected(access_contents_information, 'tpValues')

    def tpValues(self):
        """Ensures the items don't show up in the left pane.
        """
        return ()

    security.declareProtected(access_contents_information, 'objectCount')

    def objectCount(self):
        """Returns the number of items in the folder."""
        return self._count()

    security.declareProtected(access_contents_information, 'has_key')

    def has_key(self, id):
        """Indicates whether the folder has an item by ID.
        """
        return self._tree.has_key(id)

    security.declareProtected(access_contents_information, 'objectIds')

    def objectIds(self, spec=None):
        # Returns a list of subobject ids of the current object.
        # If 'spec' is specified, returns objects whose meta_type
        # matches 'spec'.

        mti = self._mt_index
        if spec is None:
            spec = mti.keys()  #all meta types

        if isinstance(spec, StringType):
            spec = [spec]
        set = None
        for meta_type in spec:
            ids = mti.get(meta_type, None)
            if ids is not None:
                set = union(set, ids)
        if set is None:
            return ()
        else:
            return set.keys()

    security.declareProtected(access_contents_information, 'objectValues')

    def objectValues(self, spec=None):
        # Returns a list of actual subobjects of the current object.
        # If 'spec' is specified, returns only objects whose meta_type
        # match 'spec'.
        return LazyMap(self._getOb, self.objectIds(spec))

    security.declareProtected(access_contents_information, 'objectItems')

    def objectItems(self, spec=None):
        # Returns a list of (id, subobject) tuples of the current object.
        # If 'spec' is specified, returns only objects whose meta_type match
        # 'spec'
        return LazyMap(lambda id, _getOb=self._getOb: (id, _getOb(id)),
                       self.objectIds(spec))

    security.declareProtected(access_contents_information, 'objectMap')

    def objectMap(self):
        # Returns a tuple of mappings containing subobject meta-data.
        return LazyMap(
            lambda (k, v): {
                'id': k,
                'meta_type': getattr(v, 'meta_type', None)
            }, self._tree.items(), self._count())

    # superValues() looks for the _objects attribute, but the implementation
    # would be inefficient, so superValues() support is disabled.
    _objects = ()

    security.declareProtected(access_contents_information, 'objectIds_d')

    def objectIds_d(self, t=None):
        ids = self.objectIds(t)
        res = {}
        for id in ids:
            res[id] = 1
        return res

    security.declareProtected(access_contents_information, 'objectMap_d')

    def objectMap_d(self, t=None):
        return self.objectMap()

    def _checkId(self, id, allow_dup=0):
        if not allow_dup and self.has_key(id):
            raise BadRequestException, ('The id "%s" is invalid--'
                                        'it is already in use.' % id)

    def _setObject(self,
                   id,
                   object,
                   roles=None,
                   user=None,
                   set_owner=1,
                   suppress_events=False):
        ob = object  # better name, keep original function signature
        v = self._checkId(id)
        if v is not None:
            id = v

        # If an object by the given id already exists, remove it.
        if self.has_key(id):
            self._delObject(id)

        if not suppress_events:
            notify(ObjectWillBeAddedEvent(ob, self, id))

        self._setOb(id, ob)
        ob = self._getOb(id)

        if set_owner:
            # TODO: eventify manage_fixupOwnershipAfterAdd
            # This will be called for a copy/clone, or a normal _setObject.
            ob.manage_fixupOwnershipAfterAdd()

            # Try to give user the local role "Owner", but only if
            # no local roles have been set on the object yet.
            if getattr(ob, '__ac_local_roles__', _marker) is None:
                user = getSecurityManager().getUser()
                if user is not None:
                    userid = user.getId()
                    if userid is not None:
                        ob.manage_setLocalRoles(userid, ['Owner'])

        if not suppress_events:
            notify(ObjectAddedEvent(ob, self, id))
            notifyContainerModified(self)

        OFS.subscribers.compatibilityCall('manage_afterAdd', ob, ob, self)

        return id

    def _delObject(self, id, dp=1, suppress_events=False):
        ob = self._getOb(id)

        OFS.subscribers.compatibilityCall('manage_beforeDelete', ob, ob, self)

        if not suppress_events:
            notify(ObjectWillBeRemovedEvent(ob, self, id))

        self._delOb(id)

        if not suppress_events:
            notify(ObjectRemovedEvent(ob, self, id))
            notifyContainerModified(self)

    # Aliases for mapping-like access.
    __len__ = objectCount
    keys = objectIds
    values = objectValues
    items = objectItems

    # backward compatibility
    hasObject = has_key

    security.declareProtected(access_contents_information, 'get')

    def get(self, name, default=None):
        return self._getOb(name, default)

    # Utility for generating unique IDs.

    security.declareProtected(access_contents_information, 'generateId')

    def generateId(self, prefix='item', suffix='', rand_ceiling=999999999):
        """Returns an ID not used yet by this folder.

        The ID is unlikely to collide with other threads and clients.
        The IDs are sequential to optimize access to objects
        that are likely to have some relation.
        """
        tree = self._tree
        n = self._v_nextid
        attempt = 0
        while 1:
            if n % 4000 != 0 and n <= rand_ceiling:
                id = '%s%d%s' % (prefix, n, suffix)
                if not tree.has_key(id):
                    break
            n = randint(1, rand_ceiling)
            attempt = attempt + 1
            if attempt > MAX_UNIQUEID_ATTEMPTS:
                # Prevent denial of service
                raise ExhaustedUniqueIdsError
        self._v_nextid = n + 1
        return id

    def __getattr__(self, name):
        # Boo hoo hoo!  Zope 2 prefers implicit acquisition over traversal
        # to subitems, and __bobo_traverse__ hooks don't work with
        # restrictedTraverse() unless __getattr__() is also present.
        # Oh well.
        res = self._tree.get(name)
        if res is None:
            raise AttributeError, name
        return res
Exemplo n.º 30
0
class GranularIndex(CatalogFieldIndex):
    """Indexes integer values using multiple granularity levels.

    The multiple levels of granularity make it possible to query large
    ranges without loading many IFTreeSets from the forward index.
    """
    implements(
        ICatalogIndex,
        IStatistics,
    )

    def __init__(self, discriminator, levels=(1000,)):
        """Create an index.
        
        levels is a sequence of integer coarseness levels.
        The default is (1000,).
        """
        self._levels = tuple(levels)
        super(GranularIndex, self).__init__(discriminator)

    def clear(self):
        """Initialize all mappings."""
        # The forward index maps an indexed value to IFSet(docids)
        self._fwd_index = self.family.IO.BTree()
        # The reverse index maps a docid to its index value
        self._rev_index = self.family.II.BTree()
        self._num_docs = Length(0)
        # self._granular_indexes: [(level, BTree(value -> IFSet([docid])))]
        self._granular_indexes = [(level, self.family.IO.BTree())
            for level in self._levels]

    def index_doc(self, docid, obj):
        if callable(self.discriminator):
            value = self.discriminator(obj, _marker)
        else:
            value = getattr(obj, self.discriminator, _marker)

        if value is _marker:
            # unindex the previous value
            self.unindex_doc(docid)
            return

        if not isinstance(value, int):
            raise ValueError(
                'GranularIndex cannot index non-integer value %s' % value)

        rev_index = self._rev_index
        if docid in rev_index:
            if docid in self._fwd_index.get(value, ()):
                # There's no need to index the doc; it's already up to date.
                return
            # unindex doc if present
            self.unindex_doc(docid)

        # Insert into forward index.
        set = self._fwd_index.get(value)
        if set is None:
            set = self.family.IF.TreeSet()
            self._fwd_index[value] = set
        set.insert(docid)

        # increment doc count
        self._num_docs.change(1)

        # Insert into reverse index.
        rev_index[docid] = value

        for level, ndx in self._granular_indexes:
            v = value // level
            set = ndx.get(v)
            if set is None:
                set = self.family.IF.TreeSet()
                ndx[v] = set
            set.insert(docid)

    def unindex_doc(self, docid):
        rev_index = self._rev_index
        value = rev_index.get(docid)
        if value is None:
            return  # not in index

        del rev_index[docid]

        self._num_docs.change(-1)

        ndx = self._fwd_index
        try:
            set = ndx[value]
            set.remove(docid)
            if not set:
                del ndx[value]
        except KeyError:
            pass

        for level, ndx in self._granular_indexes:
            v = value // level
            try:
                set = ndx[v]
                set.remove(docid)
                if not set:
                    del ndx[v]
            except KeyError:
                pass

    def search(self, queries, operator='or'):
        sets = []
        for query in queries:
            if isinstance(query, Range):
                query = query.as_tuple()
            else:
                query = (query, query)

            set = self.family.IF.multiunion(self.docids_in_range(*query))
            sets.append(set)

        result = None

        if len(sets) == 1:
            result = sets[0]
        elif operator == 'and':
            sets.sort()
            for set in sets:
                result = self.family.IF.intersection(set, result)
        else:
            result = self.family.IF.multiunion(sets)

        return result

    def docids_in_range(self, min, max):
        """List the docids for an integer range, inclusive on both ends.

        min or max can be None, making them unbounded.

        Returns an iterable of IFSets.
        """
        for level, ndx in sorted(self._granular_indexes, reverse=True):
            # Try to fill the range using coarse buckets first.
            # Use only buckets that completely fill the range.
            # For example, if start is 2 and level is 10, then we can't
            # use bucket 0; only buckets 1 and greater are useful.
            # Similarly, if end is 18 and level is 10, then we can't use
            # bucket 1; only buckets 0 and less are useful.
            if min is not None:
                a = (min + level - 1) // level
            else:
                a = None
            if max is not None:
                b = (max - level + 1) // level
            else:
                b = None
            # a and b are now coarse bucket values (or None).
            if a is None or b is None or a <= b:
                sets = []
                if a is not None and min < a * level:
                    # include the gap before
                    sets.extend(self.docids_in_range(min, a * level - 1))
                sets.extend(ndx.values(a, b))
                if b is not None and (b + 1) * level - 1 < max:
                    # include the gap after
                    sets.extend(self.docids_in_range((b + 1) * level, max))
                return sets

        return self._fwd_index.values(min, max)
Exemplo n.º 31
0
class FormSaveDataAdapter(FormActionAdapter):
    """A form action adapter that will save form input data and 
       return it in csv- or tab-delimited format."""

    schema = FormAdapterSchema.copy() + Schema((
        LinesField('ExtraData',
            widget=MultiSelectionWidget(
                label=_(u'label_savedataextra_text', default='Extra Data'),
                description=_(u'help_savedataextra_text', default=u"""
                    Pick any extra data you'd like saved with the form input.
                    """),
                format='checkbox',
                ),
            vocabulary = 'vocabExtraDataDL',
            ),
        StringField('DownloadFormat',
            searchable=0,
            required=1,
            default='csv',
            vocabulary = 'vocabFormatDL',
            widget=SelectionWidget(
                label=_(u'label_downloadformat_text', default=u'Download Format'),
                ),
            ),
        BooleanField("UseColumnNames",
            required=False,
            searchable=False,
            widget=BooleanWidget(
                label = _(u'label_usecolumnnames_text', default=u"Include Column Names"),
                description = _(u'help_usecolumnnames_text', default=u"Do you wish to have column names on the first line of downloaded input?"),
                ),
            ),
        ExLinesField('SavedFormInput',
            edit_accessor='getSavedFormInputForEdit',
            mutator='setSavedFormInput',
            searchable=0,
            required=0,
            primary=1,
            schemata="saved data",
            read_permission=DOWNLOAD_SAVED_PERMISSION,
            widget=TextAreaWidget(
                label=_(u'label_savedatainput_text', default=u"Saved Form Input"),
                description=_(u'help_savedatainput_text'),
                ),
            ),
    ))

    schema.moveField('execCondition', pos='bottom')

    meta_type      = 'FormSaveDataAdapter'
    portal_type    = 'FormSaveDataAdapter'
    archetype_name = 'Save Data Adapter'

    immediate_view = 'fg_savedata_view_p3'
    default_view   = 'fg_savedata_view_p3'
    suppl_views    = ('fg_savedata_tabview_p3', 'fg_savedata_recview_p3',)

    security       = ClassSecurityInfo()


    def _migrateStorage(self):
        # we're going to use an LOBTree for storage. we need to
        # consider the possibility that self is from an
        # older version that uses the native Archetypes storage
        # or the former IOBTree (<= 1.6.0b2 )
        # in the SavedFormInput field.
        updated = base_hasattr(self, '_inputStorage') and \
                  base_hasattr(self, '_inputItems') and \
                  base_hasattr(self, '_length')

        if not updated:
            try:
                saved_input = self.getSavedFormInput()
            except AttributeError:
                saved_input = []

            self._inputStorage = SavedDataBTree()
            i = 0
            self._inputItems = 0
            self._length = Length()

            if len(saved_input):
                for row in saved_input:
                    self._inputStorage[i] = row
                    i += 1
                self.SavedFormInput = []
                self._inputItems = i
                self._length.set(i)


    security.declareProtected(DOWNLOAD_SAVED_PERMISSION, 'getSavedFormInput')
    def getSavedFormInput(self):
        """ returns saved input as an iterable;
            each row is a sequence of fields.
        """

        if base_hasattr(self, '_inputStorage'):
            return self._inputStorage.values()
        else:
            return self.SavedFormInput


    security.declareProtected(DOWNLOAD_SAVED_PERMISSION, 'getSavedFormInputItems')
    def getSavedFormInputItems(self):
        """ returns saved input as an iterable;
            each row is an (id, sequence of fields) tuple
        """
        if base_hasattr(self, '_inputStorage'):
            return self._inputStorage.items()
        else:
            return enumerate(self.SavedFormInput)


    security.declareProtected(ModifyPortalContent, 'getSavedFormInputForEdit')
    def getSavedFormInputForEdit(self, **kwargs):
        """ returns saved as CSV text """
        delimiter = self.csvDelimiter()
        sbuf = StringIO()
        writer = csv.writer(sbuf, delimiter=delimiter)
        for row in self.getSavedFormInput():
            writer.writerow( row )
        res = sbuf.getvalue()
        sbuf.close()

        return res


    security.declareProtected(ModifyPortalContent, 'setSavedFormInput')
    def setSavedFormInput(self, value, **kwargs):
        """ expects value as csv text string, stores as list of lists """

        self._migrateStorage()

        self._inputStorage.clear()
        i = 0
        self._inputItems = 0
        self._length.set(0)

        if len(value):
            delimiter = self.csvDelimiter()
            sbuf = StringIO( value )
            reader = csv.reader(sbuf, delimiter=delimiter)
            for row in reader:
                if row:
                    self._inputStorage[i] = row
                    i += 1
                self._inputItems = i
                self._length.set(i)
            sbuf.close()

        # logger.debug("setSavedFormInput: %s items" % self._inputItems)


    security.declareProtected(ModifyPortalContent, 'clearSavedFormInput')
    def clearSavedFormInput(self, **kwargs):
        """ convenience method to clear input buffer """
        
        REQUEST = kwargs.get('request', self.REQUEST)
        if REQUEST.form.has_key('clearSavedFormInput'):
            # we're processing a request from the web;
            # check for CSRF
            plone.protect.CheckAuthenticator(REQUEST)
            plone.protect.PostOnly(REQUEST)

        self._migrateStorage()

        self._inputStorage.clear()
        self._inputItems = 0
        self._length.set(0)


    security.declareProtected(DOWNLOAD_SAVED_PERMISSION, 'getSavedFormInputById')
    def getSavedFormInputById(self, id):
        """ Return the data stored for record with 'id' """
        lst =  [field.replace('\r','').replace('\n', r'\n') for field in self._inputStorage[id]]
        return lst

 
    security.declareProtected(ModifyPortalContent, 'manage_saveData')
    def manage_saveData(self, id,  data):
        """ Save the data for record with 'id' """

        self._migrateStorage()

        lst = list()
        for i in range(0, len(self.getColumnNames())):
            lst.append(getattr(data, 'item-%d' % i, '').replace(r'\n', '\n'))
 
        self._inputStorage[id] = lst
        self.REQUEST.RESPONSE.redirect(self.absolute_url() + '/view')


    security.declareProtected(ModifyPortalContent, 'manage_deleteData')
    def manage_deleteData(self, id):
        """ Delete the data for record with 'id' """

        self._migrateStorage()

        del self._inputStorage[id]
        self._inputItems -= 1
        self._length.change(-1)
        
        self.REQUEST.RESPONSE.redirect(self.absolute_url() + '/view')


    def _addDataRow(self, value):

        self._migrateStorage()

        if isinstance(self._inputStorage, IOBTree):
            # 32-bit IOBTree; use a key which is more likely to conflict
            # but which won't overflow the key's bits
            id = self._inputItems
            self._inputItems += 1
        else:
            # 64-bit LOBTree
            id = int(time.time() * 1000)
            while id in self._inputStorage: # avoid collisions during testing
                id += 1
        self._inputStorage[id] = value
        self._length.change(1)


    security.declareProtected(ModifyPortalContent, 'addDataRow')
    def addDataRow(self, value):
        """ a wrapper for the _addDataRow method """
        
        self._addDataRow(value)

    
    def onSuccess(self, fields, REQUEST=None, loopstop=False):
        """
        saves data.
        """

        if LP_SAVE_TO_CANONICAL and not loopstop:
            # LinguaPlone functionality:
            # check to see if we're in a translated
            # form folder, but not the canonical version.
            parent = self.aq_parent
            if safe_hasattr(parent, 'isTranslation') and \
               parent.isTranslation() and not parent.isCanonical():
                # look in the canonical version to see if there is
                # a matching (by id) save-data adapter.
                # If so, call its onSuccess method
                cf = parent.getCanonical()
                target = cf.get(self.getId())
                if target is not None and target.meta_type == 'FormSaveDataAdapter':
                    target.onSuccess(fields, REQUEST, loopstop=True)
                    return

        from ZPublisher.HTTPRequest import FileUpload

        data = []
        for f in fields:
            if f.isFileField():
                file = REQUEST.form.get('%s_file' % f.fgField.getName())
                if isinstance(file, FileUpload) and file.filename != '':
                    file.seek(0)
                    fdata = file.read()
                    filename = file.filename
                    mimetype, enc = guess_content_type(filename, fdata, None)
                    if mimetype.find('text/') >= 0:
                        # convert to native eols
                        fdata = fdata.replace('\x0d\x0a', '\n').replace('\x0a', '\n').replace('\x0d', '\n')
                        data.append( '%s:%s:%s:%s' %  (filename, mimetype, enc, fdata) )
                    else:
                        data.append( '%s:%s:%s:Binary upload discarded' %  (filename, mimetype, enc) )
                else:
                    data.append( 'NO UPLOAD' )
            elif not f.isLabel():
                val = REQUEST.form.get(f.fgField.getName(),'')
                if not type(val) in StringTypes:
                    # Zope has marshalled the field into
                    # something other than a string
                    val = str(val)
                data.append(val)

        if self.ExtraData:
            for f in self.ExtraData:
                if f == 'dt':
                    data.append( str(DateTime()) )
                else:
                    data.append( getattr(REQUEST, f, '') )


        self._addDataRow( data )


    security.declareProtected(DOWNLOAD_SAVED_PERMISSION, 'getColumnNames')
    def getColumnNames(self):
        """Returns a list of column names"""
        
        names = [field.getName() for field in self.fgFields(displayOnly=True)]
        for f in self.ExtraData:
            names.append(f)
        
        return names
        

    security.declareProtected(DOWNLOAD_SAVED_PERMISSION, 'getColumnTitles')
    def getColumnTitles(self):
        """Returns a list of column titles"""
        
        names = [field.widget.label for field in self.fgFields(displayOnly=True)]
        for f in self.ExtraData:
            names.append(self.vocabExtraDataDL().getValue(f, ''))
        
        return names
        

    def _cleanInputForTSV(self, value):
        # make data safe to store in tab-delimited format

        return  str(value).replace('\x0d\x0a', r'\n').replace('\x0a', r'\n').replace('\x0d', r'\n').replace('\t', r'\t')


    security.declareProtected(DOWNLOAD_SAVED_PERMISSION, 'download_tsv')
    def download_tsv(self, REQUEST=None, RESPONSE=None):
        """Download the saved data
        """

        filename = self.id
        if filename.find('.') < 0:
            filename = '%s.tsv' % filename
        header_value = contentDispositionHeader('attachment', self.getCharset(), filename=filename)
        RESPONSE.setHeader("Content-Disposition", header_value)
        RESPONSE.setHeader("Content-Type", 'text/tab-separated-values;charset=%s' % self.getCharset())

        if getattr(self, 'UseColumnNames', False):
            res = "%s\n" % '\t'.join( self.getColumnNames() )
            if isinstance(res, unicode):
                res = res.encode(self.getCharset())
        else:
            res = ''

        for row in self.getSavedFormInput():
            res = '%s%s\n' % (res, '\t'.join( [self._cleanInputForTSV(col) for col in row] ))

        return res


    security.declareProtected(DOWNLOAD_SAVED_PERMISSION, 'download_csv')
    def download_csv(self, REQUEST=None, RESPONSE=None):
        """Download the saved data
        """

        filename = self.id
        if filename.find('.') < 0:
            filename = '%s.csv' % filename
        header_value = contentDispositionHeader('attachment', self.getCharset(), filename=filename)
        RESPONSE.setHeader("Content-Disposition", header_value)
        RESPONSE.setHeader("Content-Type", 'text/comma-separated-values;charset=%s' % self.getCharset())

        if getattr(self, 'UseColumnNames', False):
            delimiter = self.csvDelimiter()
            res = "%s\n" % delimiter.join( self.getColumnNames() )
            if isinstance(res, unicode):
                res = res.encode(self.getCharset())
        else:
            res = ''

        return '%s%s' % (res, self.getSavedFormInputForEdit())


    security.declareProtected(DOWNLOAD_SAVED_PERMISSION, 'download')
    def download(self, REQUEST=None, RESPONSE=None):
        """Download the saved data
        """

        format = getattr(self, 'DownloadFormat', 'tsv')
        if format == 'tsv':
            return self.download_tsv(REQUEST, RESPONSE)
        else:
            assert format == 'csv', 'Unknown download format'
            return self.download_csv(REQUEST, RESPONSE)


    security.declareProtected(DOWNLOAD_SAVED_PERMISSION, 'rowAsColDict')
    def rowAsColDict(self, row, cols):
        """ Where row is a data sequence and cols is a column name sequence,
            returns a dict of colname:column. This is a convenience method
            used in the record view.
        """
    
        colcount = len(cols)

        rdict = {}
        for i in range(0, len(row)):
            if i < colcount:
                rdict[cols[i]] = row[i]
            else:
                rdict['column-%s' % i] = row[i]
        return rdict


    security.declareProtected(DOWNLOAD_SAVED_PERMISSION, 'inputAsDictionaries')
    def inputAsDictionaries(self):
        """returns saved data as an iterable of dictionaries
        """

        cols = self.getColumnNames()

        for row in self.getSavedFormInput():
            yield self.rowAsColDict(row, cols)
        

    # alias for old mis-naming
    security.declareProtected(DOWNLOAD_SAVED_PERMISSION, 'InputAsDictionaries')
    InputAsDictionaries = inputAsDictionaries


    security.declareProtected(DOWNLOAD_SAVED_PERMISSION, 'formatMIME')
    def formatMIME(self):
        """MIME format selected for download
        """

        format = getattr(self, 'DownloadFormat', 'tsv')
        if format == 'tsv':
            return 'text/tab-separated-values'
        else:
            assert format == 'csv', 'Unknown download format'
            return 'text/comma-separated-values'
    
    security.declarePrivate('csvDelimiter')
    def csvDelimiter(self):
    
        """Delimiter character for CSV downloads
        """
        fgt = getToolByName(self, 'formgen_tool')
        return fgt.getCSVDelimiter()
    
    security.declareProtected(DOWNLOAD_SAVED_PERMISSION, 'itemsSaved')
    def itemsSaved(self):
        """Download the saved data
        """

        if base_hasattr(self, '_length'):
            return self._length()
        elif base_hasattr(self, '_inputItems'):
            return self._inputItems
        else:
            return len(self.SavedFormInput)


    def vocabExtraDataDL(self):
        """ returns vocabulary for extra data """

        return DisplayList( (
                ('dt',
                    self.translate( msgid='vocabulary_postingdt_text',
                    domain='ploneformgen',
                    default='Posting Date/Time')
                    ),
                ('HTTP_X_FORWARDED_FOR','HTTP_X_FORWARDED_FOR',),
                ('REMOTE_ADDR','REMOTE_ADDR',),
                ('HTTP_USER_AGENT','HTTP_USER_AGENT',),
                ) )


    def vocabFormatDL(self):
        """ returns vocabulary for format """

        return DisplayList( (
                ('tsv',
                    self.translate( msgid='vocabulary_tsv_text',
                    domain='ploneformgen',
                    default='Tab-Separated Values')
                    ),
                ('csv',
                    self.translate( msgid='vocabulary_csv_text',
                    domain='ploneformgen',
                    default='Comma-Separated Values')
                    ),
            ) )
Exemplo n.º 32
0
class HypatiaDateRecurringIndex(KeywordIndex, FieldIndex):
    def discriminate(self, obj, default):
        """ See interface IIndexInjection """
        if callable(self.discriminator):
            value = self.discriminator(obj, _marker)
        else:
            value = getattr(obj, self.discriminator, _marker)

        if value is _marker:
            return default

        if isinstance(value, Persistent):
            raise ValueError('Catalog cannot index persistent object %s' %
                             value)

        if isinstance(value, Broken):
            raise ValueError('Catalog cannot index broken object %s' % value)

        if not isinstance(value, dict):
            raise ValueError(
                'Catalog can only index dict with '
                'attr and date keys, or date and recurdef keys, given %s' %
                value)
        # examples:
        # {'attr': 'dates',
        #  'date': datetime.datetime.now()}
        # will get dates_recurrence attribute on the obj to get iCal string
        # for recurrence definition
        # or
        # {'date': datetime.datetime.now(),
        #  'recurdef': ICALSTRING}
        # no access to obj attributes at all

        date = value.get('date')
        default_recurdef = value.get('recurdef', _marker)
        if default_recurdef is not _marker:
            recurdef = default_recurdef
        else:
            attr_recurdef = value.get('attr') + '_recurrence'
            recurdef = getattr(obj, attr_recurdef, None)

        if callable(recurdef):
            recurdef = recurdef()

        if not recurdef:
            dates = [date]
        else:
            dates = recurrence_sequence_ical(date, recrule=recurdef)

        # dates is a generator
        return tuple(dates)

    def normalize(self, dates):
        return [dt2int(date) for date in dates]

# below is the same implementation as Keyword Index, but replacing
# self._fwd_index = self.family.OO.BTree() by self._fwd_index = self.family.IO.BTree()
# family.OO.Set by family.II.Set
# family.OO.difference by family.II.difference

    def reset(self):
        """Initialize forward and reverse mappings."""
        # The forward index maps index keywords to a sequence of docids
        self._fwd_index = self.family.IO.BTree()

        # The reverse index maps a docid to its keywords
        self._rev_index = self.family.IO.BTree()
        self._num_docs = Length(0)
        self._not_indexed = self.family.IF.TreeSet()

    def index_doc(self, docid, obj):
        seq = self.discriminate(obj, _marker)

        if seq is _marker:
            if not (docid in self._not_indexed):
                # unindex the previous value
                self.unindex_doc(docid)
                # Store docid in set of unindexed docids
                self._not_indexed.add(docid)
            return None

        if docid in self._not_indexed:
            # Remove from set of unindexed docs if it was in there.
            self._not_indexed.remove(docid)

        if isinstance(seq, string_types):
            raise TypeError('seq argument must be a list/tuple of strings')

        old_kw = self._rev_index.get(docid, None)
        if not seq:
            if old_kw:
                self.unindex_doc(docid)
            return

        seq = self.normalize(seq)

        new_kw = self.family.II.Set(seq)

        if old_kw is None:
            self._insert_forward(docid, new_kw)
            self._insert_reverse(docid, new_kw)
            self._num_docs.change(1)
        else:
            # determine added and removed keywords
            kw_added = self.family.II.difference(new_kw, old_kw)
            kw_removed = self.family.II.difference(old_kw, new_kw)

            if not (kw_added or kw_removed):
                return

            # removed keywords are removed from the forward index
            for word in kw_removed:
                fwd = self._fwd_index[word]
                fwd.remove(docid)
                if not fwd:
                    del self._fwd_index[word]

            # now update reverse and forward indexes
            self._insert_forward(docid, kw_added)
            self._insert_reverse(docid, new_kw)

    def applyInRange(self, start, end, excludemin=False, excludemax=False):
        if start is not None:
            start = dt2int(start)

        if end is not None:
            end = dt2int(end)

        return self.family.IF.multiunion(
            self._fwd_index.values(start,
                                   end,
                                   excludemin=excludemin,
                                   excludemax=excludemax))

    def document_repr(self, docid, default=None):
        result = self._rev_index.get(docid, default)
        if result is not default:
            return ', '.join([int2dt(r).isoformat() for r in result])


#            return repr(result)
        return default

    def inrange_with_not_indexed(self,
                                 start,
                                 end,
                                 excludemin=False,
                                 excludemax=False):
        return InRangeWithNotIndexed(self, start, end, excludemin, excludemax)

    def sort(self,
             docids,
             reverse=False,
             limit=None,
             sort_type=None,
             raise_unsortable=True,
             from_=None,
             until=None):
        if from_ is not None:
            from_ = dt2int(from_)

        if until is not None:
            until = dt2int(until)

        if limit is not None:
            limit = int(limit)
            if limit < 1:
                raise ValueError('limit must be 1 or greater')

        if not docids:
            return []

        numdocs = self._num_docs.value
        if not numdocs:
            if raise_unsortable:
                raise Unsortable(docids)
            return []

        if sort_type == interfaces.STABLE:
            sort_type = interfaces.TIMSORT

        elif sort_type == interfaces.OPTIMAL:
            sort_type = None

        if reverse:
            raise NotImplementedError
        else:
            return self.sort_forward(docids,
                                     limit,
                                     numdocs,
                                     from_=from_,
                                     until=until,
                                     sort_type=sort_type,
                                     raise_unsortable=raise_unsortable)

    def sort_forward(self,
                     docids,
                     limit,
                     numdocs,
                     from_,
                     until,
                     sort_type=None,
                     raise_unsortable=True):

        rlen = len(docids)

        # See http://www.zope.org/Members/Caseman/ZCatalog_for_2.6.1
        # for an overview of why we bother doing all this work to
        # choose the right sort algorithm.

        if sort_type is None:
            if limit and nbest_ascending_wins(limit, rlen, numdocs):
                # nbest beats timsort reliably if this is true
                sort_type = interfaces.NBEST

            else:
                sort_type = interfaces.TIMSORT

        if sort_type == interfaces.NBEST:
            if limit is None:
                raise ValueError('nbest requires a limit')
            return self.nbest_ascending(docids,
                                        limit,
                                        from_=from_,
                                        until=until,
                                        raise_unsortable=raise_unsortable)
        elif sort_type == interfaces.TIMSORT:
            return self.timsort_ascending(docids,
                                          limit,
                                          from_=from_,
                                          until=until,
                                          raise_unsortable=raise_unsortable)
        else:
            raise ValueError('Unknown sort type %s' % sort_type)

    def nbest_ascending(self,
                        docids,
                        limit,
                        from_,
                        until,
                        raise_unsortable=False):
        if limit is None:  #pragma NO COVERAGE
            raise RuntimeError('n-best used without limit')

        # lifted from heapq.nsmallest

        h = nsort(docids, self._rev_index, ASC, from_, until)
        it = iter(h)
        result = sorted(islice(it, 0, limit))
        if not result:  #pragma NO COVERAGE
            raise StopIteration
        insort = bisect.insort
        pop = result.pop
        los = result[-1]  # los --> Largest of the nsmallest
        for elem in it:
            if los <= elem:
                continue
            insort(result, elem)
            pop()
            los = result[-1]

        missing_docids = []

        for value, docid in result:
            if value is ASC:
                missing_docids.append(docid)
            else:
                yield docid

        if raise_unsortable and missing_docids:
            raise Unsortable(missing_docids)

    def timsort_ascending(self,
                          docids,
                          limit,
                          from_,
                          until,
                          raise_unsortable=True):
        return self._timsort(
            docids,
            from_=from_,
            until=until,
            limit=limit,
            reverse=False,
            raise_unsortable=raise_unsortable,
        )

    def _timsort(
        self,
        docids,
        from_,
        until,
        limit=None,
        reverse=False,
        raise_unsortable=True,
    ):

        n = 0
        missing_docids = []

        def get(k, rev_index=self._rev_index):
            v = rev_index.get(k, ASC)
            if v is ASC:
                missing_docids.append(k)
            else:
                v = get_first_occurence(v, from_=from_, until=until)
            return v

        for docid in sorted(docids, key=get, reverse=reverse):
            if docid in missing_docids:
                # skip docids not in this index
                continue
            n += 1
            yield docid
            if limit and n >= limit:
                raise StopIteration

        if raise_unsortable and missing_docids:
            raise Unsortable(missing_docids)
Exemplo n.º 33
0
class PathIndex(Persistent, SimpleItem):

    """Index for paths returned by getPhysicalPath.

    A path index stores all path components of the physical path of an object.

    Internal datastructure:

    - a physical path of an object is split into its components

    - every component is kept as a  key of a OOBTree in self._indexes

    - the value is a mapping 'level of the path component' to
      'all docids with this path component on this level'
    """

    __implements__ = (PluggableIndex.UniqueValueIndex,)
    implements(IPathIndex, IUniqueValueIndex)

    meta_type="PathIndex"

    manage_options= (
        {'label': 'Settings',
         'action': 'manage_main',
         'help': ('PathIndex','PathIndex_Settings.stx')},
    )

    query_options = ("query", "level", "operator")

    def __init__(self,id,caller=None):
        self.id = id
        self.operators = ('or','and')
        self.useOperator = 'or'
        self.clear()

    def clear(self):
        self._depth = 0
        self._index = OOBTree()
        self._unindex = IOBTree()
        self._length = Length(0)

    def insertEntry(self, comp, id, level):
        """Insert an entry.

           comp is a path component
           id is the docid
           level is the level of the component inside the path
        """

        if not self._index.has_key(comp):
            self._index[comp] = IOBTree()

        if not self._index[comp].has_key(level):
            self._index[comp][level] = IITreeSet()

        self._index[comp][level].insert(id)
        if level > self._depth:
            self._depth = level

    def index_object(self, docid, obj ,threshold=100):
        """ hook for (Z)Catalog """

        f = getattr(obj, self.id, None)
        if f is not None:
            if safe_callable(f):
                try:
                    path = f()
                except AttributeError:
                    return 0
            else:
                path = f

            if not isinstance(path, (StringType, TupleType)):
                raise TypeError('path value must be string or tuple of strings')
        else:
            try:
                path = obj.getPhysicalPath()
            except AttributeError:
                return 0

        if isinstance(path, (ListType, TupleType)):
            path = '/'+ '/'.join(path[1:])
        comps = filter(None, path.split('/'))

        if not self._unindex.has_key(docid):
            self._length.change(1)

        for i in range(len(comps)):
            self.insertEntry(comps[i], docid, i)
        self._unindex[docid] = path
        return 1

    def unindex_object(self, docid):
        """ hook for (Z)Catalog """

        if not self._unindex.has_key(docid):
            LOG.debug('Attempt to unindex nonexistent document with id %s'
                      % docid)
            return

        comps =  self._unindex[docid].split('/')

        for level in range(len(comps[1:])):
            comp = comps[level+1]

            try:
                self._index[comp][level].remove(docid)

                if not self._index[comp][level]:
                    del self._index[comp][level]

                if not self._index[comp]:
                    del self._index[comp]
            except KeyError:
                LOG.debug('Attempt to unindex document with id %s failed'
                          % docid)

        self._length.change(-1)
        del self._unindex[docid]

    def search(self, path, default_level=0):
        """
        path is either a string representing a
        relative URL or a part of a relative URL or
        a tuple (path,level).

        level >= 0  starts searching at the given level
        level <  0  not implemented yet
        """

        if isinstance(path, StringType):
            level = default_level
        else:
            level = int(path[1])
            path  = path[0]

        comps = filter(None, path.split('/'))

        if len(comps) == 0:
            return IISet(self._unindex.keys())

        if level >= 0:
            results = []
            for i in range(len(comps)):
                comp = comps[i]
                if not self._index.has_key(comp): return IISet()
                if not self._index[comp].has_key(level+i): return IISet()
                results.append( self._index[comp][level+i] )

            res = results[0]
            for i in range(1,len(results)):
                res = intersection(res,results[i])
            return res

        else:
            results = IISet()
            for level in range(0,self._depth + 1):
                ids = None
                error = 0
                for cn in range(0,len(comps)):
                    comp = comps[cn]
                    try:
                        ids = intersection(ids,self._index[comp][level+cn])
                    except KeyError:
                        error = 1
                if error==0:
                    results = union(results,ids)
            return results

    def numObjects(self):
        """ return the number distinct values """
        return len(self._unindex)

    def indexSize(self):
        """ return the number of indexed objects"""
        return len(self)

    def __len__(self):
        return self._length()

    def _apply_index(self, request, cid=''):
        """ hook for (Z)Catalog
            'request' --  mapping type (usually {"path": "..." }
             additionaly a parameter "path_level" might be passed
             to specify the level (see search())

            'cid' -- ???
        """

        record = parseIndexRequest(request,self.id,self.query_options)
        if record.keys==None: return None

        level    = record.get("level",0)
        operator = record.get('operator',self.useOperator).lower()

        # depending on the operator we use intersection of union
        if operator == "or":  set_func = union
        else: set_func = intersection

        res = None
        for k in record.keys:
            rows = self.search(k,level)
            res = set_func(res,rows)

        if res:
            return res, (self.id,)
        else:
            return IISet(), (self.id,)

    def hasUniqueValuesFor(self, name):
        """has unique values for column name"""
        return name == self.id

    def uniqueValues(self, name=None, withLength=0):
        """ needed to be consistent with the interface """
        return self._index.keys()

    def getIndexSourceNames(self):
        """ return names of indexed attributes """
        return ('getPhysicalPath', )

    def getEntryForObject(self, docid, default=_marker):
        """ Takes a document ID and returns all the information
            we have on that specific object.
        """
        try:
            return self._unindex[docid]
        except KeyError:
            # XXX Why is default ignored?
            return None

    manage = manage_main = DTMLFile('dtml/managePathIndex', globals())
    manage_main._setName('manage_main')
Exemplo n.º 34
0
class IdStore(persistent.Persistent):
    """
    Persistent storage of objects which generates non-conflictiong unique IDs
    for them
    """

    _v_nextid = None

    family = family32

    def __init__(self, family=family32):
        """
        :param family: Family of BTrees to use
        """
        self.tree = family.IO.BTree()
        self.length = Length()

    def _generateId(self):
        """Generate an id which is not yet taken.

        This tries to allocate sequential ids so they fall into the same BTree
        bucket, and randomizes if it stumbles upon a used one.

        This algorithm is taken from zope.intid but it will cause performance
        degradation due to fragmentation if used too often, we need something
        better eventually
        """
        nextid = self._v_nextid
        while True:
            if nextid is None:
                nextid = random.randrange(0, self.family.maxint)
            uid = nextid
            if uid not in self.tree:
                nextid += 1
                if nextid > self.family.maxint:
                    nextid = None
                self._v_nextid = nextid
                return uid
            nextid = None

    def add(self, obj):
        """
        Add object to the storage

        :param obj: Object to store (persistent.Persistent but not necessarily)
        :return: Unique ID
        :rtype: int
        """
        if not hasattr(self, "length"):
            self.length = Length(len(self.tree))
        while True:
            uid = self._generateId()
            if self.tree.insert(
                    uid,
                    obj):  # We use this feature of BTrees to avoid conflicts
                # _v_* and _p_* are not saved in the database
                # However assigning _v_* unghostifies the object while _p_* doesn't
                # We don't want to force-unghostify the object in some cases
                obj._p_uid = uid
                self.length.change(1)
                return uid

    def remove(self, iobj):
        """
        Remove object from the storage
        :param obj: Object or its integer unique id
        """
        if not hasattr(self, "length"):
            self.length = Length(len(self.tree))
        if type(iobj) in (int, long):
            del self.tree[iobj]
            self.length.change(-1)
        elif hasattr(iobj, "_p_uid"):
            del self.tree[iobj._p_uid]
            iobj._p_uid = None
            self.length.change(-1)
        else:
            raise TypeError("Argument should be either uid or object itself")

    def __getitem__(self, uid):
        """
        :param int uid: Get object by its unique ID
        """
        return self.tree[uid]

    def __delitem__(self, uid):
        """
        :param int uid: Get object by its unique ID
        """
        self.remove(uid)

    def __len__(self):
        if not hasattr(self, "length"):
            self.length = Length(len(self.tree))
        return self.length.value
Exemplo n.º 35
0
class PersistentWaitingQueue(Persistent):
    """
    A Waiting queue, implemented using a map structure (BTree...)
    It is persistent, but very vulnerable to conflicts. This is due to the
    fact that sets are used as container, and there can happen a situation
    where two different sets are assigned to the same timestamp. This will
    for sure result in conflict.

    That said, the commits of objects like these have to be carefully
    synchronized. See `indico.modules.scheduler.controllers` for more info
    (particularly the way we use the 'spool').
    """
    def __init__(self):
        super(PersistentWaitingQueue, self).__init__()
        self._reset()

    def _reset(self):
        # this counter keeps the number of elements
        self._elem_counter = Length(0)
        self._container = IOBTree()

    def _gc_bin(self, t):
        """
        'garbage-collect' bins
        """
        if len(self._container[t]) == 0:
            del self._container[t]

    def _check_gc_consistency(self):
        """
        'check that there are no empty bins'
        """
        for t in self._container:
            if len(self._container[t]) == 0:
                return False

        return True

    def enqueue(self, t, obj):
        """
        Add an element to the queue
        """

        if t not in self._container:
            self._container[t] = OOTreeSet()

        if obj in self._container[t]:
            raise DuplicateElementException(obj)

        self._container[t].add(obj)
        self._elem_counter.change(1)

    def dequeue(self, t, obj):
        """
        Remove an element from the queue
        """
        self._container[t].remove(obj)
        self._gc_bin(t)
        self._elem_counter.change(-1)

    def _next_timestamp(self):
        """
        Return the next 'priority' to be served
        """
        i = iter(self._container)

        try:
            t = i.next()
            return t
        except StopIteration:
            return None

    def peek(self):
        """
        Return the next element
        """
        t = self._next_timestamp()
        if t:
            # just to be sure
            assert (len(self._container[t]) != 0)

            # find the next element
            i = iter(self._container[t])
            # store it
            elem = i.next()

            # return the element
            return t, elem
        else:
            return None

    def pop(self):
        """
        Remove and return the next set of elements to be processed
        """
        pair = self.peek()
        if pair:
            self.dequeue(*pair)

            # return the element
            return pair
        else:
            return None

    def nbins(self):
        """
        Return the number of 'bins' (map entries) currently used
        """
        # get 'real' len()
        return len(self._container)

    def __len__(self):
        return self._elem_counter()

    def __getitem__(self, param):
        return self._container.__getitem__(param)

    def __iter__(self):

        # tree iterator
        for tstamp in iter(self._container):
            cur_set = self._container[tstamp]
            try:
                # set iterator
                for elem in cur_set:
                    yield tstamp, elem
            except StopIteration:
                pass
Exemplo n.º 36
0
class Lexicon(Persistent):

    implements(ILexicon)

    def __init__(self, *pipeline):
        self._wids = OIBTree()  # word -> wid
        self._words = IOBTree()  # wid -> word
        # wid 0 is reserved for words that aren't in the lexicon (OOV -- out
        # of vocabulary).  This can happen, e.g., if a query contains a word
        # we never saw before, and that isn't a known stopword (or otherwise
        # filtered out).  Returning a special wid value for OOV words is a
        # way to let clients know when an OOV word appears.
        self.length = Length()
        self._pipeline = pipeline

    def length(self):
        """Return the number of unique terms in the lexicon."""
        # Overridden in instances
        return len(self._wids)

    def words(self):
        return self._wids.keys()

    def wids(self):
        return self._words.keys()

    def items(self):
        return self._wids.items()

    def sourceToWordIds(self, text):
        last = _text2list(text)
        for element in self._pipeline:
            last = element.process(last)
        if not hasattr(self.length, 'change'):
            # Make sure length is overridden with a BTrees.Length.Length
            self.length = Length(self.length())
        # Strategically unload the length value so that we get the most
        # recent value written to the database to minimize conflicting wids
        # Because length is independent, this will load the most
        # recent value stored, regardless of whether MVCC is enabled
        self.length._p_deactivate()
        return map(self._getWordIdCreate, last)

    def termToWordIds(self, text):
        last = _text2list(text)
        for element in self._pipeline:
            process = getattr(element, "process_post_glob", element.process)
            last = process(last)
        wids = []
        for word in last:
            wids.append(self._wids.get(word, 0))
        return wids

    def parseTerms(self, text):
        last = _text2list(text)
        for element in self._pipeline:
            process = getattr(element, "processGlob", element.process)
            last = process(last)
        return last

    def isGlob(self, word):
        return "*" in word or "?" in word

    def get_word(self, wid):
        return self._words[wid]

    def get_wid(self, word):
        return self._wids.get(word, 0)

    def globToWordIds(self, pattern):
        # Implement * and ? just as in the shell, except the pattern
        # must not start with either of these
        prefix = ""
        while pattern and pattern[0] not in "*?":
            prefix += pattern[0]
            pattern = pattern[1:]
        if not pattern:
            # There were no globbing characters in the pattern
            wid = self._wids.get(prefix, 0)
            if wid:
                return [wid]
            else:
                return []
        if not prefix:
            # The pattern starts with a globbing character.
            # This is too efficient, so we raise an exception.
            raise QueryError("pattern %r shouldn't start with glob character" %
                             pattern)
        pat = prefix
        for c in pattern:
            if c == "*":
                pat += ".*"
            elif c == "?":
                pat += "."
            else:
                pat += re.escape(c)
        pat += "$"
        prog = re.compile(pat)
        keys = self._wids.keys(prefix)  # Keys starting at prefix
        wids = []
        for key in keys:
            if not key.startswith(prefix):
                break
            if prog.match(key):
                wids.append(self._wids[key])
        return wids

    def _getWordIdCreate(self, word):
        wid = self._wids.get(word)
        if wid is None:
            wid = self._new_wid()
            self._wids[word] = wid
            self._words[wid] = word
        return wid

    def _new_wid(self):
        self.length.change(1)
        while self._words.has_key(self.length()):  # just to be safe
            self.length.change(1)
        return self.length()
Exemplo n.º 37
0
class KeywordIndex(Persistent):
    """Keyword index"""

    family = BTrees.family32

    # If a word is referenced by at least tree_threshold docids,
    # use a TreeSet for that word instead of a Set.
    tree_threshold = 64

    def __init__(self, family=None):
        if family is not None:
            self.family = family
        self.clear()

    def clear(self):
        """Initialize forward and reverse mappings."""

        # The forward index maps index keywords to a sequence of docids
        self._fwd_index = self.family.OO.BTree()

        # The reverse index maps a docid to its keywords
        # TODO: Using a vocabulary might be the better choice to store
        # keywords since it would allow use to use integers instead of strings
        self._rev_index = self.family.IO.BTree()
        self._num_docs = Length(0)

    def documentCount(self):
        """Return the number of documents in the index."""
        return self._num_docs()

    def wordCount(self):
        """Return the number of indexed words"""
        return len(self._fwd_index)

    def has_doc(self, docid):
        return bool(docid in self._rev_index)

    def normalize(self, seq):
        """Perform normalization on sequence of keywords.

        Return normalized sequence. This method may be
        overriden by subclasses.

        """
        return seq

    def index_doc(self, docid, seq):
        if isinstance(seq, six.string_types):
            raise TypeError('seq argument must be a list/tuple of strings')

        old_kw = self._rev_index.get(docid, None)
        if not seq:
            if old_kw:
                self.unindex_doc(docid)
            return

        seq = self.normalize(seq)

        new_kw = self.family.OO.Set(seq)

        if old_kw is None:
            self._insert_forward(docid, new_kw)
            self._insert_reverse(docid, new_kw)
            self._num_docs.change(1)
        else:

            # determine added and removed keywords
            kw_added = self.family.OO.difference(new_kw, old_kw)
            kw_removed = self.family.OO.difference(old_kw, new_kw)

            # removed keywords are removed from the forward index
            for word in kw_removed:
                fwd = self._fwd_index[word]
                fwd.remove(docid)
                if not fwd:
                    del self._fwd_index[word]

            # now update reverse and forward indexes
            self._insert_forward(docid, kw_added)
            self._insert_reverse(docid, new_kw)

    def unindex_doc(self, docid):
        idx  = self._fwd_index

        try:
            for word in self._rev_index[docid]:
                idx[word].remove(docid)
                if not idx[word]:
                    del idx[word]
        except KeyError:
            msg = 'WAAA!  Inconsistent'
            return

        try:
            del self._rev_index[docid]
        except KeyError: #pragma NO COVERAGE
            msg = 'WAAA!  Inconsistent'

        self._num_docs.change(-1)

    def _insert_forward(self, docid, words):
        """insert a sequence of words into the forward index """

        idx = self._fwd_index
        get_word_idx = idx.get
        IF = self.family.IF
        Set = IF.Set
        TreeSet = IF.TreeSet
        for word in words:
            word_idx = get_word_idx(word)
            if word_idx is None:
                idx[word] = word_idx = Set()
            word_idx.insert(docid)
            if (not isinstance(word_idx, TreeSet) and
                    len(word_idx) >= self.tree_threshold):
                # Convert to a TreeSet.
                idx[word] = TreeSet(word_idx)

    def _insert_reverse(self, docid, words):
        """ add words to forward index """

        if words:
            self._rev_index[docid] = words

    def search(self, query, operator='and'):
        """Execute a search given by 'query'."""
        if isinstance(query, six.string_types):
            query = [query]

        query = self.normalize(query)

        sets = []
        for word in query:
            docids = self._fwd_index.get(word, self.family.IF.Set())
            sets.append(docids)

        if operator == 'or':
            rs = self.family.IF.multiunion(sets)
        elif operator == 'and':
            # sort smallest to largest set so we intersect the smallest
            # number of document identifiers possible
            sets.sort(key=len)
            rs = None
            for set in sets:
                rs = self.family.IF.intersection(rs, set)
                if not rs:
                    break
        else:
            raise TypeError('Keyword index only supports `and` and `or` '
                            'operators, not `%s`.' % operator)

        if rs:
            return rs
        else:
            return self.family.IF.Set()

    def apply(self, query):
        operator = 'and'
        if isinstance(query, dict):
            if 'operator' in query:
                operator = query['operator']
            query = query['query']
        return self.search(query, operator=operator)

    def optimize(self):
        """Optimize the index. Call this after changing tree_threshold.

        This converts internal data structures between
        Sets and TreeSets based on tree_threshold.
        """
        idx = self._fwd_index
        IF = self.family.IF
        Set = IF.Set
        TreeSet = IF.TreeSet
        items = list(self._fwd_index.items())
        for word, word_idx in items:
            if len(word_idx) >= self.tree_threshold:
                if not isinstance(word_idx, TreeSet):
                    # Convert to a TreeSet.
                    idx[word] = TreeSet(word_idx)
            else:
                if isinstance(word_idx, TreeSet):
                    # Convert to a Set.
                    idx[word] = Set(word_idx)
Exemplo n.º 38
0
class CatalogTool(PloneBaseTool, BaseTool):
    """Plone's catalog tool"""

    meta_type = 'Plone Catalog Tool'
    security = ClassSecurityInfo()
    toolicon = 'skins/plone_images/book_icon.png'
    _counter = None

    manage_catalogAdvanced = DTMLFile('www/catalogAdvanced', globals())

    manage_options = (
        {
            'action': 'manage_main',
            'label': 'Contents'
        },
        {
            'action': 'manage_catalogView',
            'label': 'Catalog'
        },
        {
            'action': 'manage_catalogIndexes',
            'label': 'Indexes'
        },
        {
            'action': 'manage_catalogSchema',
            'label': 'Metadata'
        },
        {
            'action': 'manage_catalogAdvanced',
            'label': 'Advanced'
        },
        {
            'action': 'manage_catalogReport',
            'label': 'Query Report'
        },
        {
            'action': 'manage_catalogPlan',
            'label': 'Query Plan'
        },
        {
            'action': 'manage_propertiesForm',
            'label': 'Properties'
        },
    )

    def __init__(self):
        ZCatalog.__init__(self, self.getId())

    def _removeIndex(self, index):
        # Safe removal of an index.
        try:
            self.manage_delIndex(index)
        except:
            pass

    def _listAllowedRolesAndUsers(self, user):
        # Makes sure the list includes the user's groups.
        result = user.getRoles()
        if 'Anonymous' in result:
            # The anonymous user has no further roles
            return ['Anonymous']
        result = list(result)
        if hasattr(aq_base(user), 'getGroups'):
            groups = ['user:%s' % x for x in user.getGroups()]
            if groups:
                result = result + groups
        # Order the arguments from small to large sets
        result.insert(0, 'user:%s' % user.getId())
        result.append('Anonymous')
        return result

    @security.private
    def indexObject(self, object, idxs=None):
        # Add object to catalog.
        # The optional idxs argument is a list of specific indexes
        # to populate (all of them by default).
        if idxs is None:
            idxs = []
        self.reindexObject(object, idxs)

    @security.protected(ManageZCatalogEntries)
    def catalog_object(self,
                       object,
                       uid=None,
                       idxs=None,
                       update_metadata=1,
                       pghandler=None):
        if idxs is None:
            idxs = []
        self._increment_counter()

        w = object
        if not IIndexableObject.providedBy(object):
            # This is the CMF 2.2 compatible approach, which should be used
            # going forward
            wrapper = queryMultiAdapter((object, self), IIndexableObject)
            if wrapper is not None:
                w = wrapper

        ZCatalog.catalog_object(self,
                                w,
                                uid,
                                idxs,
                                update_metadata,
                                pghandler=pghandler)

    @security.protected(ManageZCatalogEntries)
    def uncatalog_object(self, *args, **kwargs):
        self._increment_counter()
        return BaseTool.uncatalog_object(self, *args, **kwargs)

    def _increment_counter(self):
        if self._counter is None:
            self._counter = Length()
        self._counter.change(1)

    @security.private
    def getCounter(self):
        processQueue()
        return self._counter is not None and self._counter() or 0

    @security.private
    def allow_inactive(self, query_kw):
        """Check, if the user is allowed to see inactive content.
        First, check if the user is allowed to see inactive content site-wide.
        Second, if there is a 'path' key in the query, check if the user is
        allowed to see inactive content for these paths.
        Conservative check: as soon as one path is disallowed, return False.
        If a path cannot be traversed, ignore it.
        """
        allow_inactive = _checkPermission(AccessInactivePortalContent, self)
        if allow_inactive:
            return True

        paths = query_kw.get('path', False)
        if not paths:
            return False

        if isinstance(paths, dict):
            # Like: {'path': {'depth': 0, 'query': ['/Plone/events/']}}
            # Or: {'path': {'depth': 0, 'query': '/Plone/events/'}}
            paths = paths.get('query', [])

        if isinstance(paths, six.string_types):
            paths = [paths]

        objs = []
        site = getSite()
        for path in list(paths):
            path = path.encode('utf-8')  # paths must not be unicode
            try:
                site_path = '/'.join(site.getPhysicalPath())
                parts = path[len(site_path) + 1:].split('/')
                parent = site.unrestrictedTraverse('/'.join(parts[:-1]))
                objs.append(parent.restrictedTraverse(parts[-1]))
            except (KeyError, AttributeError, Unauthorized):
                # When no object is found don't raise an error
                pass

        if not objs:
            return False

        allow = True
        for ob in objs:
            allow = allow and\
                _checkPermission(AccessInactivePortalContent, ob)

        return allow

    @security.protected(SearchZCatalog)
    def searchResults(self, query=None, **kw):
        # Calls ZCatalog.searchResults with extra arguments that
        # limit the results to what the user is allowed to see.
        #
        # This version uses the 'effectiveRange' DateRangeIndex.
        #
        # It also accepts a keyword argument show_inactive to disable
        # effectiveRange checking entirely even for those without portal
        # wide AccessInactivePortalContent permission.

        # Make sure any pending index tasks have been processed
        processQueue()

        kw = kw.copy()
        show_inactive = kw.get('show_inactive', False)
        if isinstance(query, dict) and not show_inactive:
            show_inactive = 'show_inactive' in query

        user = _getAuthenticatedUser(self)
        kw['allowedRolesAndUsers'] = self._listAllowedRolesAndUsers(user)

        if not show_inactive and not self.allow_inactive(kw):
            kw['effectiveRange'] = DateTime()

        sort_on = kw.get('sort_on')
        if sort_on and sort_on not in self.indexes():
            # I get crazy sort_ons like '194' or 'null'.
            kw.pop('sort_on')

        return ZCatalog.searchResults(self, query, **kw)

    __call__ = searchResults

    def search(self, query, sort_index=None, reverse=0, limit=None, merge=1):
        # Wrap search() the same way that searchResults() is

        # Make sure any pending index tasks have been processed
        processQueue()

        user = _getAuthenticatedUser(self)
        query['allowedRolesAndUsers'] = self._listAllowedRolesAndUsers(user)

        if not self.allow_inactive(query):
            query['effectiveRange'] = DateTime()

        return super(CatalogTool, self).search(query, sort_index, reverse,
                                               limit, merge)

    @security.protected(ManageZCatalogEntries)
    def clearFindAndRebuild(self):
        # Empties catalog, then finds all contentish objects (i.e. objects
        # with an indexObject method), and reindexes them.
        # This may take a long time.

        def indexObject(obj, path):
            if (base_hasattr(obj, 'indexObject')
                    and safe_callable(obj.indexObject)):
                try:
                    obj.indexObject()

                    # index conversions from plone.app.discussion
                    annotions = IAnnotations(obj)
                    catalog = getToolByName(obj, "portal_catalog")
                    if DISCUSSION_ANNOTATION_KEY in annotions:
                        conversation = annotions[DISCUSSION_ANNOTATION_KEY]
                        conversation = conversation.__of__(obj)
                        for comment in conversation.getComments():
                            try:
                                if catalog:
                                    catalog.indexObject(comment)
                            except StopIteration:  # pragma: no cover
                                pass

                except TypeError:
                    # Catalogs have 'indexObject' as well, but they
                    # take different args, and will fail
                    pass

        self.manage_catalogClear()
        portal = aq_parent(aq_inner(self))
        portal.ZopeFindAndApply(portal,
                                search_sub=True,
                                apply_func=indexObject)

    @security.protected(ManageZCatalogEntries)
    def manage_catalogRebuild(self, RESPONSE=None, URL1=None):
        """Clears the catalog and indexes all objects with an 'indexObject'
        method. This may take a long time.
        """
        elapse = time.time()
        c_elapse = time.clock()

        self.clearFindAndRebuild()

        elapse = time.time() - elapse
        c_elapse = time.clock() - c_elapse

        msg = ('Catalog Rebuilt\n'
               'Total time: %s\n'
               'Total CPU time: %s' % (repr(elapse), repr(c_elapse)))
        logger.info(msg)

        if RESPONSE is not None:
            RESPONSE.redirect(URL1 +
                              '/manage_catalogAdvanced?manage_tabs_message=' +
                              urllib.parse.quote(msg))
Exemplo n.º 39
0
class GranularIndex(CatalogFieldIndex):
    """Indexes integer values using multiple granularity levels.

    The multiple levels of granularity make it possible to query large
    ranges without loading many IFTreeSets from the forward index.
    """
    implements(
        ICatalogIndex,
        IStatistics,
    )

    def __init__(self, discriminator, levels=(1000, )):
        """Create an index.

        levels is a sequence of integer coarseness levels.
        The default is (1000,).
        """
        self._levels = tuple(levels)
        super(GranularIndex, self).__init__(discriminator)

    def clear(self):
        """Initialize all mappings."""
        # The forward index maps an indexed value to IFSet(docids)
        self._fwd_index = self.family.IO.BTree()
        # The reverse index maps a docid to its index value
        self._rev_index = self.family.II.BTree()
        self._num_docs = Length(0)
        # self._granular_indexes: [(level, BTree(value -> IFSet([docid])))]
        self._granular_indexes = [(level, self.family.IO.BTree())
                                  for level in self._levels]

    def index_doc(self, docid, obj):
        if callable(self.discriminator):
            value = self.discriminator(obj, _marker)
        else:
            value = getattr(obj, self.discriminator, _marker)

        if value is _marker:
            # unindex the previous value
            self.unindex_doc(docid)
            return

        if not isinstance(value, int):
            raise ValueError(
                'GranularIndex cannot index non-integer value %s' % value)

        rev_index = self._rev_index
        if docid in rev_index:
            if docid in self._fwd_index.get(value, ()):
                # There's no need to index the doc; it's already up to date.
                return
            # unindex doc if present
            self.unindex_doc(docid)

        # Insert into forward index.
        set = self._fwd_index.get(value)
        if set is None:
            set = self.family.IF.TreeSet()
            self._fwd_index[value] = set
        set.insert(docid)

        # increment doc count
        self._num_docs.change(1)

        # Insert into reverse index.
        rev_index[docid] = value

        for level, ndx in self._granular_indexes:
            v = value // level
            set = ndx.get(v)
            if set is None:
                set = self.family.IF.TreeSet()
                ndx[v] = set
            set.insert(docid)

    def unindex_doc(self, docid):
        rev_index = self._rev_index
        value = rev_index.get(docid)
        if value is None:
            return  # not in index

        del rev_index[docid]

        self._num_docs.change(-1)

        ndx = self._fwd_index
        try:
            set = ndx[value]
            set.remove(docid)
            if not set:
                del ndx[value]
        except KeyError:
            pass

        for level, ndx in self._granular_indexes:
            v = value // level
            try:
                set = ndx[v]
                set.remove(docid)
                if not set:
                    del ndx[v]
            except KeyError:
                pass

    def search(self, queries, operator='or'):
        sets = []
        for query in queries:
            if isinstance(query, Range):
                query = query.as_tuple()
            else:
                query = (query, query)

            set = self.family.IF.multiunion(self.docids_in_range(*query))
            sets.append(set)

        result = None

        if len(sets) == 1:
            result = sets[0]
        elif operator == 'and':
            sets.sort()
            for set in sets:
                result = self.family.IF.intersection(set, result)
        else:
            result = self.family.IF.multiunion(sets)

        return result

    def docids_in_range(self, min, max):
        """List the docids for an integer range, inclusive on both ends.

        min or max can be None, making them unbounded.

        Returns an iterable of IFSets.
        """
        for level, ndx in sorted(self._granular_indexes, reverse=True):
            # Try to fill the range using coarse buckets first.
            # Use only buckets that completely fill the range.
            # For example, if start is 2 and level is 10, then we can't
            # use bucket 0; only buckets 1 and greater are useful.
            # Similarly, if end is 18 and level is 10, then we can't use
            # bucket 1; only buckets 0 and less are useful.
            if min is not None:
                a = (min + level - 1) // level
            else:
                a = None
            if max is not None:
                b = (max - level + 1) // level
            else:
                b = None
            # a and b are now coarse bucket values (or None).
            if a is None or b is None or a <= b:
                sets = []
                if a is not None and min < a * level:
                    # include the gap before
                    sets.extend(self.docids_in_range(min, a * level - 1))
                sets.extend(ndx.values(a, b))
                if b is not None and (b + 1) * level - 1 < max:
                    # include the gap after
                    sets.extend(self.docids_in_range((b + 1) * level, max))
                return sets

        return self._fwd_index.values(min, max)
Exemplo n.º 40
0
class Folder(Persistent):
    """
    A folder implementation which acts much like a Python dictionary.

    keys are Unicode strings; values are arbitrary Python objects.
    """

    # _num_objects=None below is b/w compat for older instances of
    # folders which don't have a BTrees.Length object as a
    # _num_objects attribute.
    _num_objects = None

    __name__ = None
    __parent__ = None

    # Default uses ordering of underlying BTree.
    _order = None

    def _get_order(self):
        if self._order is not None:
            return list(self._order)
        return self.data.keys()

    def _set_order(self, value):
        # XXX:  should we test against self.data.keys()?
        self._order = tuple([unicodify(x) for x in value])

    def _del_order(self):
        del self._order

    order = property(_get_order, _set_order, _del_order)

    def __init__(self, data=None):
        if data is None:
            data = {}
        self.data = OOBTree(data)
        self._num_objects = Length(len(data))

    def keys(self):
        """See IFolder."""
        return self.order

    def __iter__(self):
        return iter(self.order)

    def values(self):
        """See IFolder."""
        if self._order is not None:
            return [self.data[name] for name in self.order]
        return self.data.values()

    def items(self):
        """See IFolder."""
        if self._order is not None:
            return [(name, self.data[name]) for name in self.order]
        return self.data.items()

    def __len__(self):
        """See IFolder."""
        if self._num_objects is None:
            # can be arbitrarily expensive
            return len(self.data)
        return self._num_objects()

    def __nonzero__(self):
        """See IFolder."""
        return True

    def __getitem__(self, name):
        """See IFolder."""
        name = unicodify(name)
        return self.data[name]

    def get(self, name, default=None):
        """See IFolder."""
        name = unicodify(name)
        return self.data.get(name, default)

    def __contains__(self, name):
        """See IFolder."""
        name = unicodify(name)
        return name in self.data

    def __setitem__(self, name, other):
        """See IFolder."""
        return self.add(name, other)

    def add(self, name, other, send_events=True):
        """See IFolder."""
        if not isinstance(name, basestring):
            raise TypeError("Name must be a string rather than a %s" %
                            name.__class__.__name__)
        if not name:
            raise TypeError("Name must not be empty")

        name = unicodify(name)

        if name in self.data:
            raise KeyError('An object named %s already exists' % name)

        if send_events:
            objectEventNotify(ObjectWillBeAddedEvent(other, self, name))
        other.__parent__ = self
        other.__name__ = name

        # backwards compatibility: add a Length _num_objects to folders that
        # have none
        if self._num_objects is None:
            self._num_objects = Length(len(self.data))

        self.data[name] = other
        self._num_objects.change(1)

        if self._order is not None:
            self._order += (name, )

        if send_events:
            objectEventNotify(ObjectAddedEvent(other, self, name))

    def __delitem__(self, name):
        """See IFolder."""
        return self.remove(name)

    def remove(self, name, send_events=True):
        """See IFolder."""
        name = unicodify(name)
        other = self.data[name]

        if send_events:
            objectEventNotify(ObjectWillBeRemovedEvent(other, self, name))

        if hasattr(other, '__parent__'):
            del other.__parent__

        if hasattr(other, '__name__'):
            del other.__name__

        # backwards compatibility: add a Length _num_objects to folders that
        # have none
        if self._num_objects is None:
            self._num_objects = Length(len(self.data))

        del self.data[name]
        self._num_objects.change(-1)

        if self._order is not None:
            self._order = tuple([x for x in self._order if x != name])

        if send_events:
            objectEventNotify(ObjectRemovedEvent(other, self, name))

        return other

    def pop(self, name, default=marker):
        """See IFolder."""
        try:
            result = self.remove(name)
        except KeyError:
            if default is marker:
                raise
            return default
        return result

    def __repr__(self):
        klass = self.__class__
        classname = '%s.%s' % (klass.__module__, klass.__name__)
        return '<%s object %r at %#x>' % (classname, self.__name__, id(self))
Exemplo n.º 41
0
class Folder(Persistent):
    """ A folder implementation which acts much like a Python dictionary.

    Keys must be Unicode strings; values must be arbitrary Python objects.
    """
    family = BTrees.family64

    __name__ = None
    __parent__ = None

    # Default uses ordering of underlying BTree.
    _order = None  # tuple of names
    _order_oids = None  # tuple of oids
    _reorderable = None

    def __init__(self, data=None, family=None):
        """ Constructor.  Data may be an initial dictionary mapping object
        name to object. """
        if family is not None:
            self.family = family
        if data is None:
            data = {}
        self.data = self.family.OO.BTree(data)
        self._num_objects = Length(len(data))

    def set_order(self, names, reorderable=None):
        """ Sets the folder order. ``names`` is a list of names for existing
        folder items, in the desired order.  All names that currently exist in
        the folder must be mentioned in ``names``, or a :exc:`ValueError` will
        be raised.

        If ``reorderable`` is passed, value, it must be ``None``, ``True`` or
        ``False``.  If it is ``None``, the reorderable flag will not be reset
        from its current value.  If it is anything except ``None``, it will be
        treated as a boolean and the reorderable flag will be set to that
        value.  The ``reorderable`` value of a folder will be returned by that
        folder's :meth:`~substanced.folder.Folder.is_reorderable` method.  The
        :meth:`~substanced.folder.Folder.is_reorderable` method is used by the
        SDI folder contents view to indicate that the folder can or cannot be
        reordered via the web UI.

        If ``reorderable`` is set to ``True``, the
        :meth:`~substanced.folder.Folder.reorder` method will work properly,
        otherwise it will raise a :exc:`ValueError` when called.
        """
        nameset = set(names)
        if len(self) != len(nameset):
            raise ValueError('Must specify all names when calling set_order')

        if len(names) != len(nameset):
            raise ValueError('No repeated items allowed in names')

        order = []
        order_oids = []

        for name in names:
            assert (isinstance(name, string_types))
            name = u(name)
            oid = get_oid(self[name])
            order.append(name)
            order_oids.append(oid)

        self._order = tuple(order)
        self._order_oids = tuple(order_oids)
        assert (len(self._order) == len(self._order_oids))

        if reorderable is not None:
            self._reorderable = bool(reorderable)

    def unset_order(self):
        """ Remove set order from a folder, making it unordered, and
        non-reorderable."""
        if self._order is not None:
            del self._order
        if self._order_oids is not None:
            del self._order_oids
        if self._reorderable is not None:
            del self._reorderable

    def reorder(self, names, before):
        """ Move one or more items from a folder into new positions inside that
        folder. ``names`` is a list of ids of existing folder subobject names,
        which will be inserted in order before the item named ``before``. All
        other items are left in the original order. If ``before`` is ``None``,
        the items will be appended after the last item in the current order. If
        this method is called on a folder which does not have an order set, or
        which is not reorderable, a :exc:`ValueError` will be raised. A
        :exc:`KeyError` is raised, if ``before`` does not correspond to any
        item, and is not ``None``."""
        if not self._reorderable:
            raise ValueError('Folder is not reorderable')

        before_idx = None

        if len(set(names)) != len(names):
            raise ValueError('No repeated values allowed in names')

        if before is not None:
            if not before in self._order:
                raise FolderKeyError(before)
            before_idx = self._order.index(before)

        assert (len(self._order) == len(self._order_oids))

        order_names = list(self._order)
        order_oids = list(self._order_oids)

        reorder_names = []
        reorder_oids = []

        for name in names:
            assert (isinstance(name, string_types))
            name = u(name)
            if not name in order_names:
                raise FolderKeyError(name)
            idx = order_names.index(name)
            oid = order_oids[idx]
            order_names[idx] = None
            order_oids[idx] = None
            reorder_names.append(name)
            reorder_oids.append(oid)

        assert (len(reorder_names) == len(reorder_oids))

        # NB: technically we could use filter(None, oids) and filter(None,
        # names) because names cannot be empty string and oid 0 is disallowed,
        # but just in case this becomes untrue later we define "filt" instead

        def filt(L):
            return [x for x in L if x is not None]

        if before_idx is None:
            order_names = filt(order_names)
            order_names.extend(reorder_names)
            order_oids = filt(order_oids)
            order_oids.extend(reorder_oids)
        else:
            before_idx_names = filt(order_names[:before_idx])
            after_idx_names = filt(order_names[before_idx:])
            before_idx_oids = filt(order_oids[:before_idx])
            after_idx_oids = filt(order_oids[before_idx:])
            assert (len(before_idx_names +
                        after_idx_names) == len(before_idx_oids +
                                                after_idx_oids))
            order_names = before_idx_names + reorder_names + after_idx_names
            order_oids = before_idx_oids + reorder_oids + after_idx_oids

        for oid, name in zip(order_oids, order_names):
            # belt and suspenders check
            assert oid == get_oid(self[name])

        self._order = tuple(order_names)
        self._order_oids = tuple(order_oids)

    def is_ordered(self):
        """ Return true if the folder has a manually set ordering, false
        otherwise."""
        return self._order is not None

    def is_reorderable(self):
        """ Return true if the folder can be reordered, false otherwise."""
        return self._reorderable

    def sort(self, oids, reverse=False, limit=None, **kw):
        # used by the hypatia resultset "sort" method when the folder contents
        # view uses us as a "sort index"
        if self._order_oids is not None:
            ids = [oid for oid in self._order_oids if oid in oids]
        else:
            ids = []
            for resource in self.values():
                oid = get_oid(resource)
                if oid in oids:
                    ids.append(oid)
        if reverse:
            ids = ids[::-1]
        if limit is not None:
            ids = ids[:limit]
        return ids

    def find_service(self, service_name):
        """ Return a service named by ``service_name`` in this folder *or any
        parent service folder* or ``None`` if no such service exists.  A
        shortcut for :func:`substanced.service.find_service`."""
        return find_service(self, service_name)

    def find_services(self, service_name):
        """ Returns a sequence of service objects named by ``service_name``
        in this folder's lineage or an empty sequence if no such service
        exists.  A shortcut for :func:`substanced.service.find_services`"""
        return find_services(self, service_name)

    def add_service(self, name, obj, registry=None, **kw):
        """ Add a service to this folder named ``name``."""
        if registry is None:
            registry = get_current_registry()
        kw['registry'] = registry
        self.add(name, obj, **kw)
        alsoProvides(obj, IService)

    def keys(self):
        """ Return an iterable sequence of object names present in the folder.

        Respect order, if set.
        """
        if self._order is not None:
            return self._order
        return self.data.keys()

    order = property(keys, set_order, unset_order)  # b/c

    def __iter__(self):
        """ An alias for ``keys``
        """
        return iter(self.keys())

    def values(self):
        """ Return an iterable sequence of the values present in the folder.

        Respect ``order``, if set.
        """
        if self._order is not None:
            return [self.data[name] for name in self.keys()]
        return self.data.values()

    def items(self):
        """ Return an iterable sequence of (name, value) pairs in the folder.

        Respect ``order``, if set.
        """
        if self._order is not None:
            return [(name, self.data[name]) for name in self.keys()]
        return self.data.items()

    def __len__(self):
        """ Return the number of objects in the folder.
        """
        return self._num_objects()

    def __nonzero__(self):
        """ Return ``True`` unconditionally.
        """
        return True

    __bool__ = __nonzero__

    def __repr__(self):
        klass = self.__class__
        classname = '%s.%s' % (klass.__module__, klass.__name__)
        return '<%s object %r at %#x>' % (classname, self.__name__, id(self))

    def __getitem__(self, name):
        """ Return the object named ``name`` added to this folder or raise
        ``KeyError`` if no such object exists.  ``name`` must be a Unicode
        object or directly decodeable to Unicode using the system default
        encoding.
        """
        with statsd_timer('folder.get'):
            name = u(name)
            return wrap_if_broken(self.data[name])

    def get(self, name, default=None):
        """ Return the object named by ``name`` or the default.

        ``name`` must be a Unicode object or a bytestring object.

        If ``name`` is a bytestring object, it must be decodable using the
        system default encoding.
        """
        with statsd_timer('folder.get'):
            name = u(name)
            return wrap_if_broken(self.data.get(name, default))

    def __contains__(self, name):
        """ Does the container contains an object named by name?

        ``name`` must be a Unicode object or a bytestring object.

        If ``name`` is a bytestring object, it must be decodable using the
        system default encoding.
        """
        name = u(name)
        return name in self.data

    def __setitem__(self, name, other):
        """ Set object ``other`` into this folder under the name ``name``.

        ``name`` must be a Unicode object or a bytestring object.

        If ``name`` is a bytestring object, it must be decodable using the
        system default encoding.

        ``name`` cannot be the empty string.

        When ``other`` is seated into this folder, it will also be decorated
        with a ``__parent__`` attribute (a reference to the folder into which
        it is being seated) and ``__name__`` attribute (the name passed in to
        this function.  It must not already have a ``__parent__`` attribute
        before being seated into the folder, or an exception will be raised.

        If a value already exists in the foldr under the name ``name``, raise
        :exc:`KeyError`.

        When this method is called, the object will be added to the objectmap,
        an :class:`substanced.event.ObjectWillBeAdded` event will be emitted
        before the object obtains a ``__name__`` or ``__parent__`` value, then
        a :class:`substanced.event.ObjectAdded` will be emitted after the
        object obtains a ``__name__`` and ``__parent__`` value.
        """
        return self.add(name, other)

    def validate_name(self, name, reserved_names=()):
        """
        Validate the ``name`` passed to ensure that it's addable to the folder.
        Returns the name decoded to Unicode if it passes all addable checks.
        It's not addable if:

        - the name is not decodeable to Unicode.

        - the name starts with ``@@`` (conflicts with explicit view names).

        - the name has slashes in it (WSGI limitation).

        - the name is empty.

        If any of these conditions are untrue, raise a :exc:`ValueError`.  If
        the name passed is in the list of ``reserved_names``, raise a
        :exc:`ValueError`.
        """
        if not isinstance(name, STRING_TYPES):
            raise ValueError("Name must be a string rather than a %s" %
                             name.__class__.__name__)
        if not name:
            raise ValueError("Name must not be empty")

        try:
            name = u(name)
        except UnicodeDecodeError:  #pragma NO COVER (on Py3k)
            raise ValueError('Name "%s" not decodeable to unicode' % name)

        if name in reserved_names:
            raise ValueError('%s is a reserved name' % name)

        if name.startswith('@@'):
            raise ValueError('Names which start with "@@" are not allowed')

        if '/' in name:
            raise ValueError('Names which contain a slash ("/") are not '
                             'allowed')

        return name

    def check_name(self, name, reserved_names=()):
        """ Perform all the validation checks implied by
        :meth:`~substanced.folder.Folder.validate_name` against the ``name``
        supplied but also fail with a
        :class:`~substanced.folder.FolderKeyError` if an object with the name
        ``name`` already exists in the folder."""

        name = self.validate_name(name, reserved_names=reserved_names)

        if name in self.data:
            raise FolderKeyError('An object named %s already exists' % name)

        return name

    def add(self,
            name,
            other,
            send_events=True,
            reserved_names=(),
            duplicating=None,
            moving=None,
            loading=False,
            registry=None):
        """ Same as ``__setitem__``.

        If ``send_events`` is False, suppress the sending of folder events.
        Don't allow names in the ``reserved_names`` sequence to be added.

        If ``duplicating`` not ``None``, it must be the object which is being
        duplicated; a result of a non-``None`` duplicating means that oids will
        be replaced in objectmap.  If ``moving`` is not ``None``, it must be
        the folder from which the object is moving; this will be the ``moving``
        attribute of events sent by this function too.  If ``loading`` is
        ``True``, the ``loading`` attribute of events sent as a result of
        calling this method will be ``True`` too.

        This method returns the name used to place the subobject in the
        folder (a derivation of ``name``, usually the result of
        ``self.check_name(name)``).
        """
        if registry is None:
            registry = get_current_registry()

        name = self.check_name(name, reserved_names)

        if getattr(other, '__parent__', None):
            raise ValueError(
                'obj %s added to folder %s already has a __parent__ attribute, '
                'please remove it completely from its existing parent (%s) '
                'before trying to readd it to this one' %
                (other, self, self.__parent__))

        with statsd_timer('folder.add'):

            objectmap = find_objectmap(self)

            if objectmap is not None:

                basepath = resource_path_tuple(self)

                for node in postorder(other):
                    node_path = node_path_tuple(node)
                    path_tuple = basepath + (name, ) + node_path[1:]
                    # the below gives node an objectid; if the will-be-added
                    # event is the result of a duplication, replace the oid of
                    # the node with a new one
                    objectmap.add(
                        node,
                        path_tuple,
                        duplicating=duplicating is not None,
                        moving=moving is not None,
                    )

            if send_events:
                event = ObjectWillBeAdded(
                    other,
                    self,
                    name,
                    duplicating=duplicating,
                    moving=moving,
                    loading=loading,
                )
                self._notify(event, registry)

            other.__parent__ = self
            other.__name__ = name

            self.data[name] = other
            self._num_objects.change(1)

            if self._order is not None:
                oid = get_oid(other)
                self._order += (name, )
                self._order_oids += (oid, )

            if send_events:
                event = ObjectAdded(
                    other,
                    self,
                    name,
                    duplicating=duplicating,
                    moving=moving,
                    loading=loading,
                )
                self._notify(event, registry)

            return name

    def pop(self, name, default=marker, registry=None):
        """ Remove the item stored in the under ``name`` and return it.

        If ``name`` doesn't exist in the folder, and ``default`` **is not**
        passed, raise a :exc:`KeyError`.

        If ``name`` doesn't exist in the folder, and ``default`` **is**
        passed, return ``default``.

        When the object stored under ``name`` is removed from this folder,
        remove its ``__parent__`` and ``__name__`` values.

        When this method is called, emit an
        :class:`substanced.event.ObjectWillBeRemoved` event before the
        object loses its ``__name__`` or ``__parent__`` values.  Emit an
        :class:`substanced.event.ObjectRemoved` after the object loses its
        ``__name__`` and ``__parent__`` value,
        """
        if registry is None:
            registry = get_current_registry()
        try:
            result = self.remove(name, registry=registry)
        except KeyError:
            if default is marker:
                raise
            return default
        return result

    def _notify(self, event, registry=None):
        if registry is None:
            registry = get_current_registry()
        registry.subscribers((event, event.object, self), None)

    def __delitem__(self, name):
        """ Remove the object from this folder stored under ``name``.

        ``name`` must be a Unicode object or a bytestring object.

        If ``name`` is a bytestring object, it must be decodable using the
        system default encoding.

        If no object is stored in the folder under ``name``, raise a
        :exc:`KeyError`.

        When the object stored under ``name`` is removed from this folder,
        remove its ``__parent__`` and ``__name__`` values.

        When this method is called, the removed object will be removed from the
        objectmap, a :class:`substanced.event.ObjectWillBeRemoved` event will
        be emitted before the object loses its ``__name__`` or ``__parent__``
        values and a :class:`substanced.event.ObjectRemoved` will be emitted
        after the object loses its ``__name__`` and ``__parent__`` value,
        """
        return self.remove(name)

    def remove(self,
               name,
               send_events=True,
               moving=None,
               loading=False,
               registry=None):
        """ Same thing as ``__delitem__``.

        If ``send_events`` is false, suppress the sending of folder events.

        If ``moving`` is not ``None``, the ``moving`` argument must be the
        folder to which the named object will be moving.  This value will be
        passed along as the ``moving`` attribute of the events sent as the
        result of this action.  If ``loading`` is ``True``, the ``loading``
        attribute of events sent as a result of calling this method will be
        ``True`` too.
        """
        name = u(name)
        other = wrap_if_broken(self.data[name])
        oid = get_oid(other, None)

        if registry is None:
            registry = get_current_registry()

        with statsd_timer('folder.remove'):

            if send_events:
                event = ObjectWillBeRemoved(other,
                                            self,
                                            name,
                                            moving=moving,
                                            loading=loading)
                self._notify(event, registry)

            if hasattr(other, '__parent__'):
                try:
                    del other.__parent__
                except AttributeError:
                    # this might be a broken object
                    pass

            if hasattr(other, '__name__'):
                try:
                    del other.__name__
                except AttributeError:
                    # this might be a broken object
                    pass

            del self.data[name]
            self._num_objects.change(-1)

            if self._order is not None:
                assert (len(self._order) == len(self._order_oids))
                idx = self._order.index(name)
                order = list(self._order)
                order.pop(idx)
                order_oids = list(self._order_oids)
                order_oids.pop(idx)
                self._order = tuple(order)
                self._order_oids = tuple(order_oids)

            objectmap = find_objectmap(self)

            removed_oids = set([oid])

            if objectmap is not None and oid is not None:
                removed_oids = objectmap.remove(oid, moving=moving is not None)

            if send_events:
                event = ObjectRemoved(other,
                                      self,
                                      name,
                                      removed_oids,
                                      moving=moving,
                                      loading=loading)
                self._notify(event, registry)

            return other

    def copy(self, name, other, newname=None, registry=None):
        """
        Copy a subobject named ``name`` from this folder to the folder
        represented by ``other``.  If ``newname`` is not none, it is used as
        the target object name; otherwise the existing subobject name is
        used.
        """
        if newname is None:
            newname = name

        if registry is None:
            registry = get_current_registry()

        with statsd_timer('folder.copy'):
            obj = self[name]
            newobj = copy(obj)
            return other.add(newname,
                             newobj,
                             duplicating=obj,
                             registry=registry)

    def move(self, name, other, newname=None, registry=None):
        """
        Move a subobject named ``name`` from this folder to the folder
        represented by ``other``.  If ``newname`` is not none, it is used as
        the target object name; otherwise the existing subobject name is
        used.

        This operation is done in terms of a remove and an add.  The Removed
        and WillBeRemoved events as well as the Added and WillBeAdded events
        sent will indicate that the object is moving.
        """
        if newname is None:
            newname = name
        if registry is None:
            registry = get_current_registry()
        ob = self.remove(name, moving=other, registry=registry)
        other.add(newname, ob, moving=self, registry=registry)
        return ob

    def rename(self, oldname, newname, registry=None):
        """
        Rename a subobject from oldname to newname.

        This operation is done in terms of a remove and an add.  The Removed
        and WillBeRemoved events sent will indicate that the object is
        moving.
        """
        if registry is None:
            registry = get_current_registry()
        return self.move(oldname, self, newname, registry=registry)

    def replace(self, name, newobject, send_events=True, registry=None):
        """ Replace an existing object named ``name`` in this folder with a
        new object ``newobject``.  If there isn't an object named ``name`` in
        this folder, an exception will *not* be raised; instead, the new
        object will just be added.

        This operation is done in terms of a remove and an add.  The Removed
        and WillBeRemoved events will be sent for the old object, and the
        WillBeAdded and Added events will be sent for the new object.
        """
        if registry is None:
            registry = get_current_registry()
        if name in self:
            self.remove(name, send_events=send_events)
        self.add(name, newobject, send_events=send_events, registry=registry)

    def load(self, name, newobject, registry=None):
        """ A replace method used by the code that loads an existing dump.
        Events sent during this replace will have a true ``loading`` flag."""
        if registry is None:
            registry = get_current_registry()
        if name in self:
            self.remove(name, loading=True)
        self.add(name, newobject, loading=True, registry=registry)

    def clear(self, registry=None):
        """ Clear all items from the folder.  This is the equivalent of calling
        ``.remove`` with each key that exists in the folder. """
        if registry is None:
            registry = get_current_registry()
        for name in self:
            self.remove(name, registry=registry)
Exemplo n.º 42
0
class EventEndDateIndex(Persistent):
    """ List of bookings ordered by their event's ending date
    """
    def __init__(self):
        self._tree = LOBTree.LOBTree()
        self._count = Length(0)

    ## private class methods ##
    @classmethod
    def _dateToKey(cls, date):
        if date:
            return datetimeToUnixTimeInt(date)
        else:
            return None

    @classmethod
    def _keyToDate(cls, key):
        if key:
            return unixTimeToDatetime(key)
        else:
            return None

    @classmethod
    def _bookingToKey(cls, booking):
        return cls._dateToKey(
            booking.getConference().getAdjustedEndDate(tz='UTC'))

    ## public instance methods ##
    def clear(self):
        """ Clears all the information stored
        """
        self._tree = LOBTree.LOBTree()
        self._count = Length(0)

    def getCount(self):
        """ Returns the number of bookings (not keys) stored
        """
        return self._count(
        )  #to get the value of a Length object, one has to "call" the object

    def indexBooking(self, booking):
        """ Stores a booking in the index
        """
        key = EventEndDateIndex._bookingToKey(booking)
        if not key in self._tree:
            self._tree[key] = DateBookingList()
        self._tree[key].addBooking(booking)
        self._count.change(1)

    def unindexBooking(self, booking):
        """ Removes a booking from the index
        """
        key = EventEndDateIndex._bookingToKey(booking)
        try:
            self._tree[key].removeBooking(booking)
            if self._tree[key].getCount() == 0:
                del self._tree[key]
            self._count.change(-1)
        except KeyError:
            Logger.get('Vidyo').warning(
                "Could not unindex booking: (confId=%s, id=%s) from Vidyo's GlobalData. Tried with key: %s."
                % (booking.getConference().getId(), booking.getId(), str(key)))

    def moveBooking(self, booking, oldDate):
        """ Changes the position of a booking in the index
        """
        oldKey = EventEndDateIndex._dateToKey(oldDate)
        newKey = EventEndDateIndex._bookingToKey(booking)
        try:
            self._tree[oldKey].removeBooking(booking)
            if self._tree[oldKey].getCount() == 0:
                del self._tree[oldKey]
            if not newKey in self._tree:
                self._tree[newKey] = DateBookingList()
            self._tree[newKey].addBooking(booking)
        except KeyError:
            Logger.get('Vidyo').warning(
                "Could not move booking: (confId=%s, id=%s) from Vidyo's GlobalData. Tried moving from key: %s to key: %s."
                % (booking.getConference().getId(), booking.getId(),
                   str(oldKey), str(newKey)))

    def iterbookings(self, minDate=None, maxDate=None):
        """ Will return an iterator over Vidyo bookings attached to conferences whose
            end date is between minDate and maxDate
        """
        minKey = EventEndDateIndex._dateToKey(minDate)
        maxKey = EventEndDateIndex._dateToKey(maxDate)
        for bookingList in self._tree.itervalues(min=minKey, max=maxKey):
            for b in bookingList.iterbookings():
                yield b

    def deleteKeys(self, minDate=None, maxDate=None):
        """
        """
        minKey = EventEndDateIndex._dateToKey(minDate)
        maxKey = EventEndDateIndex._dateToKey(maxDate)
        for key in list(self._tree.keys(
                min=minKey,
                max=maxKey)):  #we want a copy because we are going to modify
            self._deleteKey(key)

    def _deleteKey(self, key):
        Logger.get("Vidyo").info(
            "Vidyo EventEndDateIndex: deleting key %s (%s)" %
            (str(key), str(EventEndDateIndex._keyToDate(key)) + " (UTC)"))
        self._count.change(-self._tree[key].getCount())
        del self._tree[key]

    def initialize(self, dbi=None):
        """ Cleans the indexes, and then indexes all the vidyo bookings from all the conferences
            WARNING: obviously, this can potentially take a while
        """
        i = 0
        self.clear()
        for conf in ConferenceHolder().getList():
            csbm = Catalog.getIdx("cs_bookingmanager_conference").get(
                conf.getId())
            for booking in csbm.getBookingList():
                if booking.getType() == "Vidyo" and booking.isCreated():
                    self.indexBooking(booking)
            i += 1
            if dbi and i % 100 == 0:
                dbi.commit()
        if dbi:
            dbi.commit()
Exemplo n.º 43
0
class CatalogTool(PloneBaseTool, BaseTool):
    """Plone's catalog tool"""

    meta_type = 'Plone Catalog Tool'
    security = ClassSecurityInfo()
    toolicon = 'skins/plone_images/book_icon.png'
    _counter = None

    manage_catalogAdvanced = DTMLFile('www/catalogAdvanced', globals())

    manage_options = (
        {'action': 'manage_main', 'label': 'Contents'},
        {'action': 'manage_catalogView', 'label': 'Catalog'},
        {'action': 'manage_catalogIndexes', 'label': 'Indexes'},
        {'action': 'manage_catalogSchema', 'label': 'Metadata'},
        {'action': 'manage_catalogAdvanced', 'label': 'Advanced'},
        {'action': 'manage_catalogReport', 'label': 'Query Report'},
        {'action': 'manage_catalogPlan', 'label': 'Query Plan'},
        {'action': 'manage_propertiesForm', 'label': 'Properties'},
    )

    def __init__(self):
        ZCatalog.__init__(self, self.getId())

    def _removeIndex(self, index):
        # Safe removal of an index.
        try:
            self.manage_delIndex(index)
        except:
            pass

    def _listAllowedRolesAndUsers(self, user):
        # Makes sure the list includes the user's groups.
        result = user.getRoles()
        if 'Anonymous' in result:
            # The anonymous user has no further roles
            return ['Anonymous']
        result = list(result)
        if hasattr(aq_base(user), 'getGroups'):
            groups = ['user:%s' % x for x in user.getGroups()]
            if groups:
                result = result + groups
        # Order the arguments from small to large sets
        result.insert(0, 'user:%s' % user.getId())
        result.append('Anonymous')
        return result

    @security.private
    def indexObject(self, object, idxs=None):
        # Add object to catalog.
        # The optional idxs argument is a list of specific indexes
        # to populate (all of them by default).
        if idxs is None:
            idxs = []
        self.reindexObject(object, idxs)

    @security.protected(ManageZCatalogEntries)
    def catalog_object(self, object, uid=None, idxs=None,
                       update_metadata=1, pghandler=None):
        if idxs is None:
            idxs = []
        self._increment_counter()

        w = object
        if not IIndexableObject.providedBy(object):
            # This is the CMF 2.2 compatible approach, which should be used
            # going forward
            wrapper = queryMultiAdapter((object, self), IIndexableObject)
            if wrapper is not None:
                w = wrapper

        ZCatalog.catalog_object(self, w, uid, idxs,
                                update_metadata, pghandler=pghandler)

    @security.protected(ManageZCatalogEntries)
    def uncatalog_object(self, *args, **kwargs):
        self._increment_counter()
        return BaseTool.uncatalog_object(self, *args, **kwargs)

    def _increment_counter(self):
        if self._counter is None:
            self._counter = Length()
        self._counter.change(1)

    @security.private
    def getCounter(self):
        processQueue()
        return self._counter is not None and self._counter() or 0

    @security.private
    def allow_inactive(self, query_kw):
        """Check, if the user is allowed to see inactive content.
        First, check if the user is allowed to see inactive content site-wide.
        Second, if there is a 'path' key in the query, check if the user is
        allowed to see inactive content for these paths.
        Conservative check: as soon as one path is disallowed, return False.
        If a path cannot be traversed, ignore it.
        """
        allow_inactive = _checkPermission(AccessInactivePortalContent, self)
        if allow_inactive:
            return True

        paths = query_kw.get('path', False)
        if not paths:
            return False

        if isinstance(paths, dict):
            # Like: {'path': {'depth': 0, 'query': ['/Plone/events/']}}
            # Or: {'path': {'depth': 0, 'query': '/Plone/events/'}}
            paths = paths.get('query', [])

        if isinstance(paths, six.string_types):
            paths = [paths]

        objs = []
        site = getSite()
        for path in list(paths):
            if six.PY2:
                path = path.encode('utf-8')  # paths must not be unicode
            try:
                site_path = '/'.join(site.getPhysicalPath())
                parts = path[len(site_path) + 1:].split('/')
                parent = site.unrestrictedTraverse('/'.join(parts[:-1]))
                objs.append(parent.restrictedTraverse(parts[-1]))
            except (KeyError, AttributeError, Unauthorized):
                # When no object is found don't raise an error
                pass

        if not objs:
            return False

        allow = True
        for ob in objs:
            allow = allow and\
                _checkPermission(AccessInactivePortalContent, ob)

        return allow

    @security.protected(SearchZCatalog)
    def searchResults(self, query=None, **kw):
        # Calls ZCatalog.searchResults with extra arguments that
        # limit the results to what the user is allowed to see.
        #
        # This version uses the 'effectiveRange' DateRangeIndex.
        #
        # It also accepts a keyword argument show_inactive to disable
        # effectiveRange checking entirely even for those without portal
        # wide AccessInactivePortalContent permission.

        # Make sure any pending index tasks have been processed
        processQueue()

        kw = kw.copy()
        show_inactive = kw.get('show_inactive', False)
        if isinstance(query, dict) and not show_inactive:
            show_inactive = 'show_inactive' in query

        user = _getAuthenticatedUser(self)
        kw['allowedRolesAndUsers'] = self._listAllowedRolesAndUsers(user)

        if not show_inactive and not self.allow_inactive(kw):
            kw['effectiveRange'] = DateTime()

        # filter out invalid sort_on indexes
        sort_on = kw.get('sort_on') or []
        if isinstance(sort_on, six.string_types):
            sort_on = [sort_on]
        valid_indexes = self.indexes()
        try:
            sort_on = [idx for idx in sort_on if idx in valid_indexes]
        except TypeError:
            # sort_on is not iterable
            sort_on = []
        if not sort_on:
            kw.pop('sort_on', None)
        else:
            kw['sort_on'] = sort_on

        return ZCatalog.searchResults(self, query, **kw)

    __call__ = searchResults

    def search(self, query,
               sort_index=None, reverse=0, limit=None, merge=1):
        # Wrap search() the same way that searchResults() is

        # Make sure any pending index tasks have been processed
        processQueue()

        user = _getAuthenticatedUser(self)
        query['allowedRolesAndUsers'] = self._listAllowedRolesAndUsers(user)

        if not self.allow_inactive(query):
            query['effectiveRange'] = DateTime()

        return super(CatalogTool, self).search(
            query, sort_index, reverse, limit, merge)

    @security.protected(ManageZCatalogEntries)
    def clearFindAndRebuild(self):
        # Empties catalog, then finds all contentish objects (i.e. objects
        # with an indexObject method), and reindexes them.
        # This may take a long time.
        idxs = list(self.indexes())

        def indexObject(obj, path):
            if (base_hasattr(obj, 'reindexObject') and
                    safe_callable(obj.reindexObject)):
                try:
                    self.reindexObject(obj, idxs=idxs)
                    # index conversions from plone.app.discussion
                    annotions = IAnnotations(obj)
                    if DISCUSSION_ANNOTATION_KEY in annotions:
                        conversation = annotions[DISCUSSION_ANNOTATION_KEY]
                        conversation = conversation.__of__(obj)
                        for comment in conversation.getComments():
                            try:
                                self.indexObject(comment, idxs=idxs)
                            except StopIteration:  # pragma: no cover
                                pass
                except TypeError:
                    # Catalogs have 'indexObject' as well, but they
                    # take different args, and will fail
                    pass
        self.manage_catalogClear()
        portal = aq_parent(aq_inner(self))
        portal.ZopeFindAndApply(
            portal,
            search_sub=True,
            apply_func=indexObject
        )

    @security.protected(ManageZCatalogEntries)
    def manage_catalogRebuild(self, RESPONSE=None, URL1=None):
        """Clears the catalog and indexes all objects with an 'indexObject'
        method. This may take a long time.
        """
        elapse = time.time()
        c_elapse = time.clock()

        self.clearFindAndRebuild()

        elapse = time.time() - elapse
        c_elapse = time.clock() - c_elapse

        msg = ('Catalog Rebuilt\n'
               'Total time: %s\n'
               'Total CPU time: %s' % (repr(elapse), repr(c_elapse)))
        logger.info(msg)

        if RESPONSE is not None:
            RESPONSE.redirect(
                URL1 + '/manage_catalogAdvanced?manage_tabs_message=' +
                urllib.parse.quote(msg))
Exemplo n.º 44
0
class FieldIndex(BaseIndexMixin, persistent.Persistent):
    """ Field indexing.

    Query types supported:

    - Eq

    - NotEq

    - Gt

    - Ge

    - Lt

    - Le

    - In

    - NotIn

    - Any

    - NotAny

    - InRange

    - NotInRange
    """

    def __init__(self, discriminator, family=None):
        if family is not None:
            self.family = family
        if not callable(discriminator):
            if not isinstance(discriminator, string_types):
                raise ValueError('discriminator value must be callable or a '
                                 'string')
        self.discriminator = discriminator
        self.reset()

    def reset(self):
        """Initialize forward and reverse mappings."""
        # The forward index maps indexed values to a sequence of docids
        self._fwd_index = self.family.OO.BTree()
        # The reverse index maps a docid to its index value
        self._rev_index = self.family.IO.BTree()
        self._num_docs = Length(0)
        self._not_indexed = self.family.IF.TreeSet()

    def not_indexed(self):
        return self._not_indexed

    def not_indexed_count(self):
        return len(self._not_indexed)

    def indexed(self):
        return self._rev_index.keys()

    def indexed_count(self):
        return self._num_docs()

    def word_count(self):
        """See interface IIndexStatistics"""
        return len(self._fwd_index)

    def document_repr(self, docid, default=None):
        result = self._rev_index.get(docid, default)
        if result is not default:
            return repr(result)
        return default

    def index_doc(self, docid, value):
        """See interface IIndexInjection"""
        value = self.discriminate(value, _marker)

        if value is _marker:
            if not (docid in self._not_indexed):
                # unindex the previous value
                self.unindex_doc(docid)
                # Store docid in set of unindexed docids
                self._not_indexed.add(docid)
            return None

        if docid in self._not_indexed:
            # Remove from set of unindexed docs if it was in there.
            self._not_indexed.remove(docid)
        
        rev_index = self._rev_index
        if docid in rev_index:
            if docid in self._fwd_index.get(value, ()):
                # no need to index the doc, its already up to date
                return
            # unindex doc if present
            self.unindex_doc(docid)

        # Insert into forward index.
        set = self._fwd_index.get(value)
        if set is None:
            set = self.family.IF.TreeSet()
            self._fwd_index[value] = set
            
        set.insert(docid)

        # increment doc count
        self._num_docs.change(1)

        # Insert into reverse index.
        rev_index[docid] = value

    def unindex_doc(self, docid):
        """See interface IIndexInjection.
        """
        _not_indexed = self._not_indexed
        if docid in _not_indexed:
            _not_indexed.remove(docid)

        rev_index = self._rev_index
        value = rev_index.get(docid, _marker)
        if value is _marker:
            return # not in index

        del rev_index[docid]

        try:
            set = self._fwd_index[value]
            set.remove(docid)
        except KeyError:    #pragma NO COVERAGE
            # This is fishy, but we don't want to raise an error.
            # We should probably log something.
            # but keep it from throwing a dirty exception
            set = 1

        if not set:
            del self._fwd_index[value]

        self._num_docs.change(-1)

    def reindex_doc(self, docid, value):
        """ See interface IIndexInjection """
        # the base index's index_doc method special-cases a reindex
        return self.index_doc(docid, value)

    def sort(
        self,
        docids,
        reverse=False,
        limit=None,
        sort_type=None,
        raise_unsortable=True,
        ):
        if limit is not None:
            limit = int(limit)
            if limit < 1:
                raise ValueError('limit must be 1 or greater')

        if not docids:
            return []

        numdocs = self._num_docs.value
        if not numdocs:
            return []

        if sort_type == interfaces.STABLE:
            sort_type = interfaces.TIMSORT

        elif sort_type == interfaces.OPTIMAL:
            sort_type = None

        if reverse:
            return self.sort_reverse(
                docids,
                limit,
                numdocs,
                sort_type,
                raise_unsortable,
                )
        else:
            return self.sort_forward(
                docids,
                limit,
                numdocs,
                sort_type,
                raise_unsortable,
                )

    def sort_forward(
        self,
        docids,
        limit,
        numdocs,
        sort_type=None,
        raise_unsortable=True,
        ):

        rlen = len(docids)

        # See http://www.zope.org/Members/Caseman/ZCatalog_for_2.6.1
        # for an overview of why we bother doing all this work to
        # choose the right sort algorithm.
        
        if sort_type is None:
            if fwscan_wins(limit, rlen, numdocs):
                # forward scan beats both n-best and timsort reliably
                # if this is true
                sort_type = interfaces.FWSCAN

            elif limit and nbest_ascending_wins(limit, rlen, numdocs):
                # nbest beats timsort reliably if this is true
                sort_type = interfaces.NBEST

            else:
                sort_type = interfaces.TIMSORT

        if sort_type == interfaces.FWSCAN:
            return self.scan_forward(docids, limit, raise_unsortable)
        elif sort_type == interfaces.NBEST:
            if limit is None:
                raise ValueError('nbest requires a limit')
            return self.nbest_ascending(docids, limit, raise_unsortable)
        elif sort_type == interfaces.TIMSORT:
            return self.timsort_ascending(docids, limit, raise_unsortable)
        else:
            raise ValueError('Unknown sort type %s' % sort_type)

    def sort_reverse(
        self,
        docids,
        limit,
        numdocs,
        sort_type=None,
        raise_unsortable=True,
        ):
        if sort_type is None:
            rlen = len(docids)
            if limit:
                if (limit < 300) or (limit/float(rlen) > 0.09):
                    sort_type = interfaces.NBEST
                else:
                    sort_type = interfaces.TIMSORT
            else:
                sort_type = interfaces.TIMSORT

        if sort_type == interfaces.NBEST:
            if limit is None:
                raise ValueError('nbest requires a limit')
            return self.nbest_descending(docids, limit, raise_unsortable)
        elif sort_type == interfaces.TIMSORT:
            return self.timsort_descending(docids, limit, raise_unsortable)
        else:
            raise ValueError('Unknown sort type %s' % sort_type)

    def scan_forward(self, docids, limit=None, raise_unsortable=True):
        fwd_index = self._fwd_index

        # make a copy so we don't mutate what we're passed.
        docids = self.family.IF.TreeSet(docids)

        n = 0
        for set in fwd_index.values():
            for docid in set:
                if docid in docids:
                    n+=1
                    docids.remove(docid)
                    yield docid
                    if limit and n >= limit:
                        raise StopIteration

        if raise_unsortable and docids:
            raise Unsortable(docids)

    def nbest_ascending(self, docids, limit, raise_unsortable=False):
        if limit is None: #pragma NO COVERAGE
            raise RuntimeError('n-best used without limit')

        # lifted from heapq.nsmallest

        h = nsort(docids, self._rev_index, ASC)
        it = iter(h)
        result = sorted(islice(it, 0, limit))
        if not result: #pragma NO COVERAGE
            raise StopIteration
        insort = bisect.insort
        pop = result.pop
        los = result[-1]    # los --> Largest of the nsmallest
        for elem in it:
            if los <= elem:
                continue
            insort(result, elem)
            pop()
            los = result[-1]

        missing_docids = []

        for value, docid in result:
            if value is ASC:
                missing_docids.append(docid)
            else:
                yield docid

        if raise_unsortable and missing_docids:
            raise Unsortable(missing_docids)

    def nbest_descending(self, docids, limit, raise_unsortable=True):
        if limit is None: #pragma NO COVERAGE
            raise RuntimeError('N-Best used without limit')
        iterable = nsort(docids, self._rev_index, DESC)
        missing_docids = []
        for value, docid in heapq.nlargest(limit, iterable):
            if value is DESC:
                missing_docids.append(docid)
            else:
                yield docid
        if raise_unsortable and missing_docids:
            raise Unsortable(missing_docids)

    def timsort_ascending(self, docids, limit, raise_unsortable=True):
        return self._timsort(
            docids,
            limit,
            reverse=False,
            raise_unsortable=raise_unsortable,
            )

    def timsort_descending(self, docids, limit, raise_unsortable=True):
        return self._timsort(
            docids,
            limit,
            reverse=True,
            raise_unsortable=raise_unsortable,
            )

    def _timsort(
        self,
        docids,
        limit=None,
        reverse=False,
        raise_unsortable=True,
        ):
        
        n = 0
        missing_docids = []

        def get(k, rev_index=self._rev_index):
            v = rev_index.get(k, ASC)
            if v is ASC:
                missing_docids.append(k)
            return v

        for docid in sorted(docids, key=get, reverse=reverse):
            if docid in missing_docids:
                # skip docids not in this index
                continue
            n += 1
            yield docid
            if limit and n >= limit:
                raise StopIteration

        if raise_unsortable and missing_docids:
            raise Unsortable(missing_docids)

    def search(self, queries, operator='or'):
        sets = []
        for q in queries:
            if isinstance(q, RangeValue):
                q = q.as_tuple()
            else:
                q = (q, q)
            set = self.family.IF.multiunion(self._fwd_index.values(*q))
            sets.append(set)

        result = None

        if len(sets) == 1:
            result = sets[0]
        elif operator == 'and':
            for _, set in sorted([(len(x), x) for x in sets]):
                result = self.family.IF.intersection(set, result)
        else:
            result = self.family.IF.multiunion(sets)

        return result

    def apply(self, q):
        if isinstance(q, dict):
            val = q['query']
            if isinstance(val, RangeValue):
                val = [val]
            elif not isinstance(val, (list, tuple)):
                val = [val]
            operator = q.get('operator', 'or')
            result = self.search(val, operator)
        else:
            if isinstance(q, tuple) and len(q) == 2:
                # b/w compat stupidity; this needs to die
                q = RangeValue(*q)
                q = [q]
            elif not isinstance(q, (list, tuple)):
                q = [q]
            result = self.search(q, 'or')

        return result

    def applyEq(self, value):
        return self.apply(value)

    def eq(self, value):
        return query.Eq(self, value)

    def applyNotEq(self, *args, **kw):
        return self._negate(self.applyEq, *args, **kw)

    def noteq(self, value):
        return query.NotEq(self, value)

    def applyGe(self, min_value):
        return self.applyInRange(min_value, None)

    def ge(self, value):
        return query.Ge(self, value)

    def applyLe(self, max_value):
        return self.applyInRange(None, max_value)

    def le(self, value):
        return query.Le(self, value)

    def applyGt(self, min_value):
        return self.applyInRange(min_value, None, excludemin=True)

    def gt(self, value):
        return query.Gt(self, value)

    def applyLt(self, max_value):
        return self.applyInRange(None, max_value, excludemax=True)

    def lt(self, value):
        return query.Lt(self, value)

    def applyAny(self, values):
        queries = list(values)
        return self.search(queries, operator='or')

    def any(self, value):
        return query.Any(self, value)

    def applyNotAny(self, *args, **kw):
        return self._negate(self.applyAny, *args, **kw)

    def notany(self, value):
        return query.NotAny(self, value)

    def applyInRange(self, start, end, excludemin=False, excludemax=False):
        return self.family.IF.multiunion(
            self._fwd_index.values(
                start, end, excludemin=excludemin, excludemax=excludemax)
        )


    def inrange(self, start, end, excludemin=False, excludemax=False):
        return query.InRange(self, start, end, excludemin, excludemax)

    def applyNotInRange(self, *args, **kw):
        return self._negate(self.applyInRange, *args, **kw)

    def notinrange(self, start, end, excludemin=False, excludemax=False):
        return query.NotInRange(self, start, end, excludemin, excludemax)
Exemplo n.º 45
0
class UUIDIndex(UnIndex):
    """Index for uuid fields with an unique value per key.

    The internal structure is:

    self._index = {datum:documentId]}
    self._unindex = {documentId:datum}

    For each datum only one documentId can exist.
    """

    meta_type = "UUIDIndex"

    manage_options = (
        {'label': 'Settings', 'action': 'manage_main'},
        {'label': 'Browse', 'action': 'manage_browse'},
    )

    query_options = ["query", "range"]

    manage = manage_main = DTMLFile('dtml/manageUUIDIndex', globals())
    manage_main._setName('manage_main')
    manage_browse = DTMLFile('../dtml/browseIndex', globals())

    def clear(self):
        self._length = Length()
        self._index = OIBTree()
        self._unindex = IOBTree()
        self._counter = Length()

    def numObjects(self):
        """Return the number of indexed objects. Since we have a 1:1 mapping
        from documents to values, we can reuse the stored length.
        """
        return self.indexSize()

    def uniqueValues(self, name=None, withLengths=0):
        """returns the unique values for name

        if withLengths is true, returns a sequence of
        tuples of (value, length)
        """
        if name is None:
            name = self.id
        elif name != self.id:
            raise StopIteration

        if not withLengths:
            for key in self._index.keys():
                yield key
        else:
            # We know the length for each value is one
            for key in self._index.keys():
                yield (key, 1)

    def insertForwardIndexEntry(self, entry, documentId):
        """Take the entry provided and put it in the correct place
        in the forward index.
        """
        if entry is None:
            return

        old_docid = self._index.get(entry, _marker)
        if old_docid is _marker:
            self._index[entry] = documentId
            self._length.change(1)
        elif old_docid != documentId:
            logger.error("A different document with value '%s' already "
                "exists in the index.'" % entry)

    def removeForwardIndexEntry(self, entry, documentId):
        """Take the entry provided and remove any reference to documentId
        in its entry in the index.
        """
        old_docid = self._index.get(entry, _marker)
        if old_docid is not _marker:
            del self._index[entry]
            self._length.change(-1)

    def _get_object_datum(self, obj, attr):
        # for a uuid it never makes sense to acquire a parent value via
        # Acquisition
        has_attr = getattr(aq_base(obj), attr, _marker)
        if has_attr is _marker:
            return _marker
        return super(UUIDIndex, self)._get_object_datum(obj, attr)
Exemplo n.º 46
0
class CachingCatalog(Catalog):
    implements(ICatalog)

    os = os  # for unit tests
    generation = None  # b/c

    def __init__(self):
        super(CachingCatalog, self).__init__()
        self.generation = Length(0)

    def clear(self):
        self.invalidate()
        super(CachingCatalog, self).clear()

    def index_doc(self, *arg, **kw):
        self.invalidate()
        super(CachingCatalog, self).index_doc(*arg, **kw)

    def unindex_doc(self, *arg, **kw):
        self.invalidate()
        super(CachingCatalog, self).unindex_doc(*arg, **kw)

    def reindex_doc(self, *arg, **kw):
        self.invalidate()
        super(CachingCatalog, self).reindex_doc(*arg, **kw)

    def __setitem__(self, *arg, **kw):
        self.invalidate()
        super(CachingCatalog, self).__setitem__(*arg, **kw)

    @MetricMod('CS.%s')
    @metricmethod
    def search(self, *arg, **kw):
        use_cache = True

        if 'use_cache' in kw:
            use_cache = kw.pop('use_cache')

        if 'NO_CATALOG_CACHE' in self.os.environ:
            use_cache = False

        if 'tags' in kw:
            # The tags index changes without invalidating the catalog,
            # so don't cache any query involving the tags index.
            use_cache = False

        if not use_cache:
            return self._search(*arg, **kw)

        cache = queryUtility(ICatalogSearchCache)

        if cache is None:
            return self._search(*arg, **kw)

        key = cPickle.dumps((arg, kw))

        generation = self.generation

        if generation is None:
            generation = Length(0)

        genval = generation.value

        if (genval == 0) or (genval > cache.generation):
            # an update in another process requires that the local cache be
            # invalidated
            cache.clear()
            cache.generation = genval

        if cache.get(key) is None:
            start = time.time()
            num, docids = self._search(*arg, **kw)

            # We don't cache large result sets because the time it takes to
            # unroll the result set turns out to be far more time than it
            # takes to run the search. In a particular instance using OSI's
            # catalog a search that took 0.015s but returned nearly 35,295
            # results took over 50s to unroll the result set for caching,
            # significantly slowing search performance.
            if num > LARGE_RESULT_SET:
                return num, docids

            # we need to unroll here; a btree-based structure may have
            # a reference to its connection
            docids = list(docids)
            cache[key] = (num, docids)

        return cache.get(key)

    @metricmethod
    def _search(self, *arg, **kw):
        start = time.time()
        res = super(CachingCatalog, self).search(*arg, **kw)
        duration = time.time() - start
        notify(CatalogQueryEvent(self, kw, duration, res))
        return res

    def invalidate(self):
        # Increment the generation; this tells *another process* that
        # its catalog cache needs to be cleared
        generation = self.generation

        if generation is None:
            generation = self.generation = Length(0)

        if generation.value >= sys.maxint:
            # don't keep growing the generation integer; wrap at sys.maxint
            self.generation.set(0)
        else:
            self.generation.change(1)

        # Clear the cache for *this process*
        cache = queryUtility(ICatalogSearchCache)
        if cache is not None:
            cache.clear()
            cache.generation = self.generation.value
Exemplo n.º 47
0
class BTreeFolder2Base (Persistent):
    """Base for BTree-based folders.
    """

    security = ClassSecurityInfo()

    manage_options=(
        ({'label':'Contents', 'action':'manage_main',},
         ) + Folder.manage_options[1:]
        )

    security.declareProtected(view_management_screens,
                              'manage_main')
    manage_main = DTMLFile('contents', globals())

    _tree = None      # OOBTree: { id -> object }
    _count = None     # A BTrees.Length
    _v_nextid = 0     # The integer component of the next generated ID
    _mt_index = None  # OOBTree: { meta_type -> OIBTree: { id -> 1 } }
    title = ''


    def __init__(self, id=None):
        if id is not None:
            self.id = id
        self._initBTrees()

    def _initBTrees(self):
        self._tree = OOBTree()
        self._count = Length()
        self._mt_index = OOBTree()


    def _populateFromFolder(self, source):
        """Fill this folder with the contents of another folder.
        """
        for name in source.objectIds():
            value = source._getOb(name, None)
            if value is not None:
                self._setOb(name, aq_base(value))


    security.declareProtected(view_management_screens, 'manage_fixCount')
    def manage_fixCount(self):
        """Calls self._fixCount() and reports the result as text.
        """
        old, new = self._fixCount()
        path = '/'.join(self.getPhysicalPath())
        if old == new:
            return "No count mismatch detected in BTreeFolder2 at %s." % path
        else:
            return ("Fixed count mismatch in BTreeFolder2 at %s. "
                    "Count was %d; corrected to %d" % (path, old, new))


    def _fixCount(self):
        """Checks if the value of self._count disagrees with
        len(self.objectIds()). If so, corrects self._count. Returns the
        old and new count values. If old==new, no correction was
        performed.
        """
        old = self._count()
        new = len(self.objectIds())
        if old != new:
            self._count.set(new)
        return old, new


    security.declareProtected(view_management_screens, 'manage_cleanup')
    def manage_cleanup(self):
        """Calls self._cleanup() and reports the result as text.
        """
        v = self._cleanup()
        path = '/'.join(self.getPhysicalPath())
        if v:
            return "No damage detected in BTreeFolder2 at %s." % path
        else:
            return ("Fixed BTreeFolder2 at %s.  "
                    "See the log for more details." % path)


    def _cleanup(self):
        """Cleans up errors in the BTrees.

        Certain ZODB bugs have caused BTrees to become slightly insane.
        Fortunately, there is a way to clean up damaged BTrees that
        always seems to work: make a new BTree containing the items()
        of the old one.

        Returns 1 if no damage was detected, or 0 if damage was
        detected and fixed.
        """
        from BTrees.check import check
        path = '/'.join(self.getPhysicalPath())
        try:
            check(self._tree)
            for key in self._tree.keys():
                if not self._tree.has_key(key):
                    raise AssertionError(
                        "Missing value for key: %s" % repr(key))
            check(self._mt_index)
            for key, value in self._mt_index.items():
                if (not self._mt_index.has_key(key)
                    or self._mt_index[key] is not value):
                    raise AssertionError(
                        "Missing or incorrect meta_type index: %s"
                        % repr(key))
                check(value)
                for k in value.keys():
                    if not value.has_key(k):
                        raise AssertionError(
                            "Missing values for meta_type index: %s"
                            % repr(key))
            return 1
        except AssertionError:
            LOG.warn( 'Detected damage to %s. Fixing now.' % path,
                exc_info=True)
            try:
                self._tree = OOBTree(self._tree)
                mt_index = OOBTree()
                for key, value in self._mt_index.items():
                    mt_index[key] = OIBTree(value)
                self._mt_index = mt_index
            except:
                LOG.error('Failed to fix %s.' % path,
                    exc_info=True)
                raise
            else:
                LOG.info('Fixed %s.' % path)
            return 0


    def _getOb(self, id, default=_marker):
        """Return the named object from the folder.
        """
        tree = self._tree
        if default is _marker:
            ob = tree[id]
            return ob.__of__(self)
        else:
            ob = tree.get(id, _marker)
            if ob is _marker:
                return default
            else:
                return ob.__of__(self)


    def _setOb(self, id, object):
        """Store the named object in the folder.
        """
        tree = self._tree
        if tree.has_key(id):
            raise KeyError('There is already an item named "%s".' % id)
        tree[id] = object
        self._count.change(1)
        # Update the meta type index.
        mti = self._mt_index
        meta_type = getattr(object, 'meta_type', None)
        if meta_type is not None:
            ids = mti.get(meta_type, None)
            if ids is None:
                ids = OIBTree()
                mti[meta_type] = ids
            ids[id] = 1


    def _delOb(self, id):
        """Remove the named object from the folder.
        """
        tree = self._tree
        meta_type = getattr(tree[id], 'meta_type', None)
        del tree[id]
        self._count.change(-1)
        # Update the meta type index.
        if meta_type is not None:
            mti = self._mt_index
            ids = mti.get(meta_type, None)
            if ids is not None and ids.has_key(id):
                del ids[id]
                if not ids:
                    # Removed the last object of this meta_type.
                    # Prune the index.
                    del mti[meta_type]


    security.declareProtected(view_management_screens, 'getBatchObjectListing')
    def getBatchObjectListing(self, REQUEST=None):
        """Return a structure for a page template to show the list of objects.
        """
        if REQUEST is None:
            REQUEST = {}
        pref_rows = int(REQUEST.get('dtpref_rows', 20))
        b_start = int(REQUEST.get('b_start', 1))
        b_count = int(REQUEST.get('b_count', 1000))
        b_end = b_start + b_count - 1
        url = self.absolute_url() + '/manage_main'
        idlist = self.objectIds()  # Pre-sorted.
        count = self.objectCount()

        if b_end < count:
            next_url = url + '?b_start=%d' % (b_start + b_count)
        else:
            b_end = count
            next_url = ''

        if b_start > 1:
            prev_url = url + '?b_start=%d' % max(b_start - b_count, 1)
        else:
            prev_url = ''

        formatted = []
        formatted.append(listtext0 % pref_rows)
        for i in range(b_start - 1, b_end):
            optID = escape(idlist[i])
            formatted.append(listtext1 % (escape(optID, quote=1), optID))
        formatted.append(listtext2)
        return {'b_start': b_start, 'b_end': b_end,
                'prev_batch_url': prev_url,
                'next_batch_url': next_url,
                'formatted_list': ''.join(formatted)}


    security.declareProtected(view_management_screens,
                              'manage_object_workspace')
    def manage_object_workspace(self, ids=(), REQUEST=None):
        '''Redirects to the workspace of the first object in
        the list.'''
        if ids and REQUEST is not None:
            REQUEST.RESPONSE.redirect(
                '%s/%s/manage_workspace' % (
                self.absolute_url(), quote(ids[0])))
        else:
            return self.manage_main(self, REQUEST)


    security.declareProtected(access_contents_information,
                              'tpValues')
    def tpValues(self):
        """Ensures the items don't show up in the left pane.
        """
        return ()


    security.declareProtected(access_contents_information,
                              'objectCount')
    def objectCount(self):
        """Returns the number of items in the folder."""
        return self._count()


    security.declareProtected(access_contents_information, 'has_key')
    def has_key(self, id):
        """Indicates whether the folder has an item by ID.
        """
        return self._tree.has_key(id)


    security.declareProtected(access_contents_information,
                              'objectIds')
    def objectIds(self, spec=None):
        # Returns a list of subobject ids of the current object.
        # If 'spec' is specified, returns objects whose meta_type
        # matches 'spec'.
        if spec is not None:
            if isinstance(spec, StringType):
                spec = [spec]
            mti = self._mt_index
            set = None
            for meta_type in spec:
                ids = mti.get(meta_type, None)
                if ids is not None:
                    set = union(set, ids)
            if set is None:
                return ()
            else:
                return set.keys()
        else:
            return self._tree.keys()


    security.declareProtected(access_contents_information,
                              'objectValues')
    def objectValues(self, spec=None):
        # Returns a list of actual subobjects of the current object.
        # If 'spec' is specified, returns only objects whose meta_type
        # match 'spec'.
        return LazyMap(self._getOb, self.objectIds(spec))


    security.declareProtected(access_contents_information,
                              'objectItems')
    def objectItems(self, spec=None):
        # Returns a list of (id, subobject) tuples of the current object.
        # If 'spec' is specified, returns only objects whose meta_type match
        # 'spec'
        return LazyMap(lambda id, _getOb=self._getOb: (id, _getOb(id)),
                       self.objectIds(spec))


    security.declareProtected(access_contents_information,
                              'objectMap')
    def objectMap(self):
        # Returns a tuple of mappings containing subobject meta-data.
        return LazyMap(lambda (k, v):
                       {'id': k, 'meta_type': getattr(v, 'meta_type', None)},
                       self._tree.items(), self._count())

    # superValues() looks for the _objects attribute, but the implementation
    # would be inefficient, so superValues() support is disabled.
    _objects = ()


    security.declareProtected(access_contents_information,
                              'objectIds_d')
    def objectIds_d(self, t=None):
        ids = self.objectIds(t)
        res = {}
        for id in ids:
            res[id] = 1
        return res


    security.declareProtected(access_contents_information,
                              'objectMap_d')
    def objectMap_d(self, t=None):
        return self.objectMap()


    def _checkId(self, id, allow_dup=0):
        if not allow_dup and self.has_key(id):
            raise BadRequestException, ('The id "%s" is invalid--'
                                        'it is already in use.' % id)


    def _setObject(self, id, object, roles=None, user=None, set_owner=1,
                   suppress_events=False):
        ob = object # better name, keep original function signature
        v = self._checkId(id)
        if v is not None:
            id = v

        # If an object by the given id already exists, remove it.
        if self.has_key(id):
            self._delObject(id)

        if not suppress_events:
            notify(ObjectWillBeAddedEvent(ob, self, id))

        self._setOb(id, ob)
        ob = self._getOb(id)

        if set_owner:
            # TODO: eventify manage_fixupOwnershipAfterAdd
            # This will be called for a copy/clone, or a normal _setObject.
            ob.manage_fixupOwnershipAfterAdd()

            # Try to give user the local role "Owner", but only if
            # no local roles have been set on the object yet.
            if getattr(ob, '__ac_local_roles__', _marker) is None:
                user = getSecurityManager().getUser()
                if user is not None:
                    userid = user.getId()
                    if userid is not None:
                        ob.manage_setLocalRoles(userid, ['Owner'])

        if not suppress_events:
            notify(ObjectAddedEvent(ob, self, id))
            notifyContainerModified(self)

        OFS.subscribers.compatibilityCall('manage_afterAdd', ob, ob, self)

        return id


    def _delObject(self, id, dp=1, suppress_events=False):
        ob = self._getOb(id)

        OFS.subscribers.compatibilityCall('manage_beforeDelete', ob, ob, self)

        if not suppress_events:
            notify(ObjectWillBeRemovedEvent(ob, self, id))

        self._delOb(id)

        if not suppress_events:
            notify(ObjectRemovedEvent(ob, self, id))
            notifyContainerModified(self)


    # Aliases for mapping-like access.
    __len__ = objectCount
    keys = objectIds
    values = objectValues
    items = objectItems

    # backward compatibility
    hasObject = has_key

    security.declareProtected(access_contents_information, 'get')
    def get(self, name, default=None):
        return self._getOb(name, default)


    # Utility for generating unique IDs.

    security.declareProtected(access_contents_information, 'generateId')
    def generateId(self, prefix='item', suffix='', rand_ceiling=999999999):
        """Returns an ID not used yet by this folder.

        The ID is unlikely to collide with other threads and clients.
        The IDs are sequential to optimize access to objects
        that are likely to have some relation.
        """
        tree = self._tree
        n = self._v_nextid
        attempt = 0
        while 1:
            if n % 4000 != 0 and n <= rand_ceiling:
                id = '%s%d%s' % (prefix, n, suffix)
                if not tree.has_key(id):
                    break
            n = randint(1, rand_ceiling)
            attempt = attempt + 1
            if attempt > MAX_UNIQUEID_ATTEMPTS:
                # Prevent denial of service
                raise ExhaustedUniqueIdsError
        self._v_nextid = n + 1
        return id

    def __getattr__(self, name):
        # Boo hoo hoo!  Zope 2 prefers implicit acquisition over traversal
        # to subitems, and __bobo_traverse__ hooks don't work with
        # restrictedTraverse() unless __getattr__() is also present.
        # Oh well.
        res = self._tree.get(name)
        if res is None:
            raise AttributeError, name
        return res
Exemplo n.º 48
0
class UnIndex(SimpleItem):
    """Simple forward and reverse index.
    """

    zmi_icon = 'fas fa-info-circle'
    _counter = None
    operators = ('or', 'and')
    useOperator = 'or'
    query_options = ()

    def __init__(self, id, ignore_ex=None, call_methods=None,
                 extra=None, caller=None):
        """Create an unindex

        UnIndexes are indexes that contain two index components, the
        forward index (like plain index objects) and an inverted
        index.  The inverted index is so that objects can be unindexed
        even when the old value of the object is not known.

        e.g.

        self._index = {datum:[documentId1, documentId2]}
        self._unindex = {documentId:datum}

        The arguments are:

          'id' -- the name of the item attribute to index.  This is
          either an attribute name or a record key.

          'ignore_ex' -- should be set to true if you want the index
          to ignore exceptions raised while indexing instead of
          propagating them.

          'call_methods' -- should be set to true if you want the index
          to call the attribute 'id' (note: 'id' should be callable!)
          You will also need to pass in an object in the index and
          uninded methods for this to work.

          'extra' -- a mapping object that keeps additional
          index-related parameters - subitem 'indexed_attrs'
          can be string with comma separated attribute names or
          a list

          'caller' -- reference to the calling object (usually
          a (Z)Catalog instance
        """

        def _get(o, k, default):
            """ return a value for a given key of a dict/record 'o' """
            if isinstance(o, dict):
                return o.get(k, default)
            else:
                return getattr(o, k, default)

        self.id = id
        self.ignore_ex = ignore_ex  # currently unimplemented
        self.call_methods = call_methods

        # allow index to index multiple attributes
        ia = _get(extra, 'indexed_attrs', id)
        if isinstance(ia, str):
            self.indexed_attrs = ia.split(',')
        else:
            self.indexed_attrs = list(ia)
        self.indexed_attrs = [
            attr.strip() for attr in self.indexed_attrs if attr]
        if not self.indexed_attrs:
            self.indexed_attrs = [id]

        self.clear()

    def __len__(self):
        return self._length()

    def getId(self):
        return self.id

    def clear(self):
        self._length = Length()
        self._index = OOBTree()
        self._unindex = IOBTree()

        if self._counter is None:
            self._counter = Length()
        else:
            self._increment_counter()

    def __nonzero__(self):
        return not not self._unindex

    def histogram(self):
        """Return a mapping which provides a histogram of the number of
        elements found at each point in the index.
        """
        histogram = {}
        for item in self._index.items():
            if isinstance(item, int):
                entry = 1  # "set" length is 1
            else:
                key, value = item
                entry = len(value)
            histogram[entry] = histogram.get(entry, 0) + 1
        return histogram

    def referencedObjects(self):
        """Generate a list of IDs for which we have referenced objects."""
        return self._unindex.keys()

    def getEntryForObject(self, documentId, default=_marker):
        """Takes a document ID and returns all the information we have
        on that specific object.
        """
        if default is _marker:
            return self._unindex.get(documentId)
        return self._unindex.get(documentId, default)

    def removeForwardIndexEntry(self, entry, documentId):
        """Take the entry provided and remove any reference to documentId
        in its entry in the index.
        """
        indexRow = self._index.get(entry, _marker)
        if indexRow is not _marker:
            try:
                indexRow.remove(documentId)
                if not indexRow:
                    del self._index[entry]
                    self._length.change(-1)
            except ConflictError:
                raise
            except AttributeError:
                # index row is an int
                try:
                    del self._index[entry]
                except KeyError:
                    # swallow KeyError because it was probably
                    # removed and then _length AttributeError raised
                    pass
                if isinstance(self.__len__, Length):
                    self._length = self.__len__
                    del self.__len__
                self._length.change(-1)
            except Exception:
                LOG.error('%(context)s: unindex_object could not remove '
                          'documentId %(doc_id)s from index %(index)r.  This '
                          'should not happen.', dict(
                              context=self.__class__.__name__,
                              doc_id=documentId,
                              index=self.id),
                          exc_info=sys.exc_info())
        else:
            LOG.error('%(context)s: unindex_object tried to '
                      'retrieve set %(entry)r from index %(index)r '
                      'but couldn\'t.  This should not happen.', dict(
                          context=self.__class__.__name__,
                          entry=entry,
                          index=self.id))

    def insertForwardIndexEntry(self, entry, documentId):
        """Take the entry provided and put it in the correct place
        in the forward index.

        This will also deal with creating the entire row if necessary.
        """
        indexRow = self._index.get(entry, _marker)

        # Make sure there's actually a row there already. If not, create
        # a set and stuff it in first.
        if indexRow is _marker:
            # We always use a set to avoid getting conflict errors on
            # multiple threads adding a new row at the same time
            self._index[entry] = IITreeSet((documentId, ))
            self._length.change(1)
        else:
            try:
                indexRow.insert(documentId)
            except AttributeError:
                # Inline migration: index row with one element was an int at
                # first (before Zope 2.13).
                indexRow = IITreeSet((indexRow, documentId))
                self._index[entry] = indexRow

    def index_object(self, documentId, obj, threshold=None):
        """ wrapper to handle indexing of multiple attributes """

        fields = self.getIndexSourceNames()
        res = 0
        for attr in fields:
            res += self._index_object(documentId, obj, threshold, attr)

        if res > 0:
            self._increment_counter()

        return res > 0

    def _index_object(self, documentId, obj, threshold=None, attr=''):
        """ index and object 'obj' with integer id 'documentId'"""
        returnStatus = 0

        # First we need to see if there's anything interesting to look at
        datum = self._get_object_datum(obj, attr)
        if datum is None:
            # Prevent None from being indexed. None doesn't have a valid
            # ordering definition compared to any other object.
            # BTrees 4.0+ will throw a TypeError
            # "object has default comparison" and won't let it be indexed.
            return 0

        datum = self._convert(datum, default=_marker)

        # We don't want to do anything that we don't have to here, so we'll
        # check to see if the new and existing information is the same.
        oldDatum = self._unindex.get(documentId, _marker)
        if datum != oldDatum:
            if oldDatum is not _marker:
                self.removeForwardIndexEntry(oldDatum, documentId)
                if datum is _marker:
                    try:
                        del self._unindex[documentId]
                    except ConflictError:
                        raise
                    except Exception:
                        LOG.error('%(context)s: oldDatum was there, '
                                  'now it\'s not for documentId %(doc_id)s '
                                  'from index %(index)r.  This '
                                  'should not happen.', dict(
                                      context=self.__class__.__name__,
                                      doc_id=documentId,
                                      index=self.id),
                                  exc_info=sys.exc_info())

            if datum is not _marker:
                self.insertForwardIndexEntry(datum, documentId)
                self._unindex[documentId] = datum

            returnStatus = 1

        return returnStatus

    def _get_object_datum(self, obj, attr):
        # self.id is the name of the index, which is also the name of the
        # attribute we're interested in.  If the attribute is callable,
        # we'll do so.
        try:
            datum = getattr(obj, attr)
            if safe_callable(datum):
                datum = datum()
        except (AttributeError, TypeError):
            datum = _marker
        return datum

    def _increment_counter(self):
        if self._counter is None:
            self._counter = Length()
        self._counter.change(1)

    def getCounter(self):
        """Return a counter which is increased on index changes"""
        return self._counter is not None and self._counter() or 0

    def numObjects(self):
        """Return the number of indexed objects."""
        return len(self._unindex)

    def indexSize(self):
        """Return the size of the index in terms of distinct values."""
        return len(self)

    def unindex_object(self, documentId):
        """ Unindex the object with integer id 'documentId' and don't
        raise an exception if we fail
        """
        unindexRecord = self._unindex.get(documentId, _marker)
        if unindexRecord is _marker:
            return None

        self._increment_counter()

        self.removeForwardIndexEntry(unindexRecord, documentId)
        try:
            del self._unindex[documentId]
        except ConflictError:
            raise
        except Exception:
            LOG.debug('%(context)s: attempt to unindex nonexistent '
                      'documentId %(doc_id)s from index %(index)r. This '
                      'should not happen.', dict(
                          context=self.__class__.__name__,
                          doc_id=documentId,
                          index=self.id),
                      exc_info=True)

    def _apply_not(self, not_parm, resultset=None):
        index = self._index
        setlist = []
        for k in not_parm:
            s = index.get(k, None)
            if s is None:
                continue
            elif isinstance(s, int):
                s = IISet((s, ))
            setlist.append(s)
        return multiunion(setlist)

    def _convert(self, value, default=None):
        return value

    def getRequestCache(self):
        """returns dict for caching per request for interim results
        of an index search. Returns 'None' if no REQUEST attribute
        is available"""

        cache = None
        REQUEST = aq_get(self, 'REQUEST', None)
        if REQUEST is not None:
            catalog = aq_parent(aq_parent(aq_inner(self)))
            if catalog is not None:
                # unique catalog identifier
                key = '_catalogcache_{0}_{1}'.format(
                    catalog.getId(), id(catalog))
                cache = REQUEST.get(key, None)
                if cache is None:
                    cache = REQUEST[key] = RequestCache()

        return cache

    def getRequestCacheKey(self, record, resultset=None):
        """returns an unique key of a search record"""
        params = []

        # record operator (or, and)
        params.append(('operator', record.operator))

        # not / exclude operator
        not_value = record.get('not', None)
        if not_value is not None:
            not_value = frozenset(not_value)
            params.append(('not', not_value))

        # record options
        for op in ['range', 'usage']:
            op_value = record.get(op, None)
            if op_value is not None:
                params.append((op, op_value))

        # record keys
        rec_keys = frozenset(record.keys)
        params.append(('keys', rec_keys))

        # build record identifier
        rid = frozenset(params)

        # unique index identifier
        iid = '_{0}_{1}_{2}'.format(self.__class__.__name__,
                                    self.id, self.getCounter())
        return (iid, rid)

    def _apply_index(self, request, resultset=None):
        """Apply the index to query parameters given in the request arg.

        If the query does not match the index, return None, otherwise
        return a tuple of (result, used_attributes), where used_attributes
        is again a tuple with the names of all used data fields.

        If not `None`, the resultset argument
        indicates that the search result is relevant only on this set,
        i.e. everything outside resultset is of no importance.
        The index can use this information for optimizations.
        """
        record = IndexQuery(request, self.id, self.query_options,
                            self.operators, self.useOperator)
        if record.keys is None:
            return None
        return (self.query_index(record, resultset=resultset), (self.id, ))

    def query_index(self, record, resultset=None):
        """Search the index with the given IndexQuery object.

        If not `None`, the resultset argument
        indicates that the search result is relevant only on this set,
        i.e. everything outside resultset is of no importance.
        The index can use this information for optimizations.
        """
        index = self._index
        r = None
        opr = None

        # not / exclude parameter
        not_parm = record.get('not', None)

        operator = record.operator

        cachekey = None
        cache = self.getRequestCache()
        if cache is not None:
            cachekey = self.getRequestCacheKey(record)
            if cachekey is not None:
                cached = None
                if operator == 'or':
                    cached = cache.get(cachekey, None)
                else:
                    cached_setlist = cache.get(cachekey, None)
                    if cached_setlist is not None:
                        r = resultset
                        for s in cached_setlist:
                            # the result is bound by the resultset
                            r = intersection(r, s)
                            # If intersection, we can't possibly get a
                            # smaller result
                            if not r:
                                break
                        cached = r

                if cached is not None:
                    if isinstance(cached, int):
                        cached = IISet((cached, ))

                    if not_parm:
                        not_parm = list(map(self._convert, not_parm))
                        exclude = self._apply_not(not_parm, resultset)
                        cached = difference(cached, exclude)

                    return cached

        if not record.keys and not_parm:
            # convert into indexed format
            not_parm = list(map(self._convert, not_parm))
            # we have only a 'not' query
            record.keys = [k for k in index.keys() if k not in not_parm]
        else:
            # convert query arguments into indexed format
            record.keys = list(map(self._convert, record.keys))

        # Range parameter
        range_parm = record.get('range', None)
        if range_parm:
            opr = 'range'
            opr_args = []
            if range_parm.find('min') > -1:
                opr_args.append('min')
            if range_parm.find('max') > -1:
                opr_args.append('max')

        if record.get('usage', None):
            # see if any usage params are sent to field
            opr = record.usage.lower().split(':')
            opr, opr_args = opr[0], opr[1:]

        if opr == 'range':  # range search
            if 'min' in opr_args:
                lo = min(record.keys)
            else:
                lo = None
            if 'max' in opr_args:
                hi = max(record.keys)
            else:
                hi = None
            if hi:
                setlist = index.values(lo, hi)
            else:
                setlist = index.values(lo)

            # If we only use one key, intersect and return immediately
            if len(setlist) == 1:
                result = setlist[0]
                if isinstance(result, int):
                    result = IISet((result,))

                if cachekey is not None:
                    if operator == 'or':
                        cache[cachekey] = result
                    else:
                        cache[cachekey] = [result]

                if not_parm:
                    exclude = self._apply_not(not_parm, resultset)
                    result = difference(result, exclude)
                return result

            if operator == 'or':
                tmp = []
                for s in setlist:
                    if isinstance(s, int):
                        s = IISet((s,))
                    tmp.append(s)
                r = multiunion(tmp)

                if cachekey is not None:
                    cache[cachekey] = r
            else:
                # For intersection, sort with smallest data set first
                tmp = []
                for s in setlist:
                    if isinstance(s, int):
                        s = IISet((s,))
                    tmp.append(s)
                if len(tmp) > 2:
                    setlist = sorted(tmp, key=len)
                else:
                    setlist = tmp

                # 'r' is not invariant of resultset. Thus, we
                # have to remember 'setlist'
                if cachekey is not None:
                    cache[cachekey] = setlist

                r = resultset
                for s in setlist:
                    # the result is bound by the resultset
                    r = intersection(r, s)
                    # If intersection, we can't possibly get a smaller result
                    if not r:
                        break

        else:  # not a range search
            # Filter duplicates
            setlist = []
            for k in record.keys:
                if k is None:
                    # Prevent None from being looked up. None doesn't
                    # have a valid ordering definition compared to any
                    # other object. BTrees 4.0+ will throw a TypeError
                    # "object has default comparison".
                    continue
                try:
                    s = index.get(k, None)
                except TypeError:
                    # key is not valid for this Btree so the value is None
                    LOG.error(
                        '%(context)s: query_index tried '
                        'to look up key %(key)r from index %(index)r '
                        'but key was of the wrong type.', dict(
                            context=self.__class__.__name__,
                            key=k,
                            index=self.id,
                        )
                    )
                    s = None
                # If None, try to bail early
                if s is None:
                    if operator == 'or':
                        # If union, we can possibly get a bigger result
                        continue
                    # If intersection, we can't possibly get a smaller result
                    if cachekey is not None:
                        # If operator is 'and', we have to cache a list of
                        # IISet objects
                        cache[cachekey] = [IISet()]
                    return IISet()
                elif isinstance(s, int):
                    s = IISet((s,))
                setlist.append(s)

            # If we only use one key return immediately
            if len(setlist) == 1:
                result = setlist[0]
                if isinstance(result, int):
                    result = IISet((result,))

                if cachekey is not None:
                    if operator == 'or':
                        cache[cachekey] = result
                    else:
                        cache[cachekey] = [result]

                if not_parm:
                    exclude = self._apply_not(not_parm, resultset)
                    result = difference(result, exclude)
                return result

            if operator == 'or':
                # If we already get a small result set passed in, intersecting
                # the various indexes with it and doing the union later is
                # faster than creating a multiunion first.

                if resultset is not None and len(resultset) < 200:
                    smalllist = []
                    for s in setlist:
                        smalllist.append(intersection(resultset, s))
                    r = multiunion(smalllist)

                    # 'r' is not invariant of resultset.  Thus, we
                    # have to remember the union of 'setlist'. But
                    # this is maybe a performance killer. So we do not cache.
                    # if cachekey is not None:
                    #    cache[cachekey] = multiunion(setlist)

                else:
                    r = multiunion(setlist)
                    if cachekey is not None:
                        cache[cachekey] = r
            else:
                # For intersection, sort with smallest data set first
                if len(setlist) > 2:
                    setlist = sorted(setlist, key=len)

                # 'r' is not invariant of resultset. Thus, we
                # have to remember the union of 'setlist'
                if cachekey is not None:
                    cache[cachekey] = setlist

                r = resultset
                for s in setlist:
                    r = intersection(r, s)
                    # If intersection, we can't possibly get a smaller result
                    if not r:
                        break

        if isinstance(r, int):
            r = IISet((r, ))
        if r is None:
            return IISet()
        if not_parm:
            exclude = self._apply_not(not_parm, resultset)
            r = difference(r, exclude)
        return r

    def hasUniqueValuesFor(self, name):
        """has unique values for column name"""
        if name == self.id:
            return 1
        return 0

    def getIndexSourceNames(self):
        """Return sequence of indexed attributes."""
        return getattr(self, 'indexed_attrs', [self.id])

    def getIndexQueryNames(self):
        """Indicate that this index applies to queries for the index's name."""
        return (self.id,)

    def uniqueValues(self, name=None, withLengths=0):
        """returns the unique values for name

        if withLengths is true, returns a sequence of
        tuples of (value, length)
        """
        if name is None:
            name = self.id
        elif name != self.id:
            return

        if not withLengths:
            for key in self._index.keys():
                yield key
        else:
            for key, value in self._index.items():
                if isinstance(value, int):
                    yield (key, 1)
                else:
                    yield (key, len(value))

    def keyForDocument(self, id):
        # This method is superseded by documentToKeyMap
        return self._unindex[id]

    def documentToKeyMap(self):
        return self._unindex

    def items(self):
        items = []
        for k, v in self._index.items():
            if isinstance(v, int):
                v = IISet((v,))
            items.append((k, v))
        return items
Exemplo n.º 49
0
class HypatiaDateRecurringIndex(KeywordIndex, FieldIndex):
    def discriminate(self, obj, default):
        """ See interface IIndexInjection """
        if callable(self.discriminator):
            value = self.discriminator(obj, _marker)
        else:
            value = getattr(obj, self.discriminator, _marker)

        if value is _marker:
            return default

        if isinstance(value, Persistent):
            raise ValueError('Catalog cannot index persistent object %s' %
                             value)

        if isinstance(value, Broken):
            raise ValueError('Catalog cannot index broken object %s' % value)

        if not isinstance(value, dict):
            raise ValueError(
                'Catalog can only index dict with '
                'attr and date keys, or date and recurdef keys, given %s' %
                value)
        # examples:
        # {'attr': 'dates',
        #  'date': datetime.datetime.now()}
        # will get dates_recurrence attribute on the obj to get iCal string
        # for recurrence definition
        # or
        # {'date': datetime.datetime.now(),
        #  'recurdef': ICALSTRING}
        # no access to obj attributes at all

        date = value.get('date')
        default_recurdef = value.get('recurdef', _marker)
        if default_recurdef is not _marker:
            recurdef = default_recurdef
        else:
            attr_recurdef = value.get('attr') + '_recurrence'
            recurdef = getattr(obj, attr_recurdef, None)

        if callable(recurdef):
            recurdef = recurdef()

        if not recurdef:
            dates = [date]
        else:
            dates = recurrence_sequence_ical(date, recrule=recurdef)

        # dates is a generator
        return tuple(dates)

    def normalize(self, dates):
        return [dt2int(date) for date in dates]

# below is the same implementation as Keyword Index, but replacing
# self._fwd_index = self.family.OO.BTree() by self._fwd_index = self.family.IO.BTree()
# family.OO.Set by family.II.Set
# family.OO.difference by family.II.difference

    def reset(self):
        """Initialize forward and reverse mappings."""
        # The forward index maps index keywords to a sequence of docids
        self._fwd_index = self.family.IO.BTree()

        # The reverse index maps a docid to its keywords
        self._rev_index = self.family.IO.BTree()
        self._num_docs = Length(0)
        self._not_indexed = self.family.IF.TreeSet()

    def index_doc(self, docid, obj):
        seq = self.discriminate(obj, _marker)

        if seq is _marker:
            if not (docid in self._not_indexed):
                # unindex the previous value
                self.unindex_doc(docid)
                # Store docid in set of unindexed docids
                self._not_indexed.add(docid)
            return None

        if docid in self._not_indexed:
            # Remove from set of unindexed docs if it was in there.
            self._not_indexed.remove(docid)

        if isinstance(seq, string_types):
            raise TypeError('seq argument must be a list/tuple of strings')

        old_kw = self._rev_index.get(docid, None)
        if not seq:
            if old_kw:
                self.unindex_doc(docid)
            return

        seq = self.normalize(seq)

        new_kw = self.family.II.Set(seq)

        if old_kw is None:
            self._insert_forward(docid, new_kw)
            self._insert_reverse(docid, new_kw)
            self._num_docs.change(1)
        else:
            # determine added and removed keywords
            kw_added = self.family.II.difference(new_kw, old_kw)
            kw_removed = self.family.II.difference(old_kw, new_kw)

            if not (kw_added or kw_removed):
                return

            # removed keywords are removed from the forward index
            for word in kw_removed:
                fwd = self._fwd_index[word]
                fwd.remove(docid)
                if not fwd:
                    del self._fwd_index[word]

            # now update reverse and forward indexes
            self._insert_forward(docid, kw_added)
            self._insert_reverse(docid, new_kw)

    def applyInRange(self, start, end, excludemin=False, excludemax=False):
        if start is not None:
            start = dt2int(start)

        if end is not None:
            end = dt2int(end)

        return self.family.IF.multiunion(
            self._fwd_index.values(start,
                                   end,
                                   excludemin=excludemin,
                                   excludemax=excludemax))

    def document_repr(self, docid, default=None):
        result = self._rev_index.get(docid, default)
        if result is not default:
            return ', '.join([int2dt(r).isoformat() for r in result])


#            return repr(result)
        return default

    def inrange_with_not_indexed(self,
                                 start,
                                 end,
                                 excludemin=False,
                                 excludemax=False):
        return InRangeWithNotIndexed(self, start, end, excludemin, excludemax)
Exemplo n.º 50
0
class CatalogTool(PloneBaseTool, BaseTool):
    """Plone's catalog tool"""

    implements(IPloneCatalogTool)

    meta_type = 'Plone Catalog Tool'
    security = ClassSecurityInfo()
    toolicon = 'skins/plone_images/book_icon.png'
    _counter = None

    manage_catalogAdvanced = DTMLFile('www/catalogAdvanced', globals())

    manage_options = (
        {'action': 'manage_main', 'label': 'Contents'},
        {'action': 'manage_catalogView', 'label': 'Catalog'},
        {'action': 'manage_catalogIndexes', 'label': 'Indexes'},
        {'action': 'manage_catalogSchema', 'label': 'Metadata'},
        {'action': 'manage_catalogAdvanced', 'label': 'Advanced'},
        {'action': 'manage_catalogReport', 'label': 'Query Report'},
        {'action': 'manage_catalogPlan', 'label': 'Query Plan'},
        {'action': 'manage_propertiesForm', 'label': 'Properties'},
    )

    def __init__(self):
        ZCatalog.__init__(self, self.getId())

    def _removeIndex(self, index):
        """Safe removal of an index.
        """
        try:
            self.manage_delIndex(index)
        except:
            pass

    def _listAllowedRolesAndUsers(self, user):
        """Makes sure the list includes the user's groups.
        """
        result = user.getRoles()
        if 'Anonymous' in result:
            # The anonymous user has no further roles
            return ['Anonymous']
        result = list(result)
        if hasattr(aq_base(user), 'getGroups'):
            # remove the AuthenticatedUsers group, the Authenticated role is
            # already included in the user.getRoles() list
            groups = ['user:%s' % x for x in user.getGroups() if
                x != 'AuthenticatedUsers']
            if groups:
                result = result + groups
        result.append('Anonymous')
        result.append('user:%s' % user.getId())
        return result

    security.declarePrivate('indexObject')
    def indexObject(self, object, idxs=[]):
        """Add object to catalog.

        The optional idxs argument is a list of specific indexes
        to populate (all of them by default).
        """
        self.reindexObject(object, idxs)

    security.declareProtected(ManageZCatalogEntries, 'catalog_object')
    def catalog_object(self, object, uid=None, idxs=[],
                       update_metadata=1, pghandler=None):
        self._increment_counter()

        w = object
        if not IIndexableObject.providedBy(object):
            # This is the CMF 2.2 compatible approach, which should be used
            # going forward
            wrapper = queryMultiAdapter((object, self), IIndexableObject)
            if wrapper is not None:
                w = wrapper

        ZCatalog.catalog_object(self, w, uid, idxs,
                                update_metadata, pghandler=pghandler)

    security.declareProtected(ManageZCatalogEntries, 'catalog_object')
    def uncatalog_object(self, *args, **kwargs):
        self._increment_counter()
        return BaseTool.uncatalog_object(self, *args, **kwargs)

    def _increment_counter(self):
        if self._counter is None:
            self._counter = Length()
        self._counter.change(1)

    security.declarePrivate('getCounter')
    def getCounter(self):
        return self._counter is not None and self._counter() or 0

    security.declareProtected(SearchZCatalog, 'searchResults')
    def searchResults(self, REQUEST=None, **kw):
        """Calls ZCatalog.searchResults with extra arguments that
        limit the results to what the user is allowed to see.

        This version uses the 'effectiveRange' DateRangeIndex.

        It also accepts a keyword argument show_inactive to disable
        effectiveRange checking entirely even for those without portal
        wide AccessInactivePortalContent permission.
        """
        kw = kw.copy()
        show_inactive = kw.get('show_inactive', False)
        if isinstance(REQUEST, dict) and not show_inactive:
            show_inactive = 'show_inactive' in REQUEST

        user = _getAuthenticatedUser(self)
        kw['allowedRolesAndUsers'] = self._listAllowedRolesAndUsers(user)

        if not show_inactive and not _checkPermission(
            AccessInactivePortalContent, self):

            kw['effectiveRange'] = DateTime()

        return ZCatalog.searchResults(self, REQUEST, **kw)

    __call__ = searchResults

    security.declareProtected(ManageZCatalogEntries, 'clearFindAndRebuild')
    def clearFindAndRebuild(self):
        """Empties catalog, then finds all contentish objects (i.e. objects
           with an indexObject method), and reindexes them.
           This may take a long time.
        """
        def indexObject(obj, path):
            if (base_hasattr(obj, 'indexObject') and
                safe_callable(obj.indexObject)):
                try:
                    obj.indexObject()
                except TypeError:
                    # Catalogs have 'indexObject' as well, but they
                    # take different args, and will fail
                    pass
        self.manage_catalogClear()
        portal = aq_parent(aq_inner(self))
        portal.ZopeFindAndApply(portal, search_sub=True,
            apply_func=indexObject)

    security.declareProtected(ManageZCatalogEntries, 'manage_catalogRebuild')
    def manage_catalogRebuild(self, RESPONSE=None, URL1=None):
        """Clears the catalog and indexes all objects with an 'indexObject'
        method. This may take a long time.
        """
        elapse = time.time()
        c_elapse = time.clock()

        self.clearFindAndRebuild()

        elapse = time.time() - elapse
        c_elapse = time.clock() - c_elapse

        if RESPONSE is not None:
            RESPONSE.redirect(
              URL1 + '/manage_catalogAdvanced?manage_tabs_message=' +
              urllib.quote('Catalog Rebuilt\n'
                           'Total time: %s\n'
                           'Total CPU time: %s' % (`elapse`, `c_elapse`)))
Exemplo n.º 51
0
class CatalogTool(PloneBaseTool, BaseTool):
    """Plone's catalog tool"""

    meta_type = 'Plone Catalog Tool'
    security = ClassSecurityInfo()
    toolicon = 'skins/plone_images/book_icon.png'
    _counter = None

    manage_catalogAdvanced = DTMLFile('www/catalogAdvanced', globals())

    manage_options = (
        {'action': 'manage_main', 'label': 'Contents'},
        {'action': 'manage_catalogView', 'label': 'Catalog'},
        {'action': 'manage_catalogIndexes', 'label': 'Indexes'},
        {'action': 'manage_catalogSchema', 'label': 'Metadata'},
        {'action': 'manage_catalogAdvanced', 'label': 'Advanced'},
        {'action': 'manage_catalogReport', 'label': 'Query Report'},
        {'action': 'manage_catalogPlan', 'label': 'Query Plan'},
        {'action': 'manage_propertiesForm', 'label': 'Properties'},
    )

    def __init__(self):
        ZCatalog.__init__(self, self.getId())

    def _removeIndex(self, index):
        # Safe removal of an index.
        try:
            self.manage_delIndex(index)
        except:
            pass

    def _listAllowedRolesAndUsers(self, user):
        # Makes sure the list includes the user's groups.
        result = user.getRoles()
        if 'Anonymous' in result:
            # The anonymous user has no further roles
            return ['Anonymous']
        result = list(result)
        if hasattr(aq_base(user), 'getGroups'):
            groups = ['user:%s' % x for x in user.getGroups()]
            if groups:
                result = result + groups
        # Order the arguments from small to large sets
        result.insert(0, 'user:%s' % user.getId())
        result.append('Anonymous')
        return result

    @security.private
    def indexObject(self, object, idxs=None):
        # Add object to catalog.
        # The optional idxs argument is a list of specific indexes
        # to populate (all of them by default).
        if idxs is None:
            idxs = []
        self.reindexObject(object, idxs)

    @security.protected(ManageZCatalogEntries)
    def catalog_object(self, object, uid=None, idxs=None,
                       update_metadata=1, pghandler=None):
        if idxs is None:
            idxs = []
        self._increment_counter()

        w = object
        if not IIndexableObject.providedBy(object):
            # This is the CMF 2.2 compatible approach, which should be used
            # going forward
            wrapper = queryMultiAdapter((object, self), IIndexableObject)
            if wrapper is not None:
                w = wrapper

        ZCatalog.catalog_object(self, w, uid, idxs,
                                update_metadata, pghandler=pghandler)

    @security.protected(ManageZCatalogEntries)
    def uncatalog_object(self, *args, **kwargs):
        self._increment_counter()
        return BaseTool.uncatalog_object(self, *args, **kwargs)

    def _increment_counter(self):
        if self._counter is None:
            self._counter = Length()
        self._counter.change(1)

    @security.private
    def getCounter(self):
        return self._counter is not None and self._counter() or 0

    @security.protected(SearchZCatalog)
    def searchResults(self, REQUEST=None, **kw):
        # Calls ZCatalog.searchResults with extra arguments that
        # limit the results to what the user is allowed to see.
        #
        # This version uses the 'effectiveRange' DateRangeIndex.
        #
        # It also accepts a keyword argument show_inactive to disable
        # effectiveRange checking entirely even for those without portal
        # wide AccessInactivePortalContent permission.

        kw = kw.copy()
        show_inactive = kw.get('show_inactive', False)
        if isinstance(REQUEST, dict) and not show_inactive:
            show_inactive = 'show_inactive' in REQUEST

        user = _getAuthenticatedUser(self)
        kw['allowedRolesAndUsers'] = self._listAllowedRolesAndUsers(user)

        if not show_inactive \
           and not _checkPermission(AccessInactivePortalContent, self):
            kw['effectiveRange'] = DateTime()

        return ZCatalog.searchResults(self, REQUEST, **kw)

    __call__ = searchResults

    def search(self, *args, **kw):
        # Wrap search() the same way that searchResults() is
        query = {}

        if args:
            query = args[0]
        elif 'query_request' in kw:
            query = kw.get('query_request')

        kw['query_request'] = query.copy()

        user = _getAuthenticatedUser(self)
        query['allowedRolesAndUsers'] = self._listAllowedRolesAndUsers(user)

        if not _checkPermission(AccessInactivePortalContent, self):
            query['effectiveRange'] = DateTime()

        kw['query_request'] = query

        return super(CatalogTool, self).search(**kw)

    @security.protected(ManageZCatalogEntries)
    def clearFindAndRebuild(self):
        # Empties catalog, then finds all contentish objects (i.e. objects
        # with an indexObject method), and reindexes them.
        # This may take a long time.

        def indexObject(obj, path):
            if (base_hasattr(obj, 'indexObject') and
                    safe_callable(obj.indexObject)):
                try:
                    obj.indexObject()

                    # index conversions from plone.app.discussion
                    annotions = IAnnotations(obj)
                    catalog = getToolByName(obj, "portal_catalog")
                    if DISCUSSION_ANNOTATION_KEY in annotions:
                        conversation = annotions[DISCUSSION_ANNOTATION_KEY]
                        conversation = conversation.__of__(obj)
                        for comment in conversation.getComments():
                            try:
                                if catalog:
                                    catalog.indexObject(comment)
                            except StopIteration:  # pragma: no cover
                                pass


                except TypeError:
                    # Catalogs have 'indexObject' as well, but they
                    # take different args, and will fail
                    pass
        self.manage_catalogClear()
        portal = aq_parent(aq_inner(self))
        portal.ZopeFindAndApply(
            portal,
            search_sub=True,
            apply_func=indexObject
        )

    @security.protected(ManageZCatalogEntries)
    def manage_catalogRebuild(self, RESPONSE=None, URL1=None):
        """Clears the catalog and indexes all objects with an 'indexObject'
        method. This may take a long time.
        """
        elapse = time.time()
        c_elapse = time.clock()

        self.clearFindAndRebuild()

        elapse = time.time() - elapse
        c_elapse = time.clock() - c_elapse

        msg = ('Catalog Rebuilt\n'
               'Total time: %s\n'
               'Total CPU time: %s' % (repr(elapse), repr(c_elapse)))
        logger.info(msg)

        if RESPONSE is not None:
            RESPONSE.redirect(
                URL1 + '/manage_catalogAdvanced?manage_tabs_message=' +
                urllib.quote(msg))
Exemplo n.º 52
0
class CatalogTool(PloneBaseTool, BaseTool):
    """Plone's catalog tool"""

    implements(IPloneCatalogTool)

    meta_type = 'Plone Catalog Tool'
    security = ClassSecurityInfo()
    toolicon = 'skins/plone_images/book_icon.png'
    _counter = None

    manage_catalogAdvanced = DTMLFile('www/catalogAdvanced', globals())

    manage_options = (
        {
            'action': 'manage_main',
            'label': 'Contents'
        },
        {
            'action': 'manage_catalogView',
            'label': 'Catalog'
        },
        {
            'action': 'manage_catalogIndexes',
            'label': 'Indexes'
        },
        {
            'action': 'manage_catalogSchema',
            'label': 'Metadata'
        },
        {
            'action': 'manage_catalogAdvanced',
            'label': 'Advanced'
        },
        {
            'action': 'manage_catalogReport',
            'label': 'Query Report'
        },
        {
            'action': 'manage_catalogPlan',
            'label': 'Query Plan'
        },
        {
            'action': 'manage_propertiesForm',
            'label': 'Properties'
        },
    )

    def __init__(self):
        ZCatalog.__init__(self, self.getId())

    def _removeIndex(self, index):
        """Safe removal of an index.
        """
        try:
            self.manage_delIndex(index)
        except:
            pass

    def _listAllowedRolesAndUsers(self, user):
        """Makes sure the list includes the user's groups.
        """
        result = user.getRoles()
        if 'Anonymous' in result:
            # The anonymous user has no further roles
            return ['Anonymous']
        result = list(result)
        if hasattr(aq_base(user), 'getGroups'):
            groups = ['user:%s' % x for x in user.getGroups()]
            if groups:
                result = result + groups
        # Order the arguments from small to large sets
        result.insert(0, 'user:%s' % user.getId())
        result.append('Anonymous')
        return result

    security.declarePrivate('indexObject')

    def indexObject(self, object, idxs=None):
        """Add object to catalog.

        The optional idxs argument is a list of specific indexes
        to populate (all of them by default).
        """
        if idxs is None:
            idxs = []
        self.reindexObject(object, idxs)

    security.declareProtected(ManageZCatalogEntries, 'catalog_object')

    def catalog_object(self,
                       object,
                       uid=None,
                       idxs=None,
                       update_metadata=1,
                       pghandler=None):
        if idxs is None:
            idxs = []
        self._increment_counter()

        w = object
        if not IIndexableObject.providedBy(object):
            # This is the CMF 2.2 compatible approach, which should be used
            # going forward
            wrapper = queryMultiAdapter((object, self), IIndexableObject)
            if wrapper is not None:
                w = wrapper

        ZCatalog.catalog_object(self,
                                w,
                                uid,
                                idxs,
                                update_metadata,
                                pghandler=pghandler)

    security.declareProtected(ManageZCatalogEntries, 'catalog_object')

    def uncatalog_object(self, *args, **kwargs):
        self._increment_counter()
        return BaseTool.uncatalog_object(self, *args, **kwargs)

    def _increment_counter(self):
        if self._counter is None:
            self._counter = Length()
        self._counter.change(1)

    security.declarePrivate('getCounter')

    def getCounter(self):
        return self._counter is not None and self._counter() or 0

    security.declareProtected(SearchZCatalog, 'searchResults')

    def searchResults(self, REQUEST=None, **kw):
        """Calls ZCatalog.searchResults with extra arguments that
        limit the results to what the user is allowed to see.

        This version uses the 'effectiveRange' DateRangeIndex.

        It also accepts a keyword argument show_inactive to disable
        effectiveRange checking entirely even for those without portal
        wide AccessInactivePortalContent permission.
        """
        kw = kw.copy()
        show_inactive = kw.get('show_inactive', False)
        if isinstance(REQUEST, dict) and not show_inactive:
            show_inactive = 'show_inactive' in REQUEST

        user = _getAuthenticatedUser(self)
        kw['allowedRolesAndUsers'] = self._listAllowedRolesAndUsers(user)

        if not show_inactive and not _checkPermission(
                AccessInactivePortalContent, self):

            kw['effectiveRange'] = DateTime()

        return ZCatalog.searchResults(self, REQUEST, **kw)

    __call__ = searchResults

    security.declareProtected(ManageZCatalogEntries, 'clearFindAndRebuild')

    def clearFindAndRebuild(self):
        """Empties catalog, then finds all contentish objects (i.e. objects
           with an indexObject method), and reindexes them.
           This may take a long time.
        """
        def indexObject(obj, path):
            if (base_hasattr(obj, 'indexObject')
                    and safe_callable(obj.indexObject)):
                try:
                    obj.indexObject()
                except TypeError:
                    # Catalogs have 'indexObject' as well, but they
                    # take different args, and will fail
                    pass

        self.manage_catalogClear()
        portal = aq_parent(aq_inner(self))
        portal.ZopeFindAndApply(portal,
                                search_sub=True,
                                apply_func=indexObject)

    security.declareProtected(ManageZCatalogEntries, 'manage_catalogRebuild')

    def manage_catalogRebuild(self, RESPONSE=None, URL1=None):
        """Clears the catalog and indexes all objects with an 'indexObject'
        method. This may take a long time.
        """
        elapse = time.time()
        c_elapse = time.clock()

        self.clearFindAndRebuild()

        elapse = time.time() - elapse
        c_elapse = time.clock() - c_elapse

        if RESPONSE is not None:
            RESPONSE.redirect(URL1 +
                              '/manage_catalogAdvanced?manage_tabs_message=' +
                              urllib.quote('Catalog Rebuilt\n'
                                           'Total time: %s\n'
                                           'Total CPU time: %s' %
                                           (repr(elapse), repr(c_elapse))))
Exemplo n.º 53
0
class BTreeOrderStorage(Persistent):
    implements(IOrderStorage)
    title = u"BTree Storage"

    def __init__(self):
        self._orderStorage = BTreeImplementation()
        self._orderCount = 0
        self._length = Length()

    def getOrder(self, id):
        return self._orderStorage[id]

    def getAllOrders(self):
        return list(self._orderStorage.values())

    def _addDataRow(self, value):
        """Adds a row of data to the internal storage
        """

        # Get id for order to be added and increment internal counter
        id = self._orderCount + 1
        self._orderCount += 1

        self._orderStorage[id] = value
        self._length.change(1)
        return id


    security.declareProtected(ModifyPortalContent, 'createOrder')

    def createOrder(self, status=None, date=None, customer_data=None,
                    shipping_data=None, cart_data=None, total=None):
        """ a wrapper for the _addDataRow method """

        new_order = Order()
        new_order.status = status
        new_order.date = date
        new_order.total = Decimal(total)

        order_id = self._addDataRow(new_order)
        new_order.order_id = order_id

        # calc order number
        now = DateTime()
        order_prefix = '%03d%s' % (now.dayOfYear() + 500, now.yy())
        order_number = '%s%04d' % (order_prefix, order_id)
        new_order.title = order_number

        for key in customer_data.keys():
            setattr(new_order, "customer_%s" % key, customer_data[key])

        for key in shipping_data.keys():
            setattr(new_order, "shipping_%s" % key, shipping_data[key])

        # store cart in order
        all_cart_items = []
        vat_amount_total = Decimal('0.0')
        for key in cart_data.keys():
            cart_items = CartItems()
            cart_items.sku_code = cart_data[key]['skucode']
            cart_items.quantity = cart_data[key]['quantity']
            cart_items.title = cart_data[key]['title']
            cart_items.price = Decimal(cart_data[key]['price'])
            cart_items.show_price = cart_data[key]['show_price']
            cart_items.total = Decimal(cart_data[key]['total'])
            cart_items.supplier_name = cart_data[key]['supplier_name']
            cart_items.supplier_email = cart_data[key]['supplier_email']
            cart_items.vat_rate = cart_data[key]['vat_rate']
            cart_items.vat_amount = Decimal(cart_data[key]['vat_amount'])
            cart_items.order_id = order_id
            cart_items.order = new_order

            all_cart_items.append(cart_items)
            vat_amount_total += cart_items.vat_amount

        new_order.cartitems = all_cart_items
        new_order.vat_amount = vat_amount_total
        return order_id

    def cancelOrder(self, order_id):
        del self._orderStorage[order_id]
        self._length.change(-1)

    def flush(self):
        pass

    def getFieldNames(self):
        o = Order()
        field_names = [f for f in dir(o) if not (f.startswith('_') or \
                                                 f.startswith('get') or \
                                                 f == 'cartitems')]
        return field_names
Exemplo n.º 54
0
class UserRatingStorage(Contained, Persistent):
    """BTree-based storage for user ratings, keeps a running
    statistics tally for efficiency."""
    implements(IUserRating, IRatingStorage)

    _average = 0.0
    _anon_average = 0.0
    _most_recent = None
    _penultimate = None
    _length = None
    _anon_length = None
    # BBB
    scale = 5

    def __init__(self):
        """Setup our data structures"""
        self._anon_ratings = IOBTree()
        self._ratings = OOBTree()
        self._sessions = OOBTree()
        self._length = Length()
        self._anon_length = Length()

    def rate(self, rating, username=None, session_key=None):
        """Set a rating for a particular user"""
        # We keep a running average for efficiency, we need to
        # have the current statistics to do so
        orig_total = self._average * self.numberOfRatings
        orig_rating = self._ratings.get(username, 0.0)
        rating = Rating(rating, username)
        if username:
            self._ratings[username] = rating
            if not orig_rating:
                # If the user hadn't set a rating yet, the number of
                # ratings grew
                self._length.change(1)
        else:
            # For anonymous users, we use a sequential key, which
            # may lead to conflicts.  There's probably a better way
            anon_total = self._anon_average * self._anon_count
            # Update the corresponding BTree length to get the key
            self._anon_length.change(1)
            self._anon_ratings[self._anon_count] = rating
            self._anon_average = (anon_total + rating)/self._anon_count
            # If a session key was passed in for an anonymous user
            # store it with a datestamp
            if session_key:
                self._sessions[session_key] = datetime.utcnow()
        # Calculate the updated average
        self._average = (orig_total + rating - orig_rating)/self.numberOfRatings

        # If this isn't just a change in the last rating update the
        # most recent rating
        if self._most_recent and username != self._most_recent.userid:
            self._penultimate = self._most_recent

        # Mark this new rating as the most recent
        self._most_recent = rating

        return rating

    def userRating(self, username=None):
        """Retreive the rating for the specified user, or the average
        anonymous rating if no user is specified"""
        if username is not None:
            return self._ratings.get(username, None)
        else:
            if self._anon_count:
                return Rating(self._anon_average)
            else:
                return None

    def remove_rating(self, username):
        """Remove the rating for a given user"""
        orig_total = self._average * self.numberOfRatings
        rating = self._ratings[username]
        del self._ratings[username]
        self._length.change(-1)
        # Since we want to keep track of the most recent rating, we
        # need to replace it with the second most recent if the most
        # recent was deleted
        if rating is self.most_recent:
            self._most_recent = self._penultimate
            self._penultimate = None
            if self._most_recent is None:
                ordered = sorted(self.all_user_ratings(True),
                                 key=lambda x: x.timestamp)
                if ordered:
                    self._most_recent = ordered[-1]
                    if len(ordered > 1):
                        self._penultimate = ordered[-2]
                    else:
                        self._penultimate = None
                else:
                    self._most_recent = None
                    self._penultimate = None
        # Update the average
        self._average = float(self.numberOfRatings and
                                 (orig_total - rating)/self.numberOfRatings)

    @property
    def _anon_count(self):
        # Dynamic Migration
        if self._anon_length is None:
            self._anon_length = Length(len(self._anon_ratings))
        return self._anon_length()

    @property
    def most_recent(self):
        return self._most_recent

    def all_user_ratings(self, include_anon=False):
        ratings = self._ratings.values()
        if include_anon:
            ratings = chain(ratings, self._anon_ratings.values())
        return ratings

    def all_raters(self):
        return self._ratings.keys()

    @property
    def numberOfRatings(self):
        # Dynamic Migration
        if self._length is None:
            self._length = Length(len(self._ratings))
        return self._length() + self._anon_count

    @property
    def averageRating(self):
        return self._average

    def last_anon_rating(self, session_key):
        """Returns a timestamp indicating the last time the anonymous user
        with the given session_key rated the object."""
        return self._sessions.get(session_key, None)
Exemplo n.º 55
0
class GraphDB(Persistent):
    def __init__(self):

        self._init()

        self.node_catalog = Catalog()
        self.edge_catalog = Catalog()

    def _init(self):
        self.nodes = IOBTree()
        self.edges = IOBTree()
        self.edgedata = IOBTree()

        self.outgoing = IOBTree()
        self.incoming = IOBTree()

        self.typeids = PObject()

        self._nodeid = Length(0)
        self._edgeid = Length(0)
        self._typeid = Length(0)

    def nodeid(self):
        self._nodeid.change(1)
        return self._nodeid.value

    def edgeid(self):
        self._edgeid.change(1)
        return self._edgeid.value

    def typeid(self, name):
        if not hasattr(self.typeids, name):
            self._typeid.change(1)
            setattr(self.typeids, name, self._typeid.value)
            self.revtypes[self._typeid.value] = name
        return getattr(self.typeids, name)

    @property
    def revtypes(self):
        if (not hasattr(self, '_v_revtypes')) or (not bool(self._v_revtypes)):
            dir(self.typeids)
            dir(self.typeids)
            self._v_revtypes = dict([
                (v, k) for k, v in list(self.typeids.__dict__.items())
            ])
        return self._v_revtypes

    def getType(self, typeid):
        if type(typeid) != int:
            #lets assume an edge
            typeid = typeid[2]
        return self.revtypes[typeid]

    def addNode(self, **kwargs):
        if '_id' not in kwargs:
            _id = self.nodeid()
        else:
            _id = kwargs.pop('_id')
        self.nodes[_id] = kwargs
        ln = self.lightNode(_id, kwargs)
        self.node_catalog.index_doc(_id, ln)
        return ln

    def lightNode(self, _id, node=None):
        "{'_id':nodeid, ...other attributes...}"
        if node == None:
            node = self.nodes[_id]
        out = dict(node)
        out['_id'] = _id
        return out

    def addEdge(self, start, end, edgetype, **kwargs):
        _id = self.edgeid()

        if type(edgetype) != int:
            edgetype = self.typeid(edgetype)

        if type(start) == dict:
            start = start['_id']
        if type(end) == dict:
            end = end['_id']

        edge = [start, end, edgetype]
        self.edges[_id] = edge

        if kwargs:
            self.edgedata[_id] = kwargs
            le = self.lightEdge(_id, edge)
            self.edge_catalog.index_doc(_id, le)

        le = self.lightEdge(_id, edge)
        # edgeid:nodeid
        data = self.outgoing.setdefault(edgetype,
                                        IOBTree()).setdefault(start, {})
        data[_id] = end
        self.outgoing[edgetype][start] = data

        data = self.incoming.setdefault(edgetype,
                                        IOBTree()).setdefault(end, {})
        data[_id] = start
        self.incoming[edgetype][end] = data

        return le

    def lightEdge(self, _id, edge=None):
        '[sourceid targetid typeid kwargs edgeid]'
        if edge == None:
            edge = self.edges[_id]
        out = list(edge)
        out.append(self.edgedata.get(_id, {}))
        out.append(_id)
        return out

    def delEdge(self, edge):
        if type(edge) == int:
            edge = self.lightEdge(edge)

        start, end, edgetype, props, edgeid = edge

        data = self.outgoing[edgetype][start]
        del (data[edgeid])
        self.outgoing[edgetype][start] = data

        data = self.incoming[edgetype][end]
        del (data[edgeid])
        self.incoming[edgetype][end] = data

        del (self.edges[edgeid])
        if edgeid in self.edgedata:
            self.edge_catalog.unindex_doc(edgeid)
            #del(self.edges[edgeid])

    def delNode(self, node):
        if type(node) == int:
            node = self.lightNode(node)
        nodeid = node['_id']

        for edgetype in list(self.outgoing.keys()):
            if len(self.outgoing[edgetype].get(nodeid, {})) > 0:
                raise StillConnected('outgoing',
                                     self.outgoing[edgetype][nodeid])

        for edgetype in list(self.incoming.keys()):
            if len(self.incoming[edgetype].get(nodeid, {})) > 0:
                raise StillConnected('incoming',
                                     self.incoming[edgetype][nodeid])

        #all good, lets delete
        for edgetype in list(self.outgoing.keys()):
            if nodeid in self.outgoing[edgetype]:
                del (self.outgoing[edgetype][nodeid])

        for edgetype in list(self.incoming.keys()):
            if nodeid in self.incoming[edgetype]:
                del (self.incoming[edgetype][nodeid])

        self.node_catalog.unindex_doc(nodeid)
        del (self.nodes[nodeid])

    def updateNode(self, lightnode):
        nodeid = lightnode['_id']
        data = dict(lightnode)
        self.nodes[nodeid] = data
        self.node_catalog.reindex_doc(nodeid, lightnode)

    def updateEdge(self, lightedge):
        edgeid = lightedge[4]
        edge = list(lightedge[:4])
        data = lightedge[3]
        self.edges[edgeid] = edge
        if data:
            self.edgedata[edgeid] = data
            self.edge_catalog.reindex_doc(edgeid, lightedge)
        elif edgeid in self.edgedata:
            del (self.edgedata[edgeid])
            self.edge_catalog.unindex_doc(edgeid)

    def kwQuery(self, **kwargs):
        kwitems = list(kwargs.items())
        key, value = kwitems[0]
        query = rc_query.Eq(key, value)
        for k, v in kwitems[1:]:
            query = query & rc_query.Eq(k, v)
        return query

    def queryNode(self, **kwargs):
        result = self.node_catalog.query(self.kwQuery(**kwargs))
        return [self.lightNode(i) for i in result[1]]

    def queryEdge(self, **kwargs):
        result = self.edge_catalog.query(self.kwQuery(**kwargs))
        return [self.lightEdge(i) for i in result[1]]

    def prepareTypes(self, types=None):
        if types is None:
            return types
        else:
            if type(types) not in (list, tuple):
                types = [types]
            out = []
            for t in types:
                if type(t) == str:
                    t = self.typeid(t)
                out.append(t)
            return out


################## Higher Level API, functionality > speed ###################

    def getAllEdges(self, nodeids, directions=None, types=None, returnIds=0):

        if not islisttype(nodeids):
            nodeids = [nodeids]
        if directions == None:
            directions = ['i', 'o']
        elif type(directions) not in (list, tuple):
            directions = [directions]

        types = self.prepareTypes(types)

        tmp = []
        for n in nodeids:
            if type(n) != int:
                n = n['_id']
            tmp.append(n)
        nodeids = tmp

        out = EdgeDict()

        for direction in directions:
            if direction.startswith('i'):
                d = 'incoming'
            elif direction.startswith('o'):
                d = 'outgoing'
            else:
                raise 'unknown'

            result = []
            container = getattr(self, d)

            for edgetype in list(container.keys()):
                if types != None and edgetype not in types:
                    continue
                for n in nodeids:
                    edges = container[edgetype].get(n, {})
                    if returnIds:
                        result.extend(list(edges.keys()))
                    else:
                        for key in list(edges.keys()):
                            result.append(self.edge(key))
            out[direction] = result
        if len(directions) == 1:
            return result
        else:
            return out

    # XXX work in progress
    def getEdges(self, start, end, edgetype):
        #import ipdb; ipdb.set_trace()
        if type(edgetype) != int:
            edgetype = self.typeid(edgetype)

        if type(start) != int:
            start = start['_id']
        if type(end) != int:
            end = end['_id']

        out = []
        targets = self.outgoing.get(edgetype, {}).get(start, {})
        for edgeid, nodeid in list(targets.items()):
            if nodeid == end:
                out.append(self.lightEdge(edgeid))
        return out

    # XXX work in progress
    def addUniqueEdge(self, start, end, edgetype, **kwargs):
        if not self.getEdges(start, end, edgetype):
            return self.addEdge(start, end, edgetype, **kwargs)

    def clean(self):
        #import ipdb; ipdb.set_trace()
        for k in list(self.edges.keys()):
            self.delEdge(k)

        for k in list(self.nodes.keys()):
            self.delNode(k)

        self._init()

    def render(self, filename='graphagus', label='name', source=False):
        from graphviz import Digraph

        dot = Digraph('Graphagus dump', format='svg')

        for k in list(self.nodes.keys()):
            n = self.lightNode(k)
            dot.node(str(k), n[label])

        for k in list(self.edges.keys()):
            e = self.lightEdge(k)
            dot.edge(str(e[0]), str(e[1]), self.getType(e))
        if source:
            return dot.source
        else:
            dot.render(filename, cleanup=True)

    def edge(self, lightEdge):
        if type(lightEdge) == int:
            lightEdge = self.lightEdge(lightEdge)
        return Edge(self, lightEdge)

    def node(self, lightNode):
        if type(lightNode) == int:
            lightNode = self.lightNode(lightNode)
        return Node(self, lightNode)
Exemplo n.º 56
0
class Folder(Persistent):
    """ A folder implementation which acts much like a Python dictionary.

    Keys must be Unicode strings; values must be arbitrary Python objects.
    """
    family = BTrees.family64

    __name__ = None
    __parent__ = None

    # Default uses ordering of underlying BTree.
    _order = None # tuple of names
    _order_oids = None # tuple of oids
    _reorderable = None

    def __init__(self, data=None, family=None):
        """ Constructor.  Data may be an initial dictionary mapping object
        name to object. """
        if family is not None:
            self.family = family
        if data is None:
            data = {}
        self.data = self.family.OO.BTree(data)
        self._num_objects = Length(len(data))

    def set_order(self, names, reorderable=None):
        """ Sets the folder order. ``names`` is a list of names for existing
        folder items, in the desired order.  All names that currently exist in
        the folder must be mentioned in ``names``, or a :exc:`ValueError` will
        be raised.

        If ``reorderable`` is passed, value, it must be ``None``, ``True`` or
        ``False``.  If it is ``None``, the reorderable flag will not be reset
        from its current value.  If it is anything except ``None``, it will be
        treated as a boolean and the reorderable flag will be set to that
        value.  The ``reorderable`` value of a folder will be returned by that
        folder's :meth:`~substanced.folder.Folder.is_reorderable` method.  The
        :meth:`~substanced.folder.Folder.is_reorderable` method is used by the
        SDI folder contents view to indicate that the folder can or cannot be
        reordered via the web UI.

        If ``reorderable`` is set to ``True``, the
        :meth:`~substanced.folder.Folder.reorder` method will work properly,
        otherwise it will raise a :exc:`ValueError` when called.
        """
        nameset = set(names)
        if len(self) != len(nameset):
            raise ValueError('Must specify all names when calling set_order')

        if len(names) != len(nameset):
            raise ValueError('No repeated items allowed in names')

        order = []
        order_oids = []

        for name in names:
            assert(isinstance(name, string_types))
            name = u(name)
            oid = get_oid(self[name])
            order.append(name)
            order_oids.append(oid)

        self._order = tuple(order)
        self._order_oids = tuple(order_oids)
        assert(len(self._order) == len(self._order_oids))

        if reorderable is not None:
            self._reorderable = bool(reorderable)

    def unset_order(self):
        """ Remove set order from a folder, making it unordered, and
        non-reorderable."""
        if self._order is not None:
            del self._order
        if self._order_oids is not None:
            del self._order_oids
        if self._reorderable is not None:
            del self._reorderable

    def reorder(self, names, before):
        """ Move one or more items from a folder into new positions inside that
        folder. ``names`` is a list of ids of existing folder subobject names,
        which will be inserted in order before the item named ``before``. All
        other items are left in the original order. If ``before`` is ``None``,
        the items will be appended after the last item in the current order. If
        this method is called on a folder which does not have an order set, or
        which is not reorderable, a :exc:`ValueError` will be raised. A
        :exc:`KeyError` is raised, if ``before`` does not correspond to any
        item, and is not ``None``."""
        if not self._reorderable:
            raise ValueError('Folder is not reorderable')

        before_idx = None

        if len(set(names)) != len(names):
            raise ValueError('No repeated values allowed in names')

        if before is not None:
            if not before in self._order:
                raise FolderKeyError(before)
            before_idx = self._order.index(before)

        assert(len(self._order) == len(self._order_oids))

        order_names = list(self._order)
        order_oids = list(self._order_oids)

        reorder_names = []
        reorder_oids = []

        for name in names:
            assert(isinstance(name, string_types))
            name = u(name)
            if not name in order_names:
                raise FolderKeyError(name)
            idx = order_names.index(name)
            oid = order_oids[idx]
            order_names[idx] = None
            order_oids[idx] = None
            reorder_names.append(name)
            reorder_oids.append(oid)

        assert(len(reorder_names) == len(reorder_oids))

        # NB: technically we could use filter(None, oids) and filter(None,
        # names) because names cannot be empty string and oid 0 is disallowed,
        # but just in case this becomes untrue later we define "filt" instead

        def filt(L):
            return [x for x in L if x is not None]

        if before_idx is None:
            order_names = filt(order_names)
            order_names.extend(reorder_names)
            order_oids = filt(order_oids)
            order_oids.extend(reorder_oids)
        else:
            before_idx_names = filt(order_names[:before_idx])
            after_idx_names = filt(order_names[before_idx:])
            before_idx_oids = filt(order_oids[:before_idx])
            after_idx_oids = filt(order_oids[before_idx:])
            assert(
                len(before_idx_names+after_idx_names) ==
                len(before_idx_oids+after_idx_oids)
                )
            order_names =  before_idx_names + reorder_names + after_idx_names
            order_oids = before_idx_oids + reorder_oids + after_idx_oids

        for oid, name in zip(order_oids, order_names):
            # belt and suspenders check
            assert oid == get_oid(self[name])

        self._order = tuple(order_names)
        self._order_oids = tuple(order_oids)

    def is_ordered(self):
        """ Return true if the folder has a manually set ordering, false
        otherwise."""
        return self._order is not None

    def is_reorderable(self):
        """ Return true if the folder can be reordered, false otherwise."""
        return self._reorderable

    def sort(self, oids, reverse=False, limit=None, **kw):
        # used by the hypatia resultset "sort" method when the folder contents
        # view uses us as a "sort index"
        if self._order_oids is not None:
            ids = [oid for oid in self._order_oids if oid in oids]
        else:
            ids = []
            for resource in self.values():
                oid = get_oid(resource)
                if oid in oids:
                    ids.append(oid)
        if reverse:
            ids = ids[::-1]
        if limit is not None:
            ids = ids[:limit]
        return ids

    def find_service(self, service_name):
        """ Return a service named by ``service_name`` in this folder *or any
        parent service folder* or ``None`` if no such service exists.  A
        shortcut for :func:`substanced.service.find_service`."""
        return find_service(self, service_name)

    def find_services(self, service_name):
        """ Returns a sequence of service objects named by ``service_name``
        in this folder's lineage or an empty sequence if no such service
        exists.  A shortcut for :func:`substanced.service.find_services`"""
        return find_services(self, service_name)

    def add_service(self, name, obj, registry=None, **kw):
        """ Add a service to this folder named ``name``."""
        if registry is None:
            registry = get_current_registry()
        kw['registry'] = registry
        self.add(name, obj, **kw)
        obj.__is_service__ = True

    def keys(self):
        """ Return an iterable sequence of object names present in the folder.

        Respect order, if set.
        """
        if self._order is not None:
            return self._order
        return self.data.keys()

    order = property(keys, set_order, unset_order) # b/c

    def __iter__(self):
        """ An alias for ``keys``
        """
        return iter(self.keys())

    def values(self):
        """ Return an iterable sequence of the values present in the folder.

        Respect ``order``, if set.
        """
        if self._order is not None:
            return [self.data[name] for name in self.keys()]
        return self.data.values()

    def items(self):
        """ Return an iterable sequence of (name, value) pairs in the folder.

        Respect ``order``, if set.
        """
        if self._order is not None:
            return [(name, self.data[name]) for name in self.keys()]
        return self.data.items()

    def __len__(self):
        """ Return the number of objects in the folder.
        """
        return self._num_objects()

    def __nonzero__(self):
        """ Return ``True`` unconditionally.
        """
        return True
    __bool__ = __nonzero__

    def __repr__(self):
        klass = self.__class__
        classname = '%s.%s' % (klass.__module__, klass.__name__)
        return '<%s object %r at %#x>' % (classname,
                                          self.__name__,
                                          id(self))

    def __getitem__(self, name):
        """ Return the object named ``name`` added to this folder or raise
        ``KeyError`` if no such object exists.  ``name`` must be a Unicode
        object or directly decodeable to Unicode using the system default
        encoding.
        """
        with statsd_timer('folder.get'):
            name = u(name)
            return wrap_if_broken(self.data[name])

    def get(self, name, default=None):
        """ Return the object named by ``name`` or the default.

        ``name`` must be a Unicode object or a bytestring object.

        If ``name`` is a bytestring object, it must be decodable using the
        system default encoding.
        """
        with statsd_timer('folder.get'):
            name = u(name)
            return wrap_if_broken(self.data.get(name, default))

    def __contains__(self, name):
        """ Does the container contains an object named by name?

        ``name`` must be a Unicode object or a bytestring object.

        If ``name`` is a bytestring object, it must be decodable using the
        system default encoding.
        """
        name = u(name)
        return name in self.data

    def __setitem__(self, name, other):
        """ Set object ``other' into this folder under the name ``name``.

        ``name`` must be a Unicode object or a bytestring object.

        If ``name`` is a bytestring object, it must be decodable using the
        system default encoding.

        ``name`` cannot be the empty string.

        When ``other`` is seated into this folder, it will also be decorated
        with a ``__parent__`` attribute (a reference to the folder into which
        it is being seated) and ``__name__`` attribute (the name passed in to
        this function.  It must not already have a ``__parent__`` attribute
        before being seated into the folder, or an exception will be raised.

        If a value already exists in the foldr under the name ``name``, raise
        :exc:`KeyError`.

        When this method is called, the object will be added to the objectmap,
        an :class:`substanced.event.ObjectWillBeAdded` event will be emitted
        before the object obtains a ``__name__`` or ``__parent__`` value, then
        a :class:`substanced.event.ObjectAdded` will be emitted after the
        object obtains a ``__name__`` and ``__parent__`` value.
        """
        return self.add(name, other)

    def validate_name(self, name, reserved_names=()):
        """
        Validate the ``name`` passed to ensure that it's addable to the folder.
        Returns the name decoded to Unicode if it passes all addable checks.
        It's not addable if:

        - the name is not decodeable to Unicode.

        - the name starts with ``@@`` (conflicts with explicit view names).

        - the name has slashes in it (WSGI limitation).

        - the name is empty.

        If any of these conditions are untrue, raise a :exc:`ValueError`.  If
        the name passed is in the list of ``reserved_names``, raise a
        :exc:`ValueError`.
        """
        if not isinstance(name, STRING_TYPES):
            raise ValueError("Name must be a string rather than a %s" %
                             name.__class__.__name__)
        if not name:
            raise ValueError("Name must not be empty")

        try:
            name = u(name)
        except UnicodeDecodeError: #pragma NO COVER (on Py3k)
            raise ValueError('Name "%s" not decodeable to unicode' % name)

        if name in reserved_names:
            raise ValueError('%s is a reserved name' % name)

        if name.startswith('@@'):
            raise ValueError('Names which start with "@@" are not allowed')

        if '/' in name:
            raise ValueError('Names which contain a slash ("/") are not '
                             'allowed')

        return name

    def check_name(self, name, reserved_names=()):
        """ Perform all the validation checks implied by
        :meth:`~substanced.folder.Folder.validate_name` against the ``name``
        supplied but also fail with a
        :class:`~substanced.folder.FolderKeyError` if an object with the name
        ``name`` already exists in the folder."""

        name = self.validate_name(name, reserved_names=reserved_names)

        if name in self.data:
            raise FolderKeyError('An object named %s already exists' % name)

        return name

    def add(self, name, other, send_events=True, reserved_names=(),
            duplicating=None, moving=None, loading=False, registry=None):
        """ Same as ``__setitem__``.

        If ``send_events`` is False, suppress the sending of folder events.
        Don't allow names in the ``reserved_names`` sequence to be added.

        If ``duplicating`` not ``None``, it must be the object which is being
        duplicated; a result of a non-``None`` duplicating means that oids will
        be replaced in objectmap.  If ``moving`` is not ``None``, it must be
        the folder from which the object is moving; this will be the ``moving``
        attribute of events sent by this function too.  If ``loading`` is
        ``True``, the ``loading`` attribute of events sent as a result of
        calling this method will be ``True`` too.

        This method returns the name used to place the subobject in the
        folder (a derivation of ``name``, usually the result of
        ``self.check_name(name)``).
        """
        if registry is None:
            registry = get_current_registry()

        name = self.check_name(name, reserved_names)

        if getattr(other, '__parent__', None):
            raise ValueError(
                'obj %s added to folder %s already has a __parent__ attribute, '
                'please remove it completely from its existing parent (%s) '
                'before trying to readd it to this one' % (
                    other, self, self.__parent__)
                )

        with statsd_timer('folder.add'):

            objectmap = find_objectmap(self)

            if objectmap is not None:

                basepath = resource_path_tuple(self)

                for node in postorder(other):
                    node_path = node_path_tuple(node)
                    path_tuple = basepath + (name,) + node_path[1:]
                    # the below gives node an objectid; if the will-be-added
                    # event is the result of a duplication, replace the oid of
                    # the node with a new one
                    objectmap.add(
                        node,
                        path_tuple,
                        duplicating=duplicating is not None,
                        moving=moving is not None,
                        )

            if send_events:
                event = ObjectWillBeAdded(
                    other, self, name, duplicating=duplicating, moving=moving,
                    loading=loading,
                    )
                self._notify(event, registry)

            other.__parent__ = self
            other.__name__ = name

            self.data[name] = other
            self._num_objects.change(1)

            if self._order is not None:
                oid = get_oid(other)
                self._order += (name,)
                self._order_oids += (oid,)

            if send_events:
                event = ObjectAdded(
                    other, self, name, duplicating=duplicating, moving=moving,
                    loading=loading,
                    )
                self._notify(event, registry)

            return name

    def pop(self, name, default=marker, registry=None):
        """ Remove the item stored in the under ``name`` and return it.

        If ``name`` doesn't exist in the folder, and ``default`` **is not**
        passed, raise a :exc:`KeyError`.

        If ``name`` doesn't exist in the folder, and ``default`` **is**
        passed, return ``default``.

        When the object stored under ``name`` is removed from this folder,
        remove its ``__parent__`` and ``__name__`` values.

        When this method is called, emit an
        :class:`substanced.event.ObjectWillBeRemoved` event before the
        object loses its ``__name__`` or ``__parent__`` values.  Emit an
        :class:`substanced.event.ObjectRemoved` after the object loses its
        ``__name__`` and ``__parent__`` value,
        """
        if registry is None:
            registry = get_current_registry()
        try:
            result = self.remove(name, registry=registry)
        except KeyError:
            if default is marker:
                raise
            return default
        return result

    def _notify(self, event, registry=None):
        if registry is None:
            registry = get_current_registry()
        registry.subscribers((event, event.object, self), None)

    def __delitem__(self, name):
        """ Remove the object from this folder stored under ``name``.

        ``name`` must be a Unicode object or a bytestring object.

        If ``name`` is a bytestring object, it must be decodable using the
        system default encoding.

        If no object is stored in the folder under ``name``, raise a
        :exc:`KeyError`.

        When the object stored under ``name`` is removed from this folder,
        remove its ``__parent__`` and ``__name__`` values.

        When this method is called, the removed object will be removed from the
        objectmap, a :class:`substanced.event.ObjectWillBeRemoved` event will
        be emitted before the object loses its ``__name__`` or ``__parent__``
        values and a :class:`substanced.event.ObjectRemoved` will be emitted
        after the object loses its ``__name__`` and ``__parent__`` value,
        """
        return self.remove(name)

    def remove(self, name, send_events=True, moving=None, loading=False,
               registry=None):
        """ Same thing as ``__delitem__``.

        If ``send_events`` is false, suppress the sending of folder events.

        If ``moving`` is not ``None``, the ``moving`` argument must be the
        folder to which the named object will be moving.  This value will be
        passed along as the ``moving`` attribute of the events sent as the
        result of this action.  If ``loading`` is ``True``, the ``loading``
        attribute of events sent as a result of calling this method will be
        ``True`` too.
        """
        name = u(name)
        other = wrap_if_broken(self.data[name])
        oid = get_oid(other, None)

        if registry is None:
            registry = get_current_registry()

        with statsd_timer('folder.remove'):

            if send_events:
                event = ObjectWillBeRemoved(
                    other, self, name, moving=moving, loading=loading
                    )
                self._notify(event, registry)

            if hasattr(other, '__parent__'):
                try:
                    del other.__parent__
                except AttributeError:
                    # this might be a broken object
                    pass

            if hasattr(other, '__name__'):
                try:
                    del other.__name__
                except AttributeError:
                    # this might be a broken object
                    pass

            del self.data[name]
            self._num_objects.change(-1)

            if self._order is not None:
                assert(len(self._order) == len(self._order_oids))
                idx = self._order.index(name)
                order = list(self._order)
                order.pop(idx)
                order_oids = list(self._order_oids)
                order_oids.pop(idx)
                self._order = tuple(order)
                self._order_oids = tuple(order_oids)

            objectmap = find_objectmap(self)

            removed_oids = set([oid])

            if objectmap is not None and oid is not None:
                removed_oids = objectmap.remove(oid, moving=moving is not None)

            if send_events:
                event = ObjectRemoved(other, self, name, removed_oids,
                                      moving=moving, loading=loading)
                self._notify(event, registry)

            return other

    def copy(self, name, other, newname=None, registry=None):
        """
        Copy a subobject named ``name`` from this folder to the folder
        represented by ``other``.  If ``newname`` is not none, it is used as
        the target object name; otherwise the existing subobject name is
        used.
        """
        if newname is None:
            newname = name

        if registry is None:
            registry = get_current_registry()

        with statsd_timer('folder.copy'):
            obj = self[name]
            newobj = copy(obj)
            return other.add(
                newname, newobj, duplicating=obj, registry=registry
                )

    def move(self, name, other, newname=None, registry=None):
        """
        Move a subobject named ``name`` from this folder to the folder
        represented by ``other``.  If ``newname`` is not none, it is used as
        the target object name; otherwise the existing subobject name is
        used.

        This operation is done in terms of a remove and an add.  The Removed
        and WillBeRemoved events as well as the Added and WillBeAdded events
        sent will indicate that the object is moving.
        """
        if newname is None:
            newname = name
        if registry is None:
            registry = get_current_registry()
        ob = self.remove(
            name,
            moving=other,
            registry=registry
            )
        other.add(
            newname,
            ob,
            moving=self,
            registry=registry
            )
        return ob

    def rename(self, oldname, newname, registry=None):
        """
        Rename a subobject from oldname to newname.

        This operation is done in terms of a remove and an add.  The Removed
        and WillBeRemoved events sent will indicate that the object is
        moving.
        """
        if registry is None:
            registry = get_current_registry()
        return self.move(oldname, self, newname, registry=registry)

    def replace(self, name, newobject, send_events=True, registry=None):
        """ Replace an existing object named ``name`` in this folder with a
        new object ``newobject``.  If there isn't an object named ``name`` in
        this folder, an exception will *not* be raised; instead, the new
        object will just be added.

        This operation is done in terms of a remove and an add.  The Removed
        and WillBeRemoved events will be sent for the old object, and the
        WillBeAdded and Added events will be sent for the new object.
        """
        if registry is None:
            registry = get_current_registry()
        if name in self:
            self.remove(name, send_events=send_events)
        self.add(name, newobject, send_events=send_events, registry=registry)

    def load(self, name, newobject, registry=None):
        """ A replace method used by the code that loads an existing dump.
        Events sent during this replace will have a true ``loading`` flag."""
        if registry is None:
            registry = get_current_registry()
        if name in self:
            self.remove(name, loading=True)
        self.add(name, newobject, loading=True, registry=registry)
Exemplo n.º 57
0
class Folder(Persistent):
    """ A folder implementation which acts much like a Python dictionary.

    Keys must be Unicode strings; values must be arbitrary Python objects.
    """
    family = BTrees.family64

    __name__ = None
    __parent__ = None

    # Default uses ordering of underlying BTree.
    _order = None

    def _get_order(self):
        if self._order is not None:
            return list(self._order)
        return self.data.keys()

    def _set_order(self, value):
        # XXX:  should we test against self.data.keys()?
        self._order = tuple([unicode(x) for x in value])

    def _del_order(self):
        del self._order
    order = property(_get_order, _set_order, _del_order)

    def __init__(self, data=None, family=None):
        """ Constructor.  Data may be an initial dictionary mapping object
        name to object. """
        if family is not None:
            self.family = family
        if data is None:
            data = {}
        self.data = self.family.OO.BTree(data)
        self._num_objects = Length(len(data))

    def find_service(self, service_name):
        """ Return a service named by ``service_name`` in this folder's
        ``__services__`` folder *or any parent service folder* or ``None`` if
        no such service exists.  A shortcut for
        :func:`substanced.service.find_service`."""
        return find_service(self, service_name)

    def find_services(self, service_name):
        """ Returns a sequence of service objects named by ``service_name``
        in this folder's lineage or an empty sequence if no such service
        exists.  A shortcut for :func:`substanced.service.find_services`"""
        return find_services(self, service_name)

    def add_service(self, name, obj, registry=None):
        """ Add a service to this folder's ``__services__`` folder named
        ``name``."""
        if registry is None:
            registry = get_current_registry()
        services = self.get(SERVICES_NAME)
        if services is None:
            services = registry.content.create('Services')
            self.add(SERVICES_NAME, services, reserved_names=())
        services.add(name, obj)

    def keys(self):
        """ Return an iterable sequence of object names present in the folder.

        Respect ``order``, if set.
        """
        return self.order

    def __iter__(self):
        """ An alias for ``keys``
        """
        return iter(self.order)

    def values(self):
        """ Return an iterable sequence of the values present in the folder.

        Respect ``order``, if set.
        """
        if self._order is not None:
            return [self.data[name] for name in self.order]
        return self.data.values()

    def items(self):
        """ Return an iterable sequence of (name, value) pairs in the folder.

        Respect ``order``, if set.
        """
        if self._order is not None:
            return [(name, self.data[name]) for name in self.order]
        return self.data.items()

    def __len__(self):
        """ Return the number of objects in the folder.
        """
        return self._num_objects()

    def __nonzero__(self):
        """ Return ``True`` unconditionally.
        """
        return True

    def __repr__(self):
        klass = self.__class__
        classname = '%s.%s' % (klass.__module__, klass.__name__)
        return '<%s object %r at %#x>' % (classname,
                                          self.__name__,
                                          id(self))

    def __getitem__(self, name):
        """ Return the object named ``name`` added to this folder or raise
        ``KeyError`` if no such object exists.  ``name`` must be a Unicode
        object or directly decodeable to Unicode using the system default
        encoding.
        """
        name = unicode(name)
        return self.data[name]

    def get(self, name, default=None):
        """ Return the object named by ``name`` or the default.

        ``name`` must be a Unicode object or a bytestring object.

        If ``name`` is a bytestring object, it must be decodable using the
        system default encoding.
        """
        name = unicode(name)
        return self.data.get(name, default)

    def __contains__(self, name):
        """ Does the container contains an object named by name?

        ``name`` must be a Unicode object or a bytestring object.

        If ``name`` is a bytestring object, it must be decodable using the
        system default encoding.
        """
        name = unicode(name)
        return name in self.data

    def __setitem__(self, name, other):
        """ Set object ``other' into this folder under the name ``name``.

        ``name`` must be a Unicode object or a bytestring object.

        If ``name`` is a bytestring object, it must be decodable using the
        system default encoding.

        ``name`` cannot be the empty string.

        When ``other`` is seated into this folder, it will also be
        decorated with a ``__parent__`` attribute (a reference to the
        folder into which it is being seated) and ``__name__``
        attribute (the name passed in to this function.

        If a value already exists in the foldr under the name ``name``, raise
        :exc:`KeyError`.

        When this method is called, emit an
        :class:`substanced.event.ObjectWillBeAdded` event before the
        object obtains a ``__name__`` or ``__parent__`` value.  Emit an
        :class:`substanced.event.ObjectAdded` after the object obtains a
        ``__name__`` and ``__parent__`` value.
        """
        return self.add(name, other)

    def check_name(self, name, reserved_names=RESERVED_NAMES):
        """

        Check the ``name`` passed to ensure that it's addable to the folder.
        Returns the name decoded to Unicode if it passes all addable checks.
        It's not addable if:

        -  an object by the name already exists in the folder

        - the name is not decodeable to Unicode.

        - the name starts with ``@@`` (conflicts with explicit view names).

        - the name has slashes in it (WSGI limitation).

        - the name is empty.

        If any of these conditions are untrue, raise a :exc:`ValueError`.  If
        the name passed is in the list of ``reserved_names``, raise a
        :exc:`ValueError`.
        """
        if not isinstance(name, basestring):
            raise ValueError("Name must be a string rather than a %s" %
                             name.__class__.__name__)
        if not name:
            raise ValueError("Name must not be empty")

        try:
            name = unicode(name)
        except UnicodeDecodeError:
            raise ValueError('Name "%s" not decodeable to unicode' % name)

        if name in reserved_names:
            raise ValueError('%s is a reserved name' % name)

        if name.startswith('@@'):
            raise ValueError('Names which start with "@@" are not allowed')

        if '/' in name:
            raise ValueError('Names which contain a slash ("/") are not '
                             'allowed')

        if name in self.data:
            raise FolderKeyError('An object named %s already exists' % name)

        return name

    def add(self, name, other, send_events=True, reserved_names=RESERVED_NAMES,
            duplicating=False, registry=None):
        """ Same as ``__setitem__``.

        If ``send_events`` is False, suppress the sending of folder events.
        Don't allow names in the ``reserved_names`` sequence to be
        added. If ``duplicating`` is True, oids will be replaced in
        objectmap.
        """
        if registry is None:
            registry = get_current_registry()
        name = self.check_name(name, reserved_names)

        if send_events:
            event = ObjectWillBeAdded(other, self, name, duplicating)
            self._notify(event, registry)

        other.__parent__ = self
        other.__name__ = name

        self.data[name] = other
        self._num_objects.change(1)

        if self._order is not None:
            self._order += (name,)

        if send_events:
            event = ObjectAdded(other, self, name)
            self._notify(event, registry)

    def pop(self, name, default=marker):
        """ Remove the item stored in the under ``name`` and return it.

        If ``name`` doesn't exist in the folder, and ``default`` **is not**
        passed, raise a :exc:`KeyError`.

        If ``name`` doesn't exist in the folder, and ``default`` **is**
        passed, return ``default``.

        When the object stored under ``name`` is removed from this folder,
        remove its ``__parent__`` and ``__name__`` values.

        When this method is called, emit an
        :class:`substanced.event.ObjectWillBeRemoved` event before the
        object loses its ``__name__`` or ``__parent__`` values.  Emit an
        :class:`substanced.event.ObjectRemoved` after the object loses its
        ``__name__`` and ``__parent__`` value,
        """
        try:
            result = self.remove(name)
        except KeyError:
            if default is marker:
                raise
            return default
        return result

    def _notify(self, event, registry=None):
        if registry is None:
            registry = get_current_registry()
        registry.subscribers((event, event.object, self), None)

    def __delitem__(self, name):
        """ Remove the object from this folder stored under ``name``.

        ``name`` must be a Unicode object or a bytestring object.

        If ``name`` is a bytestring object, it must be decodable using the
        system default encoding.

        If no object is stored in the folder under ``name``, raise a
        :exc:`KeyError`.

        When the object stored under ``name`` is removed from this folder,
        remove its ``__parent__`` and ``__name__`` values.

        When this method is called, emit an
        :class:`substanced.event.ObjectWillBeRemoved` event before the
        object loses its ``__name__`` or ``__parent__`` values.  Emit an
        :class:`substanced.event.ObjectRemoved` after the object loses
        its ``__name__`` and ``__parent__`` value,
        """
        return self.remove(name)

    def remove(self, name, send_events=True, moving=False):
        """ Same thing as ``__delitem__``.

        If ``send_events`` is false, suppress the sending of folder events.
        If ``moving`` is True, the events sent will indicate that a move is
        in process.
        """
        name = unicode(name)
        other = self.data[name]

        if send_events:
            event = ObjectWillBeRemoved(other, self, name, moving)
            self._notify(event)

        if hasattr(other, '__parent__'):
            del other.__parent__

        if hasattr(other, '__name__'):
            del other.__name__

        del self.data[name]
        self._num_objects.change(-1)

        if self._order is not None:
            self._order = tuple([x for x in self._order if x != name])

        if send_events:
            event = ObjectRemoved(other, self, name, moving)
            self._notify(event)

        return other

    def copy(self, name, other, newname=None):
        """
        Copy a subobject named ``name`` from this folder to the folder
        represented by ``other``.  If ``newname`` is not none, it is used as
        the target object name; otherwise the existing subobject name is
        used.
        """
        if newname is None:
            newname = name

        with tempfile.TemporaryFile() as f:
            obj = self.get(name)
            obj._p_jar.exportFile(obj._p_oid, f)
            f.seek(0)
            new_obj = obj._p_jar.importFile(f)
            del new_obj.__parent__
            obj = other.add(newname, new_obj, duplicating=True)
            return obj

    def move(self, name, other, newname=None):
        """
        Move a subobject named ``name`` from this folder to the folder
        represented by ``other``.  If ``newname`` is not none, it is used as
        the target object name; otherwise the existing subobject name is
        used.

        This operation is done in terms of a remove and an add.  The Removed
        and WillBeRemoved events sent will indicate that the object is
        moving.
        """
        if newname is None:
            newname = name
        ob = self.remove(name, moving=True)
        other.add(newname, ob)
        return ob

    def rename(self, oldname, newname):
        """
        Rename a subobject from oldname to newname.

        This operation is done in terms of a remove and an add.  The Removed
        and WillBeRemoved events sent will indicate that the object is
        moving.
        """
        return self.move(oldname, self, newname)

    def replace(self, name, newobject):
        """ Replace an existing object named ``name`` in this folder with a
        new object ``newobject``.  If there isn't an object named ``name`` in
        this folder, an exception will *not* be raised; instead, the new
        object will just be added.

        This operation is done in terms of a remove and an add.  The Removed
        and WillBeRemoved events will be sent for the old object, and the
        WillBeAdded and Add events will be sent for the new object.
        """
        if name in self:
            del self[name]
        self[name] = newobject