view frontends/src/quick_frontend/ @ 1849:a754d6381550

quick_frontend (blog): correction for the previous patch: - "comments_node" and "comments_service" are not needed when "comments" is already here - to set "allow_comments" to True when updating a main item was enough
author souliane <>
date Mon, 22 Feb 2016 13:44:14 +0100
parents 446a1539c207
children d3354c80bd1f
line wrap: on
line source

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

# helper class for making a SAT frontend
# Copyright (C) 2011-2016 Jérôme Poisson <>

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# GNU Affero General Public License for more details.

# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <>.

# from sat.core.i18n import _, D_
from sat.core.log import getLogger
log = getLogger(__name__)

from sat_frontends.quick_frontend.constants import Const as C
from sat_frontends.quick_frontend import quick_widgets
from import jid
from import common

    # FIXME: to be removed when an acceptable solution is here
    unicode('') # XXX: unicode doesn't exist in pyjamas
except (TypeError, AttributeError): # Error raised is not the same depending on pyjsbuild options
    unicode = str


class Item(object):
    """Manage all (meta)data of an item"""

    def __init__(self, data):
        @param data(dict): microblog data as return by bridge methods
            if data values are not defined, set default values
        """ = data['id']
        self.title = data.get('title')
        self.title_rich = None
        self.title_xhtml = data.get('title_xhtml')
        self.tags = list(common.dict2iter('tag', data))
        self.content = data.get('content')
        self.content_rich = None
        self.content_xhtml = data.get('content_xhtml') = data['author']
            author_jid = data['author_jid']
            self.author_jid = jid.JID(author_jid) if author_jid else None
        except KeyError:
            self.author_jid = None

            self.author_verified = C.bool(data['author_jid_verified'])
        except KeyError:
            self.author_verified = False

            self.updated = float(data['updated'])  # XXX: int doesn't work here (pyjamas bug)
        except KeyError:
            self.updated = None

            self.published = float(data['published'])  # XXX: int doesn't work here (pyjamas bug)
        except KeyError:
            self.published = None

        self.comments = data.get('comments')
            self.comments_service = jid.JID(data['comments_service'])
        except KeyError:
            self.comments_service = None
        self.comments_node = data.get('comments_node')

    # def loadComments(self):
    #     """Load all the comments"""
    #     index = str(main_entry.comments_count - main_entry.hidden_count)
    #     rsm = {'max': str(main_entry.hidden_count), 'index': index}
    #'getMblogComments', self.mblogsInsert, main_entry.comments_service, main_entry.comments_node, rsm)

class EntriesManager(object):
    """Class which manages list of (micro)blog entries"""

    def __init__(self, manager):
        @param manager (EntriesManager, None): parent EntriesManager
            must be None for QuickBlog (and only for QuickBlog)
        self.manager = manager
        if manager is None:
   = self
        self.entries = []
        self.edit_entry = None

    def level(self):
        """indicate how deep is this entry in the tree

        if level == -1, we have a QuickBlog
        if level == 0, we have a main item
        else we have a comment
        level = -1
        manager = self.manager
        while manager is not None:
            level += 1
            manager = manager.manager
        return level

    def _addMBItems(self, items_tuple, service=None, node=None):
        """Add Microblog items to this panel
        update is NOT called after addition

        @param items_tuple(tuple): (items_data,items_metadata) tuple as returned by mbGet
        items, metadata = items_tuple
        for item in items:
            self.addEntry(item, service=service, node=node, with_update=False)

    def _addMBItemsWithComments(self, items_tuple, service=None, node=None):
        """Add Microblog items to this panel
        update is NOT called after addition

        @param items_tuple(tuple): (items_data,items_metadata) tuple as returned by mbGet
        items, metadata = items_tuple
        for item, comments in items:
            self.addEntry(item, comments, service=service, node=node, with_update=False)

    def addEntry(self, item=None, comments=None, service=None, node=None, with_update=True, editable=False, edit_entry=False):
        """Add a microblog entry

        @param editable (bool): True if the entry can be modified
        @param item (dict, None): blog item data, or None for an empty entry
        @param comments (list, None): list of comments data if available
        @param service (jid.JID, None): service where the entry is coming from
        @param service (unicode, None): node hosting the entry
        @param with_update (bool): if True, udpate is called with the new entry
        @param edit_entry(bool): if True, will be in self.edit_entry instead of
            self.entries, so it can be managed separately (e.g. first or last
            entry regardless of sorting)
        new_entry = ENTRY_CLS(self, item, comments, service=service, node=node)
        if edit_entry:
            self.edit_entry = new_entry
        if with_update:
        return new_entry

    def update(self, entry=None):
        """Update the display with entries

        @param entry (Entry, None): if not None, must be the new entry.
            If None, all the items will be checked to update the display
        # update is separated from addEntry to allow adding
        # several entries at once, and updating at the end
        raise NotImplementedError

