base.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603
  1. import copy
  2. import functools
  3. import datetime
  4. import decimal
  5. from functools import update_wrapper
  6. from inspect import getfullargspec
  7. from django import forms
  8. from django.apps import apps
  9. from django.conf import settings
  10. from django.contrib import messages
  11. from django.contrib.auth import get_permission_codename
  12. from django.core.exceptions import ValidationError
  13. from django.core.serializers.json import DjangoJSONEncoder
  14. from django.urls.base import reverse
  15. from django.http import HttpResponse
  16. from django.template import Context, Template
  17. from django.template.response import TemplateResponse
  18. from django.utils import six
  19. from django.utils.decorators import method_decorator, classonlymethod
  20. from django.utils.encoding import force_text, smart_text, smart_str
  21. from django.utils.functional import Promise
  22. from django.utils.http import urlencode
  23. from django.utils.itercompat import is_iterable
  24. from django.utils.safestring import mark_safe
  25. from django.utils.text import capfirst
  26. from django.utils.translation import ugettext as _
  27. from django.views.decorators.csrf import csrf_protect
  28. from django.views.generic import View
  29. from collections import OrderedDict
  30. from xadmin.util import static, json, vendor, sortkeypicker
  31. from xadmin.models import Log
  32. csrf_protect_m = method_decorator(csrf_protect)
  33. class IncorrectPluginArg(Exception):
  34. pass
  35. def get_content_type_for_model(obj):
  36. from django.contrib.contenttypes.models import ContentType
  37. return ContentType.objects.get_for_model(obj, for_concrete_model=False)
  38. def filter_chain(filters, token, func, *args, **kwargs):
  39. if token == -1:
  40. return func()
  41. else:
  42. def _inner_method():
  43. fm = filters[token]
  44. fargs = getfullargspec(fm)[0]
  45. if len(fargs) == 1:
  46. # Only self arg
  47. result = func()
  48. if result is None:
  49. return fm()
  50. else:
  51. raise IncorrectPluginArg(u'Plugin filter method need a arg to receive parent method result.')
  52. else:
  53. return fm(func if fargs[1] == '__' else func(), *args, **kwargs)
  54. return filter_chain(filters, token - 1, _inner_method, *args, **kwargs)
  55. def filter_hook(func):
  56. tag = func.__name__
  57. func.__doc__ = "``filter_hook``\n\n" + (func.__doc__ or "")
  58. @functools.wraps(func)
  59. def method(self, *args, **kwargs):
  60. def _inner_method():
  61. return func(self, *args, **kwargs)
  62. if self.plugins:
  63. filters = [(getattr(getattr(p, tag), 'priority', 10), getattr(p, tag))
  64. for p in self.plugins if callable(getattr(p, tag, None))]
  65. filters = [f for p, f in sorted(filters, key=lambda x:x[0])]
  66. return filter_chain(filters, len(filters) - 1, _inner_method, *args, **kwargs)
  67. else:
  68. return _inner_method()
  69. return method
  70. def inclusion_tag(file_name, context_class=Context, takes_context=False):
  71. def wrap(func):
  72. @functools.wraps(func)
  73. def method(self, context, nodes, *arg, **kwargs):
  74. _dict = func(self, context, nodes, *arg, **kwargs)
  75. from django.template.loader import get_template, select_template
  76. cls_str = str if six.PY3 else basestring
  77. if isinstance(file_name, Template):
  78. t = file_name
  79. elif not isinstance(file_name, cls_str) and is_iterable(file_name):
  80. t = select_template(file_name)
  81. else:
  82. t = get_template(file_name)
  83. _dict['autoescape'] = context.autoescape
  84. _dict['use_l10n'] = context.use_l10n
  85. _dict['use_tz'] = context.use_tz
  86. _dict['admin_view'] = context['admin_view']
  87. csrf_token = context.get('csrf_token', None)
  88. if csrf_token is not None:
  89. _dict['csrf_token'] = csrf_token
  90. nodes.append(t.render(_dict))
  91. return method
  92. return wrap
  93. class JSONEncoder(DjangoJSONEncoder):
  94. def default(self, o):
  95. if isinstance(o, datetime.datetime):
  96. return o.strftime('%Y-%m-%d %H:%M:%S')
  97. elif isinstance(o, datetime.date):
  98. return o.strftime('%Y-%m-%d')
  99. elif isinstance(o, decimal.Decimal):
  100. return str(o)
  101. elif isinstance(o, Promise):
  102. return force_text(o)
  103. else:
  104. try:
  105. return super(JSONEncoder, self).default(o)
  106. except Exception:
  107. return smart_text(o)
  108. class BaseAdminObject(object):
  109. def get_view(self, view_class, option_class=None, *args, **kwargs):
  110. opts = kwargs.pop('opts', {})
  111. return self.admin_site.get_view_class(view_class, option_class, **opts)(self.request, *args, **kwargs)
  112. def get_model_view(self, view_class, model, *args, **kwargs):
  113. return self.get_view(view_class, self.admin_site._registry.get(model), *args, **kwargs)
  114. def get_admin_url(self, name, *args, **kwargs):
  115. return reverse('%s:%s' % (self.admin_site.app_name, name), args=args, kwargs=kwargs)
  116. def get_model_url(self, model, name, *args, **kwargs):
  117. return reverse(
  118. '%s:%s_%s_%s' % (self.admin_site.app_name, model._meta.app_label,
  119. model._meta.model_name, name),
  120. args=args, kwargs=kwargs, current_app=self.admin_site.name)
  121. def get_model_perm(self, model, name):
  122. return '%s.%s_%s' % (model._meta.app_label, name, model._meta.model_name)
  123. def has_model_perm(self, model, name, user=None):
  124. user = user or self.user
  125. return user.has_perm(self.get_model_perm(model, name)) or (name == 'view' and self.has_model_perm(model, 'change', user))
  126. def get_query_string(self, new_params=None, remove=None):
  127. if new_params is None:
  128. new_params = {}
  129. if remove is None:
  130. remove = []
  131. p = dict(self.request.GET.items()).copy()
  132. arr_keys = list(p.keys())
  133. for r in remove:
  134. for k in arr_keys:
  135. if k.startswith(r):
  136. del p[k]
  137. for k, v in new_params.items():
  138. if v is None:
  139. if k in p:
  140. del p[k]
  141. else:
  142. p[k] = v
  143. return '?%s' % urlencode(p)
  144. def get_form_params(self, new_params=None, remove=None):
  145. if new_params is None:
  146. new_params = {}
  147. if remove is None:
  148. remove = []
  149. p = dict(self.request.GET.items()).copy()
  150. arr_keys = list(p.keys())
  151. for r in remove:
  152. for k in arr_keys:
  153. if k.startswith(r):
  154. del p[k]
  155. for k, v in new_params.items():
  156. if v is None:
  157. if k in p:
  158. del p[k]
  159. else:
  160. p[k] = v
  161. return mark_safe(''.join(
  162. '<input type="hidden" name="%s" value="%s"/>' % (k, v) for k, v in p.items() if v))
  163. def render_response(self, content, response_type='json'):
  164. if response_type == 'json':
  165. response = HttpResponse(content_type="application/json; charset=UTF-8")
  166. response.write(
  167. json.dumps(content, cls=JSONEncoder, ensure_ascii=False))
  168. return response
  169. return HttpResponse(content)
  170. def template_response(self, template, context):
  171. return TemplateResponse(self.request, template, context)
  172. def message_user(self, message, level='info'):
  173. """
  174. Send a message to the user. The default implementation
  175. posts a message using the django.contrib.messages backend.
  176. """
  177. if hasattr(messages, level) and callable(getattr(messages, level)):
  178. getattr(messages, level)(self.request, message)
  179. def static(self, path):
  180. return static(path)
  181. def vendor(self, *tags):
  182. return vendor(*tags)
  183. def log(self, flag, message, obj=None):
  184. log = Log(
  185. user=self.user,
  186. ip_addr=self.request.META['REMOTE_ADDR'],
  187. action_flag=flag,
  188. message=message
  189. )
  190. if obj:
  191. log.content_type = get_content_type_for_model(obj)
  192. log.object_id = obj.pk
  193. log.object_repr = force_text(obj)
  194. log.save()
  195. class BaseAdminPlugin(BaseAdminObject):
  196. def __init__(self, admin_view):
  197. self.admin_view = admin_view
  198. self.admin_site = admin_view.admin_site
  199. if hasattr(admin_view, 'model'):
  200. self.model = admin_view.model
  201. self.opts = admin_view.model._meta
  202. def init_request(self, *args, **kwargs):
  203. pass
  204. class BaseAdminView(BaseAdminObject, View):
  205. """ Base Admin view, support some comm attrs."""
  206. base_template = 'xadmin/base.html'
  207. need_site_permission = True
  208. def __init__(self, request, *args, **kwargs):
  209. self.request = request
  210. self.request_method = request.method.lower()
  211. self.user = request.user
  212. self.base_plugins = [p(self) for p in getattr(self,
  213. "plugin_classes", [])]
  214. self.args = args
  215. self.kwargs = kwargs
  216. self.init_plugin(*args, **kwargs)
  217. self.init_request(*args, **kwargs)
  218. @classonlymethod
  219. def as_view(cls):
  220. def view(request, *args, **kwargs):
  221. self = cls(request, *args, **kwargs)
  222. if hasattr(self, 'get') and not hasattr(self, 'head'):
  223. self.head = self.get
  224. if self.request_method in self.http_method_names:
  225. handler = getattr(
  226. self, self.request_method, self.http_method_not_allowed)
  227. else:
  228. handler = self.http_method_not_allowed
  229. return handler(request, *args, **kwargs)
  230. # take name and docstring from class
  231. update_wrapper(view, cls, updated=())
  232. view.need_site_permission = cls.need_site_permission
  233. return view
  234. def init_request(self, *args, **kwargs):
  235. pass
  236. def init_plugin(self, *args, **kwargs):
  237. plugins = []
  238. for p in self.base_plugins:
  239. p.request = self.request
  240. p.user = self.user
  241. p.args = self.args
  242. p.kwargs = self.kwargs
  243. result = p.init_request(*args, **kwargs)
  244. if result is not False:
  245. plugins.append(p)
  246. self.plugins = plugins
  247. @filter_hook
  248. def get_context(self):
  249. return {'admin_view': self, 'media': self.media, 'base_template': self.base_template}
  250. @property
  251. def media(self):
  252. return self.get_media()
  253. @filter_hook
  254. def get_media(self):
  255. return forms.Media()
  256. class CommAdminView(BaseAdminView):
  257. base_template = 'xadmin/base_site.html'
  258. menu_template = 'xadmin/includes/sitemenu_default.html'
  259. site_title = getattr(settings, "XADMIN_TITLE", _(u"Django Xadmin"))
  260. site_footer = getattr(settings, "XADMIN_FOOTER_TITLE", _(u"my-company.inc"))
  261. global_models_icon = {}
  262. default_model_icon = None
  263. apps_label_title = {}
  264. apps_icons = {}
  265. def get_site_menu(self):
  266. return None
  267. @filter_hook
  268. def get_nav_menu(self):
  269. site_menu = list(self.get_site_menu() or [])
  270. had_urls = []
  271. def get_url(menu, had_urls):
  272. if 'url' in menu:
  273. had_urls.append(menu['url'])
  274. if 'menus' in menu:
  275. for m in menu['menus']:
  276. get_url(m, had_urls)
  277. get_url({'menus': site_menu}, had_urls)
  278. nav_menu = OrderedDict()
  279. for model, model_admin in self.admin_site._registry.items():
  280. if getattr(model_admin, 'hidden_menu', False):
  281. continue
  282. app_label = model._meta.app_label
  283. app_icon = None
  284. model_dict = {
  285. 'title': smart_text(capfirst(model._meta.verbose_name_plural)),
  286. 'url': self.get_model_url(model, "changelist"),
  287. 'icon': self.get_model_icon(model),
  288. 'perm': self.get_model_perm(model, 'view'),
  289. 'order': model_admin.order,
  290. }
  291. if model_dict['url'] in had_urls:
  292. continue
  293. app_key = "app:%s" % app_label
  294. if app_key in nav_menu:
  295. nav_menu[app_key]['menus'].append(model_dict)
  296. else:
  297. # Find app title
  298. app_title = smart_text(app_label.title())
  299. if app_label.lower() in self.apps_label_title:
  300. app_title = self.apps_label_title[app_label.lower()]
  301. else:
  302. app_title = smart_text(apps.get_app_config(app_label).verbose_name)
  303. # find app icon
  304. if app_label.lower() in self.apps_icons:
  305. app_icon = self.apps_icons[app_label.lower()]
  306. nav_menu[app_key] = {
  307. 'title': app_title,
  308. 'menus': [model_dict],
  309. }
  310. app_menu = nav_menu[app_key]
  311. if app_icon:
  312. app_menu['first_icon'] = app_icon
  313. elif ('first_icon' not in app_menu or
  314. app_menu['first_icon'] == self.default_model_icon) and model_dict.get('icon'):
  315. app_menu['first_icon'] = model_dict['icon']
  316. if 'first_url' not in app_menu and model_dict.get('url'):
  317. app_menu['first_url'] = model_dict['url']
  318. for menu in nav_menu.values():
  319. menu['menus'].sort(key=sortkeypicker(['order', 'title']))
  320. nav_menu = list(nav_menu.values())
  321. nav_menu.sort(key=lambda x: x['title'])
  322. site_menu.extend(nav_menu)
  323. return site_menu
  324. @filter_hook
  325. def get_context(self):
  326. context = super(CommAdminView, self).get_context()
  327. if not settings.DEBUG and 'nav_menu' in self.request.session:
  328. nav_menu = json.loads(self.request.session['nav_menu'])
  329. else:
  330. menus = copy.copy(self.get_nav_menu())
  331. def check_menu_permission(item):
  332. need_perm = item.pop('perm', None)
  333. if need_perm is None:
  334. return True
  335. elif callable(need_perm):
  336. return need_perm(self.user)
  337. elif need_perm == 'super':
  338. return self.user.is_superuser
  339. else:
  340. return self.user.has_perm(need_perm)
  341. def filter_item(item):
  342. if 'menus' in item:
  343. before_filter_length = len(item['menus'])
  344. item['menus'] = [filter_item(
  345. i) for i in item['menus'] if check_menu_permission(i)]
  346. after_filter_length = len(item['menus'])
  347. if after_filter_length == 0 and before_filter_length > 0:
  348. return None
  349. return item
  350. nav_menu = [filter_item(item) for item in menus if check_menu_permission(item)]
  351. nav_menu = list(filter(lambda x: x, nav_menu))
  352. if not settings.DEBUG:
  353. self.request.session['nav_menu'] = json.dumps(nav_menu, cls=JSONEncoder, ensure_ascii=False)
  354. self.request.session.modified = True
  355. def check_selected(menu, path):
  356. selected = False
  357. if 'url' in menu:
  358. chop_index = menu['url'].find('?')
  359. if chop_index == -1:
  360. selected = path.startswith(menu['url'])
  361. else:
  362. selected = path.startswith(menu['url'][:chop_index])
  363. if 'menus' in menu:
  364. for m in menu['menus']:
  365. _s = check_selected(m, path)
  366. if _s:
  367. selected = True
  368. if selected:
  369. menu['selected'] = True
  370. return selected
  371. for menu in nav_menu:
  372. check_selected(menu, self.request.path)
  373. context.update({
  374. 'menu_template': self.menu_template,
  375. 'nav_menu': nav_menu,
  376. 'site_title': self.site_title,
  377. 'site_footer': self.site_footer,
  378. 'breadcrumbs': self.get_breadcrumb()
  379. })
  380. return context
  381. @filter_hook
  382. def get_model_icon(self, model):
  383. icon = self.global_models_icon.get(model)
  384. if icon is None and model in self.admin_site._registry:
  385. icon = getattr(self.admin_site._registry[model],
  386. 'model_icon', self.default_model_icon)
  387. return icon
  388. @filter_hook
  389. def get_breadcrumb(self):
  390. return [{
  391. 'url': self.get_admin_url('index'),
  392. 'title': _('Home')
  393. }]
  394. class ModelAdminView(CommAdminView):
  395. fields = None
  396. exclude = None
  397. ordering = None
  398. model = None
  399. remove_permissions = []
  400. def __init__(self, request, *args, **kwargs):
  401. self.opts = self.model._meta
  402. self.app_label = self.model._meta.app_label
  403. self.model_name = self.model._meta.model_name
  404. self.model_info = (self.app_label, self.model_name)
  405. super(ModelAdminView, self).__init__(request, *args, **kwargs)
  406. @filter_hook
  407. def get_context(self):
  408. new_context = {
  409. "opts": self.opts,
  410. "app_label": self.app_label,
  411. "model_name": self.model_name,
  412. "verbose_name": force_text(self.opts.verbose_name),
  413. 'model_icon': self.get_model_icon(self.model),
  414. }
  415. context = super(ModelAdminView, self).get_context()
  416. context.update(new_context)
  417. return context
  418. @filter_hook
  419. def get_breadcrumb(self):
  420. bcs = super(ModelAdminView, self).get_breadcrumb()
  421. item = {'title': self.opts.verbose_name_plural}
  422. if self.has_view_permission():
  423. item['url'] = self.model_admin_url('changelist')
  424. bcs.append(item)
  425. return bcs
  426. @filter_hook
  427. def get_object(self, object_id):
  428. """
  429. Get model object instance by object_id, used for change admin view
  430. """
  431. # first get base admin view property queryset, return default model queryset
  432. model = self.model
  433. try:
  434. object_id = model._meta.pk.to_python(object_id)
  435. return model.objects.get(pk=object_id)
  436. except (model.DoesNotExist, ValidationError):
  437. return None
  438. @filter_hook
  439. def get_object_url(self, obj):
  440. if self.has_change_permission(obj):
  441. return self.model_admin_url("change", getattr(obj, self.opts.pk.attname))
  442. elif self.has_view_permission(obj):
  443. return self.model_admin_url("detail", getattr(obj, self.opts.pk.attname))
  444. else:
  445. return None
  446. def model_admin_url(self, name, *args, **kwargs):
  447. return reverse(
  448. "%s:%s_%s_%s" % (self.admin_site.app_name, self.opts.app_label,
  449. self.model_name, name), args=args, kwargs=kwargs)
  450. def get_model_perms(self):
  451. """
  452. Returns a dict of all perms for this model. This dict has the keys
  453. ``add``, ``change``, and ``delete`` mapping to the True/False for each
  454. of those actions.
  455. """
  456. return {
  457. 'view': self.has_view_permission(),
  458. 'add': self.has_add_permission(),
  459. 'change': self.has_change_permission(),
  460. 'delete': self.has_delete_permission(),
  461. }
  462. def get_template_list(self, template_name):
  463. opts = self.opts
  464. return (
  465. "xadmin/%s/%s/%s" % (
  466. opts.app_label, opts.object_name.lower(), template_name),
  467. "xadmin/%s/%s" % (opts.app_label, template_name),
  468. "xadmin/%s" % template_name,
  469. )
  470. def get_ordering(self):
  471. """
  472. Hook for specifying field ordering.
  473. """
  474. return self.ordering or () # otherwise we might try to *None, which is bad ;)
  475. @filter_hook
  476. def queryset(self):
  477. """
  478. Returns a QuerySet of all model instances that can be edited by the
  479. admin site. This is used by changelist_view.
  480. """
  481. return self.model._default_manager.get_queryset()
  482. def has_view_permission(self, obj=None):
  483. view_codename = get_permission_codename('view', self.opts)
  484. change_codename = get_permission_codename('change', self.opts)
  485. return ('view' not in self.remove_permissions) and (self.user.has_perm('%s.%s' % (self.app_label, view_codename)) or
  486. self.user.has_perm('%s.%s' % (self.app_label, change_codename)))
  487. def has_add_permission(self):
  488. codename = get_permission_codename('add', self.opts)
  489. return ('add' not in self.remove_permissions) and self.user.has_perm('%s.%s' % (self.app_label, codename))
  490. def has_change_permission(self, obj=None):
  491. codename = get_permission_codename('change', self.opts)
  492. return ('change' not in self.remove_permissions) and self.user.has_perm('%s.%s' % (self.app_label, codename))
  493. def has_delete_permission(self, request=None, obj=None):
  494. codename = get_permission_codename('delete', self.opts)
  495. return ('delete' not in self.remove_permissions) and self.user.has_perm('%s.%s' % (self.app_label, codename))