source: trunk/merengueproj/merengue/base/admin.py @ 5391

Revision 5391, 75.7 KB checked in by pmartin, 4 months ago (diff)

See #2151 Last changes in refactor of exception Permission Denied

Line 
1# Copyright (c) 2010 by Yaco Sistemas
2#
3# This file is part of Merengue.
4#
5# Merengue is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9#
10# Merengue is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13# GNU Lesser General Public License for more details.
14#
15# You should have received a copy of the GNU Lesser General Public License
16# along with Merengue.  If not, see <http://www.gnu.org/licenses/>.
17
18import datetime
19
20from django import forms
21from django import template
22from django.conf import settings
23from django.contrib import messages
24from django.core.urlresolvers import reverse
25from django.db import models, router
26from django.db.models import Q
27
28from django.db.models.related import RelatedObject
29from django.db.models.fields.related import ForeignKey, ManyToManyField, RelatedField
30from django.shortcuts import render_to_response
31from django.contrib import admin
32from django.contrib.auth.models import User
33from django.contrib.contenttypes.models import ContentType
34from django.contrib.admin.filterspecs import FilterSpec
35from django.contrib.admin.views.main import ChangeList, ERROR_FLAG
36from django.contrib.admin.options import IncorrectLookupParameters
37from django.contrib.admin.util import unquote, flatten_fieldsets
38from django.contrib.admin.models import LogEntry
39from django.contrib.sites.admin import Site, SiteAdmin
40from django.forms.models import ModelForm, BaseInlineFormSet, \
41                                fields_for_model, save_instance, modelformset_factory
42from django.forms.util import ValidationError
43from django.forms.widgets import Media
44from django.http import HttpResponseRedirect, Http404, HttpResponse
45from django.utils import simplejson
46from django.utils.datastructures import SortedDict
47from django.utils.encoding import force_unicode
48from django.utils.functional import update_wrapper
49from django.utils.html import escape
50from django.utils.importlib import import_module
51from django.utils.text import capfirst
52from django.utils.safestring import mark_safe
53from django.utils.translation import ugettext, get_language
54from django.utils.translation import ugettext_lazy as _
55
56from ajax_select.fields import AutoCompleteSelectField, AutoCompleteSelectMultipleField
57from autoreports.admin import ReportAdmin
58from cmsutils.forms.widgets import AJAXAutocompletionWidget, ReadOnlyWidget
59from oembed.models import ProviderRule, StoredOEmbed
60from announcements.models import Announcement
61from announcements.admin import AnnouncementAdmin as AnnouncementDefaultAdmin
62from merengue.base.forms import AnnouncementAdminForm
63from notification.models import NoticeType, NoticeSetting, Notice
64from notification.admin import NoticeTypeAdmin, NoticeSettingAdmin, NoticeAdmin
65from transmeta import (canonical_fieldname, get_all_translatable_fields,
66                       get_real_fieldname_in_each_language,
67                       get_fallback_fieldname, get_real_fieldname)
68
69from merengue.base.actions import related_delete_selected
70from merengue.base.adminsite import site
71from merengue.base.admin_utils import get_deleted_contents
72from merengue.base.filterspecs import ClassnameFilterSpec
73from merengue.base.forms import AdminBaseContentOwnersForm, BaseAdminModelForm
74from merengue.base.models import BaseContent, ContactInfo
75from merengue.base.widgets import CustomTinyMCE, RelatedBaseContentWidget
76from merengue.perms.admin import PermissionAdmin
77from merengue.perms import utils as perms_api
78from merengue.perms.exceptions import PermissionDenied
79from merengue.section.models import BaseSection, SectionRelatedContent
80from merengue.workflow import utils as workflow_api
81from genericforeignkey.admin import GenericAdmin
82
83# A flag to tell us if autodiscover is running.  autodiscover will set this to
84# True while running, and False when it finishes.
85LOADING = False
86
87# Don't call register but insert it at the beginning of the registry
88# otherwise, the AllFilterSpec will be taken first
89FilterSpec.filter_specs.insert(0, (lambda f: f.name == 'class_name',
90                                   ClassnameFilterSpec))
91
92
93def register_app(app_name, admin_site=None):
94    admin_function(function_name='register', app_name=app_name,
95                   admin_site=admin_site)
96
97
98def unregister_app(app_name, admin_site=None):
99    admin_function(function_name='unregister', app_name=app_name,
100                   admin_site=admin_site)
101
102
103def admin_function(function_name, app_name, admin_site=None):
104    import imp
105
106    if admin_site is None:
107        admin_site = site
108
109    # we ensure we not registered twice or unregister unexisting
110    if function_name == 'register' and app_name in admin_site.apps_registered:
111        return
112    elif function_name == 'unregister' and app_name not in admin_site.apps_registered:
113        return
114
115    # For each app, we need to look for an admin.py inside that app's
116    # package. We can't use os.path here -- recall that modules may be
117    # imported different ways (think zip files) -- so we need to get
118    # the app's __path__ and look for admin.py on that path.
119
120    # Step 1: find out the app's __path__ Import errors here will (and
121    # should) bubble up, but a missing __path__ (which is legal, but weird)
122    # fails silently -- apps that do weird things with __path__ might
123    # need to roll their own admin registration.
124    try:
125        app_path = import_module(app_name).__path__
126    except AttributeError:
127        return
128
129    # Step 2: use imp.find_module to find the app's admin.py. For some
130    # reason imp.find_module raises ImportError if the app can't be found
131    # but doesn't actually try to import the module. So skip this app if
132    # its admin.py doesn't exist
133    try:
134        imp.find_module('admin', app_path)
135    except ImportError:
136        return
137
138    # Step 3: import the app's admin file. If this has errors we want them
139    # to bubble up.
140    mod = __import__('%s.admin' % app_name, {}, {}, app_name.split('.'))
141
142    # Step 4: look for register function and call it, passing admin site
143    # as parameter
144    register_func = getattr(mod, function_name, None)
145    if register_func is not None and callable(register_func):
146        register_func(admin_site)
147
148    # finally, we add/remove this app to admin site registry
149    if function_name == 'register':
150        admin_site.apps_registered.append(app_name)
151    else:
152        admin_site.apps_registered.remove(app_name)
153
154
155def autodiscover(admin_site=None):
156    """
157    Like Django autodiscover, it search for admin.py modules and fail silently
158    when not present.
159
160    Main difference is that you can pass admin_site by parameter for
161    registration in this admin site.
162    """
163    # Bail out if autodiscover didn't finish loading from a previous call so
164    # that we avoid running autodiscover again when the URLconf is loaded by
165    # the exception handler to resolve the handler500 view.  This prevents an
166    # admin.py module with errors from re-registering models and raising a
167    # spurious AlreadyRegistered exception (see #8245).
168    global LOADING
169    if LOADING:
170        return
171    LOADING = True
172
173    if admin_site is None:
174        admin_site = site
175
176    for app in settings.INSTALLED_APPS:
177        register_app(app, admin_site)
178
179    # autodiscover was successful, reset loading flag.
180    LOADING = False
181
182
183def set_field_read_only(field, field_name, obj):
184    """ utility function for convert a widget field into a read only widget """
185    if hasattr(obj, 'get_%s_display' % field_name):
186        display_value = getattr(obj, 'get_%s_display' % field_name)()
187    else:
188        display_value = None
189    field.widget = ReadOnlyWidget(getattr(obj, field_name, ''), display_value)
190    field.required = False
191
192
193# Merengue Model Admins -----
194
195
196class ReverseAdminInline(admin.StackedInline):
197
198    class ReverseAdminFormSet(BaseInlineFormSet):
199
200        def __init__(self, data=None, files=None, instance=None,
201                     save_as_new=False, prefix=None):
202            prefix = prefix or self.fk_field_name
203            if instance is None:
204                self.instance = self.model()
205            else:
206                self.instance = instance
207            self.save_as_new = save_as_new
208            field = getattr(self.instance, self.fk_field_name, None)
209            if field:
210                qs = self.model._default_manager.filter(id=field.id)
211            else:
212                qs = self.model._default_manager.get_empty_query_set()
213            super(BaseInlineFormSet, self).__init__(data, files, prefix=prefix, queryset=qs)
214
215        def add_fields(self, form, index):
216            # we don't want BaseInlineFormSet add_fields cause it search the field
217            # defined as a foreign key in the inline model
218            return super(BaseInlineFormSet, self).add_fields(form, index)
219
220        def save_new(self, form, commit=True):
221            """Saves and returns a new model instance for the given form."""
222            attr_instance = save_instance(form, self.model(), commit=commit)
223            if attr_instance.id and self.instance and commit:
224                setattr(self.instance, self.fk_field_name, attr_instance)
225                self.instance.save()
226            return attr_instance
227
228        def save(self, commit=True):
229            attr_instance = BaseInlineFormSet.save(self, commit)
230            # If we change the related object then save the parent object in order
231            # to fire posible events
232            if commit:
233                self.instance.save()
234            return attr_instance
235
236        @classmethod
237        def get_default_prefix(cls):
238            # we make fix the prefix to "form" to avoid problems with Django >= r10019
239            # (not the production one). See ticket #1616
240            return 'form'
241    formset = ReverseAdminFormSet
242
243    def _inlineformset_factory(self, parent_model, model, form=ModelForm,
244                               formset=ReverseAdminFormSet, fk_name=None,
245                               fields=None, exclude=None,
246                               extra=3, can_order=False, can_delete=True, max_num=0,
247                               formfield_callback=lambda f: f.formfield()):
248        max_num = 1
249        if fields is not None:
250            fields = list(fields)
251        else:
252            # get all the fields for this model that will be generated.
253            fields = fields_for_model(model, fields, exclude, formfield_callback).keys()
254        kwargs = {
255            'form': form,
256            'formfield_callback': formfield_callback,
257            'formset': formset,
258            'extra': extra,
259            'can_delete': can_delete,
260            'can_order': can_order,
261            'fields': fields,
262            'exclude': exclude,
263            'max_num': max_num,
264        }
265        FormSet = modelformset_factory(model, **kwargs)
266        FormSet.fk_field_name = self.parent_fk_name
267        #FormSet.fk = fk
268        return FormSet
269
270    def get_formset(self, request, obj=None, **kwargs):
271        """Returns a BaseInlineFormSet class for use in admin add/change views."""
272        if self.declared_fieldsets:
273            fields = flatten_fieldsets(self.declared_fieldsets)
274        else:
275            fields = None
276        if self.exclude is None:
277            exclude = []
278        else:
279            exclude = list(self.exclude)
280        defaults = {
281            "form": self.form,
282            "formset": self.formset,
283            "fk_name": self.fk_name,
284            "fields": fields,
285            "exclude": exclude + kwargs.get("exclude", []),
286            "formfield_callback": self.formfield_for_dbfield,
287            "extra": self.extra,
288            "max_num": self.max_num,
289            "can_delete": False,
290        }
291        defaults.update(kwargs)
292        return self._inlineformset_factory(self.parent_model, self.model, **defaults)
293
294
295class RelatedURLsModelAdmin(admin.ModelAdmin):
296
297    def get_urls(self):
298        from django.conf.urls.defaults import patterns, url
299
300        def wrap(view):
301
302            def wrapper(*args, **kwargs):
303                #if isinstance(self, RelatedModelAdmin):
304                    #kwargs['parent_model_admin'] = self
305                return self.admin_site.admin_view(view)(*args, **kwargs)
306            return update_wrapper(wrapper, view)
307
308        info = self.model._meta.app_label, self.model._meta.module_name
309        urlpatterns = patterns('',
310            url(r'^$',
311                wrap(self.changelist_view),
312                name='%s_%s_changelist' % info),
313            url(r'^add/$',
314                wrap(self.add_view),
315                name='%s_%s_add' % info),
316            url(r'^([^/]+)/history/$',
317                wrap(self.history_view),
318                name='%s_%s_history' % info),
319            url(r'^([^/]+)/delete/$',
320                wrap(self.delete_view),
321                name='%s_%s_delete' % info),
322            url(r'^(.+)/$',
323                wrap(self.parse_path),
324                name='%s_%s_change' % info)
325        )
326        return urlpatterns
327
328    def parse_path(self, request, pathstr, extra_context=None, basecontent=None, parent_model_admin=None, parent_object=None):
329        extra_context = extra_context or {}
330        path = pathstr.split('/')
331        if len(path) == 1:
332            if isinstance(self, RelatedModelAdmin):
333                return self.change_view(request, path[0], extra_context, parent_model_admin, parent_object)
334            else:
335                return self.change_view(request, path[0], extra_context)
336        object_id = path[0]
337        basecontent = self._get_base_content(request, object_id)
338        tool_name = path[1]
339        for cl in self.model.__mro__:
340            tool = self.admin_site.tools.get(cl, {}).get(tool_name, None)
341            if tool:
342                pathstr = '/'.join(path[2:])
343                if pathstr:
344                    pathstr += '/'
345                tool.basecontent = basecontent
346                visited = getattr(request, '__visited__', [])
347                visited = [(self, basecontent)] + visited
348                setattr(request, '__visited__', visited)
349                for pattern in tool.urls:
350                    resolved = pattern.resolve(pathstr)
351                    if resolved:
352                        callback, args, kwargs = resolved
353                        # add ourselves as parent model admin to be referred from child model admin
354                        # add also parent object to be referred also in child model if needed
355                        tool.parent_model_admin = self
356                        if callback.func_name in ('changelist_view', 'change_view',
357                                                  'add_view', 'history_view', 'delete_view',
358                                                  'parse_path', 'permissions_view',
359                                                  'ajax_changelist_view'):
360                            kwargs['parent_model_admin'] = self
361                            kwargs['parent_object'] = basecontent
362                        return callback(request, *args, **kwargs)
363        raise Http404
364
365
366class BaseAdmin(GenericAdmin, ReportAdmin, RelatedURLsModelAdmin):
367    """
368    Base model class for the Merengue model admins which have models that
369    inherit from BaseContent model
370    """
371    html_fields = ()
372    autocomplete_fields = {}
373    edit_related = ()
374    removed_fields = ()
375    list_per_page = 50
376    inherit_actions = True
377    form = BaseAdminModelForm
378    basecontent = None
379    parent_model_admin = None
380
381    def __init__(self, model, admin_site):
382        super(BaseAdmin, self).__init__(model, admin_site)
383        # add all translatable fields to search_fields parameter
384        # i.e. if search_fields = ('name',) would change to ('name_es', 'name_en',)
385        trans_fields = get_all_translatable_fields(self.model)
386        trans_search_fields = []
387        for f in self.search_fields:
388            if f in trans_fields:
389                for trans_f in get_real_fieldname_in_each_language(f):
390                    trans_search_fields.append(trans_f)
391            else:
392                trans_search_fields.append(f)
393        self.search_fields = tuple(trans_search_fields)
394
395    def _media(self):
396        __media = super(BaseAdmin, self)._media()
397        __media.add_js(['merengue/js/ajaxautocompletion/jquery.autocomplete.js',
398                        'merengue/js/ajax_select/ajax_select.js'])
399        __media.add_css({'all': ('merengue/css/ajaxautocompletion/jquery.autocomplete.css',
400                                 'merengue/css/ajax_select/iconic.css')})
401        return __media
402    media = property(_media)
403
404    def get_accesible_states(self, status, user, obj):
405        return status.get_accesible_states(user, obj)
406
407    def get_form(self, request, obj=None, **kwargs):
408        form = super(BaseAdmin, self).get_form(request, obj, **kwargs)
409        keys = form.base_fields.keys()
410        if 'workflow_status' in keys:
411            form.base_fields['workflow_status'].required = True
412            if not obj:
413                status = workflow_api.workflow_by_model(form.Meta.model).get_initial_state()
414            else:
415                status = obj.workflow_status
416            form.base_fields['workflow_status'].queryset = self.get_accesible_states(status,
417                request.user, obj)
418            form.base_fields['workflow_status'].initial = status
419            if 'status' in keys:
420                form.base_fields['workflow_status'].label = form.base_fields['status'].label
421                del form.base_fields['status']
422        return form
423
424    def has_add_permission(self, request):
425        """
426            Overrides Django admin behaviour to add ownership based access control
427        """
428        return perms_api.can_manage_site(request.user)
429
430    def has_change_permission(self, request, obj=None):
431        """
432        Overrides Django admin behaviour to add ownership based access control
433        """
434        return self.has_add_permission(request)
435
436    def has_delete_permission(self, request, obj=None):
437        """
438        Overrides Django admin behaviour to add ownership based access control
439        """
440        return self.has_add_permission(request)
441
442    def _get_base_content(self, request, object_id):
443        model = self.model
444        opts = model._meta
445
446        try:
447            obj = model.objects.get(pk=unquote(object_id))
448        except model.DoesNotExist:
449            # Don't raise Http404 just yet, because we haven't checked
450            # permissions yet. We don't want an unauthenticated user to be able
451            # to determine whether a given object exists.
452            obj = None
453
454        if obj is None:
455            raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {'name': force_unicode(opts.verbose_name), 'key': escape(object_id)})
456
457        return obj
458
459    def formfield_for_dbfield(self, db_field, **kwargs):
460        field = super(BaseAdmin, self).formfield_for_dbfield(db_field, **kwargs)
461        db_fieldname = canonical_fieldname(db_field)
462        if field:
463            field.trans_candidate = getattr(db_field, 'original_fieldname', False) and True
464            field.canonical = db_fieldname
465            field.fallback = get_fallback_fieldname(db_fieldname)
466            field.current = get_real_fieldname(db_fieldname, get_language()) == db_field.name
467
468        # tinymce editor for html fields
469        if db_fieldname in self.html_fields:
470            field.widget = CustomTinyMCE()
471        elif db_field.name in self.autocomplete_fields:
472            options = self.autocomplete_fields[db_field.name].copy()
473            if 'choices' in options:
474                choices = options.pop('choices')
475                field.widget = AJAXAutocompletionWidget(choices=choices, attrs=options)
476            elif 'url' in options:  # Must have url or choices defined
477                url = options.pop('url')
478                field.widget = AJAXAutocompletionWidget(url=url, attrs=options)
479        elif db_fieldname in self.removed_fields:
480            return
481
482        if db_fieldname == 'name' and field and field.required:
483            old_clean = field.clean
484
485            def new_clean(value):
486                if isinstance(value, basestring) and not value.strip():
487                    raise ValidationError(_(u'This field is required.'))
488                return old_clean(value)
489            field.clean = new_clean
490
491        if field and isinstance(db_field, ForeignKey):
492            if db_field.related.parent_model == BaseContent:
493                request = kwargs.get('request', None)
494                field.widget = RelatedBaseContentWidget(field.widget, field.widget.rel, field.widget.admin_site, request=request)
495        if isinstance(db_field, RelatedField) and db_field.related.parent_model == User:
496            if isinstance(field, forms.ModelMultipleChoiceField):
497                field = super(db_field.__class__, db_field).formfield(form_class=AutoCompleteSelectMultipleField, channel='user')
498            else:
499                field = super(db_field.__class__, db_field).formfield(form_class=AutoCompleteSelectField, channel='user')
500        return field
501
502    def get_actions(self, request):
503        """ by default, this admin does not return all hierarchy actions of all parents model admins """
504        if self.inherit_actions:
505            return super(BaseAdmin, self).get_actions(request)
506        else:
507            return self.get_not_inherited_actions(request)
508
509    def get_not_inherited_actions(self, request):
510        """ by default, this admin does not return all hierarchy actions of all parents model admins """
511        class_actions = getattr(self.__class__, 'actions', [])
512        actions = []
513        actions.extend([self.get_action(action) for action in class_actions])
514
515        actions.sort(lambda a, b: cmp(a[2].lower(), b[2].lower()))
516        actions = SortedDict([
517            (name, (func, name, desc))
518            for func, name, desc in actions])
519
520        return actions
521
522    def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
523        """ overrided for allow editing related objects in admin with "edit_related" option """
524        # XXX: it's a little harcoded, and can be improved
525        if change and self.edit_related:
526            object = context['original']
527            edit_related_fields = []
528            for related_field in self.edit_related:
529                # we add only for editing all the edit_related fields
530                related_manager = getattr(object, related_field)
531                related_model = related_manager.model
532                field_label = capfirst(force_unicode(related_model._meta.verbose_name_plural))
533                all_related = related_model._default_manager.all()
534                selected_objects_ids = [o.id for o in related_manager.all()]
535                related_objects = []
536                for obj in all_related:
537                    related_obj_dict = {'object': obj, 'selected': False}
538                    if obj.id in selected_objects_ids:
539                        related_obj_dict['selected'] = True
540                    related_objects.append(related_obj_dict)
541
542                related_field_dict = {
543                                'field_name': related_field,
544                                'field_label': field_label,
545                                'related_objects': related_objects,
546                }
547                edit_related_fields.append(related_field_dict)
548            media = Media()
549            media.add_js([settings.ADMIN_MEDIA_PREFIX + "js/SelectBox.js",
550                          settings.ADMIN_MEDIA_PREFIX + "js/SelectFilter2.js",
551                          ])
552            context.update({
553                'edit_related_fields': edit_related_fields,
554                'media': context['media'] + media.render(),
555            })
556        return super(BaseAdmin, self).render_change_form(request, context, add, change, form_url, obj)
557
558    def save_model(self, request, obj, form, change):
559        """
560        Hack for saving related object from edit_related admin option
561        """
562        if hasattr(obj, 'last_editor'):
563            obj.last_editor = request.user
564
565        super(BaseAdmin, self).save_model(request, obj, form, change)
566        if change and self.edit_related:
567            for related_field in self.edit_related:
568                related_manager = getattr(obj, related_field)
569                related_model = related_manager.model
570                selected_ids = [int(data) for data in request.POST.getlist('edit_related_%s' % related_field)]
571                related_ids = [o.id for o in related_manager.all()]
572                # deleting related objects not selected by user
573                for id_obj in related_ids:
574                    if id_obj not in selected_ids:
575                        object_to_remove = related_model._default_manager.get(id=id_obj)
576                        related_manager.remove(object_to_remove)
577                # adding selected objects not already related to object
578                for id_obj in selected_ids:
579                    if id_obj not in related_ids:
580                        object_to_add = related_model._default_manager.get(id=id_obj)
581                        related_manager.add(object_to_add)
582
583    def confirm_action(self, request, queryset=None, extra_context=None,
584                       confirm_template="admin/confirm_action.html"):
585        """A generic confirm view for admin actions, taken from
586        django-batchadmin"""
587
588        if not queryset:
589            queryset = self.model._default_manager.none()
590
591        opts = self.model._meta
592        app_label = opts.app_label
593        selected_objects = []
594        context = {}
595        checkbox = u'''<input class="batch-select" type="checkbox" name="%(name)s"
596                    value="%(object_id)s" checked="true"/>%(model_name)s: %(object_name)s'''
597        checkbox_data = {'name': admin.ACTION_CHECKBOX_NAME,
598                         'model_name': escape(force_unicode(capfirst(opts.verbose_name))),
599                        }
600        for i, obj in enumerate(queryset):
601            if not self.has_change_permission(request, obj):
602                raise PermissionDenied(content=obj,
603                                       user=request.user)
604            checkbox_data['object_name'] = escape(obj)
605            checkbox_data['object_id'] = obj.id
606            selected_objects.append([mark_safe(checkbox % checkbox_data), []])
607            perms_needed = set()
608            context = {
609                "title": _("Are you sure?"),
610                "object_name": force_unicode(opts.verbose_name),
611                "object": obj,
612                "selected_objects": selected_objects,
613                "perms_lacking": perms_needed,
614                "opts": opts,
615                "root_path": self.admin_site.root_path,
616                "app_label": app_label,
617                "objects_id": queryset,
618            }
619            context.update(extra_context or {})
620
621        return render_to_response(confirm_template,
622                                  context,
623                                  context_instance=template.RequestContext(request))
624
625    def _base_update_extra_context(self, extra_context=None):
626        extra_context = extra_context or {}
627        extra_context.update({'model_admin': self})
628        return extra_context
629
630    def changelist_view(self, request, extra_context=None):
631        extra_context = self._base_update_extra_context(extra_context)
632        return super(BaseAdmin, self).changelist_view(request, extra_context)
633
634    def add_view(self, request, form_url='', extra_context=None):
635        extra_context = self._base_update_extra_context(extra_context)
636        return super(BaseAdmin, self).add_view(request, form_url, extra_context)
637
638    def change_view(self, request, object_id, extra_context=None):
639        extra_context = self._base_update_extra_context(extra_context)
640        return super(BaseAdmin, self).change_view(request, object_id, extra_context)
641
642    def history_view(self, request, object_id, extra_context=None):
643        extra_context = self._base_update_extra_context(extra_context)
644        return super(BaseAdmin, self).history_view(request, object_id, extra_context)
645
646    def delete_view(self, request, object_id, extra_context=None, bypass_django_perms=False):
647        """
648        Override (or semi-duplicated) Django one to handle Merengue permissions
649        """
650        extra_context = self._base_update_extra_context(extra_context)
651        opts = self.model._meta
652        app_label = opts.app_label
653
654        obj = self.get_object(request, unquote(object_id))
655
656        if not self.has_delete_permission(request, obj):
657            raise PermissionDenied(content=obj,
658                                   user=request.user)
659
660        if obj is None:
661            raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {'name': force_unicode(opts.verbose_name), 'key': escape(object_id)})
662
663        using = router.db_for_write(self.model)
664
665        # Populate deleted_objects, a data structure of all related objects that
666        # will also be deleted.
667        (deleted_objects, objects_without_delete_perm, perms_needed, protected) = get_deleted_contents((obj, ), opts, request.user, self.admin_site, using, bypass_django_perms)
668
669        # perms_needed
670
671        if request.POST:  # The user has already confirmed the deletion.
672            if perms_needed or objects_without_delete_perm or protected:
673                raise PermissionDenied(content=obj, user=request.user)
674            obj_display = force_unicode(obj)
675            self.log_deletion(request, obj, obj_display)
676            obj.delete()
677
678            self.message_user(request, _('The %(name)s "%(obj)s" was deleted successfully.') % {'name': force_unicode(opts.verbose_name), 'obj': force_unicode(obj_display)})
679
680            if not self.has_change_permission(request, None):
681                return HttpResponseRedirect("../../../../")
682            return HttpResponseRedirect("../../")
683
684        context = {
685            "title": _("Are you sure?"),
686            "object_name": force_unicode(opts.verbose_name),
687            "object": obj,
688            "deleted_objects": deleted_objects,
689            "objects_without_delete_perm": objects_without_delete_perm,
690            "perms_lacking": perms_needed,
691            "protected": protected,
692            "opts": opts,
693            "root_path": self.admin_site.root_path,
694            "app_label": app_label,
695        }
696        context.update(extra_context or {})
697        context_instance = template.RequestContext(request, current_app=self.admin_site.name)
698        return render_to_response(self.delete_confirmation_template or [
699            "admin/%s/%s/delete_confirmation.html" % (app_label, opts.object_name.lower()),
700            "admin/%s/delete_confirmation.html" % app_label,
701            "admin/delete_confirmation.html",
702        ], context, context_instance=context_instance)
703
704    def object_tools(self, request, mode, url_prefix):
705        """ Object tools for the model admin. mode can be "change", "add" or "list" """
706        tools = []
707        if mode == 'change':
708            tools.append(
709                {'url': url_prefix + 'history/', 'label': ugettext('History'), 'class': 'historylink', 'permission': 'change'},
710            )
711        elif mode == 'list':
712            tools.extend([
713                {'url': url_prefix + 'add/', 'label': ugettext('Add %s') % force_unicode(self.model._meta.verbose_name),
714                 'class': 'addlink', 'permission': 'add'},
715                {'url': url_prefix + 'report/quick/?%s' % request.GET.urlencode(), 'label': ugettext('Quick Report'), 'class': 'quickreportlink reportlink'},
716                {'url': url_prefix + 'report/advance/', 'label': ugettext('Advanced Report'), 'class': 'advancedreportlink reportlink'},
717                {'url': url_prefix + 'report/', 'label': ugettext('Reports'), 'class': 'reportslink reportlink'},
718                {'url': url_prefix + 'report/wizard/', 'label': ugettext('Wizard Report'), 'class': 'wizardreportlink reportlink'},
719            ])
720        return tools
721
722
723class BaseCategoryAdmin(BaseAdmin):
724    """
725    Base model class for the Merengue model admins which have models that
726    inherit from BaseCategory
727    """
728    ordering = (get_fallback_fieldname('name'), )
729    search_fields = (get_fallback_fieldname('name'), )
730    prepopulated_fields = {'slug': (get_fallback_fieldname('name'), )}
731
732    def has_add_permission(self, request):
733        return perms_api.has_global_permission(request.user, 'manage_category')
734
735    def has_change_permission(self, request, obj=None):
736        return self.has_add_permission(request)
737
738    def has_delete_permission(self, request, obj=None):
739        return self.has_add_permission(request)
740
741
742class PluginAdmin(BaseAdmin):
743    """ This is a class to be overriden by plugin modeladmins """
744
745    def has_add_permission(self, request):
746        return perms_api.can_manage_plugin_content(request.user)
747
748    def has_change_permission(self, request, obj=None):
749        return self.has_add_permission(request)
750
751    def has_delete_permission(self, request, obj=None):
752        return self.has_add_permission(request)
753
754
755class WorkflowBatchActionProvider(object):
756    """ Provides batch actions for changing the status of contents """
757
758    def set_as_draft(self, request, queryset):
759        return self.change_state(request, queryset, 'draft',
760                                 ugettext(u'Are you sure you want to set this items as draft?'),
761                                 'can_draft')
762    set_as_draft.short_description = _("Set as draft")
763
764    def set_as_pending(self, request, queryset):
765        return self.change_state(request, queryset, 'pending',
766                                 ugettext(u'Are you sure you want to set this items as pending?'),
767                                 'can_pending')
768    set_as_pending.short_description = _("Set as pending")
769
770    def set_as_published(self, request, queryset):
771        return self.change_state(request, queryset, 'published',
772                                 ugettext(u'Are you sure you want to set this items as published?'),
773                                 'can_published')
774    set_as_published.short_description = _("Set as published")
775
776    def change_state(self, request, queryset, state, confirm_msg, perm=None):
777        if perm:
778            perms_api.assert_has_permission_in_queryset(queryset, request.user, perm, None)
779        if not self.has_change_permission(request):
780            raise PermissionDenied(content=queryset, user=request.user)
781        selected = request.POST.getlist(admin.ACTION_CHECKBOX_NAME)
782        if selected:
783            if request.POST.get('post', False):
784                # we need loop because a weird error in Django ORM when you loop queryset before calling values_list
785                # this happens when a non superuser tries to change the status of some contents. See #2192
786                status_list = [(t[0], t[1]) for t in queryset.values_list('pk', 'status')]
787                original_status_dict = dict(status_list)
788                for content in queryset:
789                    workflow_api.change_status(content, state)
790                obj_log = ugettext("Changed to %s") % state
791                msg_data = {'number': queryset.count(),
792                            'model_name': self.opts.verbose_name,
793                            'state': state}
794                msg = ugettext(u"Successfully set %(number)d %(model_name)s as %(state)s.") % msg_data
795                for obj in queryset:
796                    obj._original_status = original_status_dict.get(obj.pk, obj.status)
797                    self.log_change(request, obj, obj_log)
798                    obj.save()
799                self.message_user(request, msg)
800            else:
801                extra_context = {'title': confirm_msg,
802                                 'action_submit': 'set_as_%s' % state}
803                return self.confirm_action(request, queryset, extra_context)
804    change_state.short_description = _(u"Change state of selected %(verbose_name_plural)s")
805
806
807class BaseOrderableAdmin(BaseAdmin):
808    """ A model admin that can reorder content by a sortablefield """
809    change_list_template = "admin/basecontent/sortable_change_list.html"
810    sortablefield = 'position'
811    sortablereverse = False
812
813    def changelist_view(self, request, extra_context=None):
814        if request.method == 'POST':
815            neworder = request.POST.get('neworder', None)
816            page = request.GET.get('p', 0)
817            if neworder is None:
818                return super(BaseOrderableAdmin, self).changelist_view(request, extra_context)
819            neworder = neworder.split(',')
820            if self.sortablereverse:
821                neworder.reverse()
822            items = self.model.objects.filter(id__in=neworder)
823            for item in items:
824                newposition = neworder.index(unicode(item.id)) + (int(page) * 50)
825                setattr(item, self.sortablefield, newposition)
826                item.save()
827
828        return super(BaseOrderableAdmin, self).changelist_view(request, extra_context)
829
830
831class BaseContentAdmin(BaseOrderableAdmin, WorkflowBatchActionProvider, PermissionAdmin):
832    """
833    Base model class for the Merengue model admins which have models that
834    inherit from BaseContent
835    """
836    change_list_template = "admin/basecontent/sortable_change_list.html"
837    list_display = ('__unicode__', 'workflow_status', 'user_modification_date', 'last_editor')
838    list_display_for_select = ('name', 'status', 'user_modification_date', 'last_editor')
839    search_fields = (get_fallback_fieldname('name'), )
840    date_hierarchy = 'creation_date'
841    list_filter = ('workflow_status', 'user_modification_date', 'last_editor', )
842    select_list_filter = ('class_name', 'status', 'user_modification_date', )
843    actions = ['set_as_draft', 'set_as_pending', 'set_as_published', 'assign_owners']
844    edit_related = ()
845    html_fields = ('description', )
846    prepopulated_fields = {'slug': (get_fallback_fieldname('name'), )}
847    autocomplete_fields = {'tags': {'url': '/%s/base/ajax/autocomplete/tags/base/basecontent/' % settings.MERENGUE_URLS_PREFIX,
848                                    'multiple': True,
849                                    'multipleSeparator': ",",
850                                    'size': 100}, }
851    exclude = ('adquire_global_permissions', )
852
853    def __init__(self, *args, **kwargs):
854        super(BaseContentAdmin, self).__init__(*args, **kwargs)
855        # Save original prepopulated fields just in case we have to remove any readonly field from it
856        self.original_prepopulated_fields = self.prepopulated_fields.copy()
857
858    def get_urls(self):
859        from django.conf.urls.defaults import patterns
860        urls = super(BaseContentAdmin, self).get_urls()
861        # override objectpermissions root path
862        my_urls = patterns('',
863            (r'^([^/]+)/permissions/$', self.admin_site.admin_view(self.changelist_view)))
864
865        return my_urls + urls
866
867    def queryset(self, request):
868        queryset = super(BaseContentAdmin, self).queryset(request)
869        return queryset.select_related("workflow_status")
870
871    def add_owners(self, request, queryset, owners):
872        if self.has_change_permission(request):
873            #selected = request.POST.getlist(admin.ACTION_CHECKBOX_NAME)
874            n = queryset.count()
875            obj_log = ugettext("Assigned owners")
876            msg = "Successfully set owners for %d %s." % (n, self.opts.verbose_name)
877            if n:
878                owner_list = User.objects.filter(id__in=owners)
879                for obj in queryset:
880                    for owner in owner_list:
881                        obj.owners.add(owner)
882                    self.log_change(request, obj, obj_log)
883                self.message_user(request, msg)
884
885    def assign_owners(self, request, queryset):
886        if not request.user.is_superuser:
887            raise PermissionDenied(user=request.user)
888        selected = request.POST.getlist(admin.ACTION_CHECKBOX_NAME)
889        if selected:
890            if request.POST.get('post', False):
891                owners = request.POST.getlist('owners')
892                return self.add_owners(request, queryset, owners)
893            form = AdminBaseContentOwnersForm()
894            extra_context = {'title': _('Are you sure you want to assign these owners to these contents?'),
895                             'action_submit': 'assign_owners',
896                             'form': form,
897                            }
898            return self.confirm_action(request, queryset, extra_context,
899                                       confirm_template='admin/basecontent/assign_owners.html')
900    assign_owners.short_description = _("Assign owners")
901
902    def has_add_permission(self, request):
903        """
904        Overrides Django admin behaviour to add ownership based access control
905        """
906        return perms_api.has_global_permission(request.user, 'edit')
907
908    def has_change_permission(self, request, obj=None):
909        """
910        Overrides Django admin behaviour to add ownership based access control
911        """
912        if obj:
913            if request.method == 'POST' and obj.no_changeable:
914                return False
915            else:  # changeable or GET
916                return perms_api.has_permission(obj, request.user, 'edit')
917        else:  # obj = None
918            if request.method == 'POST' and \
919               (request.POST.get('action', None) == u'set_as_pending' or \
920                request.POST.get('action', None) == u'set_as_published' or \
921                request.POST.get('action', None) == u'set_as_draft'):
922
923                selected_objs = [BaseContent.objects.get(id=int(key))
924                                 for key in request.POST.getlist('_selected_action')]
925                for sel_obj in selected_objs:
926                    if not self.has_change_permission(request, sel_obj):
927                        return False
928        return perms_api.has_global_permission(request.user, 'edit')
929
930    def has_delete_permission(self, request, obj=None):
931        """
932        Overrides Django admin behaviour to add ownership based access control
933        """
934        if obj:
935            if obj.no_deletable:
936                return False
937            else:  # deletable
938                return obj.can_delete(request.user)
939        else:  # obj = None
940            if request.method == 'POST' and \
941               request.POST.get('action', None) == u'delete_selected':
942                selected_objs = [BaseContent.objects.get(id=int(key))
943                                 for key in request.POST.getlist('_selected_action')]
944                for sel_obj in selected_objs:
945                    if not self.has_delete_permission(request, sel_obj):
946                        return False
947                return True
948        return False
949
950    def has_change_permission_to_any(self, request):
951        """
952        Overrides Django admin behaviour to add ownership based access control
953        """
954        return super(BaseContentAdmin, self).has_change_permission(request, None)
955
956    def get_readonly_fields(self, request, obj=None):
957        """
958        Overrides Django admin behaviour for adding non changeable fields support
959        """
960        readonly_fields = super(BaseContentAdmin, self).get_readonly_fields(request, obj)
961        if obj and obj.no_changeable_fields:
962            readonly_fields += tuple(obj.no_changeable_fields)
963        self.prepopulated_fields = self.original_prepopulated_fields.copy()
964        for f in readonly_fields:
965            if f in self.prepopulated_fields.keys():
966                del(self.prepopulated_fields[f])
967        return readonly_fields
968
969    def get_form(self, request, obj=None, **kwargs):
970        """
971        Overrides Django admin behaviour to do extra logic
972        """
973        if not request.user.is_superuser:
974            # we remove ownership selection
975            exclude = self.exclude or tuple()
976            kwargs.update({
977                'exclude': ['owners'] + list(exclude) + kwargs.get("exclude", []),
978            })
979        form = super(BaseContentAdmin, self).get_form(request, obj, **kwargs)
980        keys = form.base_fields.keys()
981        if 'workflow_status' in keys:
982            form.base_fields['workflow_status'].required = True
983            if not obj:
984                status = workflow_api.workflow_by_model(form.Meta.model).get_initial_state()
985            else:
986                status = obj.workflow_status
987            form.base_fields['workflow_status'].queryset = self.get_accesible_states(status,
988                request.user, obj)
989            form.base_fields['workflow_status'].initial = status
990            if 'status' in keys:
991                form.base_fields['workflow_status'].label = form.base_fields['status'].label
992                del form.base_fields['status']
993        if 'owners' in keys:
994            owners_field = form.base_fields['owners']
995            if owners_field.initial is None:
996                # user automatically get owner of this object
997                owners_field.initial = (request.user.id, )
998        sections = BaseSection.objects.all()
999        object_sections = obj and obj.sections.all()
1000        # We added the section of the content. Manager maybe would want to change it
1001        initial_section = object_sections and object_sections[0] or None
1002        form.base_fields['section'] = forms.ModelChoiceField(
1003            sections, required=False, initial=initial_section,
1004            label=ugettext('Section'),
1005            help_text=ugettext('Enter the section to which content belongs'),
1006        )
1007        if obj and getattr(obj, 'no_changeable', False):
1008            # Prevent changes if some one forces a save submit
1009            form.is_valid = lambda x: False
1010        return form
1011
1012    def save_model(self, request, obj, form, change):
1013        """
1014        Overrides the Django behaviour to do extra logic
1015        """
1016        # request.user will be the last editor
1017        obj.last_editor = request.user
1018
1019        # simulate auto_now=True for user_modification_date
1020        obj.user_modification_date = datetime.datetime.today()
1021
1022        super(BaseContentAdmin, self).save_model(request, obj, form, change)
1023
1024        if 'section' in form.fields:
1025            # change/remove the section of the content
1026            section = form.cleaned_data['section']
1027            object_sections = obj.sections.all()
1028            if section is not None and section not in object_sections:
1029                SectionRelatedContent.objects.create(basecontent=obj, basesection=section)
1030            elif section is None and object_sections:
1031                SectionRelatedContent.objects.filter(basecontent=obj).delete()
1032
1033    def changelist_view(self, request, extra_context=None):
1034        if request.GET.get('for_select', None):
1035            get = request.GET.copy()
1036            del(get['for_select'])
1037            request.GET = get
1038            return self.select_changelist_view(request, extra_context)
1039        return super(BaseContentAdmin, self).changelist_view(request, extra_context)
1040
1041    def select_changelist_view(self, request, extra_context=None):
1042        widget_id = request.GET.get('widget_id', None)
1043        if widget_id:
1044            del(request.GET['widget_id'])
1045            extra_query = request.session.get(widget_id, {})
1046            if extra_query:
1047                request.GET.update(extra_query)
1048        extra_context = self._base_update_extra_context(extra_context)
1049        opts = self.model._meta
1050        app_label = opts.app_label
1051        list_display = list(self.list_display_for_select)
1052
1053        try:
1054            list_display.remove('action_checkbox')
1055        except ValueError:
1056            pass
1057
1058        try:
1059            cl = ChangeList(request, self.model, list_display, list_display[0], self.select_list_filter,
1060                self.date_hierarchy, self.search_fields, self.list_select_related, self.list_per_page, self.list_editable, self)
1061        except IncorrectLookupParameters:
1062            if ERROR_FLAG in request.GET.keys():
1063                return render_to_response('admin/invalid_setup.html', {'title': _('Database error')})
1064            return HttpResponseRedirect(request.path + '?' + ERROR_FLAG + '=1')
1065        cl.formset = None
1066        cl.params.update({'for_select': 1})
1067        if widget_id:
1068            cl.params.update({'widget_id': widget_id})
1069            for key in extra_query.keys():
1070                del(cl.params[key])
1071        context = {
1072            'title': cl.title,
1073            'is_popup': cl.is_popup,
1074            'cl': cl,
1075            'media': self.media,
1076            'has_add_permission': False,
1077            'root_path': self.admin_site.root_path,
1078            'app_label': app_label,
1079            'action_form': None,
1080            'actions_on_top': [],
1081            'actions_on_bottom': [],
1082        }
1083        context.update(extra_context or {})
1084        context_instance = template.RequestContext(request, current_app=self.admin_site.name)
1085        return render_to_response(self.change_list_template or [
1086            'admin/%s/%s/change_list.html' % (app_label, opts.object_name.lower()),
1087            'admin/%s/change_list.html' % app_label,
1088            'admin/change_list.html',
1089        ], context, context_instance=context_instance)
1090
1091    def _base_update_extra_context(self, extra_context=None):
1092        extra_context = super(BaseContentAdmin, self)._base_update_extra_context(extra_context)
1093
1094        extra_context.update({'with_permissions': True})
1095        return extra_context
1096
1097if settings.USE_GIS:
1098    BaseContentAdmin.list_display += ('google_minimap', )
1099
1100
1101class BaseContentViewAdmin(BaseContentAdmin):
1102    """ An special admin to find and edit all site contents """
1103
1104    list_display = ('admin_link_markup', ) + BaseContentAdmin.list_display[1:]
1105    list_filter = BaseContentAdmin.list_filter + ('class_name', )
1106
1107    def has_add_permission(self, request):
1108        return False
1109
1110    def lookup_allowed(self, lookup, value):
1111        is_allowed = super(BaseContentViewAdmin, self).lookup_allowed(lookup, value)
1112        return is_allowed or lookup == u'id__in'
1113
1114    def queryset(self, request):
1115        qs = super(BaseContentViewAdmin, self).queryset(request)
1116        if perms_api.can_manage_site(request.user):
1117            return qs
1118        elif self.has_change_permission(request):
1119            return qs.filter(Q(owners=request.user) | Q(sections__owners=request.user))
1120
1121
1122def related_form_clean(related_field, basecontent):
1123    """ Returns a customized form.clean() method that insert the related field
1124        into cleaned_data """
1125    def _form_clean(self):
1126        cleaned_data = super(self.__class__, self).clean()
1127        if not related_field:
1128            return cleaned_data
1129        related_content = basecontent
1130        field = self.instance._meta.get_field_by_name(related_field)[0]
1131        if isinstance(field, ManyToManyField):
1132            # if m2m the cleaned_data have to be a iterable
1133            related_content = [related_content, ]
1134        cleaned_data[related_field] = related_content
1135        return cleaned_data
1136    return _form_clean
1137
1138
1139class RelatedModelAdmin(BaseAdmin):
1140    """
1141    A related model admin. This admin will be appears
1142
1143    Example use::
1144
1145      class Book(models.Model):
1146          ...
1147
1148      class Page(models.Model):
1149          book = models.ForeignKey(Book)
1150          ...
1151
1152      class PageRelatedAdmin(RelatedModelAdmin):
1153          model = Page
1154          tool_name = 'pages'
1155          tool_label = 'book pages'
1156          related_field = 'book'
1157
1158      >>> site.register_related(Page, PageRelatedAdmin, related_to=Book)
1159    """
1160    tool_name = None
1161    tool_label = None
1162    related_field = None
1163    reverse_related_field = None  # For m2m with through class
1164    one_to_one = False
1165    manage_contents = False
1166    change_form_template = 'admin/related_change_form.html'
1167    change_list_template = 'admin/related_change_list.html'
1168    object_history_template = 'admin/related_object_history.html'
1169
1170    def __init__(self, *args, **kwargs):
1171        super(RelatedModelAdmin, self).__init__(*args, **kwargs)
1172        self.parent_model_admin = None
1173        if not self.tool_name:
1174            self.tool_name = self.model._meta.module_name
1175        if not self.tool_label:
1176            self.tool_label = self.model._meta.verbose_name_plural
1177        if not self.related_field:
1178            pass
1179        for inline in self.inline_instances:
1180            inline.admin_model = self  # for allow retrieving basecontent object
1181
1182    def get_urls(self):
1183        from django.conf.urls.defaults import patterns, url
1184
1185        def wrap(view):
1186
1187            def wrapper(*args, **kwargs):
1188                kwargs['model_admin'] = self
1189                return self.admin_site.admin_view(view)(*args, **kwargs)
1190            return update_wrapper(wrapper, view)
1191
1192        info = self.model._meta.app_label, self.model._meta.module_name
1193        urlpatterns = patterns('',
1194            url(r'^ajax/$',
1195                wrap(self.ajax_changelist_view),
1196                name='ajax_%s_%s_changelist' % info),
1197        )
1198        urlpatterns += super(RelatedModelAdmin, self).get_urls()
1199        return urlpatterns
1200
1201    def _update_extra_context(self, request, extra_context=None, parent_model_admin=None, parent_object=None):
1202        if parent_model_admin:
1203            self.parent_model_admin = parent_model_admin
1204        extra_context = extra_context or {}
1205        #basecontent = self._get_base_content(request)
1206        basecontent_type_id = ContentType.objects.get_for_model(self.basecontent).id
1207        extra_context.update({'related_admin_site': self.admin_site,
1208                              'basecontent': self.basecontent,
1209                              'basecontent_opts': self.basecontent._meta,
1210                              'basecontent_type_id': basecontent_type_id,
1211                              'inside_basecontent': True,
1212                              'selected': self.tool_name,
1213                              'model_admin': self,
1214                              'parent_model_admin': self.parent_model_admin,
1215                              'parent_object': parent_object,
1216                              })
1217        return extra_context
1218
1219    def is_created_one_to_one_object(self):
1220        obj = self.model._default_manager.filter(**{self.related_field: self.basecontent})
1221        if obj:
1222            return obj[0]
1223        return None
1224
1225    def ajax_changelist_view(self, request, extra_context=None, model_admin=None, parent_model_admin=None, parent_object=None):
1226        extra_context = self._update_extra_context(request, extra_context, parent_model_admin, parent_object)
1227        if not self.has_change_permission(request, None):
1228            raise PermissionDenied(user=request.user)
1229        contents = [{'name': unicode(i), 'url': i.get_admin_absolute_url()} for i in self.queryset(request)]
1230        json_dict = simplejson.dumps({'contents': contents,
1231                                      'size': len(contents),
1232                                      'message': ugettext('No contents found')})
1233        return HttpResponse(json_dict, mimetype='text/plain')
1234
1235    def changelist_view(self, request, extra_context=None, parent_model_admin=None, parent_object=None):
1236        extra_context = self._update_extra_context(request, extra_context, parent_model_admin, parent_object)
1237        if self.one_to_one:
1238            obj_created = self.is_created_one_to_one_object()
1239            if obj_created:
1240                return HttpResponseRedirect('%s%s' % (request.get_full_path(), obj_created.pk))
1241            return HttpResponseRedirect('%sadd' % request.get_full_path())
1242        return super(RelatedModelAdmin, self).changelist_view(request, extra_context)
1243
1244    def queryset(self, request, basecontent=None):
1245        base_qs = super(RelatedModelAdmin, self).queryset(request)
1246        if basecontent is None:
1247            # we override our related content
1248            basecontent = self.basecontent
1249        return base_qs.filter(**{self.related_field: basecontent})
1250
1251    def add_view(self, request, form_url='', extra_context=None, parent_model_admin=None, parent_object=None):
1252        extra_context = self._update_extra_context(request, extra_context, parent_model_admin, parent_object)
1253        if self.one_to_one:
1254            obj_created = self.is_created_one_to_one_object()
1255            if obj_created:
1256                return HttpResponseRedirect('%s../%s' % (request.get_full_path(), obj_created.pk))
1257        return super(RelatedModelAdmin, self).add_view(request, form_url, extra_context)
1258
1259    def change_view(self, request, object_id, extra_context=None, parent_model_admin=None, parent_object=None):
1260        extra_context = self._update_extra_context(request, extra_context, parent_model_admin, parent_object)
1261        return super(RelatedModelAdmin, self).change_view(request, object_id, extra_context)
1262
1263    def delete_view(self, request, object_id, extra_context=None, parent_model_admin=None, parent_object=None):
1264        extra_context = self._update_extra_context(request, extra_context, parent_model_admin, parent_object)
1265        return super(RelatedModelAdmin, self).delete_view(request, object_id, extra_context, bypass_django_perms=True)
1266
1267    def history_view(self, request, object_id, extra_context=None, parent_model_admin=None, parent_object=None):
1268        extra_context = self._update_extra_context(request, extra_context, parent_model_admin, parent_object)
1269        return super(RelatedModelAdmin, self).history_view(request, object_id, extra_context)
1270
1271    def save_model(self, request, obj, form, change):
1272        super(RelatedModelAdmin, self).save_model(request, obj, form, change)
1273        opts = obj._meta
1274        field = opts.get_field_by_name(self.related_field)[0]
1275        if isinstance(field, RelatedObject) and \
1276           not isinstance(field.field, models.OneToOneField):
1277            # if related_field related foreign key (n elements)
1278            # we associate related object here
1279            manager = getattr(obj, field.get_accessor_name())
1280            manager_reverse = None
1281            if self.reverse_related_field:
1282                reverse_field = opts.get_field_by_name(self.reverse_related_field)[0]
1283                if isinstance(reverse_field, RelatedObject) and \
1284                    not isinstance(field.field, models.OneToOneField):
1285                    manager_reverse = getattr(self.basecontent, reverse_field.get_accessor_name())
1286            through_model = getattr(manager, 'through', None) or \
1287                            (self.reverse_related_field and getattr(manager_reverse, 'through', None))
1288            if through_model is None:
1289                # we only know how handle many 2 many without intermediate models
1290                manager.add(self.basecontent)
1291        self.custom_relate_content(request, obj, form, change)
1292        if not change and perms_api.can_use_permissions(obj):
1293            self.inherit_local_roles(request, obj)
1294
1295    def custom_relate_content(self, request, obj, form, change):
1296        """
1297        Custom relation function. to override if child classes wants.
1298        Useful for example in many2many relations with intermediate models, because
1299        we don't know how to handle this.
1300        """
1301        pass
1302
1303    def inherit_local_roles(self, request, obj):
1304        """
1305        Custom function to inherit related basecontent local roles when creating
1306        a new obj via realted model admin
1307        """
1308        if not isinstance(obj, BaseContent):
1309            return
1310        for principal, lrole in perms_api.get_all_local_roles(self.basecontent):
1311            perms_api.add_local_role(obj, principal, lrole)
1312
1313    def get_form(self, request, obj=None, **kwargs):
1314        form = super(RelatedModelAdmin, self).get_form(request, obj, **kwargs)
1315        self.remove_related_field_from_form(form)
1316        form.clean = related_form_clean(self.related_field, self.basecontent)
1317        return form
1318
1319    def remove_related_field_from_form(self, form):
1320        if self.related_field in form.base_fields.keys():
1321            form.base_fields.pop(self.related_field)
1322
1323    def object_tools(self, request, mode, url_prefix):
1324        """ Object tools for the model admin """
1325        return BaseAdmin.object_tools(self, request, mode, url_prefix)
1326
1327    def has_add_permission(self, request):
1328        if self.parent_model_admin:
1329            return self.parent_model_admin.has_change_permission(request, self.basecontent)
1330        return perms_api.has_permission(self.basecontent, request.user, 'edit')
1331
1332    def has_change_permission(self, request, obj=None):
1333        has_permission = perms_api.has_permission(obj, request.user, 'edit')
1334        if not has_permission and self.parent_model_admin:
1335            has_permission = self.parent_model_admin.has_change_permission(request, self.basecontent)
1336        return has_permission
1337
1338    def has_delete_permission(self, request, obj=None):
1339        has_permission = perms_api.has_permission(obj, request.user, 'edit')
1340        if not has_permission and self.parent_model_admin:
1341            has_permission = self.parent_model_admin.has_change_permission(request, self.basecontent)
1342        return has_permission
1343
1344    def get_actions(self, *args, **kwargs):
1345        actions = super(RelatedModelAdmin, self).get_actions(*args, **kwargs)
1346        if 'delete_selected' in actions.keys():
1347            func = related_delete_selected
1348            name = 'delete_selected'
1349            description = getattr(func, 'short_description', name.replace('_', ' '))
1350            actions['delete_selected'] = (func, name, description)
1351        return actions
1352
1353
1354class ContactInfoRelatedAdmin(RelatedModelAdmin):
1355    """ Contact info related to a content """
1356    tool_name = 'contact_info'
1357    tool_label = _('contact info')
1358    one_to_one = True
1359    related_field = 'basecontent'
1360
1361
1362class BaseOrderableInlines(admin.ModelAdmin):
1363
1364    def __init__(self, *args, **kwargs):
1365        super(BaseOrderableInlines, self).__init__(*args, **kwargs)
1366        self.tabular_inline = False
1367        self.stacked_inline = False
1368        for inline in self.inlines:
1369            if inline.__base__ == admin.TabularInline:
1370                self.tabular_inline = True
1371            elif inline.__base__ == admin.StackedInline:
1372                self.stacked_inline = True
1373
1374    def _media(self):
1375        __media = super(BaseOrderableInlines, self)._media()
1376        __media.add_js(['js/jquery-ui-1.8.dragdrop.min.js'])
1377        if self.stacked_inline:
1378            __media.add_js(['js/menu-sort-stacked.js'])
1379        if self.tabular_inline:
1380            __media.add_js(['js/menu-sort-tabular.js'])
1381        return __media
1382    media = property(_media)
1383
1384
1385class OrderableRelatedModelAdmin(RelatedModelAdmin):
1386    """
1387    A model admin that can reorder related content.
1388
1389    Example use::
1390
1391      class Book(models.Model):
1392          ...
1393
1394      class Page(models.Model):
1395          books = models.ManyToManyField(Book, through='PageBook')
1396
1397      class PageBook(models.Model):
1398          book = models.ForeignKey(Book)
1399          page = models.ForeignKey(Page)
1400          order = models.PositiveIntegerField()
1401
1402      class PageOrderableRelatedAdmin(OrderableRelatedModelAdmin):
1403          model = Page
1404          tool_name = 'pages'
1405          tool_label = 'book pages'
1406          related_field = 'books'
1407          sortablefield = 'order'
1408
1409          def get_relation_obj(self, through_model, obj):
1410              return through_model.objects.get(book=self.basecontent, page=obj)
1411
1412      >>> site.register_related(Page, PageOrderableRelatedAdmin, related_to=Book)
1413    """
1414    change_list_template = "admin/basecontent/related_sortable_change_list.html"
1415    sortablefield = 'position'
1416    sortablereverse = False
1417
1418    def get_ordering(self):
1419        """
1420        Returns ordering by sortablefield
1421        """
1422        opts = self.model._meta
1423        field = opts.get_field_by_name(self.related_field)[0]
1424        relation_lookup = field.field.rel.through.__name__.lower()
1425        return ('%s__order' % relation_lookup, 'asc')
1426
1427    def changelist_view(self, request, extra_context=None, parent_model_admin=None, parent_object=None):
1428        extra_context = self._update_extra_context(request, extra_context, parent_model_admin, parent_object)
1429        if request.method == 'POST':
1430            neworder_list = request.POST.get('neworder', None)
1431            page = request.GET.get('p', 0)
1432            if neworder_list is None:
1433                return super(OrderableRelatedModelAdmin, self).changelist_view(request, extra_context, parent_model_admin, parent_object)
1434            neworder_list = neworder_list.split(',')
1435            if self.sortablereverse:
1436                neworder_list.reverse()
1437            items = self.queryset(request).filter(id__in=neworder_list)
1438            for item in items:
1439                field = item._meta.get_field_by_name(self.related_field)[0]
1440                through_model = field.field.rel.through
1441                neworder = neworder_list.index(unicode(item.id)) + (int(page) * 50)
1442                relation = self.get_relation_obj(through_model, item)
1443                setattr(relation, self.sortablefield, neworder)
1444                relation.save()
1445
1446        return super(OrderableRelatedModelAdmin, self).changelist_view(request, extra_context, parent_model_admin, parent_object)
1447
1448    def get_relation_obj(self, through_model, obj):
1449        """
1450        Callback method that get relationship content for a item.
1451        To override in subclasses. See example implementation above.
1452        """
1453        raise NotImplementedError('You have to override this method')
1454
1455
1456class PermissionRelatedAdmin(RelatedModelAdmin, PermissionAdmin):
1457    """
1458    Model admin for permissions related to a managed content
1459    """
1460    tool_name = 'manage_permissions'
1461    tool_label = _('permissions')
1462    change_roles_template = 'admin/basecontent/role_permissions.html'
1463
1464    def changelist_view(self, request, extra_context=None, parent_model_admin=None, parent_object=None):
1465        if request.method == 'GET':
1466            workflow = workflow_api.workflow_by_model(self.basecontent.__class__)
1467            workflow_admin_link = reverse('admin:workflow_workflow_change', args=(workflow.id, ))
1468            warn_msg = ugettext('''The permissions of the content should be changed in <a href="%(url)s">the workflow</a>, not in the content itself,
1469    because if the status change, <strong>all the custom permission will be cleared</strong>. But,
1470    if you are sure you want to edit the permissions, click <a href="?enable_edit=1">here</a>.''') % {
1471                'url': '%sstates/%d/permissions/' % (workflow_admin_link, self.basecontent.workflow_status.id),
1472            }
1473            messages.warning(request, warn_msg)
1474        extra_context = self._update_extra_context(request, extra_context, parent_model_admin, parent_object)
1475        extra_context.update({'original': None,
1476                              'content': self.basecontent,
1477                              'enable_edit': request.GET.get('enable_edit', False)})
1478        return self.change_roles_permissions(request, self.basecontent.id, extra_context=extra_context)
1479
1480    def has_add_permission(self, request):
1481        if self.parent_model_admin:
1482            return self.parent_model_admin.has_change_permission(request, self.basecontent)
1483        return perms_api.has_permission(self.basecontent, request.user, 'edit')
1484
1485    def has_change_permission(self, request, obj=None):
1486        if self.parent_model_admin:
1487            return self.parent_model_admin.has_change_permission(request, self.basecontent)
1488        return perms_api.has_permission(self.basecontent, request.user, 'edit')
1489
1490    def has_delete_permission(self, request, obj=None):
1491        if self.parent_model_admin:
1492            return self.parent_model_admin.has_change_permission(request, self.basecontent)
1493        return perms_api.has_permission(self.basecontent, request.user, 'edit')
1494
1495
1496class AnnouncementAdmin(AnnouncementDefaultAdmin):
1497    form = AnnouncementAdminForm
1498
1499
1500class LogEntryRelatedContentModelAdmin(admin.ModelAdmin):
1501    change_list_template = "admin/logentry/changelog.html"
1502    list_display = ('logentry_link',
1503                    'get_link_public_url',
1504                    'get_link_contenttype',
1505                    'get_culpright',
1506                    'get_action', 'get_link_admin_url',)
1507    list_display_links = ('get_link_public_url',)
1508    date_hierarchy = 'action_time'
1509    list_filter = ('content_type', 'user')
1510    actions = None
1511
1512    def get_url(self, logentry, admin=False, url=None, label=None):
1513        if not logentry.object_id.isdigit():
1514            return _(u'Error in id')
1515        if logentry.object_id and not url:
1516            try:
1517                obj = logentry.content_type.model_class().objects.get(pk=logentry.object_id)
1518            except models.ObjectDoesNotExist:
1519                return label or _(u'deleted')
1520            if admin:
1521                url = '/admin/%s' % logentry.get_admin_url()
1522            else:
1523                get_absolute_url = getattr(obj, 'get_absolute_url', '')
1524                url = get_absolute_url and get_absolute_url() or get_absolute_url
1525            label = label or url
1526
1527        if url:
1528            if len(label) > 30:
1529                label = "%s ..." % label[:30]
1530            return mark_safe("<a href='%s'>%s</a>" % (url, label))
1531        elif label:
1532            return label
1533        return '---'
1534
1535    def logentry_link(self, logentry):
1536        return logentry.action_time
1537    logentry_link.allow_tags = False
1538    logentry_link.short_description = _(u'Log entry')
1539
1540    def get_culpright(self, logentry):
1541        user = logentry.user
1542        return mark_safe(u'<a href="/admin/auth/user/%s/">%s</a>' % (user.id, user.get_full_name() or user.username))
1543    get_culpright.allow_tags = True
1544    get_culpright.short_description = _(u'User')
1545
1546    def get_link_admin_url(self, logentry):
1547        return self.get_url(logentry, admin=True)
1548    get_link_admin_url.allow_tags = True
1549    get_link_admin_url.short_description = _(u'Admin url')
1550
1551    def get_link_public_url(self, logentry):
1552        if len(logentry.object_repr) < 40:
1553            label = logentry.object_repr
1554        else:
1555            label = "%s..." % logentry.object_repr[:37]
1556        return self.get_url(logentry, admin=False, label=label)
1557    get_link_public_url.allow_tags = True
1558    get_link_public_url.short_description = _(u'Public url')
1559
1560    def get_link_contenttype(self, logentry):
1561        model_class = logentry.content_type.model_class()
1562        return self.get_url(logentry,
1563                            url='/admin/%s/%s/' %
1564                               (model_class._meta.app_label,
1565                                model_class._meta.module_name),
1566                            label=logentry.content_type.__unicode__())
1567    get_link_contenttype.allow_tags = True
1568    get_link_contenttype.short_description = _(u'Content type')
1569
1570    def get_action(self, logentry):
1571        if logentry.is_addition():
1572            return _(u'Added')
1573        elif logentry.is_deletion():
1574            return _(u'Deleted')
1575        elif logentry.is_change():
1576            return logentry.change_message
1577        return '---'
1578    get_action.allow_tags = True
1579    get_action.short_description = _(u'Action')
1580
1581
1582def register(site):
1583    ## register admin models
1584    site.register(BaseContent, BaseContentViewAdmin)
1585    site.register(Site, SiteAdmin)
1586    site.register(ProviderRule)
1587    site.register(StoredOEmbed)
1588    site.register(Announcement, AnnouncementAdmin)
1589    site.register(LogEntry, LogEntryRelatedContentModelAdmin)
1590
1591    #default notification
1592    site.register(NoticeType, NoticeTypeAdmin)
1593    site.register(NoticeSetting, NoticeSettingAdmin)
1594    site.register(Notice, NoticeAdmin)
1595
1596    register_related_base(site, BaseContent)
1597    if settings.USE_GIS:
1598        register_related_gis(site, BaseContent)
1599
1600
1601def register_related_base(site, related_to):
1602    site.register_related(ContactInfo, ContactInfoRelatedAdmin, related_to=related_to)
1603    site.register_related(BaseContent, PermissionRelatedAdmin, related_to=related_to)
1604
1605# ----- begin monkey patching -----
1606
1607# we change ChangeList.get_ordering for allowing define a dynamic ordering
1608# Django does not allow that. We have create a ticket for fix that
1609# For more details, see django ticket http://code.djangoproject.com/ticket/12875
1610legacy_get_ordering = ChangeList.get_ordering
1611
1612
1613def new_get_ordering(self):
1614    if hasattr(self.model_admin, 'get_ordering'):
1615        return self.model_admin.get_ordering()
1616    return legacy_get_ordering(self)
1617
1618ChangeList.get_ordering = new_get_ordering
1619
1620
1621if settings.USE_GIS:
1622    from django.contrib.gis import admin as geoadmin
1623    from django.contrib.gis.db import models as geomodels
1624    from django.contrib.gis.maps.google import GoogleMap
1625    from merengue.places.models import Location
1626    from merengue.base.widgets import (OpenLayersWidgetLatitudeLongitude,
1627                                   OpenLayersInlineLatitudeLongitude)
1628
1629    GMAP = GoogleMap(key=settings.GOOGLE_MAPS_API_KEY)
1630
1631    class LocationModelAdminMixin(object):
1632
1633        title_first_fieldset = None
1634        openlayers_url = '%smerengue/js/OpenLayers/OpenLayers.js' % settings.MEDIA_URL
1635
1636        def set_fieldset(self):
1637            """Returns a BaseInlineFormSet class for use in admin add/change views."""
1638            render_message = ugettext('Click to Locate')
1639            adding = "<a name 'ajax_geolocation'>(<a href='#ajax_geolocation' class='ajax_geolocation'>%s</a>) <input id='id_input_ajax' type='text' class='input_ajax'><img id='img_ajax_loader' src='%simg/ajax-loader-transparent.gif' class='hide ajax_geolocation' />" % (render_message, settings.MEDIA_URL)
1640
1641            title_fieldset = mark_safe("%s %s" % (ugettext(u'Location Maps'), adding))
1642
1643            self.fieldsets = (
1644                (self.title_first_fieldset, {'fields': ('address', 'postal_code', )}),
1645                (title_fieldset,
1646                    {'fields': ('main_location', )}
1647                ),
1648            )
1649
1650        def get_form(self, request, obj=None):
1651            form = super(LocationModelAdminMixin, self).get_form(request, obj)
1652            self.set_fieldset()
1653            return form
1654
1655        def get_formset(self, request, obj=None, **kwargs):
1656            form_set = super(LocationModelAdminMixin, self).get_formset(request, obj)
1657            self.set_fieldset()
1658            return form_set
1659
1660        def formfield_for_dbfield(self, db_field, **kwargs):
1661            if isinstance(db_field, geomodels.GeometryField):
1662                kwargs.pop('request', None)
1663                # Setting the widget with the newly defined widget.
1664                kwargs['widget'] = self.get_map_widget(db_field)
1665                return db_field.formfield(**kwargs)
1666            else:
1667                return super(LocationModelAdminMixin, self).formfield_for_dbfield(db_field, **kwargs)
1668
1669        def _media(self):
1670            __media = super(LocationModelAdminMixin, self)._media()
1671            __media.add_js(['merengue/js/gis/osmgeoadmin.latitude.longitude.js'])
1672            return __media
1673        media = property(_media)
1674
1675    class OSMGeoAdminLatitudeLongitude(geoadmin.OSMGeoAdmin):
1676        widget = OpenLayersWidgetLatitudeLongitude
1677
1678    class InlineLocationModelAdmin(LocationModelAdminMixin):
1679
1680        def __init__(self, parent_model, admin_site):
1681            from merengue.places.admin import GoogleAdmin
1682
1683            super(InlineLocationModelAdmin, self).__init__(parent_model, admin_site)
1684            self.geoModelAdmin = GoogleAdmin(parent_model, admin_site)
1685            self.geoModelAdmin.widget = OpenLayersInlineLatitudeLongitude
1686            self.geoModelAdmin.map_template = 'admin/gis/google_inline.html'
1687
1688        def _media(self, *args, **kwargs):
1689            media_super = super(InlineLocationModelAdmin, self)._media(*args, **kwargs)
1690            media_geo = self.geoModelAdmin._media(*args, **kwargs)
1691            media_super.add_js(media_geo._js)
1692            media_super.add_css(media_geo._css)
1693            media_super.add_js(['js/gis/osmgeoadmin.latitude.longitude.js'])
1694            return media_super
1695        media = property(_media)
1696
1697        def get_formset(self, request, obj=None, **kwargs):
1698            """Returns a BaseInlineFormSet class for use in admin add/change views."""
1699            self.set_fieldset()
1700            return super(InlineLocationModelAdmin, self).get_formset(request, obj, **kwargs)
1701
1702        def get_map_widget(self, *args, **kwargs):
1703            return self.geoModelAdmin.get_map_widget(*args, **kwargs)
1704
1705    class BaseContentRelatedLocationModelAdmin(LocationModelAdminMixin, RelatedModelAdmin, OSMGeoAdminLatitudeLongitude):
1706        tool_name = 'location'
1707        tool_label = _('location')
1708        one_to_one = True
1709        related_field = 'basecontent'
1710        extra_js = [GMAP.api_url + GMAP.key]
1711        map_width = 500
1712        map_height = 300
1713        default_zoom = 10
1714        default_lat = 4500612.0
1715        default_lon = -655523.0
1716        map_template = 'admin/gis/google.html'
1717        title_first_fieldset = _('Address')
1718
1719    def setup_basecontent_admin(basecontent_admin_site):
1720        basecontent_admin_site.register(Location, BaseContentRelatedLocationModelAdmin)
1721
1722    def register_related_gis(site, related_to):
1723        site.register_related(Location, BaseContentRelatedLocationModelAdmin, related_to=related_to)
Note: See TracBrowser for help on using the repository browser.