class Entry(EntriesManager):
    """Graphical representation of an Item
    This class must be overriden by frontends"""

    def __init__(self, manager, item_data=None, comments_data=None, service=None, node=None):
        @param blog(QuickBlog): the parent QuickBlog
        @param manager(EntriesManager): the parent EntriesManager
        @param item_data(dict, None): dict containing the blog item data, or None for an empty entry
        @param comments_data(list, None): list of comments data
        assert manager is not None
        EntriesManager.__init__(self, manager)
        self.service = service
        self.node = node
        self.editable = False
        self.reset(item_data)[] = self
        if self.item.comments:
            node_tuple = (self.item.comments_service, self.item.comments_node)

    def reset(self, item_data):
        """Reset the entry with given data

        used during init (it's a set and not a reset then)
        or later (e.g. message sent, or cancellation of an edition
        @param idem_data(dict, None): data as in __init__
        if item_data is None:
   = True
            item_data = {'id': None,
                         # TODO: find a best author value
   = False
        self.item = Item(item_data)
        self.author_jid = if else self.item.author_jid
        if self.author_jid is None and self.service and self.service.node:
            self.author_jid = self.service
        self.mode = C.ENTRY_MODE_TEXT if self.item.content_xhtml is None else C.ENTRY_MODE_XHTML

    def refresh(self):
        """Refresh the display when data have been modified"""

    def setEditable(self, editable=True):
        """tell if the entry can be edited or not

        @param editable(bool): True if the entry can be edited
        #XXX: we don't use @property as property setter doesn't play well with pyjamas
        raise NotImplementedError

    def addComments(self, comments_data):
        """Add comments to this entry by calling addEntry repeatidly

        @param comments_data(tuple): data as returned by mbGetFromMany*RTResults
        # TODO: manage seperator between comments of coming from different services/nodes
        for data in comments_data:
            service, node, failure, comments, metadata = data
            for comment in comments:
                if not failure:
                    self.addEntry(comment, service=jid.JID(service), node=node)
                    log.warning("getting comment failed: {}".format(failure))

    def send(self):
        """Send entry according to parent QuickBlog configuration and current level"""

        # keys to keep other than content*, title* and tag*
        # FIXME: see how to avoid comments node hijacking (someone could bind his post to another post's comments node)
        keys_to_keep = ('id', 'comments', 'author', 'author_jid', 'published')

        mb_data = {}
        for key in keys_to_keep:
            value = getattr(self.item, key)
            if value is not None:
                mb_data[key] = unicode(value)

        for prefix in ('content', 'title'):
            for suffix in ('', '_rich', '_xhtml'):
                name = '{}{}'.format(prefix, suffix)
                value = getattr(self.item, name)
                if value is not None:
                    mb_data[name] = value

        common.iter2dict('tag', self.item.tags, mb_data)

        if not in (C.PUBLIC, C.GROUP):
            raise NotImplementedError

        if self.level == 0:
            mb_data["allow_comments"] = C.BOOL_TRUE

        if == C.GROUP:
            common.iter2dict('group',, mb_data)
            unicode(self.service or ''),
            self.node or '',

    def delete(self):
        """Remove this Entry from parent manager

        This doesn't delete any entry in PubSub, just locally
        all children entries will be recursively removed too
        # XXX: named delete and not remove to avoid conflict with pyjamas
        log.debug(u"deleting entry {}".format('EDIT ENTRY' if else
        for child in self.entries:
        except ValueError:
            if self != self.manager.edit_entry:
                log.error(u"Internal Error: entry not found in manager")
                self.manager.edit_entry = None
        if not
            # we must remove references to self
            # in QuickBlog's dictionary
            if self.item.comments:
                comments_tuple = (self.item.comments_service,
                other_entries =[comments_tuple].remove(self)
                if not other_entries:

    def retract(self):
        """Retract this item from microblog node

        if there is a comments node, it will be purged too
        # TODO: manage several comments nodes case.
        if self.item.comments:
   or "", self.item.comments_node, or ""), self.node or "",,

class QuickBlog(EntriesManager, quick_widgets.QuickWidget):

    def __init__(self, host, targets, profiles=None):
        """Panel used to show microblog

        @param targets (tuple(unicode)): contact groups displayed in this panel.
            If empty, show all microblogs from all contacts. targets is also used
            to know where to send new messages.
        EntriesManager.__init__(self, None)
        self.id2entries = {} # used to find an entry with it's item id
                             # must be kept up-to-date by Entry
        self.node2entries = {} # same as above, values are lists in case of
                               # two entries link to the same comments node
        if not targets:
            targets = () # XXX: we use empty tuple instead of None to workaround a pyjamas bug
            quick_widgets.QuickWidget.__init__(self, host, targets, C.PROF_KEY_NONE)
            self._targets_type = C.ALL
            assert isinstance(targets[0], basestring)
            quick_widgets.QuickWidget.__init__(self, host, targets[0], C.PROF_KEY_NONE)
            for target in targets[1:]:
                assert isinstance(target, basestring)
            self._targets_type = C.GROUP

    def new_message_target(self):
        if self._targets_type == C.ALL:
            return C.PUBLIC
        elif self._targets_type == C.GROUP:
            return C.GROUP
            raise ValueError("Unkown targets type")

    def __str__(self):
        return u"Blog Widget [target: {}, profile: {}]".format(', '.join(self.targets), self.profile)

    def _getResultsCb(self, data, rt_session):
        remaining, results = data
        log.debug("Got {got_len} results, {rem_len} remaining".format(got_len=len(results), rem_len=remaining))
        for result in results:
            service, node, failure, items, metadata = result
            if not failure:
                self._addMBItemsWithComments((items, metadata), service=jid.JID(service))

        if remaining:

    def _getResultsEb(self, failure):
        log.warning("microblog getFromMany error: {}".format(failure))

    def _getResults(self, rt_session):
        """Manage results from mbGetFromMany RT Session

        @param rt_session(str): session id as returned by mbGetFromMany
        """, profile=self.profile,
                                               callback=lambda data:self._getResultsCb(data, rt_session),

    def getAll(self):
        """Get all (micro)blogs from self.targets"""
        def gotSession(rt_session):

        if self._targets_type in (C.ALL, C.GROUP):
            targets = tuple(self.targets) if self._targets_type is C.GROUP else ()
  , targets, 10, 10, {}, {"subscribe":C.BOOL_TRUE}, profile=self.profile, callback=gotSession)
            own_pep =
  , (unicode(own_pep),), 10, 10, {}, {}, profile=self.profile, callback=gotSession)
            raise NotImplementedError(u'{} target type is not managed'.format(self._targets_type))

    def isJidAccepted(self, jid_):
        """Tell if a jid is actepted and must be shown in this panel

        @param jid_(jid.JID): jid to check
        @return: True if the jid is accepted
        if self._targets_type == C.ALL:
            return True
        assert self._targets_type is C.GROUP # we don't manage other types for now
        for group in self.targets:
            if[self.profile].isEntityInGroup(jid_, group):
                return True
        return False

    def addEntryIfAccepted(self, service, node, mb_data, groups, profile):
        """add entry to this panel if it's acceptable

        This method check if the entry is new or an update,
        if it below to a know node, or if it acceptable anyway
        @param service(jid.JID): jid of the emitting pubsub service
        @param node(unicode): node identifier
        @param mb_data: microblog data
        @param groups(list[unicode], None): groups which can receive this entry
            None to accept everything
        @param profile: %(doc_profile)s
            entry = self.id2entries[mb_data['id']]
        except KeyError:
            # The entry is new
                parent_entries = self.node2entries[(service, node)]
                # The node is unknown,
                # we need to check that we can accept the entry
                if (self.isJidAccepted(service)
                    or (groups is None and service ==[self.profile].whoami.bare)
                    or (groups and groups.intersection(self.targets))):
                    self.addEntry(mb_data, service=service, node=node)
                # the entry is a comment in a known node
                for parent_entry in parent_entries:
                    parent_entry.addEntry(mb_data, service=service, node=node)
            # The entry exist, it's an update

    def deleteEntryIfPresent(self, service, node, item_id, profile):
        """Delete and entry if present in this QuickBlog

        @param sender(jid.JID): jid of the entry sender
        @param mb_data: microblog data
        @param service(jid.JID): sending service
        @param node(unicode): hosting node
            entry = self.id2entries[item_id]
        except KeyError:

def registerClass(type_, cls):
    if type_ == "ENTRY":
        ENTRY_CLS = cls
    elif type == "COMMENT":
        COMMENTS_CLS = cls
        raise ValueError("type_ should be ENTRY or COMMENT")
    if COMMENTS_CLS is None: