# HG changeset patch # User souliane # Date 1417131087 -3600 # Node ID 3eb3a2c0c011c286155a107c3f7cdb0a7463833f # Parent bade589dbd5a6ae1f9fc1498bcf0797a8832fe29 browser and server side: uses RSM (XEP-0059) diff -r bade589dbd5a -r 3eb3a2c0c011 server_css/blog.css --- a/server_css/blog.css Thu Oct 23 16:56:36 2014 +0200 +++ b/server_css/blog.css Fri Nov 28 00:31:27 2014 +0100 @@ -40,7 +40,13 @@ color: rgb(51, 51, 51); } +.mblog_comments{ +} + .mblog_comment { + position: relative; + left: 2%; + width: 56%; } .mblog_header { @@ -51,6 +57,28 @@ width: 100%; } +.mblog_header_main:hover { + background-color: #f0f0f0; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + border-radius: 5px; +} + +.mblog_footer { + font-size: small; + border-top: 1px dashed LightGrey; + color: gray; + display: table; + width: 100%; +} + +.mblog_footer_main:hover { + background-color: #f0f0f0; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + border-radius: 5px; +} + .mblog_metadata { display: table-row; width: 100%; @@ -74,6 +102,44 @@ text-decoration: none; } +.comments_link { + text-decoration: none; + text-align: center; + display: block; +} + +.header { + width: 60%; + margin: auto; + margin-bottom: 20px; + text-align: center; +} + +.footer { + width: 100%; + text-align:center; +} + +.later_message { + text-decoration: none; + float: left; +} + +.later_messages { + text-decoration: none; + text-align:center; +} + +.older_message { + text-decoration: none; + float: right; +} + +.older_messages { + text-decoration: none; + text-align:center; +} + .mblog_entry h1, h2, h3, h4, h5, h6 { border-bottom: 1px solid rgb(170, 170, 170); } diff -r bade589dbd5a -r 3eb3a2c0c011 src/browser/libervia_main.py --- a/src/browser/libervia_main.py Thu Oct 23 16:56:36 2014 +0200 +++ b/src/browser/libervia_main.py Fri Nov 28 00:31:27 2014 +0100 @@ -132,7 +132,7 @@ def __init__(self): LiberviaJsonProxy.__init__(self, "/json_api", ["getContacts", "addContact", "sendMessage", "sendMblog", "sendMblogComment", - "getLastMblogs", "getMassiveLastMblogs", "getMblogComments", "getProfileJid", + "getMblogs", "getMassiveMblogs", "getMblogComments", "getProfileJid", "getHistory", "getPresenceStatuses", "joinMUC", "mucLeave", "getRoomsJoined", "inviteMUC", "launchTarotGame", "getTarotCardsPaths", "tarotGameReady", "tarotGamePlayCards", "launchRadioCollective", "getMblogs", "getMblogsWithComments", @@ -203,6 +203,7 @@ self.initialised = False self.init_cache = [] # used to cache events until initialisation is done self.cached_params = {} + self.next_rsm_index = 0 def importPlugins(self): self.plugins = {} @@ -541,10 +542,15 @@ if not xml_ui: self.panel.menu.removeItemParams() - def _ownBlogsFills(self, mblogs): - #put our own microblogs in cache, then fill all panels with them + def _ownBlogsFills(self, mblogs, mblog_panel=None): + """Put our own microblogs in cache, then fill the panels with them. + + @param mblogs (dict): dictionary mapping a publisher JID to blogs data. + @param mblog_panel (MicroblogPanel): the panel to fill, or all if None. + """ + cache = [] for publisher in mblogs: - for mblog in mblogs[publisher]: + for mblog in mblogs[publisher][0]: if 'content' not in mblog: log.warning("No content found in microblog [%s]" % mblog) continue @@ -553,13 +559,19 @@ else: _groups = None mblog_entry = panels.MicroblogItem(mblog) - self.mblog_cache.append((_groups, mblog_entry)) + cache.append((_groups, mblog_entry)) + self.mblog_cache.extend(cache) if len(self.mblog_cache) > MAX_MBLOG_CACHE: del self.mblog_cache[0:len(self.mblog_cache - MAX_MBLOG_CACHE)] - for lib_wid in self.libervia_widgets: + + widget_list = [mblog_panel] if mblog_panel else self.libervia_widgets + for lib_wid in widget_list: if isinstance(lib_wid, panels.MicroblogPanel): - self.FillMicroblogPanel(lib_wid) + self.fillMicroblogPanel(lib_wid, cache) + + if self.initialised: + return self.initialised = True # initialisation phase is finished here for event_data in self.init_cache: # so we have to send all the cached events self._personalEventCb(*event_data) @@ -577,18 +589,38 @@ for lib_wid in self.libervia_widgets: if isinstance(lib_wid, panels.MicroblogPanel): if lib_wid.accept_all(): - self.bridge.call('getMassiveLastMblogs', lib_wid.massiveInsert, 'ALL', [], 10) + self.bridge.call('getMassiveMblogs', lib_wid.massiveInsert, 'ALL', []) else: - self.bridge.call('getMassiveLastMblogs', lib_wid.massiveInsert, 'GROUP', lib_wid.accepted_groups, 10) + self.bridge.call('getMassiveMblogs', lib_wid.massiveInsert, 'GROUP', lib_wid.accepted_groups) #we ask for our own microblogs: - self.bridge.call('getMassiveLastMblogs', self._ownBlogsFills, 'JID', [self.whoami.bare], 10) + self.loadOurMainEntries() # initialize plugins which waited for the connection to be done for plugin in self.plugins.values(): if hasattr(plugin, 'profileConnected'): plugin.profileConnected() + def loadOurMainEntries(self, index=0, mblog_panel=None): + """Load a page of our own blogs from the cache or ask them to the + backend. Then fill the panels with them. + + @param index (int): starting index of the blog page to retrieve. + @param mblog_panel (MicroblogPanel): the panel to fill, or all if None. + """ + delta = index - self.next_rsm_index + if delta < 0: + assert(mblog_panel is not None) + self.fillMicroblogPanel(mblog_panel, self.mblog_cache[index:index + C.RSM_MAX_ITEMS]) + return + + def cb(result): + self._ownBlogsFills(result, mblog_panel) + + rsm = {'max': str(delta + C.RSM_MAX_ITEMS), 'index': str(self.next_rsm_index)} + self.bridge.call('getMassiveMblogs', cb, 'JID', [self.whoami.bare], rsm) + self.next_rsm_index = index + C.RSM_MAX_ITEMS + ## Signals callbacks ## def _personalEventCb(self, sender, event_type, data): @@ -647,12 +679,12 @@ or (_groups and _groups.intersection(mblog_panel.accepted_groups)): mblog_panel.addEntry(mblog_entry) - def FillMicroblogPanel(self, mblog_panel): + def fillMicroblogPanel(self, mblog_panel, mblogs): """Fill a microblog panel with entries in cache @param mblog_panel: MicroblogPanel instance """ #XXX: only our own entries are cached - for cache_entry in self.mblog_cache: + for cache_entry in mblogs: _groups, mblog_entry = cache_entry self.addBlogEntry(mblog_panel, self.whoami.bare, *cache_entry) @@ -661,7 +693,7 @@ for lib_wid in self.libervia_widgets: if isinstance(lib_wid, panels.MicroblogPanel): if lib_wid.isJidAccepted(entity): - self.bridge.call('getMassiveLastMblogs', lib_wid.massiveInsert, 'JID', [entity], 10) + self.bridge.call('getMassiveMblogs', lib_wid.massiveInsert, 'JID', [entity]) def getLiberviaWidget(self, class_, entity, ignoreOtherTabs=True): """Get the corresponding panel if it exists. diff -r bade589dbd5a -r 3eb3a2c0c011 src/browser/public/libervia.css --- a/src/browser/public/libervia.css Thu Oct 23 16:56:36 2014 +0200 +++ b/src/browser/public/libervia.css Fri Nov 28 00:31:27 2014 +0100 @@ -827,6 +827,21 @@ width: 100%; } +.microblogPanel_footer { + cursor: pointer; + text-align: center; + background-color: #ededed; + border-radius: 5px; + width: 85%; + margin: auto; + margin-top: 5px; + margin-bottom: 5px; +} + +.microblogPanel_footer a { + color: blue; +} + .microblogNewButton { width: 100%; height: 35px; @@ -845,18 +860,32 @@ .mb_entry_header { - cursor: pointer; + width: 100%; } -.selected_widget .selected_entry .mb_entry_header +.mb_entry_header_info { + cursor: pointer; + padding: 0px 5px 0px 5px; +} + +.selected_widget .selected_entry .mb_entry_header_info { background: #cf2828; border-radius: 5px 5px 0px 0px; } +.mb_entry_comments { + float: right; + padding-right: 5px; +} + +.mb_entry_comments a { + color: blue; + cursor: pointer; +} + .mb_entry_author { font-weight: bold; - padding-left: 5px; } .mb_entry_avatar { diff -r bade589dbd5a -r 3eb3a2c0c011 src/browser/sat_browser/panels.py --- a/src/browser/sat_browser/panels.py Thu Oct 23 16:56:36 2014 +0200 +++ b/src/browser/sat_browser/panels.py Fri Nov 28 00:31:27 2014 +0100 @@ -23,7 +23,7 @@ from sat_frontends.tools.strings import addURLToText from sat_frontends.tools.games import SYMBOLS -from sat.core.i18n import _ +from sat.core.i18n import _, D_ from pyjamas.ui.SimplePanel import SimplePanel from pyjamas.ui.AbsolutePanel import AbsolutePanel @@ -406,7 +406,7 @@ self.panel = FlowPanel() self.panel.setStyleName('mb_entry') - self.header = HTMLPanel('') + self.header = HorizontalPanel(StyleName='mb_entry_header') self.panel.add(self.header) self.entry_actions = VerticalPanel() @@ -442,18 +442,56 @@ self.__setIcons() def __setHeader(self): - """Set the entry header""" + """Set the entry header.""" if self.empty: return update_text = u" — ✍ " + "%s" % datetime.fromtimestamp(self.updated) - self.header.setHTML("""
- on - %(updated)s -
""" % {'author': html_tools.html_sanitize(self.author), - 'published': datetime.fromtimestamp(self.published), - 'updated': update_text if self.published != self.updated else '' - } - ) + self.header.add(HTML(""" + on + %(updated)s + """ % {'author': html_tools.html_sanitize(self.author), + 'published': datetime.fromtimestamp(self.published), + 'updated': update_text if self.published != self.updated else '' + })) + if self.comments: + self.comments_count = self.hidden_count = 0 + self.show_comments_link = HTML('') + self.header.add(self.show_comments_link) + + def updateHeader(self, comments_count=None, hidden_count=None, inc=None): + """Update the header. + + @param comments_count (int): total number of comments. + @param hidden_count (int): number of hidden comments. + @param inc (int): number to increment the total number of comments with. + """ + if comments_count is not None: + self.comments_count = comments_count + if hidden_count is not None: + self.hidden_count = hidden_count + if inc is not None: + self.comments_count += inc + + if self.hidden_count > 0: + comments = D_('comments') if self.hidden_count > 1 else D_('comment') + text = D_("show %(count)d previous %(comments)s") % {'count': self.hidden_count, + 'comments': comments} + if self not in self.show_comments_link._clickListeners: + self.show_comments_link.addClickListener(self) + else: + if self.comments_count > 1: + text = "%(count)d %(comments)s" % {'count': self.comments_count, + 'comments': D_('comments')} + elif self.comments_count == 1: + text = D_('1 comment') + else: + text = '' + try: + self.show_comments_link.removeClickListener(self) + except ValueError: + pass + + self.show_comments_link.setHTML("""%(text)s""" % {'text': text}) def __setIcons(self): """Set the entry icons (delete, update, comment)""" @@ -490,6 +528,8 @@ self.edit(True) elif sender == self.comment_label: self._comment() + elif sender == self.show_comments_link: + self._blog_panel.loadAllCommentsForEntry(self) def __modifiedCb(self, content): """Send the new content to the backend @@ -517,7 +557,7 @@ """Remove the entry if it was an empty one (used for creating a new blog post). Data for the actual new blog post will be received from the bridge""" if self.empty: - self._blog_panel.removeEntry(self.type, self.id) + self._blog_panel.removeEntry(self.type, self.id, update_header=False) if self.type == 'main_item': # restore the "New message" button self._blog_panel.refresh() else: # allow to create a new comment @@ -589,7 +629,7 @@ 'service': self.comments_service, 'node': self.comments_node } - entry = self._blog_panel.addEntry(data) + entry = self._blog_panel.addEntry(data, update_header=False) if entry is None: log.info("The entry of id %s can not be commented" % self.id) return @@ -662,7 +702,7 @@ self._blog_panel.host.bridge.call('syntaxConvert', setBubble, text, C.SYNTAX_TEXT, C.SYNTAX_XHTML) -class MicroblogPanel(base_widget.LiberviaWidget): +class MicroblogPanel(base_widget.LiberviaWidget, MouseHandler): warning_msg_public = "This message will be PUBLIC and everybody will be able to see it, even people you don't know" warning_msg_group = "This message will be published for all the people of the group %s" @@ -671,6 +711,7 @@ @param accepted_groups: groups displayed in this panel, if empty, show all microblogs from all contacts """ base_widget.LiberviaWidget.__init__(self, host, ", ".join(accepted_groups), selectable=True) + MouseHandler.__init__(self) self.setAcceptedGroup(accepted_groups) self.host = host self.entries = {} @@ -679,6 +720,12 @@ self.vpanel = VerticalPanel() self.vpanel.setStyleName('microblogPanel') self.setWidget(self.vpanel) + self.footer = HTML('', StyleName='microblogPanel_footer') + self.footer.waiting = False + self.footer.addClickListener(self) + self.footer.addMouseListener(self) + self.vpanel.add(self.footer) + self.next_rsm_index = 0 def refresh(self): """Refresh the display of this widget. If the unibox is disabled, @@ -694,7 +741,7 @@ 'new': True, 'author': self.host.whoami.bare, } - entry = self.addEntry(data) + entry = self.addEntry(data, update_header=False) entry.edit(True) if NEW_MESSAGE_USE_BUTTON: self.new_button = Button("New message", listener=addBox) @@ -708,10 +755,9 @@ @return (MicroblogEntry): the new entry being edited. """ - try: - first = self.vpanel.children[0] - except IndexError: - return None + if len(self.vpanel.children) < 2: + return None # there's only the footer + first = self.vpanel.children[0] assert(first.type == 'main_item') return first if first.empty else None @@ -729,11 +775,9 @@ @return: the created MicroblogPanel """ _items = item if isinstance(item, list) else ([] if item is None else [item]) - _type = 'ALL' if _items == [] else 'GROUP' # XXX: pyjamas doesn't support use of cls directly _new_panel = MicroblogPanel(host, _items) - host.FillMicroblogPanel(_new_panel) - host.bridge.call('getMassiveLastMblogs', _new_panel.massiveInsert, _type, _items, 10) + _new_panel.loadMoreMainEntries() host.setSelected(_new_panel) _new_panel.refresh() return _new_panel @@ -747,6 +791,27 @@ def accepted_groups(self): return self._accepted_groups + def loadAllCommentsForEntry(self, main_entry): + """Load all the comments for the given main entry. + + @param main_entry (MicroblogEntry): main entry having comments. + """ + index = str(main_entry.comments_count - main_entry.hidden_count) + rsm = {'max': str(main_entry.hidden_count), 'index': index} + self.host.bridge.call('getMblogComments', self.mblogsInsert, main_entry.comments_service, main_entry.comments_node, rsm) + + def loadMoreMainEntries(self): + if self.footer.waiting: + return + self.footer.waiting = True + self.footer.setHTML("loading...") + + self.host.loadOurMainEntries(self.next_rsm_index, self) + + type_ = 'ALL' if self.accepted_groups == [] else 'GROUP' + rsm = {'max': str(C.RSM_MAX_ITEMS), 'index': str(self.next_rsm_index)} + self.host.bridge.call('getMassiveMblogs', self.massiveInsert, type_, self.accepted_groups, rsm) + def matchEntity(self, item): """ @param item: single group as a string, list of groups @@ -802,61 +867,84 @@ def massiveInsert(self, mblogs): """Insert several microblogs at once - @param mblogs: dictionary of microblogs, as the result of getMassiveLastGroupBlogs + @param mblogs (dict): dictionary mapping a publisher to microblogs data: + - key: publisher (str) + - value: couple (list[dict], dict) with: + - list of microblogs data + - RSM response data """ - count = sum([len(value) for value in mblogs.values()]) - log.debug("Massive insertion of %d microblogs" % count) + count_pub = len(mblogs) + count_msg = sum([len(value) for value in mblogs.values()]) + log.debug("massive insertion of {count_msg} blogs for {count_pub} contacts".format(count_msg=count_msg, count_pub=count_pub)) for publisher in mblogs: - log.debug("adding blogs for [%s]" % publisher) - for mblog in mblogs[publisher]: - if not "content" in mblog: - log.warning("No content found in microblog [%s]" % mblog) - continue - self.addEntry(mblog) + log.debug("adding {count} blogs for [{publisher}]".format(count=len(mblogs[publisher]), publisher=publisher)) + self.mblogsInsert(mblogs[publisher]) + self.next_rsm_index += C.RSM_MAX_ITEMS + self.footer.waiting = False + self.footer.setHTML('show older messages') def mblogsInsert(self, mblogs): - """ Insert several microblogs at once - @param mblogs: list of microblogs + """ Insert several microblogs from the same node at once. + + @param mblogs (list): couple (list[dict], dict) with: + - list of microblogs data + - RSM response data """ + mblogs, rsm = mblogs + for mblog in mblogs: - if not "content" in mblog: + if "content" not in mblog: log.warning("No content found in microblog [%s]" % mblog) continue - self.addEntry(mblog) + self.addEntry(mblog, update_header=False) + + hashes = set([(entry['service'], entry['node']) for entry in mblogs if entry['type'] == 'comment']) + assert(len(hashes) < 2) # ensure the blogs come from the same node + if len(hashes) == 1: + main_entry = self.comments[hashes.pop()] + count = int(rsm['count']) + hidden = count - (int(rsm['index']) + len(mblogs)) + main_entry.updateHeader(count, hidden) def _chronoInsert(self, vpanel, entry, reverse=True): """ Insert an entry in chronological order @param vpanel: VerticalPanel instance @param entry: MicroblogEntry @param reverse: more recent entry on top if True, chronological order else""" + # XXX: for now we can't use "published" timestamp because the entries + # are retrieved using the "updated" field. We don't want new items + # inserted with RSM to be inserted "randomly" in the panel, they + # should be added at the bottom of the list. assert(isinstance(reverse, bool)) if entry.empty: - entry.published = time() + entry.updated = time() # we look for the right index to insert our entry: # if reversed, we insert the entry above the first entry # in the past idx = 0 - for child in vpanel.children: + for child in vpanel.children[0:-1]: # ignore the footer if not isinstance(child, MicroblogEntry): idx += 1 continue - condition_to_stop = child.empty or (child.published > entry.published) + condition_to_stop = child.empty or (child.updated > entry.updated) if condition_to_stop != reverse: # != is XOR break idx += 1 vpanel.insert(entry, idx) - def addEntry(self, data): + def addEntry(self, data, update_header=True): """Add an entry to the panel - @param data: dict containing the item data - @return: the added entry, or None + + @param data (dict): dict containing the item data + @param update_header (bool): update or not the main comment header + @return: the added MicroblogEntry instance, or None """ _entry = MicroblogEntry(self, data) if _entry.type == "comment": comments_hash = (_entry.service, _entry.node) - if not comments_hash in self.comments: + if comments_hash not in self.comments: # The comments node is not known in this panel return None parent = self.comments[comments_hash] @@ -871,6 +959,7 @@ sub_panel.setStyleName('microblogPanel') sub_panel.addStyleName('subPanel') self.vpanel.insert(sub_panel, parent_idx + 1) + for idx in xrange(0, len(sub_panel.getChildren())): comment = sub_panel.getIndexedChild(idx) if comment.id == _entry.id: @@ -880,28 +969,37 @@ return _entry # we want comments to be inserted in chronological order self._chronoInsert(sub_panel, _entry, reverse=False) + if update_header: + parent.updateHeader(inc=+1) return _entry - if _entry.id in self.entries: # update - idx = self.vpanel.getWidgetIndex(self.entries[_entry.id]) - self.vpanel.remove(self.entries[_entry.id]) - self.vpanel.insert(_entry, idx) - else: # new entry - self._chronoInsert(self.vpanel, _entry) - self.entries[_entry.id] = _entry - if _entry.comments: # entry has comments, we keep the comments service/node as a reference comments_hash = (_entry.comments_service, _entry.comments_node) self.comments[comments_hash] = _entry - self.host.bridge.call('getMblogComments', self.mblogsInsert, _entry.comments_service, _entry.comments_node) + + if _entry.id in self.entries: # update + old_entry = self.entries[_entry.id] + idx = self.vpanel.getWidgetIndex(old_entry) + counts = (old_entry.comments_count, old_entry.hidden_count) + self.vpanel.remove(old_entry) + self.vpanel.insert(_entry, idx) + _entry.updateHeader(*counts) + else: # new entry + self._chronoInsert(self.vpanel, _entry) + if _entry.comments: + self.host.bridge.call('getMblogComments', self.mblogsInsert, _entry.comments_service, _entry.comments_node) + + self.entries[_entry.id] = _entry return _entry - def removeEntry(self, type_, id_): + def removeEntry(self, type_, id_, update_header=True): """Remove an entry from the panel - @param type_: entry type ('main_item' or 'comment') - @param id_: entry id + + @param type_ (str): entry type ('main_item' or 'comment') + @param id_ (str): entry id + @param update_header (bool): update or not the main comment header """ for child in self.vpanel.getChildren(): if isinstance(child, MicroblogEntry) and type_ == 'main_item': @@ -919,6 +1017,9 @@ elif isinstance(child, VerticalPanel) and type_ == 'comment': for comment in child.getChildren(): if comment.id == id_: + if update_header: + hash_ = (comment.service, comment.node) + self.comments[hash_].updateHeader(inc=-1) comment.removeFromParent() self.selected_entry = None break @@ -998,6 +1099,14 @@ return True return False + def onClick(self, sender): + if sender == self.footer: + self.loadMoreMainEntries() + + def onMouseEnter(self, sender): + if sender == self.footer: + self.loadMoreMainEntries() + class StatusPanel(base_panels.HTMLTextEditor): diff -r bade589dbd5a -r 3eb3a2c0c011 src/common/constants.py --- a/src/common/constants.py Thu Oct 23 16:56:36 2014 +0200 +++ b/src/common/constants.py Fri Nov 28 00:31:27 2014 +0100 @@ -51,3 +51,7 @@ # Default avatar DEFAULT_AVATAR = "/media/misc/default_avatar.png" + + RSM_MAX_ITEMS = 5 + RSM_MAX_COMMENTS = 5 + diff -r bade589dbd5a -r 3eb3a2c0c011 src/server/blog.py --- a/src/server/blog.py Thu Oct 23 16:56:36 2014 +0200 +++ b/src/server/blog.py Fri Nov 28 00:31:27 2014 +0100 @@ -17,7 +17,7 @@ # 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 _ +from sat.core.i18n import _, D_ from sat_frontends.tools.strings import addURLToText from sat.core.log import getLogger log = getLogger(__name__) @@ -130,25 +130,52 @@ pub_jid = JID(pub_jid_s) d2 = defer.Deferred() item_id = None - try: - max_items = int(request.args['max_items'][0]) - except (ValueError, KeyError): - max_items = 10 + atom = None + rsm_ = {} + if len(request.postpath) > 1: if request.postpath[1] == 'atom.xml': # return the atom feed - d2.addCallbacks(self.render_atom_feed, self.render_error_blog, [request], None, [request, prof_found], None) - self.host.bridge.getLastGroupBlogsAtom(pub_jid.userhost(), max_items, C.SERVICE_PROFILE, d2.callback, d2.errback) - return - try: # check if the given path is a valid UUID - uuid.UUID(request.postpath[1]) - item_id = request.postpath[1] - except ValueError: - pass + atom = True + else: + try: # check if the given path is a valid UUID + uuid.UUID(request.postpath[1]) + item_id = request.postpath[1] + except ValueError: + pass + + # retrieve RSM request data from URL parameters + try: + max_items = int(request.args['max'][0]) + except (ValueError, KeyError): + max_items = C.RSM_MAX_ITEMS if item_id else C.RSM_MAX_COMMENTS + rsm_['max'] = unicode(max_items) + try: + rsm_['index'] = request.args['index'][0] + except (ValueError, KeyError): + try: + rsm_['before'] = request.args['before'][0] + except KeyError: + try: + rsm_['after'] = request.args['after'][0] + except KeyError: + pass + + if atom is not None: + d2.addCallbacks(self.render_atom_feed, self.render_error_blog, [request], None, [request, prof_found], None) + self.host.bridge.getGroupBlogsAtom(pub_jid.userhost(), rsm_, C.SERVICE_PROFILE, d2.callback, d2.errback) + return + d2.addCallbacks(self.render_html_blog, self.render_error_blog, [request, prof_found], None, [request, prof_found], None) - if item_id: # display one message and its comments - self.host.bridge.getGroupBlogsWithComments(pub_jid.userhost(), [item_id], C.SERVICE_PROFILE, d2.callback, d2.errback) - else: # display the last messages without comment - self.host.bridge.getLastGroupBlogs(pub_jid.userhost(), max_items, C.SERVICE_PROFILE, d2.callback, d2.errback) + if item_id: + if max_items > 0: # display one message and its comments + self.host.bridge.getGroupBlogsWithComments(pub_jid.userhost(), [item_id], {}, max_items, C.SERVICE_PROFILE, d2.callback, d2.errback) + else: # display one message, count its comments + self.host.bridge.getGroupBlogs(pub_jid.userhost(), [item_id], {}, True, C.SERVICE_PROFILE, d2.callback, d2.errback) + else: + if max_items == 1: # display one message and its comments + self.host.bridge.getGroupBlogsWithComments(pub_jid.userhost(), [], rsm_, C.RSM_MAX_COMMENTS, C.SERVICE_PROFILE, d2.callback, d2.errback) + else: # display the last messages, count their comments + self.host.bridge.getGroupBlogs(pub_jid.userhost(), [], rsm_, True, C.SERVICE_PROFILE, d2.callback, d2.errback) d1 = defer.Deferred() JID(self.host.bridge.asyncGetParamA('JabberID', 'Connection', 'value', C.SERVER_SECURITY_LIMIT, prof_found, callback=d1.callback, errback=d1.errback)) @@ -158,7 +185,12 @@ def render_html_blog(self, mblog_data, request, profile): """Retrieve the user parameters before actually rendering the static blog - @param mblog_data: list of microblog data or list of couple (microblog data, list of microblog data) + + @param mblog_data (list): couple (list, dict) with: + - a list of microblog data, or a list of couple containing: + - microblog data (main item) + - couple (comments data, RSM response data for the comments) + - RSM response data for the main items @param request: HTTP request @param profile """ @@ -184,7 +216,12 @@ """Actually render the static blog. If mblog_data is a list of dict, we are missing the comments items so we just display the main items. If mblog_data is a list of couple, each couple is associating a main item data with the list of its comments, so we render all. - @param mblog_data: list of microblog data or list of couple (microblog data, list of microblog data) + + @param mblog_data (list): couple (list, dict) with: + - a list of microblog data, or a list of couple containing: + - microblog data (main item) + - couple (comments data, RSM response data for the comments) + - RSM response data for the main items @param options: dict defining the blog's parameters @param request: the HTTP request @profile @@ -225,13 +262,74 @@ 'title': getOption(C.STATIC_BLOG_PARAM_TITLE) or "%s's microblog" % user, 'favicon': os.path.normpath(root_url + getOption('avatar')), 'banner_elt': getImageOption(C.STATIC_BLOG_PARAM_BANNER, getOption(C.STATIC_BLOG_PARAM_TITLE) or user)}) - mblog_data = [(entry if isinstance(entry, tuple) else (entry, [])) for entry in mblog_data] - mblog_data = sorted(mblog_data, key=lambda entry: (-float(entry[0].get('published', 0)))) - for entry in mblog_data: - self.__render_html_entry(entry[0], base_url, request) - comments = sorted(entry[1], key=lambda entry: (float(entry.get('published', 0)))) + mblog_data, main_rsm = mblog_data + display_single = len(mblog_data) == 1 + + # build the navigation links + count = int(main_rsm['count']) if 'count' in main_rsm else 0 + if count > 0: + index = int(main_rsm['index']) + if index > 0: + before_link = ("%(base)s?before=%(item_id)s" % {'base': base_url, 'item_id': main_rsm['first']}).encode('utf-8') + if display_single: + before_link += '&max=1' + tmp_text = D_("Later message") + class_ = 'later_message' + else: + tmp_text = D_("Later messages") + class_ = 'later_messages' + before_tag = """%(text)s""" % {'link': before_link, 'class': class_, 'text': tmp_text} + else: + before_tag = None + if index + len(mblog_data) < count: + after_link = ("%(base)s?after=%(item_id)s" % {'base': base_url, 'item_id': main_rsm['last']}).encode('utf-8') + if display_single: + after_link += '&max=1' + text = D_("Older message") + class_ = 'older_message' + else: + text = D_("Older messages") + class_ = 'older_messages' + after_tag = """%(text)s""" % {'link': after_link, 'class': class_, 'text': text} + else: + after_tag = None + + # display navigation header + request.write("""
""") + if before_tag: + request.write(before_tag) + request.write(" ") + if display_single and after_tag: + request.write(after_tag) + request.write("""
""") + + mblog_data = [(entry if isinstance(entry, tuple) else (entry, ([], {}))) for entry in mblog_data] + mblog_data = sorted(mblog_data, key=lambda entry: (-float(entry[0].get('updated', 0)))) + for main_data, comments_data in mblog_data: + self.__render_html_entry(main_data, base_url, request) + comments, comments_rsm = comments_data + + # eventually display the link to show all comments + comments_count = int(main_data['comments_count']) + delta = comments_count - len(comments) + if display_single and delta > 0: + link = ("%(base)s/%(item_id)s?max=%(max)s" % {'base': base_url, + 'item_id': main_data['id'], + 'max': main_data['comments_count']}).encode('utf-8') + text = D_("Show %(count)d previous %(comments)s") % {'count': delta, + 'comments': D_('comments') if delta > 1 else D_('comment')} + request.write("""%(text)s""" % {'link': link, 'text': text}) + + comments = sorted(comments, key=lambda entry: (float(entry.get('published', 0)))) for comment in comments: self.__render_html_entry(comment, base_url, request) + + # display navigation footer + request.write("""""") + request.write('') request.finish() @@ -244,12 +342,6 @@ timestamp = float(entry.get('published', 0)) datetime_ = datetime.fromtimestamp(timestamp) is_comment = entry['type'] == 'comment' - if is_comment: - author = (_("comment from %s") % entry['author']).encode('utf-8') - item_link = '' - else: - author = ' ' - item_link = ("%(base)s/%(item_id)s" % {'base': base_url, 'item_id': entry['id']}).encode('utf-8') def getText(key): if ('%s_xhtml' % key) in entry: @@ -264,12 +356,31 @@ return elem return """%(elem)s""" % {'link': item_link, 'elem': elem} - header = addMainItemLink("""
- -
""" % {'author': author, 'date': datetime_}) + if is_comment: + author = (_("from %s") % entry['author']).encode('utf-8') + item_link = '' + footer = '' + else: + author = ' ' + item_link = ("%(base)s/%(item_id)s" % {'base': base_url, 'item_id': entry['id']}).encode('utf-8') + comments_count = int(entry['comments_count']) + comments_text = (D_('comments') if comments_count > 1 else D_('comment')).encode('utf-8') + footer = addMainItemLink("""""" % {'count': comments_count, + 'comments': comments_text}) + + header = """
+ +
""" % {'author': author, 'date': datetime_, + 'class': '' if is_comment else 'mblog_header_main'} + if not is_comment: + header = addMainItemLink(header) title = addMainItemLink(getText('title')) body = getText('content') @@ -279,11 +390,12 @@ request.write("""
%(header)s %(content)s -
""" % - {'extra_style': 'mblog_comment' if entry['type'] == 'comment' else '', - 'item_link': item_link, - 'header': header, - 'content': body}) + %(footer)s + """ % {'extra_style': 'mblog_comment' if entry['type'] == 'comment' else '', + 'item_link': item_link, + 'header': header, + 'content': body, + 'footer': footer}) def render_atom_feed(self, feed, request): request.write(feed.encode('utf-8')) diff -r bade589dbd5a -r 3eb3a2c0c011 src/server/server.py --- a/src/server/server.py Thu Oct 23 16:56:36 2014 +0200 +++ b/src/server/server.py Fri Nov 28 00:31:27 2014 +0100 @@ -301,51 +301,46 @@ else: raise Exception("Invalid data") - def jsonrpc_getMblogs(self, publisher_jid, item_ids): + def jsonrpc_getMblogs(self, publisher_jid, item_ids, max_items=C.RSM_MAX_ITEMS): """Get specified microblogs posted by a contact @param publisher_jid: jid of the publisher @param item_ids: list of microblogs items IDs @return list of microblog data (dict)""" profile = ISATSession(self.session).profile - d = self.asyncBridgeCall("getGroupBlogs", publisher_jid, item_ids, profile) + d = self.asyncBridgeCall("getGroupBlogs", publisher_jid, item_ids, {'max': unicode(max_items)}, False, profile) return d - def jsonrpc_getMblogsWithComments(self, publisher_jid, item_ids): + def jsonrpc_getMblogsWithComments(self, publisher_jid, item_ids, max_comments=C.RSM_MAX_COMMENTS): """Get specified microblogs posted by a contact and their comments @param publisher_jid: jid of the publisher @param item_ids: list of microblogs items IDs @return list of couple (microblog data, list of microblog data)""" profile = ISATSession(self.session).profile - d = self.asyncBridgeCall("getGroupBlogsWithComments", publisher_jid, item_ids, profile) + d = self.asyncBridgeCall("getGroupBlogsWithComments", publisher_jid, item_ids, {}, max_comments, profile) return d - def jsonrpc_getLastMblogs(self, publisher_jid, max_item): - """Get last microblogs posted by a contact - @param publisher_jid: jid of the publisher - @param max_item: number of items to ask - @return list of microblog data (dict)""" - profile = ISATSession(self.session).profile - d = self.asyncBridgeCall("getLastGroupBlogs", publisher_jid, max_item, profile) - return d - - def jsonrpc_getMassiveLastMblogs(self, publishers_type, publishers_list, max_item): + def jsonrpc_getMassiveMblogs(self, publishers_type, publishers_list, rsm=None): """Get lasts microblogs posted by several contacts at once @param publishers_type: one of "ALL", "GROUP", "JID" @param publishers_list: list of publishers type (empty list of all, list of groups or list of jids) @param max_item: number of items to ask @return: dictionary key=publisher's jid, value=list of microblog data (dict)""" profile = ISATSession(self.session).profile - d = self.asyncBridgeCall("getMassiveLastGroupBlogs", publishers_type, publishers_list, max_item, profile) + if rsm is None: + rsm = {'max': unicode(C.RSM_MAX_ITEMS)} + d = self.asyncBridgeCall("getMassiveGroupBlogs", publishers_type, publishers_list, rsm, profile) self.sat_host.bridge.massiveSubscribeGroupBlogs(publishers_type, publishers_list, profile) return d - def jsonrpc_getMblogComments(self, service, node): + def jsonrpc_getMblogComments(self, service, node, rsm=None): """Get all comments of given node @param service: jid of the service hosting the node @param node: comments node """ profile = ISATSession(self.session).profile - d = self.asyncBridgeCall("getGroupBlogComments", service, node, profile) + if rsm is None: + rsm = {'max': unicode(C.RSM_MAX_COMMENTS)} + d = self.asyncBridgeCall("getGroupBlogComments", service, node, rsm, profile) return d def jsonrpc_getPresenceStatuses(self):