summaryrefslogtreecommitdiffstats
path: root/src/pfwbged/policy/subscribers/document.py
blob: e069519e65af15cf7bee97d5b3fa47748f7c6b65 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
import logging
import datetime

from Acquisition import aq_chain, aq_parent
from five import grok
from DateTime import DateTime

from zc.relation.interfaces import ICatalog
from zope.container.interfaces import INameChooser
from zope.i18n import translate, negotiate
from zope.component import getUtility
from zope.component.interfaces import ComponentLookupError
from zope.intid.interfaces import IIntIds
from zope.lifecycleevent.interfaces import IObjectAddedEvent, IObjectModifiedEvent
from OFS.interfaces import IObjectWillBeRemovedEvent
from zope.annotation.interfaces import IAnnotations

from plone import api
from plone.stringinterp.adapters import _recursiveGetMembersFromIds

from Products.DCWorkflow.interfaces import IAfterTransitionEvent
from plone.app.discussion.interfaces import ICommentingTool, IConversation

from collective.z3cform.rolefield.field import LocalRolesToPrincipalsDataManager

from collective.dms.basecontent.dmsdocument import IDmsDocument
from collective.dms.basecontent.source import PrincipalSource
from collective.task.content.task import IBaseTask, ITask
from collective.task.content.validation import IValidation
from collective.task.interfaces import IBaseTask
from collective.dms.basecontent.dmsfile import IDmsFile, IDmsAppendixFile
from pfwbged.folder.folder import IFolder

from pfwbged.basecontent.behaviors import IPfwbDocument
from pfwbged.policy import _

from mail import changeWorkflowState


try:
    from plone.app.async.interfaces import IAsyncService
except ImportError:
    IAsyncService = None


def has_pfwbgeddocument_workflow(obj):
    wtool = api.portal.get_tool('portal_workflow')
    return 'pfwbgeddocument_workflow' in wtool.getChainFor(obj)


def has_incomingmail_workflow(obj):
    wtool = api.portal.get_tool('portal_workflow')
    chain = wtool.getChainFor(obj)
    if 'incomingmail_workflow' in chain:
        return True
    if 'incomingapfmail_workflow' in chain:
        return True
    return False


@grok.subscribe(IBaseTask, IObjectAddedEvent)
def set_role_on_document(context, event):
    """Add Reader role to document for the responsible of an
    information, opinion, validation.
    """
    # recipient_groups is the "Visible par" field
    if not ITask.providedBy(context):
        document = context.getParentNode()
        if IDmsDocument.providedBy(document):
            new_recipients = tuple(frozenset(document.recipient_groups or []) | frozenset(context.responsible or []))
            cansee_dm = LocalRolesToPrincipalsDataManager(document, IDmsDocument['recipient_groups'])
            cansee_dm.set(new_recipients)
            document.reindexObjectSecurity()
    # do we have to set Editor role on document for ITask ? (if so, remove something for IDmsMail ?)


@grok.subscribe(IDmsFile, IAfterTransitionEvent)
def change_validation_state(context, event):
    """If version state is draft, change validation state from todo to refused (transition refuse).
    If version state is validated, change validation state from todo to validated (transition validate).

    """
    intids = getUtility(IIntIds)
    catalog = getUtility(ICatalog)
    version_intid = intids.queryId(context)
    if version_intid is None:
        return
    query = {'to_id': version_intid,
             'from_interfaces_flattened': IValidation,
             'from_attribute': 'target'}
    if event.new_state.id == 'draft':
        for ref in catalog.findRelations(query):
            validation = ref.from_object
            if api.content.get_state(validation) == 'todo':
                api.content.transition(validation, 'refuse')
                validation.reindexObject(idxs=['review_state'])
    elif event.new_state.id == 'validated':
        for ref in catalog.findRelations(query):
            validation = ref.from_object
            if api.content.get_state(validation) == 'todo':
                api.content.transition(validation, 'validate')
                validation.reindexObject(idxs=['review_state'])
    elif event.transition.id == 'cancel-validation':
        for ref in catalog.findRelations(query):
            validation = ref.from_object
            if api.content.get_state(validation) == 'validated':
                api.content.transition(validation, 'cancel-validation')
                validation.reindexObject(idxs=['review_state'])
    elif event.transition.id == 'cancel-refusal':
        for ref in catalog.findRelations(query):
            validation = ref.from_object
            if api.content.get_state(validation) == 'refused':
                api.content.transition(validation, 'cancel-refusal')
                validation.reindexObject(idxs=['review_state'])


