widgets.py 21 KB

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