#
# sub_admin.py (v 1.0)
# by Alex de Landgraaf ( www.alextreme.org )
#
# Use this class for restricting access on a per-item level
# in the Django admin.
#
# Usage: http://www.alextreme.org/drupal/?q=node/600
#
# This code contains ideas and bits of code from various sources
# on the web.
#
# Licensed under the same license as Django (BSD), feel free to do
# with it as you please.
#

from django.contrib import admin
from django.http import Http404, HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, render_to_response

class SubAccessAdmin(admin.ModelAdmin):
    """
    All Admin models that are to be modifiable by the sub-administrators MUST be derived
    from this object. It ensures that the permissions are set properly, creating
    a sandbox where sub-admins can't get at each others data.

    Superusers naturally have complete access. They also see which object instance was
    made by which sub-administrator ('by'-field). This field is hidden by default from the
    sub-admins.

    In order to filter out all objects the sub-admins aren't allowed access to, set the
    following variables accordingly.
    """

    # semi-abstract class, children must set the following in __init__

    filtered_list_filter = None # The field used to filter among objects (list view)
    filtered_foreignkeys = [] # Any ForeignKey fields that need pruning (add/change view)
    filtered_manytomany = [] # Any ManyToMany fields that need pruning (add/change view)
    extra_excluded = [] # Any additional fields superusers must see, but subadmins shouldn't (add/change view)
    superuser_override_by = False # Allow a superuser to modify the by-field. Useful when giving sub-admins per-object modify permissions only
    
    def override_permissions(self, request):
        """
        Avoid the sub-admin seeing the field indicating who created the
        object. Add field-names to self.extra_excluded to hide more fields.
        """
        if not request.user.is_superuser:
            self.exclude = ['by']
            for ex in self.extra_excluded:
                self.exclude += [ ex ]

    def has_permission(self, request, object):
        return self.has_change_permission(request, object)
        
    ### Overriding ModelAdmin methods ###

    def add_view(self, request, form_url='', extra_context = None):
        """
        Override permissions when the user hits the add-page in the admin.
        """
        self.override_permissions(request)
        return super(SubAccessAdmin, self).add_view(request, form_url, extra_context)

    def change_view(self, request, object_id, extra_context = None):
        """
        Override permissions when the user hits the change-page in the admin.
        """
        self.override_permissions(request)
        return super(SubAccessAdmin, self).change_view(request, object_id, extra_context)    

    def changelist_view(self, request, extra_content = None):
        """
        Only show the list_filter to superusers. This is done to avoid a sub-admin
        being able to see objects in the filter UI that he doesn't have access to.
        
        If you want to also have list_filters for sub-admins, either remove this
        method or modify it accordingly.
        """
        if request.user.is_superuser and self.filtered_list_filter:
            self.list_filter = (self.filtered_list_filter,)
        else:
            self.list_filter = ()
        return super(SubAccessAdmin, self).changelist_view(request, extra_content)

    def history_view(self, request, object_id, extra_context = None):
        """
        Although we wouldn't have permission to view this object, the history view
        for an object does show the name of the object anyway.
        """
        if not request.user.is_superuser:
            obj = get_object_or_404(self.model, pk = object_id)
            if obj.by != request.user:
                raise Http404("No %s matches the given query." % self.model._meta.object_name)
        return super(SubAccessAdmin, self).history_view(request, object_id, extra_context = None)

    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        """
        For non-superusers, filter out ForeignKey'ed objects not created by the sub-administrator.
        This affects the add- and modification-pages in the admin.
        Called for each ForeignKey field in your model
        """
        if not request.user.is_superuser:
            for field_name in self.filtered_foreignkeys:
                if field_name == db_field.name:
                    kwargs["queryset"] = db_field.rel.to.objects.filter(by = request.user)
        return super(SubAccessAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)

    def formfield_for_manytomany(self, db_field, request, **kwargs):
        """
        For non-superusers, filter out ManyToMany'ed objects not created by the sub-administrator.
        This affects the add- and modification-pages in the admin.
        Called for each ManyToMany field in your model.
        """
        if not request.user.is_superuser:
            for field_name in self.filtered_manytomany:
                if field_name == db_field.name:
                    kwargs["queryset"] = db_field.rel.to.objects.filter(by = request.user)
        return super(SubAccessAdmin, self).formfield_for_manytomany(db_field, request, **kwargs)    

    def save_model(self, request, obj, form, change):
        """
        Store the User that creates this object.
        Called when the user hits Save on an add/modify admin page.

        FIXME: Even though a sub-admin doesn't see the excluded fields, it would
        be a good idea to check that those fields weren't modified in this method.
        """
        
        if not change and not (request.user.is_superuser and self.superuser_override_by):
            obj.by = request.user
        obj.save()

    def queryset(self, request):
        """
        Limit the objects shown in the main list of objects.
        Called when the user hits the overview page.
        """
        
        self.override_permissions(request)

        l = None
        if request.user.is_superuser:
            l = self.model.objects.all()
        else:
            l = self.model.objects.filter(by = request.user)
        return l

    def has_change_permission(self, request, obj = None):
        """
        Override editting permissions if not superuser and the requesting user isn't the same as the creator.
        Called in various places in contrib.admin to determine if the user has permission to change this object.
        """

        self.override_permissions(request)
        
        has_class_permission = super(SubAccessAdmin, self).has_change_permission(request, obj)
        if not has_class_permission:
            return False
        if obj is not None and not request.user.is_superuser and request.user != obj.by:
            return False
        return True
