widgets.py 19 KB

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