widgets.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  1. from django import forms
  2. from django.core.exceptions import ValidationError
  3. from django.core.urlresolvers import reverse
  4. from django.http import Http404
  5. from django.shortcuts import redirect
  6. from django.template import RequestContext
  7. from django.utils.translation import ugettext_lazy as _
  8. from jinja2 import TemplateNotFound
  9. import math
  10. from misago.forms import Form, FormLayout, FormFields, FormFieldsets
  11. from misago.messages import Message
  12. from misago.utils.pagination import make_pagination
  13. """
  14. Class widgets
  15. """
  16. class BaseWidget(object):
  17. """
  18. Admin Widget abstract class, providing widgets with common or shared functionality
  19. """
  20. admin = None
  21. id = None
  22. fallback = None
  23. name = None
  24. help = None
  25. notfound_message = None
  26. def __new__(cls, request, **kwargs):
  27. obj = super(BaseWidget, cls).__new__(cls)
  28. if not obj.name:
  29. obj.name = obj.get_name()
  30. if not obj.help:
  31. obj.help = obj.get_help()
  32. return obj(request, **kwargs)
  33. def get_token(self, token):
  34. return '%s_%s_%s' % (self.id, token, str('%s.%s' % (self.admin.id, self.admin.model.__name__)))
  35. def get_url(self):
  36. return reverse(self.admin.get_action_attr(self.id, 'route'))
  37. def get_name(self):
  38. return self.admin.get_action_attr(self.id, 'name')
  39. def get_help(self):
  40. return self.admin.get_action_attr(self.id, 'help')
  41. def get_id(self):
  42. return 'admin_%s' % self.id
  43. def get_template(self):
  44. return ('%s/%s.html' % (self.admin.id, self.template),
  45. 'admin/%s.html' % self.template)
  46. def add_template_variables(self, variables):
  47. return variables
  48. def get_fallback_url(self):
  49. return reverse(self.fallback)
  50. def get_target(self, model):
  51. pass
  52. def get_target_name(self, model):
  53. try:
  54. if self.translate_target_name:
  55. return _(model.__dict__[self.target_name])
  56. return model.__dict__[self.target_name]
  57. except AttributeError:
  58. return None
  59. def get_and_validate_target(self, target):
  60. try:
  61. model = self.admin.model.objects.select_related().get(pk=target)
  62. self.get_target(model)
  63. return model
  64. except self.admin.model.DoesNotExist:
  65. self.request.messages.set_flash(Message(self.notfound_message), 'error', self.admin.id)
  66. except ValueError as e:
  67. self.request.messages.set_flash(Message(e.args[0]), 'error', self.admin.id)
  68. return None
  69. class ListWidget(BaseWidget):
  70. """
  71. Items list widget
  72. """
  73. actions = []
  74. columns = []
  75. sortables = {}
  76. default_sorting = None
  77. search_form = None
  78. is_filtering = False
  79. pagination = None
  80. template = 'list'
  81. hide_actions = False
  82. table_form_button = _('Go')
  83. empty_message = _('There are no items to display')
  84. empty_search_message = _('Search has returned no items')
  85. nothing_checked_message = _('You have to select at least one item.')
  86. prompt_select = False
  87. def get_item_actions(self, item):
  88. """
  89. Provides request and item, should return list of tuples with item actions in following format:
  90. (id, name, help, icon, link)
  91. """
  92. return []
  93. def action(self, icon=None, name=None, url=None, post=False, prompt=None):
  94. """
  95. Function call to make hash with item actions
  96. """
  97. if prompt:
  98. self.prompt_select = True
  99. return {
  100. 'icon': icon,
  101. 'name': name,
  102. 'url': url,
  103. 'post': post,
  104. 'prompt': prompt,
  105. }
  106. def get_search_form(self):
  107. """
  108. Build a form object with items search
  109. """
  110. return self.search_form
  111. def set_filters(self, model, filters):
  112. """
  113. Set filters on model using filters from session
  114. """
  115. return None
  116. def get_table_form(self, page_items):
  117. """
  118. Build a form object with list of all items fields
  119. """
  120. return None
  121. def table_action(self, page_items, cleaned_data):
  122. """
  123. Handle table form submission, return tuple containing message and redirect link/false
  124. """
  125. return None
  126. def get_actions_form(self, page_items):
  127. """
  128. Build a form object with list of all items actions
  129. """
  130. if not self.actions:
  131. return None # Dont build form
  132. form_fields = {}
  133. list_choices = []
  134. for action in self.actions:
  135. list_choices.append((action[0], action[1]))
  136. form_fields['list_action'] = forms.ChoiceField(choices=list_choices)
  137. list_choices = []
  138. for item in page_items:
  139. list_choices.append((item.pk, None))
  140. form_fields['list_items'] = forms.MultipleChoiceField(choices=list_choices, widget=forms.CheckboxSelectMultiple)
  141. return type('AdminListForm', (Form,), form_fields)
  142. def get_sorting(self):
  143. """
  144. Return list sorting method.
  145. A list with three values:
  146. - Field we use to sort over
  147. - Sorting direction
  148. - order_by() argument
  149. """
  150. sorting_method = None
  151. if self.request.session.get(self.get_token('sort')) and self.request.session.get(self.get_token('sort'))[0] in self.sortables:
  152. sorting_method = self.request.session.get(self.get_token('sort'))
  153. if self.request.GET.get('sort') and self.request.GET.get('sort') in self.sortables:
  154. new_sorting = self.request.GET.get('sort')
  155. sorting_dir = int(self.request.GET.get('dir')) == 1
  156. sorting_method = [
  157. new_sorting,
  158. sorting_dir,
  159. new_sorting if sorting_dir else '-%s' % new_sorting
  160. ]
  161. self.request.session[self.get_token('sort')] = sorting_method
  162. if not sorting_method:
  163. if self.sortables:
  164. new_sorting = self.sortables.keys()[0]
  165. if self.default_sorting in self.sortables:
  166. new_sorting = self.default_sorting
  167. sorting_method = [
  168. new_sorting,
  169. self.sortables[new_sorting] == True,
  170. new_sorting if self.sortables[new_sorting] else '-%s' % new_sorting
  171. ]
  172. else:
  173. sorting_method = [
  174. id,
  175. True,
  176. '-id'
  177. ]
  178. return sorting_method
  179. def sort_items(self, page_items, sorting_method):
  180. return page_items.order_by(sorting_method[2])
  181. def get_pagination_url(self, page):
  182. return reverse(self.admin.get_action_attr(self.id, 'route'), kwargs={'page': page})
  183. def get_pagination(self, total, page):
  184. if not self.pagination or total < 0:
  185. # Dont do anything if we are not paging
  186. return None
  187. return make_pagination(page, total, self.pagination)
  188. def get_items(self):
  189. if self.request.session.get(self.get_token('filter')):
  190. self.is_filtering = True
  191. return self.set_filters(self.admin.model.objects, self.request.session.get(self.get_token('filter')))
  192. return self.admin.model.objects
  193. def __call__(self, request, page=0):
  194. """
  195. Use widget as view
  196. """
  197. self.request = request
  198. # Get basic list items
  199. items_total = self.get_items()
  200. # Set extra filters?
  201. try:
  202. items_total = self.select_items(items_total).count()
  203. except AttributeError:
  204. items_total = items_total.count()
  205. # Set sorting and paginating
  206. sorting_method = self.get_sorting()
  207. try:
  208. paginating_method = self.get_pagination(items_total, page)
  209. except Http404:
  210. return redirect(self.get_url())
  211. # List items
  212. items = self.get_items()
  213. if not request.session.get(self.get_token('filter')):
  214. items = items.all()
  215. # Set extra filters?
  216. try:
  217. items = self.select_items(items)
  218. except AttributeError:
  219. pass
  220. # Sort them
  221. items = self.sort_items(items, sorting_method);
  222. # Set pagination
  223. if self.pagination:
  224. items = items[paginating_method['start']:paginating_method['stop']]
  225. # Prefetch related?
  226. try:
  227. items = self.prefetch_related(items)
  228. except AttributeError:
  229. pass
  230. # Default message
  231. message = None
  232. # See if we should make and handle search form
  233. search_form = None
  234. SearchForm = self.get_search_form()
  235. if SearchForm:
  236. if request.method == 'POST':
  237. # New search
  238. if request.POST.get('origin') == 'search':
  239. search_form = SearchForm(request.POST, request=request)
  240. if search_form.is_valid():
  241. search_criteria = {}
  242. for field, criteria in search_form.cleaned_data.items():
  243. if len(criteria) > 0:
  244. search_criteria[field] = criteria
  245. if not search_criteria:
  246. message = Message(_("No search criteria have been defined."))
  247. else:
  248. request.session[self.get_token('filter')] = search_criteria
  249. return redirect(self.get_url())
  250. else:
  251. message = Message(_("Search form contains errors."))
  252. message.type = 'error'
  253. else:
  254. search_form = SearchForm(request=request)
  255. # Kill search
  256. if request.POST.get('origin') == 'clear' and self.is_filtering and request.csrf.request_secure(request):
  257. request.session[self.get_token('filter')] = None
  258. request.messages.set_flash(Message(_("Search criteria have been cleared.")), 'info', self.admin.id)
  259. return redirect(self.get_url())
  260. else:
  261. if self.is_filtering:
  262. search_form = SearchForm(request=request, initial=request.session.get(self.get_token('filter')))
  263. else:
  264. search_form = SearchForm(request=request)
  265. # See if we sould make and handle tab form
  266. table_form = None
  267. TableForm = self.get_table_form(items)
  268. if TableForm:
  269. if request.method == 'POST' and request.POST.get('origin') == 'table':
  270. table_form = TableForm(request.POST, request=request)
  271. if table_form.is_valid():
  272. message, redirect_url = self.table_action(items, table_form.cleaned_data)
  273. if redirect_url:
  274. request.messages.set_flash(message, message.type, self.admin.id)
  275. return redirect(redirect_url)
  276. else:
  277. message = Message(table_form.non_field_errors()[0], 'error')
  278. else:
  279. table_form = TableForm(request=request)
  280. # See if we should make and handle list form
  281. list_form = None
  282. ListForm = self.get_actions_form(items)
  283. if ListForm:
  284. if request.method == 'POST' and request.POST.get('origin') == 'list':
  285. list_form = ListForm(request.POST, request=request)
  286. if list_form.is_valid():
  287. try:
  288. form_action = getattr(self, 'action_' + list_form.cleaned_data['list_action'])
  289. message, redirect_url = form_action(items, list_form.cleaned_data['list_items'])
  290. if redirect_url:
  291. request.messages.set_flash(message, message.type, self.admin.id)
  292. return redirect(redirect_url)
  293. except AttributeError:
  294. message = Message(_("Action requested is incorrect."))
  295. else:
  296. if 'list_items' in list_form.errors:
  297. message = Message(self.nothing_checked_message)
  298. elif 'list_action' in list_form.errors:
  299. message = Message(_("Action requested is incorrect."))
  300. else:
  301. message = Message(list_form.non_field_errors()[0])
  302. message.type = 'error'
  303. else:
  304. list_form = ListForm(request=request)
  305. # Little hax to keep counters correct
  306. items_shown = len(items)
  307. if items_total < items_shown:
  308. items_total = items_shown
  309. # Render list
  310. return request.theme.render_to_response(self.get_template(),
  311. self.add_template_variables({
  312. 'admin': self.admin,
  313. 'action': self,
  314. 'request': request,
  315. 'url': self.get_url(),
  316. 'messages_log': request.messages.get_messages(self.admin.id),
  317. 'message': message,
  318. 'sorting': self.sortables,
  319. 'sorting_method': sorting_method,
  320. 'pagination': paginating_method,
  321. 'list_form': FormLayout(list_form) if list_form else None,
  322. 'search_form': FormLayout(search_form) if search_form else None,
  323. 'table_form': FormFields(table_form).fields if table_form else None,
  324. 'items': items,
  325. 'items_total': items_total,
  326. 'items_shown': items_shown,
  327. }),
  328. context_instance=RequestContext(request));
  329. class FormWidget(BaseWidget):
  330. """
  331. Form page widget
  332. """
  333. template = 'form'
  334. submit_button = _("Save Changes")
  335. form = None
  336. layout = None
  337. tabbed = False
  338. target_name = None
  339. translate_target_name = False
  340. original_name = None
  341. submit_fallback = False
  342. def get_url(self, model):
  343. return reverse(self.admin.get_action_attr(self.id, 'route'))
  344. def get_form(self, target):
  345. return self.form
  346. def get_form_instance(self, form, target, initial, post=False):
  347. if post:
  348. return form(self.request.POST, request=self.request, initial=initial)
  349. return form(request=self.request, initial=initial)
  350. def get_layout(self, form, model):
  351. if self.layout:
  352. return self.layout
  353. return form.layout
  354. def get_initial_data(self, model):
  355. return {}
  356. def submit_form(self, form, model):
  357. """
  358. Handle form submission, ALWAYS return tuple with model and message
  359. """
  360. pass
  361. def __call__(self, request, target=None, slug=None):
  362. self.request = request
  363. # Fetch target?
  364. model = None
  365. if target:
  366. model = self.get_and_validate_target(target)
  367. self.original_name = self.get_target_name(model)
  368. if not model:
  369. return redirect(self.get_fallback_url())
  370. original_model = model
  371. # Get form type to instantiate
  372. FormType = self.get_form(model)
  373. #Submit form
  374. message = None
  375. if request.method == 'POST':
  376. form = self.get_form_instance(FormType, model, self.get_initial_data(model), True)
  377. if form.is_valid():
  378. try:
  379. model, message = self.submit_form(form, model)
  380. if message.type != 'error':
  381. request.messages.set_flash(message, message.type, self.admin.id)
  382. # Redirect back to right page
  383. try:
  384. if 'save_new' in request.POST and self.get_new_url:
  385. return redirect(self.get_new_url(model))
  386. except AttributeError:
  387. pass
  388. try:
  389. if 'save_edit' in request.POST and self.get_edit_url:
  390. return redirect(self.get_edit_url(model))
  391. except AttributeError:
  392. pass
  393. try:
  394. if self.get_submit_url:
  395. return redirect(self.get_submit_url(model))
  396. except AttributeError:
  397. pass
  398. return redirect(self.get_fallback_url())
  399. except ValidationError as e:
  400. message = Message(e.messages[0], 'error')
  401. else:
  402. message = Message(form.non_field_errors()[0], 'error')
  403. else:
  404. form = self.get_form_instance(FormType, model, self.get_initial_data(model))
  405. # Render form
  406. return request.theme.render_to_response(self.get_template(),
  407. self.add_template_variables({
  408. 'admin': self.admin,
  409. 'action': self,
  410. 'request': request,
  411. 'url': self.get_url(model),
  412. 'fallback': self.get_fallback_url(),
  413. 'messages_log': request.messages.get_messages(self.admin.id),
  414. 'message': message,
  415. 'tabbed': self.tabbed,
  416. 'target': self.get_target_name(original_model),
  417. 'target_model': original_model,
  418. 'form': FormLayout(form, self.get_layout(form, target)),
  419. }),
  420. context_instance=RequestContext(request));
  421. class ButtonWidget(BaseWidget):
  422. """
  423. Button Action Widget
  424. This widget handles most basic and common type of admin action - button press:
  425. - User presses button on list (for example "delete this user!")
  426. - Widget checks if request is CSRF-valid and POST
  427. - Widget optionally chcecks if target has been provided and action is allowed at all
  428. - Widget does action and redirects us back to fallback url
  429. """
  430. def __call__(self, request, target=None, slug=None):
  431. self.request = request
  432. # Fetch target?
  433. model = None
  434. if target:
  435. model = self.get_and_validate_target(target)
  436. if not model:
  437. return redirect(self.get_fallback_url())
  438. original_model = model
  439. # Crash if this is invalid request
  440. if not request.csrf.request_secure(request):
  441. request.messages.set_flash(Message(_("Action authorization is invalid.")), 'error', self.admin.id)
  442. return redirect(self.get_fallback_url())
  443. # Do something
  444. message, url = self.action(model)
  445. request.messages.set_flash(message, message.type, self.admin.id)
  446. if url:
  447. return redirect(url)
  448. return redirect(self.get_fallback_url())
  449. def action(self, target):
  450. """
  451. Action to be executed when button is pressed
  452. Define custom one in your Admin action.
  453. It should return response and message objects
  454. """
  455. pass