widgets.py 19 KB

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