from numbers import Integral
from django.dispatch import receiver
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group, Permission
from django.core.urlresolvers import reverse
from django.contrib.contenttypes.models import ContentType
from django.db.models.signals import post_save, post_delete
from django.utils.translation import ugettext_lazy as _
from wagtail.wagtailcore.signals import page_published, page_unpublished
from wagtail.wagtailcore.models import Collection, CollectionMember, Page, GroupCollectionPermission
from wagtail.wagtailimages.models import Image
from wagtail.wagtaildocs.models import Document
from .models import ApprovalPipeline, ApprovalStep, ApprovalTicket
from .signals import step_published, pipeline_published, build_approval_item_list, set_collection_edit, take_ownership, pre_transfer_ownership, release_ownership
from .approvalitem import ApprovalItem
@receiver(page_published)
[docs]def send_published_signals(sender, instance, **kwargs):
'''This simply watches for a published step or pipeline, and sends a
:func:`pipeline_published` or :func:`step_published` signal for it.'''
if isinstance(instance, ApprovalPipeline):
pipeline_published.send(sender=type(instance), instance=instance)
elif isinstance(instance, ApprovalStep):
step_published.send(sender=type(instance), instance=instance)
# The prefix string for owned collections and users
_PREFIX = "(Approval) "
@receiver(pipeline_published)
[docs]def setup_pipeline_user(sender, instance, **kwargs):
'''Setup an ApprovalPipeline user'''
User = get_user_model()
username_max_length = User._meta.get_field('username').max_length - len(_PREFIX)
username = _PREFIX + str(instance)[:username_max_length]
user = instance.user
if not user:
user = User.objects.create(username=username)
instance.user = user
instance.save()
@receiver(step_published)
[docs]def setup_group_and_collection(sender, instance, **kwargs):
'''Create or rename the step's owned groups and collections'''
pipeline = instance.get_parent().specific
group_max_length = Group._meta.get_field('name').max_length - len(_PREFIX)
group_name = _PREFIX + '{} - {}'.format(pipeline, instance)[-group_max_length:]
group = instance.group
if not group:
group = Group.objects.create(name=group_name)
access_admin = Permission.objects.get(codename='access_admin')
group.permissions.add(access_admin)
instance.group = group
if group.name != group_name:
group.name = group_name
group.save()
collection_max_length = Collection._meta.get_field('name').max_length - len(_PREFIX)
collection_name = _PREFIX + '{} - {}'.format(pipeline, instance)[-group_max_length:]
collection = instance.collection
if not collection:
root_collection = Collection.get_first_root_node()
collection = root_collection.add_child(name=collection_name)
instance.collection = collection
if collection.name != collection_name:
collection.name = collection_name
collection.save()
# We run a save regardless to ensure permissions are properly set up
instance.save()
@receiver(post_save)
[docs]def catch_collection_objects(sender, instance, created, **kwargs):
'''If newly-created objects are created inside of a collection that is
owned by an ApprovalStep, it will automatically take ownership of those
objects'''
if created and isinstance(instance, CollectionMember):
collection = instance.collection
try:
step = ApprovalStep.objects.get(collection=collection)
except ApprovalStep.DoesNotExist:
return
step.take_ownership(instance)
@receiver(post_delete)
[docs]def approvalticket_cascade_delete(sender, instance, **kwargs):
'''This deletes objects from :class:`ApprovalTicket` if they are deleted,
to avoid leaking space (a deleted object would otherwise never be freed
from the ticket database, as cascades don't work for
:class:`GenericForeignKey` without a :class:`GenericRelation` ).
Essentially, this is a custom cascade delete.'''
# This is to make sure ApprovalTicket objects don't cascade onto
# themselves, and non-integer pks don't blow up the system
if isinstance(instance.pk, Integral):
ApprovalTicket.objects.filter(
content_type=ContentType.objects.get_for_model(instance),
object_id=instance.pk).delete()
@receiver(post_delete, sender=ApprovalPipeline)
[docs]def delete_owned_user(sender, instance, **kwargs):
'''This deletes the owned user from :class:`ApprovalPipeline` when the
pipeline is deleted.'''
if instance.user:
instance.user.delete()
@receiver(post_delete, sender=ApprovalStep)
[docs]def delete_owned_group_and_collection(sender, instance, **kwargs):
'''This deletes the owned group and collection from :class:`ApprovalStep`
when the step is deleted.'''
if instance.group:
instance.group.delete()
if instance.collection:
instance.collection.delete()
@receiver(post_save, sender=ApprovalStep)
[docs]def fix_restrictions(sender, instance, **kwargs):
'''Update ApprovalStep restrictions on a save.'''
instance.fix_permissions()
@receiver(build_approval_item_list)
[docs]def add_pages(sender, approval_step, **kwargs):
'''Builds the approval item list for pages'''
for ticket in ApprovalTicket.objects.filter(
step=approval_step,
content_type=ContentType.objects.get_for_model(Page)):
page = ticket.item
specific = page.specific
# Do not allow unpublished pages. We don't want to end up with a
# non-live page in a "published" step.
if page.live:
yield ApprovalItem(
title=str(specific),
view_url=specific.url,
edit_url=reverse('wagtailadmin_pages:edit', args=(page.id,)),
delete_url=reverse('wagtailadmin_pages:delete', args=(page.id,)),
obj=page,
step=approval_step,
typename=type(specific).__name__,
uuid=ticket.pk)
@receiver(build_approval_item_list)
[docs]def add_images(sender, approval_step, **kwargs):
'''Builds the approval item list for images'''
for ticket in ApprovalTicket.objects.filter(
step=approval_step,
content_type=ContentType.objects.get_for_model(Image)):
image = ticket.item
yield ApprovalItem(
title=str(image),
view_url=image.get_rendition('original').file.url,
edit_url=reverse('wagtailimages:edit', args=(image.id,)),
delete_url=reverse('wagtailimages:delete', args=(image.id,)),
obj=image,
step=approval_step,
typename=type(image).__name__,
uuid=ticket.pk)
@receiver(build_approval_item_list)
[docs]def add_document(sender, approval_step, **kwargs):
'''Builds the approval item list for documents'''
for ticket in ApprovalTicket.objects.filter(
step=approval_step,
content_type=ContentType.objects.get_for_model(Document)):
document = ticket.item
yield ApprovalItem(
title=str(document),
view_url=document.url,
edit_url=reverse('wagtaildocs:edit', args=(document.id,)),
delete_url=reverse('wagtaildocs:delete', args=(document.id,)),
obj=document,
step=approval_step,
typename=type(document).__name__,
uuid=ticket.pk)
@receiver(set_collection_edit)
[docs]def set_image_collection_edit(sender, approval_step, edit, **kwargs):
'''Sets collection permissions for images'''
collection = approval_step.collection
group = approval_step.group
perm = Permission.objects.get(codename='change_image')
if edit:
GroupCollectionPermission.objects.get_or_create(group=group, collection=collection, permission=perm)
else:
GroupCollectionPermission.objects.filter(group=group, collection=collection, permission=perm).delete()
@receiver(set_collection_edit)
[docs]def set_document_collection_edit(sender, approval_step, edit, **kwargs):
'''Sets collection permissions for documents'''
collection = approval_step.collection
group = approval_step.group
perm = Permission.objects.get(codename='change_document')
if edit:
GroupCollectionPermission.objects.get_or_create(group=group, collection=collection, permission=perm)
else:
GroupCollectionPermission.objects.filter(group=group, collection=collection, permission=perm).delete()
@receiver(take_ownership)
[docs]def update_page_ownership(sender, approval_step, object, pipeline, **kwargs):
if isinstance(object, Page):
object.owner = pipeline.user
object.save()
@receiver(take_ownership)
[docs]def update_collection_ownership(sender, approval_step, object, pipeline, **kwargs):
'''Individual take_ownerships for each type should be implemented that also
take the collection member. This is a fallback in case something doesn't
work the way it should'''
if isinstance(object, CollectionMember):
if object.collection != approval_step.collection:
object.collection = approval_step.collection
object.save()
@receiver(take_ownership)
[docs]def update_image_ownership(sender, approval_step, object, pipeline, **kwargs):
if isinstance(object, Image):
updated = False
if object.collection != approval_step.collection:
object.collection = approval_step.collection
updated = True
if object.uploaded_by_user != pipeline.user:
object.uploaded_by_user = pipeline.user
updated = True
if updated:
object.save()
@receiver(take_ownership)
[docs]def update_document_ownership(sender, approval_step, object, pipeline, **kwargs):
if isinstance(object, Document):
updated = False
if object.collection != approval_step.collection:
object.collection = approval_step.collection
updated = True
if object.uploaded_by_user != pipeline.user:
object.uploaded_by_user = pipeline.user
updated = True
if updated:
object.save()
@receiver(release_ownership)
[docs]def release_page_permissions(sender, approval_step, object, pipeline, **kwargs):
if isinstance(object, Page):
# Release all page permissions
approval_step.set_page_group_privacy(object, False)
approval_step.set_page_edit(object, False)
approval_step.set_page_delete(object, False)
@receiver(pre_transfer_ownership)
[docs]def assert_page_live(sender, giving_step, taking_step, object, pipeline, **kwargs):
if isinstance(object, Page):
assert object.live, _('Can not approve or reject a page that is not published')