Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Email Reminders and Dashboard Updates #591

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions castle/cms/browser/content/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,29 @@
from castle.cms.utils import get_paste_data
from castle.cms.utils import is_max_paste_items
from castle.cms.utils import get_template_repository_info
from castle.cms.tasks.email import send_email_reminder
from plone.app.content.browser import actions
from plone.app.workflow.browser import sharing
from plone.app.workflow.events import LocalrolesModifiedEvent
from plone.memoize.view import memoize
from plone.uuid.interfaces import IUUID
from Products.CMFCore.utils import getToolByName
from Products.CMFPlone import PloneMessageFactory as _
from Products.CMFPlone.utils import safe_unicode
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
from Products.statusmessages.interfaces import IStatusMessage

from z3c.form import button
from z3c.form import field
from z3c.form import form
from z3c.form.widget import ComputedWidgetAttribute
from zope.interface import Interface

import plone.api as api
import sys
import zope.schema as schema
from zope.event import notify
from zExceptions import Forbidden


class ObjectPasteView(actions.ObjectPasteView):
Expand Down Expand Up @@ -326,3 +333,118 @@ def do_redirect(self, context):
utils = Utils(self.context, self.request)
target = utils.get_object_url(context)
return self.request.response.redirect(target)


class SharingView(sharing.SharingView):

def __call__(self):

postback = self.handle_form()
if postback:
# Reset python recursion limit to baseline if previously raised
sys.setrecursionlimit(1000)
return self.template()
else:
context_state = self.context.restrictedTraverse(
"@@plone_context_state")
url = context_state.view_url()
self.request.response.redirect(url)

def large_count(self):
"""
Displays an alert indicating that we're operating in a view
with a high document count, which slows down the process of
granting permissions.
"""

try:
event = LocalrolesModifiedEvent(self.context, self.request)
obj = event.object
if obj._count:
return True if obj._count.value > 1000 else False
except AttributeError:
return False

def handle_form(self):
"""
Overrides the 'handle_form' method so we can temporarily increase
the python recursion limit if needed.

Also sends a notification email to individual users assigned to a page.
"""
postback = True

form = self.request.form
submitted = form.get('form.submitted', False)
save_button = form.get('form.button.Save', None) is not None
cancel_button = form.get('form.button.Cancel', None) is not None
message = form.get('message_optional', None)
if submitted and save_button and not cancel_button:
if not self.request.get('REQUEST_METHOD', 'GET') == 'POST':
raise Forbidden

authenticator = self.context.restrictedTraverse('@@authenticator',
None)
if not authenticator.verify():
raise Forbidden

event = LocalrolesModifiedEvent(self.context, self.request)
obj = event.object
try:
if obj._count.value > 1000:
# XXX: We're increasing the recursion limit here to prevent the
# 'maximum recursion depth exceeded' error thrown by 'cPickle'
# that occurs when granting permissions in a large directory
# (i.e. the 'image-directory')
sys.setrecursionlimit(2000)
except AttributeError:
# User search form submitted, no need to get count
pass

# Update the acquire-roles setting
if self.can_edit_inherit():
inherit = bool(form.get('inherit', False))
reindex = self.update_inherit(inherit, reindex=False)
else:
reindex = False

# Update settings for users and groups
entries = form.get('entries', [])
roles = [r['id'] for r in self.roles()]

settings = []
for entry in entries:
assigned_roles=[r for r in roles if entry.get('role_%s' % r, False)]
settings.append(
dict(id=entry['id'],
type=entry['type'],
roles=assigned_roles))

if entry['type'] == 'user' and 'Reviewer' in assigned_roles:
# Only send emails to individual users assigned as 'Reviewers'
user = api.user.get(entry['id'])
email = user.getProperty('email')
if email:
data = dict(
uid=user.getId(),
name=user.getProperty('fullname') or user.getId(),
email=email,
roles=roles,
message=message
)
send_email_reminder.delay(obj, data)

if settings:
reindex = self.update_role_settings(settings, reindex=False) \
or reindex
if reindex:
self.context.reindexObjectSecurity()
notify(LocalrolesModifiedEvent(self.context, self.request))
IStatusMessage(self.request).addStatusMessage(
_(u"Changes saved."), type='info')

# Other buttons return to the sharing page
if cancel_button:
postback = False

return postback
8 changes: 8 additions & 0 deletions castle/cms/browser/content/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,14 @@
template="templates/parallaxview.pt"
/>

<browser:page
for="*"
name="sharing"
class=".actions.SharingView"
permission="cmf.ModifyPortalContent"
layer="castle.cms.interfaces.ICastleLayer"
/>

