widgets.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533
  1. from django import forms
  2. from django.core.urlresolvers import reverse
  3. from django.shortcuts import redirect
  4. from django.template import RequestContext
  5. from django.utils.translation import ugettext_lazy as _
  6. from jinja2 import TemplateNotFound
  7. import math
  8. from misago.forms import Form
  9. from misago.forms.layouts import *
  10. from misago.messages import Message
  11. from misago.search import SearchException
  12. """
  13. Class widgets
  14. """
  15. class BaseWidget(object):
  16. """
  17. Admin Widget abstract class, providing widgets with common or shared functionality
  18. """
  19. admin = None
  20. id = None
  21. fallback = None
  22. name = None
  23. help = None
  24. notfound_message = None
  25. def __new__(cls, request, **kwargs):
  26. obj = super(BaseWidget, cls).__new__(cls)
  27. if not obj.name:
  28. obj.name = obj.get_name()
  29. if not obj.help:
  30. obj.help = obj.get_help()
  31. return obj(request, **kwargs)
  32. def get_token(self, token):
  33. return '%s_%s_%s' % (self.id, token, str('%s.%s' % (self.admin.model.__module__, self.admin.model.__name__)))
  34. def get_url(self):
  35. return reverse(self.admin.get_action_attr(self.id, 'route'))
  36. def get_name(self):
  37. return self.admin.get_action_attr(self.id, 'name')
  38. def get_help(self):
  39. return self.admin.get_action_attr(self.id, 'help')
  40. def get_id(self):
  41. return 'admin_%s' % self.id
  42. def get_template(self, template):
  43. return ('%s/%ss/%s.html' % (str(self.admin.model.__module__).split('.')[1], str(self.admin.model.__name__).lower(), template),
  44. '%s/%s.html' % (str(self.admin.model.__module__).split('.')[1], template),
  45. '%ss/%s.html' % (str(self.admin.model.__name__).lower(), template),
  46. 'admin/%s.html' % template)
  47. def get_fallback_url(self, request):
  48. return reverse(self.fallback)
  49. def get_target(self, request, model):
  50. pass
  51. def get_target_name(self, model):
  52. try:
  53. return model.__dict__[self.target_name]
  54. except AttributeError:
  55. return None
  56. def get_and_validate_target(self, request, target):
  57. try:
  58. model = self.admin.model.objects.select_related().get(pk=target)
  59. self.get_target(request, model)
  60. return model
  61. except self.admin.model.DoesNotExist:
  62. request.messages.set_flash(Message(self.notfound_message), 'error', self.admin.id)
  63. except ValueError as e:
  64. request.messages.set_flash(Message(e.args[0]), 'error', self.admin.id)
  65. return None
  66. class ListWidget(BaseWidget):
  67. """
  68. Items list widget
  69. """
  70. actions =[]
  71. columns = []
  72. sortables = {}
  73. default_sorting = None
  74. search_form = None
  75. is_filtering = False
  76. pagination = None
  77. template = 'list'
  78. hide_actions = False
  79. table_form_button = _('Go')
  80. empty_message = _('There are no items to display')
  81. empty_search_message = _('Search has returned no items')
  82. nothing_checked_message = _('You have to select at least one item.')
  83. prompt_select = False
  84. def get_item_actions(self, request, item):
  85. """
  86. Provides request and item, should return list of tuples with item actions in following format:
  87. (id, name, help, icon, link)
  88. """
  89. return []
  90. def action(self, icon=None, name=None, url=None, post=False, prompt=None):
  91. """
  92. Function call to make hash with item actions
  93. """
  94. if prompt:
  95. self.prompt_select = True
  96. return {
  97. 'icon': icon,
  98. 'name': name,
  99. 'url': url,
  100. 'post': post,
  101. 'prompt': prompt,
  102. }
  103. def get_search_form(self, request):
  104. """
  105. Build a form object with items search
  106. """
  107. return self.search_form
  108. def set_filters(self, model, filters):
  109. """
  110. Set filters on model using filters from session
  111. """
  112. return None
  113. def get_table_form(self, request, page_items):
  114. """
  115. Build a form object with list of all items fields
  116. """
  117. return None
  118. def table_action(self, request, page_items, cleaned_data):
  119. """
  120. Handle table form submission, return tuple containing message and redirect link/false
  121. """
  122. return None
  123. def get_actions_form(self, page_items):
  124. """
  125. Build a form object with list of all items actions
  126. """
  127. if not self.actions:
  128. return None # Dont build form
  129. form_fields = {}
  130. list_choices = []
  131. for action in self.actions:
  132. list_choices.append((action[0], action[1]))
  133. form_fields['list_action'] = forms.ChoiceField(choices=list_choices)
  134. list_choices = []
  135. for item in page_items:
  136. list_choices.append((item.pk, None))
  137. form_fields['list_items'] = forms.MultipleChoiceField(choices=list_choices,widget=forms.CheckboxSelectMultiple)
  138. return type('AdminListForm', (Form,), form_fields)
  139. def get_sorting(self, request):
  140. """
  141. Return list sorting method.
  142. A list with three values:
  143. - Field we use to sort over
  144. - Sorting direction
  145. - order_by() argument
  146. """
  147. sorting_method = None
  148. if request.session.get(self.get_token('sort')) and request.session.get(self.get_token('sort'))[0] in self.sortables:
  149. sorting_method = request.session.get(self.get_token('sort'))
  150. if request.GET.get('sort') and request.GET.get('sort') in self.sortables:
  151. new_sorting = request.GET.get('sort')
  152. sorting_dir = int(request.GET.get('dir')) == 1
  153. sorting_method = [
  154. new_sorting,
  155. sorting_dir,
  156. new_sorting if sorting_dir else '-%s' % new_sorting
  157. ]
  158. request.session[self.get_token('sort')] = sorting_method
  159. if not sorting_method:
  160. if self.sortables:
  161. new_sorting = self.sortables.keys()[0]
  162. if self.default_sorting in self.sortables:
  163. new_sorting = self.default_sorting
  164. sorting_method = [
  165. new_sorting,
  166. self.sortables[new_sorting] == True,
  167. new_sorting if self.sortables[new_sorting] else '-%s' % new_sorting
  168. ]
  169. else:
  170. sorting_method = [
  171. id,
  172. True,
  173. '-id'
  174. ]
  175. return sorting_method
  176. def sort_items(self, request, page_items, sorting_method):
  177. return page_items.order_by(sorting_method[2])
  178. def get_pagination_url(self, page):
  179. return reverse(self.admin.get_action_attr(self.id, 'route'), kwargs={'page': page})
  180. def get_pagination(self, request, total, page):
  181. """
  182. Return list pagination.
  183. A list with three values:
  184. - Offset for ORM slicing
  185. - Length of slice
  186. - no. of prev page (or -1 for first page)
  187. - no. of next page (or -1 for last page)
  188. - Current page
  189. - Pages total
  190. """
  191. if not self.pagination or total < 0:
  192. # Dont do anything if we are not paging
  193. return None
  194. # Set basic pagination, use either Session cache or new page value
  195. pagination = {'start': 0, 'stop': 0, 'prev': -1, 'next': -1}
  196. if request.session.get(self.get_token('pagination')):
  197. pagination['start'] = request.session.get(self.get_token('pagination'))
  198. page = int(page)
  199. if page > 0:
  200. pagination['start'] = (page - 1) * self.pagination
  201. # Set page and total stat
  202. pagination['page'] = int(pagination['start'] / self.pagination) + 1
  203. pagination['total'] = int(math.ceil(total / float(self.pagination)))
  204. # Fix too large offset
  205. if pagination['start'] > total:
  206. pagination['start'] = 0
  207. # Allow prev/next?
  208. if total > self.pagination:
  209. if pagination['page'] > 1:
  210. pagination['prev'] = pagination['page'] - 1
  211. if pagination['page'] < pagination['total']:
  212. pagination['next'] = pagination['page'] + 1
  213. # Set stop offset
  214. pagination['stop'] = pagination['start'] + self.pagination
  215. return pagination
  216. def __call__(self, request, page=0):
  217. """
  218. Use widget as view
  219. """
  220. # Get basic list attributes
  221. if request.session.get(self.get_token('filter')):
  222. self.is_filtering = True
  223. items_total = self.set_filters(self.admin.model.objects, request.session.get(self.get_token('filter')))
  224. else:
  225. items_total = self.admin.model.objects
  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.admin.model.objects
  236. # Filter items?
  237. if request.session.get(self.get_token('filter')):
  238. items = self.set_filters(items, request.session.get(self.get_token('filter')))
  239. else:
  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(request, 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(request)
  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(request, 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(request, 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(request, 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. # Render list
  332. return request.theme.render_to_response(self.get_template(self.template),
  333. {
  334. 'admin': self.admin,
  335. 'action': self,
  336. 'request': request,
  337. 'url': self.get_url(),
  338. 'messages_log': request.messages.get_messages(self.admin.id),
  339. 'message': message,
  340. 'sorting': self.sortables,
  341. 'sorting_method': sorting_method,
  342. 'pagination': paginating_method,
  343. 'list_form': FormLayout(list_form) if list_form else None,
  344. 'search_form': FormLayout(search_form) if search_form else None,
  345. 'table_form': FormFields(table_form).fields if table_form else None,
  346. 'items': items,
  347. 'items_total': items_total,
  348. },
  349. context_instance=RequestContext(request));
  350. class FormWidget(BaseWidget):
  351. """
  352. Form page widget
  353. """
  354. template = 'form'
  355. submit_button = _("Save Changes")
  356. form = None
  357. layout = None
  358. tabbed = False
  359. target_name = None
  360. original_name = None
  361. submit_fallback = False
  362. def get_url(self, request, model):
  363. return reverse(self.admin.get_action_attr(self.id, 'route'))
  364. def get_form(self, request, model):
  365. return self.form
  366. def get_form_instance(self, form, request, model, initial, post=False):
  367. if post:
  368. return form(request.POST, request=request, initial=self.get_initial_data(request, model))
  369. return form(request=request, initial=self.get_initial_data(request, model))
  370. def get_layout(self, request, form, model):
  371. if self.layout:
  372. return self.layout
  373. return form.layout
  374. def get_initial_data(self, request, model):
  375. return {}
  376. def submit_form(self, request, form, model):
  377. """
  378. Handle form submission, ALWAYS return tuple with model and message
  379. """
  380. pass
  381. def __call__(self, request, target=None, slug=None):
  382. # Fetch target?
  383. model = None
  384. if target:
  385. model = self.get_and_validate_target(request, target)
  386. self.original_name = self.get_target_name(model)
  387. if not model:
  388. return redirect(self.get_fallback_url(request))
  389. original_model = model
  390. # Get form type to instantiate
  391. FormType = self.get_form(request, target)
  392. #Submit form
  393. message = None
  394. if request.method == 'POST':
  395. form = self.get_form_instance(FormType, request, model, self.get_initial_data(request, model), True)
  396. if form.is_valid():
  397. model, message = self.submit_form(request, form, model)
  398. if message.type != 'error':
  399. request.messages.set_flash(message, message.type, self.admin.id)
  400. # Redirect back to right page
  401. try:
  402. if 'save_new' in request.POST and self.get_new_url:
  403. return redirect(self.get_new_url(request, model))
  404. except AttributeError:
  405. pass
  406. try:
  407. if 'save_edit' in request.POST and self.get_edit_url:
  408. return redirect(self.get_edit_url(request, model))
  409. except AttributeError:
  410. pass
  411. try:
  412. if self.get_submit_url:
  413. return redirect(self.get_submit_url(request, model))
  414. except AttributeError:
  415. pass
  416. return redirect(self.get_fallback_url(request))
  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