# -*- coding: utf-8 -*-
from AccessControl import Unauthorized
from DateTime import DateTime
from json import dumps
from plone import api
from plone.app.event.base import default_timezone
from plone.memoize import view
from ploneintranet import api as pi_api
from ploneintranet.core import ploneintranetCoreMessageFactory as _
from ploneintranet.layout.browser.base import BasePanel
from ploneintranet.workspace.basecontent.event import EventView
from ploneintranet.workspace.basecontent.utils import dexterity_update
from ploneintranet.workspace.basecontent.utils import get_selection_classes
from ploneintranet.workspace.events import ObjectModifiedAfterCreationEvent
from ploneintranet.workspace.utils import parent_workspace
from Products.CMFCore.permissions import AddPortalContent
from Products.CMFPlone.utils import safe_unicode
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
from zope.container.interfaces import INameChooser
from zope.event import notify
from zope.lifecycleevent import Attributes
from zope.lifecycleevent import ObjectModifiedEvent
[docs]class AddBase(BasePanel):
""" Basic stuff for adding contents
"""
title = _("Create document")
template = ViewPageTemplateFile("templates/add_content.pt")
can_edit = True
_form_data_pat_inject_parts = (
"#document-body",
"#workspace-documents",
"#global-statusmessage; loading-class: ''",
)
is_add_form = True
@property
@view.memoize
def form_data_pat_validation(self):
""" Proper pat-validation options.
We need to match the timestamp of the create button.
"""
return "disable-selector:#form-buttons-create-{timestamp}".format(
timestamp=self.form_timestamp()
)
@property
@view.memoize
def portal(self):
""" Return the portal
"""
return api.portal.get()
@property
@view.memoize
def workspace_container(self):
""" Return the workspace container
"""
return self.portal.workspaces
@property
@view.memoize
def parent_workspace(self):
""" Return the parent workspace
"""
return parent_workspace(self.context)
@property
@view.memoize
def user(self):
""" The currently authenticated ploneintranet user profile (if any)
"""
return pi_api.userprofile.get_current()
@property
@view.memoize
def content_helper_view(self):
""" Use the content_helper_view
"""
return api.content.get_view("content_helper_view", self.context, self.request)
@property
@view.memoize
def allusers_json_url(self):
""" Return @@allusers.json in the proper context
"""
return "{}/@@allusers.json".format(self.parent_workspace.absolute_url())
[docs] def get_data_pat_autosuggest(self, fieldname):
""" Return the data-pat-autosuggest for a fieldname
"""
user = self.user
if fieldname == "initiator" and self.request.method == "GET" and user:
default_field_value = user.getId()
else:
default_field_value = ""
prefill_json = self.content_helper_view.safe_member_prefill(
self.context, fieldname, default=default_field_value
)
if not prefill_json:
prefill_json = "{}"
if user and fieldname == "initiator" and prefill_json == user.getId():
prefill_json = dumps({user.username: user.fullname})
return "; ".join(
(
"ajax-data-type: json",
"maximum-selection-size: 1",
"selection-classes: {}",
"ajax-url: {}".format(self.allusers_json_url),
"allow-new-words: false",
"prefill-json: {}".format(prefill_json),
)
)
[docs] def redirect(self, url):
"""
Has its own method to allow overriding
"""
url = "{}/view?show_sidebar".format(url)
return self.request.response.redirect(url)
[docs] def validate(self):
""" Validate input and return a truish
"""
return True
[docs] def get_new_unique_id(self, container):
""" This will get a new unique id according to the request
"""
form = self.request.form
suggested_id = form.get("id") or form.get("title") or self.title
chooser = INameChooser(container)
new_id = chooser.chooseName(suggested_id, container)
return new_id
[docs] def get_new_object(self, container=None):
""" This will create a new object
"""
if not container:
container = self.context
obj = api.content.create(
container=container,
type=self.portal_type,
id=self.get_new_unique_id(container),
title=self.request.get("title") or self.title,
safe_id=False,
)
return container[obj.getId()]
[docs] def update(self, obj):
""" Update the object and returns the modified fields and errors
"""
modified, errors = dexterity_update(obj)
return modified, errors
[docs] def create(self, container=None):
"""
Create content in the given container and return url.
Uses dexterity_update to set the
appropriate fields after creation.
"""
if not self.validate():
# BBB: do something clever that works with pat-inject
# at the moment the @@add_something form is not a complete page
# but just some markup,
# so we cannot show that one here
pass
with pi_api.env.indexing_disabled(self.request):
new = self.get_new_object(container)
modified, errors = self.update(new)
if not errors:
api.portal.show_message(
_("Item created."), request=self.request, type="success"
)
new.reindexObject()
descriptions = []
if modified:
descriptions = [
Attributes(interface, *fields)
for interface, fields in modified.items()
]
notify(ObjectModifiedAfterCreationEvent(new, *descriptions))
else:
api.portal.show_message(
_("There was a problem: %s." % errors),
request=self.request,
type="error",
)
return new.absolute_url()
def __call__(self):
"""Render form, or handle POST and redirect"""
title = safe_unicode(self.request.form.get("title", None))
portal_type = self.request.form.get("portal_type", "")
container_path = self.request.form.get("container", None)
if container_path:
container = self.context.restrictedTraverse(container_path)
else:
container = self.context
if title is not None:
self.portal_type = portal_type.strip()
self.title = title.strip()
if self.portal_type in api.portal.get_tool("portal_types"):
url = self.create(container)
return self.redirect(url)
return self.template()
[docs]class AddContent(AddBase):
"""
"""
tabs = [
{
"css_class": "icon-doc-text current",
"id": "sheet-text",
"label": _("Rich text"),
},
{"css_class": "icon-file-image", "id": "sheet-image", "label": _("Image")},
]
[docs]class AddFolder(AddBase):
template = ViewPageTemplateFile("templates/add_folder.pt")
title = _("Create folder")
_form_data_pat_inject_parts = ("#workspace-documents", "nav.breadcrumbs")
[docs]class AddLink(AddBase):
""" The add link view
"""
template = ViewPageTemplateFile("templates/add_form.pt")
title = _("Create link")
form_portal_type = "Link"
form_input_title_placeholder = _("Link name")
[docs]class AddEvent(AddBase):
template = ViewPageTemplateFile("templates/add_event.pt")
title = _("Create event")
_add_event_tab = {
"css_class": "icon-calendar",
"id": "sheet-event",
"label": _("Event"),
}
_add_calendar_tab = {
"css_class": "icon-calendar-1",
"id": "sheet-calendar",
"label": _("Calendar"),
}
@property
def tabs(self):
tabs = []
if self.can_add_events():
tabs.append(self._add_event_tab)
if self.can_add_calendars():
tabs.append(self._add_calendar_tab)
if tabs:
tabs[0]["css_class"] += " current"
return tabs
@property
@view.memoize_contextless
def calendar_container(self):
return api.portal.get().get("calendars", None)
@property
@view.memoize_contextless
def app_calendar_view(self):
return api.content.get_view(
"app-calendar", api.portal.get().apps.calendar, self.request
)
[docs] @view.memoize
def can_add_events(self):
""" Check if we can add events.
Start first checking the current context and then see
if we can add them somewhere else
"""
if self.parent_workspace and api.user.has_permission(
AddPortalContent, obj=self.parent_workspace
):
return True
view = api.content.get_view(
context=self.context, request=self.request, name="calendar_picker.json"
)
return view.has_writable_workspaces()
[docs] @view.memoize
def can_add_calendars(self):
if not self.request.get("app", None):
return False
if self.request.get("in_calendar", None):
return False
return api.user.has_permission(
"Add portal content", obj=self.calendar_container
)
[docs] def fix_start_end(self):
""" If the start date is lower than the end one,
modify the request setting end = start + 1 hour
"""
localized_start = DateTime(
"%s %s"
% (
" ".join(self.request.get("start")),
self.request.get("timezone", default_timezone()),
)
)
localized_end = localized_start + 1.0 / 24
# If you know a smarter way to hijack the request,
# please modify the following lines:)
self.request.end = [
localized_end.strftime("%Y-%m-%d"),
localized_end.strftime("%H:%M"),
]
self.request.form["end"] = self.request.end
self.request.other["end"] = self.request.end
ts = api.portal.get_tool("translation_service")
msg = _(
"dates_hijacked",
default=(
u"Start time should be lower than end time. "
u"The system set the end time to: ${date}"
),
mapping={u"date": ts.toLocalizedTime(localized_end)},
)
api.portal.show_message(msg, request=self.request, type="warning")
[docs] def validate(self):
""" Override base content validation
Return truish if valid
"""
if self.request.get("start") > self.request.get("end"):
self.fix_start_end()
return True
# The following two methods are used to determine the next step
# after adding or editing events.
#
# This is an issue because we can add events or edit them
# and this can happen from either within a workspace or from the app
# or through a modal openend through the + icon.
# All six combinations require different redirects.
# The whole situation is not fully trivial, because once we are on
# the event edit form, the context is the event and not the workspace
# or app anymore, so we don't easily know where we are.
@property
@view.memoize
def _form_data_pat_inject_parts(self):
""" Returns the correct dpi config """
in_app = self.request.get("app")
selectors = []
if in_app:
# In the app, we want to reload the left hand sidebar
# and the calendar part in document-content
selectors.append("#sidebar")
selectors.append("#document-content")
else:
# Probably on a workspace
selectors.append("#workspace-events")
selectors.append("#document-body")
parts = ["source: {0}; target: {0};".format(selector) for selector in selectors]
# and of course we want to update the status message
parts.append(
"source: {0}; target: {0}; loading-class: ''".format(
"#global-statusmessage"
)
)
return parts
[docs] def redirect(self, url):
""" Try to find the proper redirect context.
"""
# See if we are an event that resides in a workspace
# This is currently always the case, we don't have others.
workspace = self.parent_workspace
# in_calendar tells us if the add or edit has been triggered from
# within the calendar view. false if the add or edit was through
# + or direct edit
in_calendar = self.request.get("in_calendar")
# in_app tells us if we are working from within the calendar app.
# contains the relative path to app without leading / if set.
in_app = self.request.get("app")
if in_app:
# Just redirect to the calendar app view, injection will pick
# what it needs from it
url = "%s/@@app-calendar" % in_app
else:
# somewhere else, expectedly on a workspace
if workspace:
if in_calendar:
# Stay in the calendar
# Go to the workspace calendar
container = self.request.get("container")
if container and container != u"/".join(
workspace.getPhysicalPath()
):
url = "%s/@@workspace-calendar?all_calendars=1"
else:
url = "%s/@@workspace-calendar"
url = url % workspace.absolute_url()
else:
# Load the event and show its form
# we return the original object url
url = "%s/event_view?show_sidebar=1" % url
else:
# if not render the app view
pass
return self.request.response.redirect(url)
[docs] def round_date(self, dt):
""" Round the datetime minutes and seconds to the next quarter,
i.e. '2000/01/01 00:35:21' becomes '2000/01/01 00:30:00'
"""
remainder = float(dt) % 900
if not remainder:
return dt
return dt + (900 - float(dt) % 900) / 86400
@property
@view.memoize
def default_datetime(self):
""" Return the default date (the requested one or now)
The request may come from several sources, e.g.:
1. from the sidebar
2. from the calendar month view
3. from the calendar day and week views
Each of this cases may have a date parameter in different formats.
We try to convert it to a DateTime.
"""
requested_date = self.request.get("date", "")
# The requeste_date, when called by the calendar day and week view,
# will look like '2017-03-29T13:30:00'
if "T" in requested_date:
# Strip the "T" to have the local timezone
requested_date = requested_date.replace("T", " ")
else:
# 'T' in not there, assume we have only the date in the request
requested_date += " 09:00"
try:
requested_date = " ".join((requested_date, DateTime().localZone()))
date = DateTime(requested_date)
except SyntaxError:
date = DateTime()
return date
@property
@view.memoize
def default_start(self):
""" The rounded default_datetime.
"""
return self.round_date(self.default_datetime)
@property
@view.memoize
def default_end(self):
""" Like default_start, but add a time interval (in hours)
We will have a 'T' in the request parameter
if the request comes from the day or the week calendar view
"""
delta = "T" in self.request.get("date", "") and 0.5 or 1.0
return self.default_start + delta / 24
[docs] def get_selection_classes(self, field, default=None):
""" identify all groups in the invitees """
return get_selection_classes(self.context, field, default)
[docs]class EditEvent(EventView, AddEvent):
""" Base class for editing events """
template = ViewPageTemplateFile("templates/panel-edit-event.pt")
title = _("Edit event")
is_add_form = False
def __call__(self):
"""Render form, or handle POST and redirect"""
if self.should_update():
if not self.can_edit:
raise Unauthorized("You cannot modify this event")
self.update()
return self.redirect(self.context.absolute_url())
return self.template()
[docs]class EditCalendar(AddEvent):
""" """
template = ViewPageTemplateFile("templates/panel-edit-calendar.pt")
title = _("Edit calendar")
def __call__(self):
"""Render form, or handle POST and redirect"""
if "form.buttons.edit" in self.request:
modified, errors = self.update(self.context)
if not errors:
api.portal.show_message(
_("Your changes have been saved."),
request=self.request,
type="success",
)
self.context.reindexObject()
notify(ObjectModifiedEvent(self.context))
else:
api.portal.show_message(
_("There was a problem: %s." % errors),
request=self.request,
type="error",
)
return self.redirect(self.request.get("app") or self.context.absolute_url())
return self.template()