<adapter
factory=".actions.default_template_title"
name="default"
Expand Down
27 changes: 27 additions & 0 deletions castle/cms/browser/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,33 @@ def get_recently_modified(self):
def get_recently_created(self):
query = dict(sort_on='created', sort_order='reverse')
return self._paging(query, 'created')

def get_user_modified(self):
member = api.user.get_current()
query = dict(
sort_on='modified',
sort_order='reverse',
actors=member.getUserName()
)
return self._paging(query, 'user-modified')

def get_user_created(self):
member = api.user.get_current()
query = dict(
sort_on='created',
sort_order='reverse',
Creator=member.getId()
)
return self._paging(query, 'user-created')

def get_user_assigned(self):
member = api.user.get_current()
query = dict(
sort_on='created',
sort_order='reverse',
assigned_users=member.getUserName()
)
return self._paging(query, 'user-assigned')

def get_in_review(self):
query = dict(sort_on='modified', review_state='pending', sort_order='reverse')
Expand Down
4 changes: 4 additions & 0 deletions castle/cms/cron/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,7 @@ def link_report(argv=sys.argv):

def auto_publish_retract(argv=sys.argv):
return run_it('_auto_publish_retract')


def email_reminders(argv=sys.argv):
return run_it('_email_reminders')
81 changes: 81 additions & 0 deletions castle/cms/cron/_email_reminders.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
from AccessControl.SecurityManagement import newSecurityManager
from DateTime import DateTime
from plone import api
from Products.CMFPlone.interfaces.siteroot import IPloneSiteRoot
from tendo import singleton
from zope.component.hooks import setSite
from castle.cms import cache
from castle.cms import utils
from copy import deepcopy

import logging


logger = logging.getLogger('castle.cms')


def send_reminders(site):
setSite(site)

cache_key = '-'.join(api.portal.get().getPhysicalPath()[1:]) + '-email-reminders'
reminder_cache = {}

try:
reminder_cache = cache.get(cache_key)
except KeyError:
cache.set(cache_key, reminder_cache)
portal_catalog = api.portal.get_tool('portal_catalog')

for key, item in reminder_cache.items():
results = portal_catalog.searchResults({'portal_type': item.get('portal_type'), 'id': item.get('pid')})
for brain in results:
obj = brain.getObject()
roles = obj.get_local_roles_for_userid(item.get('uid'))
obj_path = '/'.join(obj.getPhysicalPath())
if 'Reviewer' in roles and item.get('reminder_date') < DateTime():
try:
recipients=item['email']
subject="Page Assigned: %s" % (
api.portal.get_registry_record('plone.site_title'))
html="""
<p>Hi %s,</p>
<p>You have been assigned a new page:</p>
<p> %s </p>
<p>When your task is complete, you may un-assign yourself from this page.</p>""" % (
item['name'], obj_path)
message = item.get('message')
if message:
html += """
<p> %s </p>
""" % (message)

utils.send_email(
recipients=recipients,
subject=subject,
html=html
)
except Exception:
logger.warn('Could not send assignment email ', exc_info=True)
else:
new_cache = deepcopy(reminder_cache) # Not sure if deepcopy is necessary
new_cache.pop(key, None)
cache.set(cache_key, new_cache)


def run(app):
singleton.SingleInstance('autopublish')

user = app.acl_users.getUser('admin') # noqa
newSecurityManager(None, user.__of__(app.acl_users)) # noqa

for oid in app.objectIds(): # noqa
obj = app[oid] # noqa
if IPloneSiteRoot.providedBy(obj):
try:
send_reminders(obj)
except Exception:
logger.error('Could not update content for %s' % oid, exc_info=True)


if __name__ == '__main__':
run(app) # noqa
8 changes: 6 additions & 2 deletions castle/cms/exportimport/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
"hasImage": "BooleanIndex",
"trashed": "BooleanIndex",
"has_private_parents": "BooleanIndex",
"self_or_child_has_title_description_and_image": "BooleanIndex"
"self_or_child_has_title_description_and_image": "BooleanIndex",
"actors": "KeywordIndex",
"assigned_users": "KeywordIndex"
}

REMOVE_INDEXES = [
Expand All @@ -34,7 +36,9 @@
'image_info',
'navigation_label',
'has_private_parents',
'self_or_child_has_title_description_and_image'
'self_or_child_has_title_description_and_image',
'actors',
'assigned_users'
]

REMOVE_METADATA = [
Expand Down
Loading
Loading