@grok.subscribe(IDmsFile, IObjectWillBeRemovedEvent)
def delete_tasks(context, event):
    """Delete validations and opinions when a version is deleted.
    """
    try:
        intids = getUtility(IIntIds)
    except ComponentLookupError:  # when we remove the Plone site
        return
    catalog = getUtility(ICatalog)
    version_intid = intids.getId(context)
    query = {'to_id': version_intid,
             'from_interfaces_flattened': IBaseTask,
             'from_attribute': 'target'}
    for rv in catalog.findRelations(query):
        obj = rv.from_object
        #obj.aq_parent.manage_delObjects([obj.getId()])  # we don't want to verify Delete object permission on object
        del aq_parent(obj)[obj.getId()]


@grok.subscribe(IDmsFile, IObjectAddedEvent)
def version_is_signed_at_creation(context, event):
    """If checkbox signed is checked, finish version without validation after creation"""
    if context.signed:
        api.content.transition(context, 'finish_without_validation')
        context.reindexObject(idxs=['review_state'])


### Workflow for other documents
# @grok.subscribe(IPfwbDocument, IObjectAddedEvent)
def create_task_after_creation(context, event):
    """Create a task attributed to creator after document creation"""
    # only applies to "other documents"
    if not has_pfwbgeddocument_workflow(context):
        return

    creator = context.Creator()
    params = {'responsible': [],
              'title': translate(_(u'Process document'),
                                 context=context.REQUEST),
              }
    task_id = context.invokeFactory('task', 'process-document', **params)
    task = context[task_id]
    datamanager = LocalRolesToPrincipalsDataManager(task, ITask['responsible'])
    datamanager.set((creator,))
    task.reindexObject()


@grok.subscribe(ITask, IAfterTransitionEvent)
def task_in_progress(context, event):
    """When a task change state, change parent state
    """
    if event.new_state.id == 'in-progress':
        # go up in the acquisition chain to find the first task (i.e. the one which is just below the document)
        for obj in aq_chain(context):
            obj = aq_parent(obj)
            if IPfwbDocument.providedBy(obj):
                break
        # only applies to "other documents"
        if not has_pfwbgeddocument_workflow(obj):
            return
        document = obj
        try:
            api.content.transition(obj=document, transition='to_process')
        except api.exc.InvalidParameterError:
            pass
        else:
            document.reindexObject(idxs=['review_state'])
    elif event.new_state.id == 'abandoned':
        obj = aq_parent(context)
        if not IPfwbDocument.providedBy(obj):
            return
        # only applies to "other documents"
        if not has_pfwbgeddocument_workflow(obj):
            return
        document = obj
        api.content.transition(obj=document, transition='directly_noaction')
        document.reindexObject(idxs=['review_state'])
    elif event.transition and event.transition.id == 'return-responsibility':
        obj = aq_parent(context)
        if not IPfwbDocument.providedBy(obj):
            return
        # only applies to "other documents"
        if not has_pfwbgeddocument_workflow(obj):
            return
        document = obj
        api.content.transition(obj=document, transition='back_to_assigning')
        document.reindexObject(idxs=['review_state'])


def transition_tasks(obj, types, status, transition):
    portal_catalog = api.portal.get_tool('portal_catalog')
    tasks = portal_catalog.unrestrictedSearchResults(
            portal_type=types, path='/'.join(obj.getPhysicalPath()))
    for brain in tasks:
        task = brain._unrestrictedGetObject()
        if api.content.get_state(obj=task) == status:
            print 'changing task', task
            with api.env.adopt_user('admin'):
                api.content.transition(obj=task, transition=transition)
            task.reindexObject(idxs=['review_state'])


