# coding=utf-8
from plone import api
from plone.app.contenttypes.behaviors.leadimage import ILeadImage
from plone.app.contenttypes.content import File
from plone.app.contenttypes.content import Image
from ploneintranet.layout.memoize.view import memoize
from ploneintranet.layout.memoize.view import memoize_contextless
from plone.registry.interfaces import IRegistry
from ploneintranet import api as pi_api
from ploneintranet.activitystream.browser.utils import link_tags
from ploneintranet.activitystream.browser.utils import link_urls
from ploneintranet.activitystream.browser.utils import link_users
from ploneintranet.attachments.utils import IAttachmentStorage
from ploneintranet.core import ploneintranetCoreMessageFactory as _
from ploneintranet.layout.interfaces import IDiazoNoTemplate
from ploneintranet.workspace.utils import in_workspace
from Products.CMFPlone.utils import safe_unicode
from Products.Five.browser import BrowserView
from zope.component import queryUtility
from zope.interface import implementer
import DateTime
import logging
logger = logging.getLogger("ploneintranet.activitystream")
[docs]class StatusUpdateView(BrowserView):
""" This view renders a status update
See templates/post.html for an explanation of the various
rendering modes.
On top of that it also powers templates/comment.html
The API could use some cleanup.
"""
newpostbox_placeholder = _(u"leave_a_comment", default=u"Leave a comment...")
@property
def in_workspace(self):
""" statusupdate context is the portal, independently whether
they are displayed on dashboard or workspace.
Also content_context is the workspace, independently where the post
is displayed. So we must rely on the request here to find out
where the post is displayed, so that we can adjust injection params
properly """
return in_workspace(self.request.PARENTS[0])
@property
@memoize_contextless
def fresh_reply_limit(self):
""" How many replies are considered fresh?
0 means "infinite"
"""
if not self.request.get("all_comments"):
return 3
else:
return 0
@property
@memoize
def commentable(self):
"""
Check whether the viewing user has the right to comment
by resolving the containing workspace IMicroblogContext
(falling back to None=ISiteRoot)
"""
add = "Plone Social: Add Microblog Status Update"
try:
return api.user.has_permission(add, obj=self.context.microblog_context)
except api.exc.UserNotFoundError:
logger.error("UserNotFoundError while rendering a statusupdate.")
return False
@property
@memoize
def portal(self):
""" Return the portal object
"""
return api.portal.get()
@property
@memoize
def portal_url(self):
""" Return the portal object url
"""
return self.portal.absolute_url()
@property
@memoize
def context_url(self):
""" Return the context url
"""
return self.portal_url
@property
@memoize
def toggle_like(self):
""" This is used to render the toggle like stuff
"""
toggle_like_base = api.content.get_view(
"toggle_like_statusupdate", self.portal, self.request
)
toggle_like_view = toggle_like_base.publishTraverse(
self.request, self.context.getId()
)
return toggle_like_view
@property
@memoize
def decorated_text(self):
""" Use this method to enrich the status update text
For example we can:
- replace \n with <br />
- add mentions
- add tags
"""
text = safe_unicode(self.context.text).replace(u"\n", u"<br />")
text = link_urls(text)
tags = getattr(self.context, "tags", None)
mentions = getattr(self.context, "mentions", None)
text += link_users(self.portal_url, mentions)
text += link_tags(self.portal_url, tags)
return text
@property
def fullname(self):
user = pi_api.userprofile.get(self.context.userid) or api.user.get(
self.context.userid
)
fullname = ""
if user:
fullname = getattr(user, "fullname", user.getProperty("fullname"))
return fullname or self.context.userid
@property
@memoize
def attachment_base_url(self):
""" This will return the base_url for making attachments
"""
base_url = "{portal_url}/@@status-attachments/{status_id}".format(
portal_url=self.portal_url, status_id=self.context.getId()
)
return base_url
[docs] def item2attachments(self, item):
""" Take the attachment storage item
and transform it into an attachment
"""
item_url = "/".join((self.attachment_base_url, safe_unicode(item.getId())))
is_image = False
if isinstance(item, Image):
is_image = True
# this suffers from a bug
# 'large' should return 768x768 but instead returns 400x400
images = api.content.get_view("images", item.aq_base, self.request)
url = "/".join((item_url, images.scale(scale="large").url.lstrip("/")))
elif pi_api.previews.has_previews(item):
url = "/".join((item_url, "@@svg_preview"))
else:
url = None
return {
"is_image": is_image,
"img_src": url,
"link": item_url,
"alt": item.id,
"title": item.id,
}
[docs] def attachments(self):
""" Get preview images for status update attachments
"""
storage = IAttachmentStorage(self.context, {})
items = storage.values()
return map(self.item2attachments, items)
@property
@memoize
def replies(self):
""" Get the replies for this statusupdate
"""
replies = list(self.context.replies())
replies.reverse()
return replies
@property
def has_older_replies(self):
""" Check if we have oilder replies that we may want to hide
"""
if not self.fresh_reply_limit:
return False
return len(self.replies) > self.fresh_reply_limit
[docs] def use_relative_date(self):
registry = queryUtility(IRegistry)
absolute_after = registry.get(
"ploneintranet.activitystream.absolute_dates_after", 0
)
if absolute_after == 0:
return True
# keep relative until {absolute_after} days
return DateTime.DateTime() - self.context.date <= absolute_after
# ----------- actions (edit, delete) ----------------
# actual write/delete handling done in subclass below
@property
def traverse(self):
"""Base URL for traversal views"""
return "{}/statusupdate/{}".format(self.portal_url, self.context.id)
@property
def actions(self):
actions = []
if self.context.can_delete:
if self.context.thread_id:
title = _("Delete comment")
else:
title = _("Delete post")
actions.append(
{
"icon": "trash",
"title": title,
"data_pat_modal": "class: small",
"url": self.traverse + "/panel-delete-post.html",
}
)
if self.context.can_edit:
if self.context.thread_id:
title = _("Edit comment")
else:
title = _("Edit post")
actions.append(
{
"icon": "edit",
"title": title,
"url": self.traverse + "/panel-edit-post.html",
"data_pat_modal": "panel-header-content: none",
}
)
# edit_tags not implemented yet
# edit_mentions not implemented yet
return actions
# ----------- content updates only ------------------
@property
@memoize
def content_context(self):
return self.context.content_context
@property
def is_content_update(self):
return bool(self.content_context)
@property
def is_content_image_update(self):
return self.content_context and isinstance(self.content_context, Image)
@property
def is_content_file_update(self):
return self.content_context and isinstance(self.content_context, File)
@property
def is_content_downloadable(self):
return self.is_content_image_update or self.is_content_file_update
@property
def content_has_leadimage(self):
return self.content_context and ILeadImage.providedBy(self.content_context)
[docs] def content_has_previews(self):
if not self.is_content_update:
return False
elif self.is_content_image_update:
return True
return pi_api.previews.has_previews(self.content_context)
[docs] def content_preview_status_css(self):
if not self.is_content_update:
return "fixme"
base = "document document-preview"
if self.is_content_image_update:
return base
if pi_api.previews.converting(self.content_context):
return base + " not-generated"
if not self.content_has_previews():
return base + " not-possible"
return base
[docs] def content_preview_urls(self):
if not self.is_content_update:
return []
if self.is_content_image_update:
return [self.content_context.absolute_url()]
return pi_api.previews.get_preview_urls(self.content_context)
[docs] def content_url(self):
if self.is_content_image_update or self.is_content_file_update:
return "{}/view".format(self.content_context.absolute_url())
elif self.is_content_update:
return self.content_context.absolute_url()
[docs]@implementer(IDiazoNoTemplate)
class StatusUpdateEditPanel(StatusUpdateView):
""" Render the edit panel for posts or comments
"""
@property
@memoize
def title(self):
if self.context.thread_id:
return _("Edit comment")
return _("Edit post")
@property
@memoize
def form_action(self):
if self.context.thread_id:
return self.traverse + "/comment-edited.html"
return self.traverse + "/post-edited.html"
@property
def selector_template(self):
if self.context.thread_id:
return "#{thread_id}-comment-{context_id} .comment-content"
return "#post-{context_id} .post-content"
@property
@memoize
def data_pat_inject(self):
selector = self.selector_template.format(
context_id=self.context.id, thread_id=self.context.thread_id
)
return "source: {selector}; target: {selector};".format(selector=selector)
[docs]@implementer(IDiazoNoTemplate)
class StatusUpdateDeletePanel(StatusUpdateView):
""" Render the delete panel for posts or comments
"""
@property
@memoize
def title(self):
if self.context.thread_id:
return _("Delete comment")
return _("Delete post")
@property
@memoize
def description(self):
if self.context.thread_id:
return _("You are about to delete this comment. Are you sure?")
return _("You are about to delete this post. Are you sure?")
@property
@memoize
def form_action(self):
if self.context.thread_id:
return self.traverse + "/comment-deleted.html"
return self.traverse + "/post-deleted.html"
@property
def selector_template(self):
if self.context.thread_id:
return "#{thread_id}-comment-{context_id}::element"
return "#post-{context_id}::element"
@property
@memoize
def data_pat_inject(self):
selector = self.selector_template.format(
context_id=self.context.id, thread_id=self.context.thread_id
)
return "source: #document-content; target: {selector};".format(
selector=selector
)
[docs]class StatusUpdateModify(StatusUpdateView):
"""
A shared view class for editing and deleting statusupdates.
"""
def __call__(self):
if self.request.method == "POST":
self.handle_action()
return super(StatusUpdateModify, self).__call__()
[docs] def handle_action(self):
"""
Handle edit/delete actions. Security is checked in backend.
Takes care to handle any HTTP POST only once, even with
a cloned request.
"""
# pop() removes id to avoid multi-handling cloned POST request
id = self.request.form.pop("statusupdate_id", None)
if not id:
return
statusupdate = pi_api.microblog.statusupdate.get(id)
if self.request.form.get("delete", False):
statusupdate.delete()
elif self.request.form.get("text", None):
statusupdate.edit(self.request.form.get("text"))