|
1 import re |
|
2 from django import http, template |
|
3 from django.contrib.admin import ModelAdmin |
|
4 from django.contrib.admin import actions |
|
5 from django.contrib.auth import authenticate, login |
|
6 from django.views.decorators.csrf import csrf_protect |
|
7 from django.db.models.base import ModelBase |
|
8 from django.core.exceptions import ImproperlyConfigured |
|
9 from django.core.urlresolvers import reverse |
|
10 from django.shortcuts import render_to_response |
|
11 from django.utils.functional import update_wrapper |
|
12 from django.utils.safestring import mark_safe |
|
13 from django.utils.text import capfirst |
|
14 from django.utils.translation import ugettext_lazy, ugettext as _ |
|
15 from django.views.decorators.cache import never_cache |
|
16 from django.conf import settings |
|
17 |
|
18 ERROR_MESSAGE = ugettext_lazy("Please enter a correct username and password. Note that both fields are case-sensitive.") |
|
19 LOGIN_FORM_KEY = 'this_is_the_login_form' |
|
20 |
|
21 class AlreadyRegistered(Exception): |
|
22 pass |
|
23 |
|
24 class NotRegistered(Exception): |
|
25 pass |
|
26 |
|
27 class AdminSite(object): |
|
28 """ |
|
29 An AdminSite object encapsulates an instance of the Django admin application, ready |
|
30 to be hooked in to your URLconf. Models are registered with the AdminSite using the |
|
31 register() method, and the root() method can then be used as a Django view function |
|
32 that presents a full admin interface for the collection of registered models. |
|
33 """ |
|
34 |
|
35 index_template = None |
|
36 app_index_template = None |
|
37 login_template = None |
|
38 logout_template = None |
|
39 password_change_template = None |
|
40 password_change_done_template = None |
|
41 |
|
42 def __init__(self, name=None, app_name='admin'): |
|
43 self._registry = {} # model_class class -> admin_class instance |
|
44 self.root_path = None |
|
45 if name is None: |
|
46 self.name = 'admin' |
|
47 else: |
|
48 self.name = name |
|
49 self.app_name = app_name |
|
50 self._actions = {'delete_selected': actions.delete_selected} |
|
51 self._global_actions = self._actions.copy() |
|
52 |
|
53 def register(self, model_or_iterable, admin_class=None, **options): |
|
54 """ |
|
55 Registers the given model(s) with the given admin class. |
|
56 |
|
57 The model(s) should be Model classes, not instances. |
|
58 |
|
59 If an admin class isn't given, it will use ModelAdmin (the default |
|
60 admin options). If keyword arguments are given -- e.g., list_display -- |
|
61 they'll be applied as options to the admin class. |
|
62 |
|
63 If a model is already registered, this will raise AlreadyRegistered. |
|
64 """ |
|
65 if not admin_class: |
|
66 admin_class = ModelAdmin |
|
67 |
|
68 # Don't import the humongous validation code unless required |
|
69 if admin_class and settings.DEBUG: |
|
70 from django.contrib.admin.validation import validate |
|
71 else: |
|
72 validate = lambda model, adminclass: None |
|
73 |
|
74 if isinstance(model_or_iterable, ModelBase): |
|
75 model_or_iterable = [model_or_iterable] |
|
76 for model in model_or_iterable: |
|
77 if model in self._registry: |
|
78 raise AlreadyRegistered('The model %s is already registered' % model.__name__) |
|
79 |
|
80 # If we got **options then dynamically construct a subclass of |
|
81 # admin_class with those **options. |
|
82 if options: |
|
83 # For reasons I don't quite understand, without a __module__ |
|
84 # the created class appears to "live" in the wrong place, |
|
85 # which causes issues later on. |
|
86 options['__module__'] = __name__ |
|
87 admin_class = type("%sAdmin" % model.__name__, (admin_class,), options) |
|
88 |
|
89 # Validate (which might be a no-op) |
|
90 validate(admin_class, model) |
|
91 |
|
92 # Instantiate the admin class to save in the registry |
|
93 self._registry[model] = admin_class(model, self) |
|
94 |
|
95 def unregister(self, model_or_iterable): |
|
96 """ |
|
97 Unregisters the given model(s). |
|
98 |
|
99 If a model isn't already registered, this will raise NotRegistered. |
|
100 """ |
|
101 if isinstance(model_or_iterable, ModelBase): |
|
102 model_or_iterable = [model_or_iterable] |
|
103 for model in model_or_iterable: |
|
104 if model not in self._registry: |
|
105 raise NotRegistered('The model %s is not registered' % model.__name__) |
|
106 del self._registry[model] |
|
107 |
|
108 def add_action(self, action, name=None): |
|
109 """ |
|
110 Register an action to be available globally. |
|
111 """ |
|
112 name = name or action.__name__ |
|
113 self._actions[name] = action |
|
114 self._global_actions[name] = action |
|
115 |
|
116 def disable_action(self, name): |
|
117 """ |
|
118 Disable a globally-registered action. Raises KeyError for invalid names. |
|
119 """ |
|
120 del self._actions[name] |
|
121 |
|
122 def get_action(self, name): |
|
123 """ |
|
124 Explicitally get a registered global action wheather it's enabled or |
|
125 not. Raises KeyError for invalid names. |
|
126 """ |
|
127 return self._global_actions[name] |
|
128 |
|
129 def actions(self): |
|
130 """ |
|
131 Get all the enabled actions as an iterable of (name, func). |
|
132 """ |
|
133 return self._actions.iteritems() |
|
134 actions = property(actions) |
|
135 |
|
136 def has_permission(self, request): |
|
137 """ |
|
138 Returns True if the given HttpRequest has permission to view |
|
139 *at least one* page in the admin site. |
|
140 """ |
|
141 return request.user.is_active and request.user.is_staff |
|
142 |
|
143 def check_dependencies(self): |
|
144 """ |
|
145 Check that all things needed to run the admin have been correctly installed. |
|
146 |
|
147 The default implementation checks that LogEntry, ContentType and the |
|
148 auth context processor are installed. |
|
149 """ |
|
150 from django.contrib.admin.models import LogEntry |
|
151 from django.contrib.contenttypes.models import ContentType |
|
152 |
|
153 if not LogEntry._meta.installed: |
|
154 raise ImproperlyConfigured("Put 'django.contrib.admin' in your " |
|
155 "INSTALLED_APPS setting in order to use the admin application.") |
|
156 if not ContentType._meta.installed: |
|
157 raise ImproperlyConfigured("Put 'django.contrib.contenttypes' in " |
|
158 "your INSTALLED_APPS setting in order to use the admin application.") |
|
159 if not ('django.contrib.auth.context_processors.auth' in settings.TEMPLATE_CONTEXT_PROCESSORS or |
|
160 'django.core.context_processors.auth' in settings.TEMPLATE_CONTEXT_PROCESSORS): |
|
161 raise ImproperlyConfigured("Put 'django.contrib.auth.context_processors.auth' " |
|
162 "in your TEMPLATE_CONTEXT_PROCESSORS setting in order to use the admin application.") |
|
163 |
|
164 def admin_view(self, view, cacheable=False): |
|
165 """ |
|
166 Decorator to create an admin view attached to this ``AdminSite``. This |
|
167 wraps the view and provides permission checking by calling |
|
168 ``self.has_permission``. |
|
169 |
|
170 You'll want to use this from within ``AdminSite.get_urls()``: |
|
171 |
|
172 class MyAdminSite(AdminSite): |
|
173 |
|
174 def get_urls(self): |
|
175 from django.conf.urls.defaults import patterns, url |
|
176 |
|
177 urls = super(MyAdminSite, self).get_urls() |
|
178 urls += patterns('', |
|
179 url(r'^my_view/$', self.admin_view(some_view)) |
|
180 ) |
|
181 return urls |
|
182 |
|
183 By default, admin_views are marked non-cacheable using the |
|
184 ``never_cache`` decorator. If the view can be safely cached, set |
|
185 cacheable=True. |
|
186 """ |
|
187 def inner(request, *args, **kwargs): |
|
188 if not self.has_permission(request): |
|
189 return self.login(request) |
|
190 return view(request, *args, **kwargs) |
|
191 if not cacheable: |
|
192 inner = never_cache(inner) |
|
193 # We add csrf_protect here so this function can be used as a utility |
|
194 # function for any view, without having to repeat 'csrf_protect'. |
|
195 if not getattr(view, 'csrf_exempt', False): |
|
196 inner = csrf_protect(inner) |
|
197 return update_wrapper(inner, view) |
|
198 |
|
199 def get_urls(self): |
|
200 from django.conf.urls.defaults import patterns, url, include |
|
201 |
|
202 if settings.DEBUG: |
|
203 self.check_dependencies() |
|
204 |
|
205 def wrap(view, cacheable=False): |
|
206 def wrapper(*args, **kwargs): |
|
207 return self.admin_view(view, cacheable)(*args, **kwargs) |
|
208 return update_wrapper(wrapper, view) |
|
209 |
|
210 # Admin-site-wide views. |
|
211 urlpatterns = patterns('', |
|
212 url(r'^$', |
|
213 wrap(self.index), |
|
214 name='index'), |
|
215 url(r'^logout/$', |
|
216 wrap(self.logout), |
|
217 name='logout'), |
|
218 url(r'^password_change/$', |
|
219 wrap(self.password_change, cacheable=True), |
|
220 name='password_change'), |
|
221 url(r'^password_change/done/$', |
|
222 wrap(self.password_change_done, cacheable=True), |
|
223 name='password_change_done'), |
|
224 url(r'^jsi18n/$', |
|
225 wrap(self.i18n_javascript, cacheable=True), |
|
226 name='jsi18n'), |
|
227 url(r'^r/(?P<content_type_id>\d+)/(?P<object_id>.+)/$', |
|
228 'django.views.defaults.shortcut'), |
|
229 url(r'^(?P<app_label>\w+)/$', |
|
230 wrap(self.app_index), |
|
231 name='app_list') |
|
232 ) |
|
233 |
|
234 # Add in each model's views. |
|
235 for model, model_admin in self._registry.iteritems(): |
|
236 urlpatterns += patterns('', |
|
237 url(r'^%s/%s/' % (model._meta.app_label, model._meta.module_name), |
|
238 include(model_admin.urls)) |
|
239 ) |
|
240 return urlpatterns |
|
241 |
|
242 def urls(self): |
|
243 return self.get_urls(), self.app_name, self.name |
|
244 urls = property(urls) |
|
245 |
|
246 def password_change(self, request): |
|
247 """ |
|
248 Handles the "change password" task -- both form display and validation. |
|
249 """ |
|
250 from django.contrib.auth.views import password_change |
|
251 if self.root_path is not None: |
|
252 url = '%spassword_change/done/' % self.root_path |
|
253 else: |
|
254 url = reverse('admin:password_change_done', current_app=self.name) |
|
255 defaults = { |
|
256 'post_change_redirect': url |
|
257 } |
|
258 if self.password_change_template is not None: |
|
259 defaults['template_name'] = self.password_change_template |
|
260 return password_change(request, **defaults) |
|
261 |
|
262 def password_change_done(self, request): |
|
263 """ |
|
264 Displays the "success" page after a password change. |
|
265 """ |
|
266 from django.contrib.auth.views import password_change_done |
|
267 defaults = {} |
|
268 if self.password_change_done_template is not None: |
|
269 defaults['template_name'] = self.password_change_done_template |
|
270 return password_change_done(request, **defaults) |
|
271 |
|
272 def i18n_javascript(self, request): |
|
273 """ |
|
274 Displays the i18n JavaScript that the Django admin requires. |
|
275 |
|
276 This takes into account the USE_I18N setting. If it's set to False, the |
|
277 generated JavaScript will be leaner and faster. |
|
278 """ |
|
279 if settings.USE_I18N: |
|
280 from django.views.i18n import javascript_catalog |
|
281 else: |
|
282 from django.views.i18n import null_javascript_catalog as javascript_catalog |
|
283 return javascript_catalog(request, packages='django.conf') |
|
284 |
|
285 def logout(self, request): |
|
286 """ |
|
287 Logs out the user for the given HttpRequest. |
|
288 |
|
289 This should *not* assume the user is already logged in. |
|
290 """ |
|
291 from django.contrib.auth.views import logout |
|
292 defaults = {} |
|
293 if self.logout_template is not None: |
|
294 defaults['template_name'] = self.logout_template |
|
295 return logout(request, **defaults) |
|
296 logout = never_cache(logout) |
|
297 |
|
298 def login(self, request): |
|
299 """ |
|
300 Displays the login form for the given HttpRequest. |
|
301 """ |
|
302 from django.contrib.auth.models import User |
|
303 |
|
304 # If this isn't already the login page, display it. |
|
305 if not request.POST.has_key(LOGIN_FORM_KEY): |
|
306 if request.POST: |
|
307 message = _("Please log in again, because your session has expired.") |
|
308 else: |
|
309 message = "" |
|
310 return self.display_login_form(request, message) |
|
311 |
|
312 # Check that the user accepts cookies. |
|
313 if not request.session.test_cookie_worked(): |
|
314 message = _("Looks like your browser isn't configured to accept cookies. Please enable cookies, reload this page, and try again.") |
|
315 return self.display_login_form(request, message) |
|
316 else: |
|
317 request.session.delete_test_cookie() |
|
318 |
|
319 # Check the password. |
|
320 username = request.POST.get('username', None) |
|
321 password = request.POST.get('password', None) |
|
322 user = authenticate(username=username, password=password) |
|
323 if user is None: |
|
324 message = ERROR_MESSAGE |
|
325 if username is not None and u'@' in username: |
|
326 # Mistakenly entered e-mail address instead of username? Look it up. |
|
327 try: |
|
328 user = User.objects.get(email=username) |
|
329 except (User.DoesNotExist, User.MultipleObjectsReturned): |
|
330 message = _("Usernames cannot contain the '@' character.") |
|
331 else: |
|
332 if user.check_password(password): |
|
333 message = _("Your e-mail address is not your username." |
|
334 " Try '%s' instead.") % user.username |
|
335 else: |
|
336 message = _("Usernames cannot contain the '@' character.") |
|
337 return self.display_login_form(request, message) |
|
338 |
|
339 # The user data is correct; log in the user in and continue. |
|
340 else: |
|
341 if user.is_active and user.is_staff: |
|
342 login(request, user) |
|
343 return http.HttpResponseRedirect(request.get_full_path()) |
|
344 else: |
|
345 return self.display_login_form(request, ERROR_MESSAGE) |
|
346 login = never_cache(login) |
|
347 |
|
348 def index(self, request, extra_context=None): |
|
349 """ |
|
350 Displays the main admin index page, which lists all of the installed |
|
351 apps that have been registered in this site. |
|
352 """ |
|
353 app_dict = {} |
|
354 user = request.user |
|
355 for model, model_admin in self._registry.items(): |
|
356 app_label = model._meta.app_label |
|
357 has_module_perms = user.has_module_perms(app_label) |
|
358 |
|
359 if has_module_perms: |
|
360 perms = model_admin.get_model_perms(request) |
|
361 |
|
362 # Check whether user has any perm for this module. |
|
363 # If so, add the module to the model_list. |
|
364 if True in perms.values(): |
|
365 model_dict = { |
|
366 'name': capfirst(model._meta.verbose_name_plural), |
|
367 'admin_url': mark_safe('%s/%s/' % (app_label, model.__name__.lower())), |
|
368 'perms': perms, |
|
369 } |
|
370 if app_label in app_dict: |
|
371 app_dict[app_label]['models'].append(model_dict) |
|
372 else: |
|
373 app_dict[app_label] = { |
|
374 'name': app_label.title(), |
|
375 'app_url': app_label + '/', |
|
376 'has_module_perms': has_module_perms, |
|
377 'models': [model_dict], |
|
378 } |
|
379 |
|
380 # Sort the apps alphabetically. |
|
381 app_list = app_dict.values() |
|
382 app_list.sort(lambda x, y: cmp(x['name'], y['name'])) |
|
383 |
|
384 # Sort the models alphabetically within each app. |
|
385 for app in app_list: |
|
386 app['models'].sort(lambda x, y: cmp(x['name'], y['name'])) |
|
387 |
|
388 context = { |
|
389 'title': _('Site administration'), |
|
390 'app_list': app_list, |
|
391 'root_path': self.root_path, |
|
392 } |
|
393 context.update(extra_context or {}) |
|
394 context_instance = template.RequestContext(request, current_app=self.name) |
|
395 return render_to_response(self.index_template or 'admin/index.html', context, |
|
396 context_instance=context_instance |
|
397 ) |
|
398 index = never_cache(index) |
|
399 |
|
400 def display_login_form(self, request, error_message='', extra_context=None): |
|
401 request.session.set_test_cookie() |
|
402 context = { |
|
403 'title': _('Log in'), |
|
404 'app_path': request.get_full_path(), |
|
405 'error_message': error_message, |
|
406 'root_path': self.root_path, |
|
407 } |
|
408 context.update(extra_context or {}) |
|
409 context_instance = template.RequestContext(request, current_app=self.name) |
|
410 return render_to_response(self.login_template or 'admin/login.html', context, |
|
411 context_instance=context_instance |
|
412 ) |
|
413 |
|
414 def app_index(self, request, app_label, extra_context=None): |
|
415 user = request.user |
|
416 has_module_perms = user.has_module_perms(app_label) |
|
417 app_dict = {} |
|
418 for model, model_admin in self._registry.items(): |
|
419 if app_label == model._meta.app_label: |
|
420 if has_module_perms: |
|
421 perms = model_admin.get_model_perms(request) |
|
422 |
|
423 # Check whether user has any perm for this module. |
|
424 # If so, add the module to the model_list. |
|
425 if True in perms.values(): |
|
426 model_dict = { |
|
427 'name': capfirst(model._meta.verbose_name_plural), |
|
428 'admin_url': '%s/' % model.__name__.lower(), |
|
429 'perms': perms, |
|
430 } |
|
431 if app_dict: |
|
432 app_dict['models'].append(model_dict), |
|
433 else: |
|
434 # First time around, now that we know there's |
|
435 # something to display, add in the necessary meta |
|
436 # information. |
|
437 app_dict = { |
|
438 'name': app_label.title(), |
|
439 'app_url': '', |
|
440 'has_module_perms': has_module_perms, |
|
441 'models': [model_dict], |
|
442 } |
|
443 if not app_dict: |
|
444 raise http.Http404('The requested admin page does not exist.') |
|
445 # Sort the models alphabetically within each app. |
|
446 app_dict['models'].sort(lambda x, y: cmp(x['name'], y['name'])) |
|
447 context = { |
|
448 'title': _('%s administration') % capfirst(app_label), |
|
449 'app_list': [app_dict], |
|
450 'root_path': self.root_path, |
|
451 } |
|
452 context.update(extra_context or {}) |
|
453 context_instance = template.RequestContext(request, current_app=self.name) |
|
454 return render_to_response(self.app_index_template or ('admin/%s/app_index.html' % app_label, |
|
455 'admin/app_index.html'), context, |
|
456 context_instance=context_instance |
|
457 ) |
|
458 |
|
459 def root(self, request, url): |
|
460 """ |
|
461 DEPRECATED. This function is the old way of handling URL resolution, and |
|
462 is deprecated in favor of real URL resolution -- see ``get_urls()``. |
|
463 |
|
464 This function still exists for backwards-compatibility; it will be |
|
465 removed in Django 1.3. |
|
466 """ |
|
467 import warnings |
|
468 warnings.warn( |
|
469 "AdminSite.root() is deprecated; use include(admin.site.urls) instead.", |
|
470 DeprecationWarning |
|
471 ) |
|
472 |
|
473 # |
|
474 # Again, remember that the following only exists for |
|
475 # backwards-compatibility. Any new URLs, changes to existing URLs, or |
|
476 # whatever need to be done up in get_urls(), above! |
|
477 # |
|
478 |
|
479 if request.method == 'GET' and not request.path.endswith('/'): |
|
480 return http.HttpResponseRedirect(request.path + '/') |
|
481 |
|
482 if settings.DEBUG: |
|
483 self.check_dependencies() |
|
484 |
|
485 # Figure out the admin base URL path and stash it for later use |
|
486 self.root_path = re.sub(re.escape(url) + '$', '', request.path) |
|
487 |
|
488 url = url.rstrip('/') # Trim trailing slash, if it exists. |
|
489 |
|
490 # The 'logout' view doesn't require that the person is logged in. |
|
491 if url == 'logout': |
|
492 return self.logout(request) |
|
493 |
|
494 # Check permission to continue or display login form. |
|
495 if not self.has_permission(request): |
|
496 return self.login(request) |
|
497 |
|
498 if url == '': |
|
499 return self.index(request) |
|
500 elif url == 'password_change': |
|
501 return self.password_change(request) |
|
502 elif url == 'password_change/done': |
|
503 return self.password_change_done(request) |
|
504 elif url == 'jsi18n': |
|
505 return self.i18n_javascript(request) |
|
506 # URLs starting with 'r/' are for the "View on site" links. |
|
507 elif url.startswith('r/'): |
|
508 from django.contrib.contenttypes.views import shortcut |
|
509 return shortcut(request, *url.split('/')[1:]) |
|
510 else: |
|
511 if '/' in url: |
|
512 return self.model_page(request, *url.split('/', 2)) |
|
513 else: |
|
514 return self.app_index(request, url) |
|
515 |
|
516 raise http.Http404('The requested admin page does not exist.') |
|
517 |
|
518 def model_page(self, request, app_label, model_name, rest_of_url=None): |
|
519 """ |
|
520 DEPRECATED. This is the old way of handling a model view on the admin |
|
521 site; the new views should use get_urls(), above. |
|
522 """ |
|
523 from django.db import models |
|
524 model = models.get_model(app_label, model_name) |
|
525 if model is None: |
|
526 raise http.Http404("App %r, model %r, not found." % (app_label, model_name)) |
|
527 try: |
|
528 admin_obj = self._registry[model] |
|
529 except KeyError: |
|
530 raise http.Http404("This model exists but has not been registered with the admin site.") |
|
531 return admin_obj(request, rest_of_url) |
|
532 model_page = never_cache(model_page) |
|
533 |
|
534 # This global object represents the default admin site, for the common case. |
|
535 # You can instantiate AdminSite in your own code to create a custom admin site. |
|
536 site = AdminSite() |