@grok.subscribe(IDmsFile, IAfterTransitionEvent)
def version_note_finished(context, event):
    """Launched when version note is finished.
    """
    if event.new_state.id == 'finished':
        context.reindexObject(idxs=['review_state'])
        portal_catalog = api.portal.get_tool('portal_catalog')
        document = context.getParentNode()
        state = api.content.get_state(obj=document)
        # if parent is an outgoing mail, change its state to ready_to_send
        if document.portal_type in ('dmsoutgoingmail', 'pfwb.apfoutgoingmail') and state == 'writing':
            with api.env.adopt_user('admin'):
                api.content.transition(obj=document, transition='finish')
            document.reindexObject(idxs=['review_state'])
        elif IPfwbDocument.providedBy(document) and has_pfwbgeddocument_workflow(document):
            if state == 'processing':
                with api.env.adopt_user('admin'):
                    api.content.transition(obj=document, transition='process')
            elif state == "assigning":
                transition_tasks(document, types=('task'), status='todo', transition='take-responsibility')
                # the document is now in processing state because the task is in progress
                api.content.transition(obj=document, transition='process')
                document.reindexObject(idxs=['review_state'])

        if not has_incomingmail_workflow(document):
            # for all documents (but not incoming mails), we transition all
            # todo tasks to abandon.
            transition_tasks(obj=document, status='todo',
                    transition='abandon',
                    types=('opinion', 'validation', 'task',))
            document.reindexObject(idxs=['review_state'])

        version_notes = portal_catalog.unrestrictedSearchResults(portal_type='dmsmainfile',
                                                                 path='/'.join(document.getPhysicalPath()))
        # make obsolete other versions
        for version_brain in version_notes:
            version = version_brain._unrestrictedGetObject()
            if api.content.get_state(obj=version) in ('draft', 'pending', 'validated'):
                api.content.transition(obj=version, transition='obsolete')
                version.reindexObject(idxs=['review_state'])
        context.__ac_local_roles_block__ = False
        context.reindexObjectSecurity()


@grok.subscribe(IDmsDocument, IAfterTransitionEvent)
def document_is_processed(context, event):
    """When document is processed, close all tasks"""
    portal_catalog = api.portal.get_tool('portal_catalog')
    if has_pfwbgeddocument_workflow(context) and event.new_state.id not in ('processing',):
        tasks = portal_catalog.unrestrictedSearchResults(portal_type='task',
                                                         path='/'.join(context.getPhysicalPath()))
        for brain in tasks:
            task = brain._unrestrictedGetObject()
            if api.content.get_state(obj=task) == 'in-progress':
                with api.env.adopt_user('admin'):
                    api.content.transition(obj=task, transition='mark-as-done')
                task.reindexObject(idxs=['review_state'])


@grok.subscribe(IDmsDocument, IAfterTransitionEvent)
def document_is_finished(context, event):
    """When document is done for, abandon all tasks"""
    portal_catalog = api.portal.get_tool('portal_catalog')
    if event.new_state.id not in ('considered', 'noaction', 'sent'):
        return
    if has_incomingmail_workflow(context):
        return
    print 'document_is_finished'
    transition_tasks(obj=context, status='todo',
            transition='abandon',
            types=('task', 'opinion', 'validation'))


@grok.subscribe(IDmsDocument, IAfterTransitionEvent)
def document_is_reopened(context, event):
    """When a document is reoponed, create a new task"""
    if has_pfwbgeddocument_workflow(context) and event.transition is not None and event.new_state.id == 'assigning':

        # Task responsibility has just been returned
        if event.old_state.id == 'processing':
            return

        creator = api.user.get_current().getId()
        params = {'responsible': [],
                  'title': translate(_(u'Process document'),
                                     context=context.REQUEST),
                  }
        chooser = INameChooser(context)
        task_id = chooser.chooseName('process-document', context)
        task_id = context.invokeFactory('task', task_id, **params)
        task = context[task_id]
        datamanager = LocalRolesToPrincipalsDataManager(task, ITask['responsible'])
        datamanager.set((creator,))
        task.reindexObject()


