|
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.db.models.base import ModelBase |
|
7 from django.core.exceptions import ImproperlyConfigured |
|
8 from django.core.urlresolvers import reverse |
|
9 from django.shortcuts import render_to_response |
|
10 from django.utils.functional import update_wrapper |
|
11 from django.utils.safestring import mark_safe |
|
12 from django.utils.text import capfirst |
|
13 from django.utils.translation import ugettext_lazy, ugettext as _ |
|
14 from django.views.decorators.cache import never_cache |
|
15 from django.conf import settings |
|
16 try: |
|
17 set |
|
18 except NameError: |
|
19 from sets import Set as set # Python 2.3 fallback |
|
20 |
|
21 ERROR_MESSAGE = ugettext_lazy("Please enter a correct username and password. Note that both fields are case-sensitive.") |
|
22 LOGIN_FORM_KEY = 'this_is_the_login_form' |
|
23 |
|
24 class AlreadyRegistered(Exception): |
|
25 pass |
|
26 |
|
27 class NotRegistered(Exception): |
|
28 pass |
|
29 |
|
30 class AdminSite(object): |
|
31 """ |
|
32 An AdminSite object encapsulates an instance of the Django admin application, ready |
|
33 to be hooked in to your URLconf. Models are registered with the AdminSite using the |
|
34 register() method, and the root() method can then be used as a Django view function |
|
35 that presents a full admin interface for the collection of registered models. |
|
36 """ |
|
37 |
|
38 index_template = None |
|
39 login_template = None |
|
40 app_index_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_authenticated() 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 INSTALLED_APPS setting in order to use the admin application.") |
|
155 if not ContentType._meta.installed: |
|
156 raise ImproperlyConfigured("Put 'django.contrib.contenttypes' in your INSTALLED_APPS setting in order to use the admin application.") |
|
157 if 'django.core.context_processors.auth' not in settings.TEMPLATE_CONTEXT_PROCESSORS: |
|
158 raise ImproperlyConfigured("Put 'django.core.context_processors.auth' in your TEMPLATE_CONTEXT_PROCESSORS setting in order to use the admin application.") |
|
159 |
|
160 def admin_view(self, view, cacheable=False): |
|
161 """ |
|
162 Decorator to create an admin view attached to this ``AdminSite``. This |
|
163 wraps the view and provides permission checking by calling |
|
164 ``self.has_permission``. |
|
165 |
|
166 You'll want to use this from within ``AdminSite.get_urls()``: |
|
167 |
|
168 class MyAdminSite(AdminSite): |
|
169 |
|
170 def get_urls(self): |
|
171 from django.conf.urls.defaults import patterns, url |
|
172 |
|
173 urls = super(MyAdminSite, self).get_urls() |
|
174 urls += patterns('', |
|
175 url(r'^my_view/$', self.admin_view(some_view)) |
|
176 ) |
|
177 return urls |
|
178 |
|
179 By default, admin_views are marked non-cacheable using the |
|
180 ``never_cache`` decorator. If the view can be safely cached, set |
|
181 cacheable=True. |
|
182 """ |
|
183 def inner(request, *args, **kwargs): |
|
184 if not self.has_permission(request): |
|
185 return self.login(request) |
|
186 return view(request, *args, **kwargs) |
|
187 if not cacheable: |
|
188 inner = never_cache(inner) |
|
189 return update_wrapper(inner, view) |
|
190 |
|
191 def get_urls(self): |
|
192 from django.conf.urls.defaults import patterns, url, include |
|
193 |
|
194 def wrap(view, cacheable=False): |
|
195 def wrapper(*args, **kwargs): |
|
196 return self.admin_view(view, cacheable)(*args, **kwargs) |
|
197 return update_wrapper(wrapper, view) |
|
198 |
|
199 # Admin-site-wide views. |
|
200 urlpatterns = patterns('', |
|
201 url(r'^$', |
|
202 wrap(self.index), |
|
203 name='index'), |
|
204 url(r'^logout/$', |
|
205 wrap(self.logout), |
|
206 name='logout'), |
|
207 url(r'^password_change/$', |
|
208 wrap(self.password_change, cacheable=True), |
|
209 name='password_change'), |
|
210 url(r'^password_change/done/$', |
|
211 wrap(self.password_change_done, cacheable=True), |
|
212 name='password_change_done'), |
|
213 url(r'^jsi18n/$', |
|
214 wrap(self.i18n_javascript, cacheable=True), |
|
215 name='jsi18n'), |
|
216 url(r'^r/(?P<content_type_id>\d+)/(?P<object_id>.+)/$', |
|
217 'django.views.defaults.shortcut'), |
|
218 url(r'^(?P<app_label>\w+)/$', |
|
219 wrap(self.app_index), |
|
220 name='app_list') |
|
221 ) |
|
222 |
|
223 # Add in each model's views. |
|
224 for model, model_admin in self._registry.iteritems(): |
|
225 urlpatterns += patterns('', |
|
226 url(r'^%s/%s/' % (model._meta.app_label, model._meta.module_name), |
|
227 include(model_admin.urls)) |
|
228 ) |
|
229 return urlpatterns |
|
230 |
|
231 def urls(self): |
|
232 return self.get_urls(), self.app_name, self.name |
|
233 urls = property(urls) |
|
234 |
|
235 def password_change(self, request): |
|
236 """ |
|
237 Handles the "change password" task -- both form display and validation. |
|
238 """ |
|
239 from django.contrib.auth.views import password_change |
|
240 if self.root_path is not None: |
|
241 url = '%spassword_change/done/' % self.root_path |
|
242 else: |
|
243 url = reverse('admin:password_change_done', current_app=self.name) |
|
244 return password_change(request, post_change_redirect=url) |
|
245 |
|
246 def password_change_done(self, request): |
|
247 """ |
|
248 Displays the "success" page after a password change. |
|
249 """ |
|
250 from django.contrib.auth.views import password_change_done |
|
251 return password_change_done(request) |
|
252 |
|
253 def i18n_javascript(self, request): |
|
254 """ |
|
255 Displays the i18n JavaScript that the Django admin requires. |
|
256 |
|
257 This takes into account the USE_I18N setting. If it's set to False, the |
|
258 generated JavaScript will be leaner and faster. |
|
259 """ |
|
260 if settings.USE_I18N: |
|
261 from django.views.i18n import javascript_catalog |
|
262 else: |
|
263 from django.views.i18n import null_javascript_catalog as javascript_catalog |
|
264 return javascript_catalog(request, packages='django.conf') |
|
265 |
|
266 def logout(self, request): |
|
267 """ |
|
268 Logs out the user for the given HttpRequest. |
|
269 |
|
270 This should *not* assume the user is already logged in. |
|
271 """ |
|
272 from django.contrib.auth.views import logout |
|
273 return logout(request) |
|
274 logout = never_cache(logout) |
|
275 |
|
276 def login(self, request): |
|
277 """ |
|
278 Displays the login form for the given HttpRequest. |
|
279 """ |
|
280 from django.contrib.auth.models import User |
|
281 |
|
282 # If this isn't already the login page, display it. |
|
283 if not request.POST.has_key(LOGIN_FORM_KEY): |
|
284 if request.POST: |
|
285 message = _("Please log in again, because your session has expired.") |
|
286 else: |
|
287 message = "" |
|
288 return self.display_login_form(request, message) |
|
289 |
|
290 # Check that the user accepts cookies. |
|
291 if not request.session.test_cookie_worked(): |
|
292 message = _("Looks like your browser isn't configured to accept cookies. Please enable cookies, reload this page, and try again.") |
|
293 return self.display_login_form(request, message) |
|
294 else: |
|
295 request.session.delete_test_cookie() |
|
296 |
|
297 # Check the password. |
|
298 username = request.POST.get('username', None) |
|
299 password = request.POST.get('password', None) |
|
300 user = authenticate(username=username, password=password) |
|
301 if user is None: |
|
302 message = ERROR_MESSAGE |
|
303 if username is not None and u'@' in username: |
|
304 # Mistakenly entered e-mail address instead of username? Look it up. |
|
305 try: |
|
306 user = User.objects.get(email=username) |
|
307 except (User.DoesNotExist, User.MultipleObjectsReturned): |
|
308 message = _("Usernames cannot contain the '@' character.") |
|
309 else: |
|
310 if user.check_password(password): |
|
311 message = _("Your e-mail address is not your username." |
|
312 " Try '%s' instead.") % user.username |
|
313 else: |
|
314 message = _("Usernames cannot contain the '@' character.") |
|
315 return self.display_login_form(request, message) |
|
316 |
|
317 # The user data is correct; log in the user in and continue. |
|
318 else: |
|
319 if user.is_active and user.is_staff: |
|
320 login(request, user) |
|
321 return http.HttpResponseRedirect(request.get_full_path()) |
|
322 else: |
|
323 return self.display_login_form(request, ERROR_MESSAGE) |
|
324 login = never_cache(login) |
|
325 |
|
326 def index(self, request, extra_context=None): |
|
327 """ |
|
328 Displays the main admin index page, which lists all of the installed |
|
329 apps that have been registered in this site. |
|
330 """ |
|
331 app_dict = {} |
|
332 user = request.user |
|
333 for model, model_admin in self._registry.items(): |
|
334 app_label = model._meta.app_label |
|
335 has_module_perms = user.has_module_perms(app_label) |
|
336 |
|
337 if has_module_perms: |
|
338 perms = model_admin.get_model_perms(request) |
|
339 |
|
340 # Check whether user has any perm for this module. |
|
341 # If so, add the module to the model_list. |
|
342 if True in perms.values(): |
|
343 model_dict = { |
|
344 'name': capfirst(model._meta.verbose_name_plural), |
|
345 'admin_url': mark_safe('%s/%s/' % (app_label, model.__name__.lower())), |
|
346 'perms': perms, |
|
347 } |
|
348 if app_label in app_dict: |
|
349 app_dict[app_label]['models'].append(model_dict) |
|
350 else: |
|
351 app_dict[app_label] = { |
|
352 'name': app_label.title(), |
|
353 'app_url': app_label + '/', |
|
354 'has_module_perms': has_module_perms, |
|
355 'models': [model_dict], |
|
356 } |
|
357 |
|
358 # Sort the apps alphabetically. |
|
359 app_list = app_dict.values() |
|
360 app_list.sort(lambda x, y: cmp(x['name'], y['name'])) |
|
361 |
|
362 # Sort the models alphabetically within each app. |
|
363 for app in app_list: |
|
364 app['models'].sort(lambda x, y: cmp(x['name'], y['name'])) |
|
365 |
|
366 context = { |
|
367 'title': _('Site administration'), |
|
368 'app_list': app_list, |
|
369 'root_path': self.root_path, |
|
370 } |
|
371 context.update(extra_context or {}) |
|
372 context_instance = template.RequestContext(request, current_app=self.name) |
|
373 return render_to_response(self.index_template or 'admin/index.html', context, |
|
374 context_instance=context_instance |
|
375 ) |
|
376 index = never_cache(index) |
|
377 |
|
378 def display_login_form(self, request, error_message='', extra_context=None): |
|
379 request.session.set_test_cookie() |
|
380 context = { |
|
381 'title': _('Log in'), |
|
382 'app_path': request.get_full_path(), |
|
383 'error_message': error_message, |
|
384 'root_path': self.root_path, |
|
385 } |
|
386 context.update(extra_context or {}) |
|
387 context_instance = template.RequestContext(request, current_app=self.name) |
|
388 return render_to_response(self.login_template or 'admin/login.html', context, |
|
389 context_instance=context_instance |
|
390 ) |
|
391 |
|
392 def app_index(self, request, app_label, extra_context=None): |
|
393 user = request.user |
|
394 has_module_perms = user.has_module_perms(app_label) |
|
395 app_dict = {} |
|
396 for model, model_admin in self._registry.items(): |
|
397 if app_label == model._meta.app_label: |
|
398 if has_module_perms: |
|
399 perms = model_admin.get_model_perms(request) |
|
400 |
|
401 # Check whether user has any perm for this module. |
|
402 # If so, add the module to the model_list. |
|
403 if True in perms.values(): |
|
404 model_dict = { |
|
405 'name': capfirst(model._meta.verbose_name_plural), |
|
406 'admin_url': '%s/' % model.__name__.lower(), |
|
407 'perms': perms, |
|
408 } |
|
409 if app_dict: |
|
410 app_dict['models'].append(model_dict), |
|
411 else: |
|
412 # First time around, now that we know there's |
|
413 # something to display, add in the necessary meta |
|
414 # information. |
|
415 app_dict = { |
|
416 'name': app_label.title(), |
|
417 'app_url': '', |
|
418 'has_module_perms': has_module_perms, |
|
419 'models': [model_dict], |
|
420 } |
|
421 if not app_dict: |
|
422 raise http.Http404('The requested admin page does not exist.') |
|
423 # Sort the models alphabetically within each app. |
|
424 app_dict['models'].sort(lambda x, y: cmp(x['name'], y['name'])) |
|
425 context = { |
|
426 'title': _('%s administration') % capfirst(app_label), |
|
427 'app_list': [app_dict], |
|
428 'root_path': self.root_path, |
|
429 } |
|
430 context.update(extra_context or {}) |
|
431 context_instance = template.RequestContext(request, current_app=self.name) |
|
432 return render_to_response(self.app_index_template or ('admin/%s/app_index.html' % app_label, |
|
433 'admin/app_index.html'), context, |
|
434 context_instance=context_instance |
|
435 ) |
|
436 |
|
437 def root(self, request, url): |
|
438 """ |
|
439 DEPRECATED. This function is the old way of handling URL resolution, and |
|
440 is deprecated in favor of real URL resolution -- see ``get_urls()``. |
|
441 |
|
442 This function still exists for backwards-compatibility; it will be |
|
443 removed in Django 1.3. |
|
444 """ |
|
445 import warnings |
|
446 warnings.warn( |
|
447 "AdminSite.root() is deprecated; use include(admin.site.urls) instead.", |
|
448 PendingDeprecationWarning |
|
449 ) |
|
450 |
|
451 # |
|
452 # Again, remember that the following only exists for |
|
453 # backwards-compatibility. Any new URLs, changes to existing URLs, or |
|
454 # whatever need to be done up in get_urls(), above! |
|
455 # |
|
456 |
|
457 if request.method == 'GET' and not request.path.endswith('/'): |
|
458 return http.HttpResponseRedirect(request.path + '/') |
|
459 |
|
460 if settings.DEBUG: |
|
461 self.check_dependencies() |
|
462 |
|
463 # Figure out the admin base URL path and stash it for later use |
|
464 self.root_path = re.sub(re.escape(url) + '$', '', request.path) |
|
465 |
|
466 url = url.rstrip('/') # Trim trailing slash, if it exists. |
|
467 |
|
468 # The 'logout' view doesn't require that the person is logged in. |
|
469 if url == 'logout': |
|
470 return self.logout(request) |
|
471 |
|
472 # Check permission to continue or display login form. |
|
473 if not self.has_permission(request): |
|
474 return self.login(request) |
|
475 |
|
476 if url == '': |
|
477 return self.index(request) |
|
478 elif url == 'password_change': |
|
479 return self.password_change(request) |
|
480 elif url == 'password_change/done': |
|
481 return self.password_change_done(request) |
|
482 elif url == 'jsi18n': |
|
483 return self.i18n_javascript(request) |
|
484 # URLs starting with 'r/' are for the "View on site" links. |
|
485 elif url.startswith('r/'): |
|
486 from django.contrib.contenttypes.views import shortcut |
|
487 return shortcut(request, *url.split('/')[1:]) |
|
488 else: |
|
489 if '/' in url: |
|
490 return self.model_page(request, *url.split('/', 2)) |
|
491 else: |
|
492 return self.app_index(request, url) |
|
493 |
|
494 raise http.Http404('The requested admin page does not exist.') |
|
495 |
|
496 def model_page(self, request, app_label, model_name, rest_of_url=None): |
|
497 """ |
|
498 DEPRECATED. This is the old way of handling a model view on the admin |
|
499 site; the new views should use get_urls(), above. |
|
500 """ |
|
501 from django.db import models |
|
502 model = models.get_model(app_label, model_name) |
|
503 if model is None: |
|
504 raise http.Http404("App %r, model %r, not found." % (app_label, model_name)) |
|
505 try: |
|
506 admin_obj = self._registry[model] |
|
507 except KeyError: |
|
508 raise http.Http404("This model exists but has not been registered with the admin site.") |
|
509 return admin_obj(request, rest_of_url) |
|
510 model_page = never_cache(model_page) |
|
511 |
|
512 # This global object represents the default admin site, for the common case. |
|
513 # You can instantiate AdminSite in your own code to create a custom admin site. |
|
514 site = AdminSite() |