widgets.py 22 KB

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