def email_notification_of_tasks_sync(context, event, document, absolute_url, target_language=None):
    """Notify recipients of new tasks by email"""
    log = logging.getLogger('pfwbged.policy')
    log.info('sending notifications')
    document.reindexObject(idxs=['allowedRolesAndUsers'])

    for enquirer in (context.enquirer or []):
        member = context.portal_membership.getMemberById(enquirer)
        if member:
            email_from = member.getProperty('email', None)
            if email_from:
                break
    else:
        email_from = api.user.get_current().email or api.portal.get().getProperty('email_from_address') or 'admin@localhost'

    responsible_labels = []
    for responsible in (context.responsible or []):
        try:
            responsible_labels.append(
                    PrincipalSource(context).getTerm(responsible).title)
        except LookupError:
            pass
    responsible_label = ', '.join(responsible_labels)

    kwargs = {'target_language': target_language}
    subject = '%s - %s' % (context.title, document.title)
    body = translate(_('You received a request for action in the GED.'), **kwargs)

    if responsible_label:
        body += '\n\n' + translate(_('Assigned to: %s'), **kwargs) % responsible_label

    body += '\n\n' + \
            translate(_('Title: %s'), **kwargs) % context.title + \
            '\n\n' + \
            translate(_('Document: %s'), **kwargs) % document.title + \
            '\n\n' + \
            translate(_('Document Address: %s'), **kwargs) % absolute_url + \
            '\n\n'
    try:
        body += translate(_('Deadline: %s'), **kwargs) % context.deadline + '\n\n'
    except AttributeError:
        pass

    if context.note:
        body += translate(_('Note:'), **kwargs) + '\n\n' + context.note

    body += '\n\n\n-- \n' + translate(_('Sent by GED'), **kwargs)
    body = body.encode('utf-8')

    log.info('sending notifications to %r' % context.responsible)
    members = []
    for member in _recursiveGetMembersFromIds(api.portal.get(), (context.responsible or [])):
        email = member.getProperty('email', None)
        if not email:
            continue
        try:
            context.MailHost.send(body, email, email_from, subject, charset='utf-8')
        except Exception as e:
            # do not abort transaction in case of email error
            log = logging.getLogger('pfwbged.policy')
            log.exception(e)


@grok.subscribe(IBaseTask, IObjectAddedEvent)
def email_notification_of_tasks(context, event):
    # go up in the acquisition chain to find the document, this cannot be done
    # in the async job as absolute_url() needs the request object to give a
    # correct result
    document = None
    for obj in aq_chain(context):
        obj = aq_parent(obj)
        if IDmsDocument.providedBy(obj):
            document = obj
            break
    if not document:
        return
    absolute_url = document.absolute_url()

    # request is also required to get the target language
    target_language = negotiate(context.REQUEST)

    kwargs = {
        'context': context,
        'event': event,
        'document': document,
        'absolute_url': absolute_url,
        'target_language': target_language
    }

    if IAsyncService is None:
        return email_notification_of_tasks_sync(**kwargs)
    async = getUtility(IAsyncService)
    log = logging.getLogger('pfwbged.policy')
    log.info('sending notifications async')
    job = async.queueJob(email_notification_of_tasks_sync, **kwargs)


