admin.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  1. from __future__ import unicode_literals
  2. from copy import deepcopy
  3. from django.contrib import admin
  4. from django.contrib.auth import get_user_model
  5. from django.contrib.auth.admin import UserAdmin
  6. from django.contrib.auth.models import User as AuthUser
  7. from django.contrib.redirects.admin import RedirectAdmin
  8. from django.contrib.messages import error
  9. from django.core.urlresolvers import NoReverseMatch
  10. from django.forms import ValidationError, ModelForm
  11. from django.http import HttpResponseRedirect
  12. from django.shortcuts import get_object_or_404
  13. from django.utils.translation import ugettext_lazy as _
  14. from mezzanine.conf import settings
  15. from mezzanine.core.forms import DynamicInlineAdminForm
  16. from mezzanine.core.models import (
  17. Orderable, ContentTyped, SitePermission, CONTENT_STATUS_PUBLISHED)
  18. from mezzanine.utils.models import base_concrete_model
  19. from mezzanine.utils.sites import current_site_id
  20. from mezzanine.utils.static import static_lazy as static
  21. from mezzanine.utils.urls import admin_url
  22. if settings.USE_MODELTRANSLATION:
  23. from collections import OrderedDict
  24. from django.utils.translation import activate, get_language
  25. from modeltranslation.admin import (TranslationAdmin,
  26. TranslationInlineModelAdmin)
  27. class BaseTranslationModelAdmin(TranslationAdmin):
  28. """
  29. Mimic modeltranslation's TabbedTranslationAdmin but uses a
  30. custom tabbed_translation_fields.js
  31. """
  32. class Media:
  33. js = (
  34. static("modeltranslation/js/force_jquery.js"),
  35. static("mezzanine/js/%s" % settings.JQUERY_UI_FILENAME),
  36. static("mezzanine/js/admin/tabbed_translation_fields.js"),
  37. )
  38. css = {
  39. "all": (static(
  40. "mezzanine/css/admin/tabbed_translation_fields.css"),),
  41. }
  42. else:
  43. class BaseTranslationModelAdmin(admin.ModelAdmin):
  44. """
  45. Abstract class used to handle the switch between translation
  46. and no-translation class logic. We define the basic structure
  47. for the Media class so we can extend it consistently regardless
  48. of whether or not modeltranslation is used.
  49. """
  50. class Media:
  51. js = ()
  52. css = {"all": ()}
  53. User = get_user_model()
  54. class DisplayableAdminForm(ModelForm):
  55. def clean_content(form):
  56. status = form.cleaned_data.get("status")
  57. content = form.cleaned_data.get("content")
  58. if status == CONTENT_STATUS_PUBLISHED and not content:
  59. raise ValidationError(_("This field is required if status "
  60. "is set to published."))
  61. return content
  62. class DisplayableAdmin(BaseTranslationModelAdmin):
  63. """
  64. Admin class for subclasses of the abstract ``Displayable`` model.
  65. """
  66. list_display = ("title", "status", "admin_link")
  67. list_display_links = ("title",)
  68. list_editable = ("status",)
  69. list_filter = ("status", "keywords__keyword")
  70. # modeltranslation breaks date hierarchy links, see:
  71. # https://github.com/deschler/django-modeltranslation/issues/324
  72. # Once that's resolved we can restore this.
  73. date_hierarchy = None if settings.USE_MODELTRANSLATION else "publish_date"
  74. radio_fields = {"status": admin.HORIZONTAL}
  75. fieldsets = (
  76. (None, {
  77. "fields": ["title", "status", ("publish_date", "expiry_date")],
  78. }),
  79. (_("Meta data"), {
  80. "fields": ["_meta_title", "slug",
  81. ("description", "gen_description"),
  82. "keywords", "in_sitemap"],
  83. "classes": ("collapse-closed",)
  84. }),
  85. )
  86. form = DisplayableAdminForm
  87. def __init__(self, *args, **kwargs):
  88. super(DisplayableAdmin, self).__init__(*args, **kwargs)
  89. try:
  90. self.search_fields = list(set(list(self.search_fields) + list(
  91. self.model.objects.get_search_fields().keys())))
  92. except AttributeError:
  93. pass
  94. def check_permission(self, request, page, permission):
  95. """
  96. Subclasses can define a custom permission check and raise an exception
  97. if False.
  98. """
  99. pass
  100. def save_model(self, request, obj, form, change):
  101. """
  102. Save model for every language so that field auto-population
  103. is done for every each of it.
  104. """
  105. super(DisplayableAdmin, self).save_model(request, obj, form, change)
  106. if settings.USE_MODELTRANSLATION:
  107. lang = get_language()
  108. for code in OrderedDict(settings.LANGUAGES):
  109. if code != lang: # Already done
  110. try:
  111. activate(code)
  112. except:
  113. pass
  114. else:
  115. obj.save()
  116. activate(lang)
  117. class BaseDynamicInlineAdmin(object):
  118. """
  119. Admin inline that uses JS to inject an "Add another" link which
  120. when clicked, dynamically reveals another fieldset. Also handles
  121. adding the ``_order`` field and its widget for models that
  122. subclass ``Orderable``.
  123. """
  124. form = DynamicInlineAdminForm
  125. extra = 1
  126. def get_fields(self, request, obj=None):
  127. """
  128. For subclasses of ``Orderable``, the ``_order`` field must
  129. always be present and be the last field.
  130. """
  131. fields = super(BaseDynamicInlineAdmin, self).get_fields(request, obj)
  132. if issubclass(self.model, Orderable):
  133. fields = list(fields)
  134. try:
  135. fields.remove("_order")
  136. except ValueError:
  137. pass
  138. fields.append("_order")
  139. return fields
  140. def get_fieldsets(self, request, obj=None):
  141. """
  142. Same as above, but for fieldsets.
  143. """
  144. fieldsets = super(BaseDynamicInlineAdmin, self).get_fieldsets(
  145. request, obj)
  146. if issubclass(self.model, Orderable):
  147. for fieldset in fieldsets:
  148. fields = [f for f in list(fieldset[1]["fields"])
  149. if not hasattr(f, "translated_field")]
  150. try:
  151. fields.remove("_order")
  152. except ValueError:
  153. pass
  154. fieldset[1]["fields"] = fields
  155. fieldsets[-1][1]["fields"].append("_order")
  156. return fieldsets
  157. def get_inline_base_class(cls):
  158. if settings.USE_MODELTRANSLATION:
  159. class InlineBase(TranslationInlineModelAdmin, cls):
  160. """
  161. Abstract class that mimics django-modeltranslation's
  162. Translation{Tabular,Stacked}Inline. Used as a placeholder
  163. for future improvement.
  164. """
  165. pass
  166. return InlineBase
  167. return cls
  168. class TabularDynamicInlineAdmin(BaseDynamicInlineAdmin,
  169. get_inline_base_class(admin.TabularInline)):
  170. pass
  171. class StackedDynamicInlineAdmin(BaseDynamicInlineAdmin,
  172. get_inline_base_class(admin.StackedInline)):
  173. def __init__(self, *args, **kwargs):
  174. """
  175. Stacked dynamic inlines won't work without grappelli
  176. installed, as the JavaScript in dynamic_inline.js isn't
  177. able to target each of the inlines to set the value of
  178. the order field.
  179. """
  180. grappelli_name = getattr(settings, "PACKAGE_NAME_GRAPPELLI")
  181. if grappelli_name not in settings.INSTALLED_APPS:
  182. error = "StackedDynamicInlineAdmin requires Grappelli installed."
  183. raise Exception(error)
  184. super(StackedDynamicInlineAdmin, self).__init__(*args, **kwargs)
  185. class OwnableAdmin(admin.ModelAdmin):
  186. """
  187. Admin class for models that subclass the abstract ``Ownable``
  188. model. Handles limiting the change list to objects owned by the
  189. logged in user, as well as setting the owner of newly created
  190. objects to the logged in user.
  191. Remember that this will include the ``user`` field in the required
  192. fields for the admin change form which may not be desirable. The
  193. best approach to solve this is to define a ``fieldsets`` attribute
  194. that excludes the ``user`` field or simple add ``user`` to your
  195. admin excludes: ``exclude = ('user',)``
  196. """
  197. def save_form(self, request, form, change):
  198. """
  199. Set the object's owner as the logged in user.
  200. """
  201. obj = form.save(commit=False)
  202. if obj.user_id is None:
  203. obj.user = request.user
  204. return super(OwnableAdmin, self).save_form(request, form, change)
  205. def get_queryset(self, request):
  206. """
  207. Filter the change list by currently logged in user if not a
  208. superuser. We also skip filtering if the model for this admin
  209. class has been added to the sequence in the setting
  210. ``OWNABLE_MODELS_ALL_EDITABLE``, which contains models in the
  211. format ``app_label.object_name``, and allows models subclassing
  212. ``Ownable`` to be excluded from filtering, eg: ownership should
  213. not imply permission to edit.
  214. """
  215. opts = self.model._meta
  216. model_name = ("%s.%s" % (opts.app_label, opts.object_name)).lower()
  217. models_all_editable = settings.OWNABLE_MODELS_ALL_EDITABLE
  218. models_all_editable = [m.lower() for m in models_all_editable]
  219. qs = super(OwnableAdmin, self).get_queryset(request)
  220. if request.user.is_superuser or model_name in models_all_editable:
  221. return qs
  222. return qs.filter(user__id=request.user.id)
  223. class ContentTypedAdmin(object):
  224. def __init__(self, *args, **kwargs):
  225. """
  226. For subclasses that are registered with an Admin class
  227. that doesn't implement fieldsets, add any extra model fields
  228. to this instance's fieldsets. This mimics Django's behaviour of
  229. adding all model fields when no fieldsets are defined on the
  230. Admin class.
  231. """
  232. super(ContentTypedAdmin, self).__init__(*args, **kwargs)
  233. self.concrete_model = base_concrete_model(ContentTyped, self.model)
  234. # Test that the fieldsets don't differ from the concrete admin's.
  235. if (self.model is not self.concrete_model and
  236. self.fieldsets == self.base_concrete_modeladmin.fieldsets):
  237. # Make a copy so that we aren't modifying other Admin
  238. # classes' fieldsets.
  239. self.fieldsets = deepcopy(self.fieldsets)
  240. # Insert each field between the publishing fields and nav
  241. # fields. Do so in reverse order to retain the order of
  242. # the model's fields.
  243. model_fields = self.concrete_model._meta.get_fields()
  244. concrete_field = '{concrete_model}_ptr'.format(
  245. concrete_model=self.concrete_model.get_content_model_name())
  246. exclude_fields = [f.name for f in model_fields] + [concrete_field]
  247. try:
  248. exclude_fields.extend(self.exclude)
  249. except (AttributeError, TypeError):
  250. pass
  251. try:
  252. exclude_fields.extend(self.form.Meta.exclude)
  253. except (AttributeError, TypeError):
  254. pass
  255. fields = self.model._meta.fields + self.model._meta.many_to_many
  256. for field in reversed(fields):
  257. if field.name not in exclude_fields and field.editable:
  258. if not hasattr(field, "translated_field"):
  259. self.fieldsets[0][1]["fields"].insert(3, field.name)
  260. @property
  261. def base_concrete_modeladmin(self):
  262. """ The class inheriting directly from ContentModelAdmin. """
  263. candidates = [self.__class__]
  264. while candidates:
  265. candidate = candidates.pop()
  266. if ContentTypedAdmin in candidate.__bases__:
  267. return candidate
  268. candidates.extend(candidate.__bases__)
  269. raise Exception("Can't find base concrete ModelAdmin class.")
  270. def has_module_permission(self, request):
  271. """
  272. Hide subclasses from the admin menu.
  273. """
  274. return self.model is self.concrete_model
  275. def change_view(self, request, object_id, **kwargs):
  276. """
  277. For the concrete model, check ``get_content_model()``
  278. for a subclass and redirect to its admin change view.
  279. """
  280. instance = get_object_or_404(self.concrete_model, pk=object_id)
  281. content_model = instance.get_content_model()
  282. self.check_permission(request, content_model, "change")
  283. if content_model.__class__ != self.model:
  284. change_url = admin_url(content_model.__class__, "change",
  285. content_model.id)
  286. return HttpResponseRedirect(change_url)
  287. return super(ContentTypedAdmin, self).change_view(
  288. request, object_id, **kwargs)
  289. def changelist_view(self, request, extra_context=None):
  290. """ Redirect to the changelist view for subclasses. """
  291. if self.model is not self.concrete_model:
  292. return HttpResponseRedirect(
  293. admin_url(self.concrete_model, "changelist"))
  294. extra_context = extra_context or {}
  295. extra_context["content_models"] = self.get_content_models()
  296. return super(ContentTypedAdmin, self).changelist_view(
  297. request, extra_context)
  298. def get_content_models(self):
  299. """ Return all subclasses that are admin registered. """
  300. models = []
  301. for model in self.concrete_model.get_content_models():
  302. try:
  303. admin_url(model, "add")
  304. except NoReverseMatch:
  305. continue
  306. else:
  307. setattr(model, "meta_verbose_name", model._meta.verbose_name)
  308. setattr(model, "add_url", admin_url(model, "add"))
  309. models.append(model)
  310. return models
  311. ####################################
  312. # User Admin with Site Permissions #
  313. ####################################
  314. class SitePermissionInline(admin.TabularInline):
  315. model = SitePermission
  316. max_num = 1
  317. can_delete = False
  318. class SitePermissionUserAdmin(UserAdmin):
  319. inlines = [SitePermissionInline]
  320. def save_model(self, request, obj, form, change):
  321. """
  322. Provides a warning if the user is an active admin with no admin access.
  323. """
  324. super(SitePermissionUserAdmin, self).save_model(
  325. request, obj, form, change)
  326. user = self.model.objects.get(id=obj.id)
  327. has_perms = len(user.get_all_permissions()) > 0
  328. has_sites = SitePermission.objects.filter(user=user).count() > 0
  329. if user.is_active and user.is_staff and not user.is_superuser and not (
  330. has_perms and has_sites):
  331. error(request, "The user is active but won't be able to access "
  332. "the admin, due to no edit/site permissions being "
  333. "selected")
  334. # only register if User hasn't been overridden
  335. if User == AuthUser:
  336. if User in admin.site._registry:
  337. admin.site.unregister(User)
  338. admin.site.register(User, SitePermissionUserAdmin)
  339. class SiteRedirectAdmin(RedirectAdmin):
  340. """
  341. Subclass of Django's redirect admin that modifies it to behave the
  342. way most other admin classes do it Mezzanine with regard to site
  343. filtering. It filters the list view by current site, hides the site
  344. field from the change form, and assigns the current site to the
  345. redirect when first created.
  346. """
  347. fields = ("old_path", "new_path") # Excludes the site field.
  348. def get_queryset(self, request):
  349. """
  350. Filters the list view by current site.
  351. """
  352. queryset = super(SiteRedirectAdmin, self).get_queryset(request)
  353. return queryset.filter(site_id=current_site_id())
  354. def save_form(self, request, form, change):
  355. """
  356. Assigns the current site to the redirect when first created.
  357. """
  358. obj = form.save(commit=False)
  359. if not obj.site_id:
  360. obj.site_id = current_site_id()
  361. return super(SiteRedirectAdmin, self).save_form(request, form, change)
  362. if "django.contrib.redirects" in settings.INSTALLED_APPS:
  363. from django.contrib.redirects.models import Redirect
  364. if Redirect in admin.site._registry:
  365. admin.site.unregister(Redirect)
  366. admin.site.register(Redirect, SiteRedirectAdmin)