@grok.subscribe(IValidation, IAfterTransitionEvent)
def email_notification_of_validation_reversal(context, event):
    """Notify a validation requester when their previously validated
     (or refused) request has returned to pending state"""
    if not event.transition:
        return
    elif event.transition.id == 'cancel-validation':
        comment = translate(_('A previously validated version has returned to waiting validation'), context=context.REQUEST)
    elif event.transition.id == 'cancel-refusal':
        comment = translate(_('A previously refused version has returned to waiting validation'), context=context.REQUEST)
    else:
        return

    # go up in the acquisition chain to find the document
    document = None
    for obj in aq_chain(context):
        obj = aq_parent(obj)
        if IDmsDocument.providedBy(obj):
            document = obj
            break
    if not document:
        return

    email_enquirer = None
    for enquirer in (context.enquirer or []):
        member = context.portal_membership.getMemberById(enquirer)
        if member:
            email_enquirer = member.getProperty('email', None)
            if email_enquirer:
                break

    if not email_enquirer:
        return

    email_from = api.user.get_current().email or api.portal.get().getProperty('email_from_address') or 'admin@localhost'

    subject = '%s - %s' % (context.title, document.title)

    body = comment + \
            '\n\n' + \
            translate(_('Title: %s'), context=context.REQUEST) % context.title + \
            '\n\n' + \
            translate(_('Document: %s'), context=context.REQUEST) % document.title + \
            '\n\n' + \
            translate(_('Document Address: %s'), context=context.REQUEST) % document.absolute_url()

    body += '\n\n\n-- \n' + translate(_('Sent by GED'))
    body = body.encode('utf-8')

    try:
        context.MailHost.send(body, email_enquirer, email_from, subject, charset='utf-8')
    except Exception as e:
        # do not abort transaction in case of email error
        log = logging.getLogger('pfwbged.policy')
        log.exception(e)


@grok.subscribe(IValidation, IAfterTransitionEvent)
def email_notification_of_refused_task(context, event):
    if event.new_state.id != 'refused':
        return

    # go up in the acquisition chain to find the document
    document = None
    for obj in aq_chain(context):
        obj = aq_parent(obj)
        if IDmsDocument.providedBy(obj):
            document = obj
            break
    if not document:
        return

    email_enquirer = None
    for enquirer in (context.enquirer or []):
        member = context.portal_membership.getMemberById(enquirer)
        if member:
            email_enquirer = member.getProperty('email', None)
            if email_enquirer:
                break

    if not email_enquirer:
        return

    email_from = api.user.get_current().email or api.portal.get().getProperty('email_from_address') or 'admin@localhost'

    subject = '%s - %s' % (context.title, document.title)

    body = translate(_('A validation request has been refused'), context=context.REQUEST) + \
            '\n\n' + \
            translate(_('Title: %s'), context=context.REQUEST) % context.title + \
            '\n\n' + \
            translate(_('Document: %s'), context=context.REQUEST) % document.title + \
            '\n\n' + \
            translate(_('Document Address: %s'), context=context.REQUEST) % document.absolute_url() + \
            '\n\n'

    conversation = IConversation(context)
    if conversation and conversation.getComments():
        last_comment = list(conversation.getComments())[-1]
        if (datetime.datetime.utcnow() - last_comment.creation_date).seconds < 120:
            # comment less than two minutes ago, include it.
            body += translate(_('Note:'), context=context.REQUEST) + '\n\n' + last_comment.text

    body += '\n\n\n-- \n' + translate(_('Sent by GED'))
    body = body.encode('utf-8')

    try:
        context.MailHost.send(body, email_enquirer, email_from, subject, charset='utf-8')
    except Exception as e:
        # do not abort transaction in case of email error
        log = logging.getLogger('pfwbged.policy')
        log.exception(e)


def email_notification_of_canceled_subtask(context):
    document = None
    for obj in aq_chain(context):
        obj = aq_parent(obj)
        if IDmsDocument.providedBy(obj):
            document = obj
            break
    if not document:
        return
    absolute_url = document.absolute_url()

    responsible = context.responsible[0]
    principal = api.user.get(responsible)
    if not principal:
        principal = api.group.get(responsible)
    recipient_email = principal.getProperty('email') if principal else None
    if recipient_email:

        email_from = api.user.get_current().email or api.portal.get().getProperty(
            'email_from_address') or 'admin@localhost'

        subject = '%s - %s' % (context.title, document.title)

        body = translate(_('One of your tasks has been cancelled'), context=context.REQUEST) + \
            '\n\n' + \
            translate(_('Title: %s'), context=context.REQUEST) % context.title + \
            '\n\n' + \
            translate(_('Document: %s'), context=context.REQUEST) % document.title + \
            '\n\n' + \
            translate(_('Document Address: %s'), context=context.REQUEST) % document.absolute_url() + \
            '\n\n\n\n-- \n' + \
            translate(_('Sent by GED'))
        body = body.encode('utf-8')

        try:
            context.MailHost.send(body, recipient_email, email_from, subject, charset='utf-8')
        except Exception as e:
            # do not abort transaction in case of email error
            log = logging.getLogger('pfwbged.policy')
            log.exception(e)


def email_notification_of_canceled_information(context):
    document = None
    for obj in aq_chain(context):
        obj = aq_parent(obj)
        if IDmsDocument.providedBy(obj):
            document = obj
            break
    if not document:
        return
    absolute_url = document.absolute_url()

    responsible = context.responsible[0]
    principal = api.user.get(responsible)
    recipient_email = principal.getProperty('email') if principal else None
    if recipient_email:

        email_from = api.user.get_current().email or api.portal.get().getProperty(
            'email_from_address') or 'admin@localhost'

        subject = '%s - %s' % (context.title, document.title)

        body = translate(_('One document is not mentioned for your information anymore'), context=context.REQUEST) + \
            '\n\n' + \
            translate(_('Title: %s'), context=context.REQUEST) % context.title + \
            '\n\n' + \
            translate(_('Document: %s'), context=context.REQUEST) % document.title + \
            '\n\n' + \
            translate(_('Document Address: %s'), context=context.REQUEST) % document.absolute_url() + \
            '\n\n\n\n-- \n' + \
            translate(_('Sent by GED'))
        body = body.encode('utf-8')

        try:
            context.MailHost.send(body, recipient_email, email_from, subject, charset='utf-8')
        except Exception as e:
            # do not abort transaction in case of email error
            log = logging.getLogger('pfwbged.policy')
            log.exception(e)

@grok.subscribe(IValidation, IObjectWillBeRemovedEvent)
def email_notification_of_canceled_validation(context, event):
    document = None
    for obj in aq_chain(context):
        obj = aq_parent(obj)
        if IDmsDocument.providedBy(obj):
            document = obj
            break
    if not document:
        return
    absolute_url = document.absolute_url()

    for enquirer in (context.enquirer or []):
        member = context.portal_membership.getMemberById(enquirer)
        if member:
            email_from = member.getProperty('email', None)
            if email_from:
                break
    else:
        email_from = api.user.get_current().email or api.portal.get().getProperty('email_from_address') or 'admin@localhost'

    responsible = context.responsible[0]
    principal = api.user.get(responsible)
    if not principal:
        principal = api.group.get(responsible)
    recipient_email = principal.getProperty('email') if principal else None
    if not recipient_email:
        return

    subject = '%s - %s' % (context.title, document.title)

    body = translate(_('A validation request previously sent to you has been deleted'), context=context.REQUEST) + \
           '\n\n' + \
           translate(_('Title: %s'), context=context.REQUEST) % context.title + \
           '\n\n' + \
           translate(_('Document: %s'), context=context.REQUEST) % document.title + \
           '\n\n' + \
           translate(_('Document Address: %s'), context=context.REQUEST) % document.absolute_url() + \
           '\n\n\n\n-- \n' + \
           translate(_('Sent by GED'))
    body = body.encode('utf-8')

    try:
        context.MailHost.send(body, recipient_email, email_from, subject, charset='utf-8')
    except Exception as e:
        # do not abort transaction in case of email error
        log = logging.getLogger('pfwbged.policy')
        log.exception(e)


@grok.subscribe(IDmsDocument, IObjectModifiedEvent)
def log_some_history(context, event):
    for description in event.descriptions:
        if not hasattr(description, 'attributes'):
            continue
        for field in ('treated_by', 'treating_groups', 'recipient_groups'):
            if not field in description.attributes:
                continue
            annotations = IAnnotations(context)
            if not 'pfwbged_history' in annotations:
                annotations['pfwbged_history'] = []
            value = getattr(context, field)
            annotations['pfwbged_history'].append({'time': DateTime(),
                'action_id': 'pfwbged_field',
                'action': _('New value for %s: %s') % (field, ', '.join(value)),
                'actor_name': api.user.get_current().getId(),
                'attribute': field,
                'value': value,
                })
            # assign it back as a change to the list won't trigger the
            # annotation to be saved on disk.
            annotations['pfwbged_history'] = annotations['pfwbged_history'][:]


@grok.subscribe(IFolder, IObjectAddedEvent)
@grok.subscribe(IDmsDocument, IObjectAddedEvent)
def set_owner_role_on_document(context, event):
    """Makes sure a new document gets its owner role set properly."""
    for creator in context.creators:
        context.manage_setLocalRoles(creator, ['Owner'])


@grok.subscribe(IBaseTask, IObjectAddedEvent)
def set_permissions_on_task_on_add(context, event):
    '''Gives read access to a task for persons that are handling the document'''
    # go up in the acquisition chain to find the document
    document = None
    for obj in aq_chain(context):
        obj = aq_parent(obj)
        if IDmsDocument.providedBy(obj):
            document = obj
            break
    if not document:
        return

    if not hasattr(document, 'treated_by') or not document.treated_by:
        return

    with api.env.adopt_user('admin'):
        for user_id in document.treated_by:
            context.manage_addLocalRoles(user_id, ['Reader'])
            context.reindexObjectSecurity()
            context.reindexObject(idxs=['allowedRolesAndUsers'])

        document.reindexObject(idxs=['allowedRolesAndUsers'])


# not enabled for now, see #4516
#@grok.subscribe(IDmsDocument, IObjectModifiedEvent)
def set_permissions_on_task_from_doc(context, event):
    portal_catalog = api.portal.get_tool('portal_catalog')
    tasks = portal_catalog.unrestrictedSearchResults(
            portal_type=['task', 'validation'],
            path='/'.join(context.getPhysicalPath()))
    if not tasks:
        return

    for description in event.descriptions:
        if not hasattr(description, 'attributes'):
            continue
        for field in ('treated_by', 'treating_groups', 'recipient_groups'):
            if field in description.attributes:
                break
        else:
            return

    with api.env.adopt_user('admin'):
        tasks = [x.getObject() for x in tasks]
        user_ids = []
        for user_id, roles in document.get_local_roles():
            if 'Reader' in roles or 'Editor' in roles:
                user_ids.append(user_id)

        for task in tasks:
            for task_user_id, task_roles in task.get_local_roles():
                if 'Reader' in task_roles and task_user_id not in user_ids:
                    task.manage_delLocalRoles([task_user_id])
            for user_id in user_ids:
                task.manage_addLocalRoles(user_id, ['Reader'])
            task.reindexObjectSecurity()
            task.reindexObject(idxs=['allowedRolesAndUsers'])


@grok.subscribe(IDmsAppendixFile, IObjectAddedEvent)
@grok.subscribe(IDmsFile, IObjectAddedEvent)
def set_permissions_on_files_on_add(context, event):
    '''Gives read access to a version/appendix for persons that are handling
       the document'''
    # go up in the acquisition chain to find the document
    document = None
    for obj in aq_chain(context):
        obj = aq_parent(obj)
        if IDmsDocument.providedBy(obj):
            document = obj
            break
    if not document:
        return

    if not hasattr(document, 'treated_by') or not document.treated_by:
        return

    with api.env.adopt_user('admin'):
        for user_id in document.treated_by:
            context.manage_addLocalRoles(user_id, ['Reader', 'Reviewer'])
            context.reindexObjectSecurity()
            context.reindexObject(idxs=['allowedRolesAndUsers'])

        document.reindexObject(idxs=['allowedRolesAndUsers'])