from django import forms from django.core.exceptions import ValidationError from django.core.urlresolvers import reverse from django.shortcuts import redirect from django.template import RequestContext from django.utils.translation import ugettext_lazy as _ from jinja2 import TemplateNotFound import math from misago.forms import Form from misago.forms.layouts import * from misago.messages import Message from misago.search import SearchException """ Class widgets """ class BaseWidget(object): """ Admin Widget abstract class, providing widgets with common or shared functionality """ admin = None id = None fallback = None name = None help = None notfound_message = None def __new__(cls, request, **kwargs): obj = super(BaseWidget, cls).__new__(cls) if not obj.name: obj.name = obj.get_name() if not obj.help: obj.help = obj.get_help() return obj(request, **kwargs) def get_token(self, token): return '%s_%s_%s' % (self.id, token, str('%s.%s' % (self.admin.model.__module__, self.admin.model.__name__))) def get_url(self): return reverse(self.admin.get_action_attr(self.id, 'route')) def get_name(self): return self.admin.get_action_attr(self.id, 'name') def get_help(self): return self.admin.get_action_attr(self.id, 'help') def get_id(self): return 'admin_%s' % self.id def get_template(self, template): return ('%s/%s.html' % (self.admin.id, template), 'admin/%s.html' % template) def get_fallback_url(self, request): return reverse(self.fallback) def get_target(self, request, model): pass def get_target_name(self, model): try: return model.__dict__[self.target_name] except AttributeError: return None def get_and_validate_target(self, request, target): try: model = self.admin.model.objects.select_related().get(pk=target) self.get_target(request, model) return model except self.admin.model.DoesNotExist: request.messages.set_flash(Message(self.notfound_message), 'error', self.admin.id) except ValueError as e: request.messages.set_flash(Message(e.args[0]), 'error', self.admin.id) return None class ListWidget(BaseWidget): """ Items list widget """ actions =[] columns = [] sortables = {} default_sorting = None search_form = None is_filtering = False pagination = None template = 'list' hide_actions = False table_form_button = _('Go') empty_message = _('There are no items to display') empty_search_message = _('Search has returned no items') nothing_checked_message = _('You have to select at least one item.') prompt_select = False def get_item_actions(self, request, item): """ Provides request and item, should return list of tuples with item actions in following format: (id, name, help, icon, link) """ return [] def action(self, icon=None, name=None, url=None, post=False, prompt=None): """ Function call to make hash with item actions """ if prompt: self.prompt_select = True return { 'icon': icon, 'name': name, 'url': url, 'post': post, 'prompt': prompt, } def get_search_form(self, request): """ Build a form object with items search """ return self.search_form def set_filters(self, model, filters): """ Set filters on model using filters from session """ return None def get_table_form(self, request, page_items): """ Build a form object with list of all items fields """ return None def table_action(self, request, page_items, cleaned_data): """ Handle table form submission, return tuple containing message and redirect link/false """ return None def get_actions_form(self, page_items): """ Build a form object with list of all items actions """ if not self.actions: return None # Dont build form form_fields = {} list_choices = [] for action in self.actions: list_choices.append((action[0], action[1])) form_fields['list_action'] = forms.ChoiceField(choices=list_choices) list_choices = [] for item in page_items: list_choices.append((item.pk, None)) form_fields['list_items'] = forms.MultipleChoiceField(choices=list_choices,widget=forms.CheckboxSelectMultiple) return type('AdminListForm', (Form,), form_fields) def get_sorting(self, request): """ Return list sorting method. A list with three values: - Field we use to sort over - Sorting direction - order_by() argument """ sorting_method = None if request.session.get(self.get_token('sort')) and request.session.get(self.get_token('sort'))[0] in self.sortables: sorting_method = request.session.get(self.get_token('sort')) if request.GET.get('sort') and request.GET.get('sort') in self.sortables: new_sorting = request.GET.get('sort') sorting_dir = int(request.GET.get('dir')) == 1 sorting_method = [ new_sorting, sorting_dir, new_sorting if sorting_dir else '-%s' % new_sorting ] request.session[self.get_token('sort')] = sorting_method if not sorting_method: if self.sortables: new_sorting = self.sortables.keys()[0] if self.default_sorting in self.sortables: new_sorting = self.default_sorting sorting_method = [ new_sorting, self.sortables[new_sorting] == True, new_sorting if self.sortables[new_sorting] else '-%s' % new_sorting ] else: sorting_method = [ id, True, '-id' ] return sorting_method def sort_items(self, request, page_items, sorting_method): return page_items.order_by(sorting_method[2]) def get_pagination_url(self, page): return reverse(self.admin.get_action_attr(self.id, 'route'), kwargs={'page': page}) def get_pagination(self, request, total, page): """ Return list pagination. A list with three values: - Offset for ORM slicing - Length of slice - no. of prev page (or -1 for first page) - no. of next page (or -1 for last page) - Current page - Pages total """ if not self.pagination or total < 0: # Dont do anything if we are not paging return None # Set basic pagination, use either Session cache or new page value pagination = {'start': 0, 'stop': 0, 'prev': -1, 'next': -1} if request.session.get(self.get_token('pagination')): pagination['start'] = request.session.get(self.get_token('pagination')) page = int(page) if page > 0: pagination['start'] = (page - 1) * self.pagination # Set page and total stat pagination['page'] = int(pagination['start'] / self.pagination) + 1 pagination['total'] = int(math.ceil(total / float(self.pagination))) # Fix too large offset if pagination['start'] > total: pagination['start'] = 0 # Allow prev/next? if total > self.pagination: if pagination['page'] > 1: pagination['prev'] = pagination['page'] - 1 if pagination['page'] < pagination['total']: pagination['next'] = pagination['page'] + 1 # Set stop offset pagination['stop'] = pagination['start'] + self.pagination return pagination def get_items(self, request): if request.session.get(self.get_token('filter')): self.is_filtering = True return self.set_filters(self.admin.model.objects, request.session.get(self.get_token('filter'))) return self.admin.model.objects def __call__(self, request, page=0): """ Use widget as view """ # Get basic list items items_total = self.get_items(request) # Set extra filters? try: items_total = self.select_items(items_total).count() except AttributeError: items_total = items_total.count() # Set sorting and paginating sorting_method = self.get_sorting(request) paginating_method = self.get_pagination(request, items_total, page) # List items items = self.get_items(request) if not request.session.get(self.get_token('filter')): items = items.all() # Set extra filters? try: items = self.select_items(items) except AttributeError: pass # Sort them items = self.sort_items(request, items, sorting_method); # Set pagination if self.pagination: items = items[paginating_method['start']:paginating_method['stop']] # Prefetch related? try: items = self.prefetch_related(items) except AttributeError: pass # Default message message = None # See if we should make and handle search form search_form = None SearchForm = self.get_search_form(request) if SearchForm: if request.method == 'POST': # New search if request.POST.get('origin') == 'search': search_form = SearchForm(request.POST, request=request) if search_form.is_valid(): search_criteria = {} for field, criteria in search_form.cleaned_data.items(): if len(criteria) > 0: search_criteria[field] = criteria if not search_criteria: message = Message(_("No search criteria have been defined.")) else: request.session[self.get_token('filter')] = search_criteria return redirect(self.get_url()) else: message = Message(_("Search form contains errors.")) message.type = 'error' else: search_form = SearchForm(request=request) # Kill search if request.POST.get('origin') == 'clear' and self.is_filtering and request.csrf.request_secure(request): request.session[self.get_token('filter')] = None request.messages.set_flash(Message(_("Search criteria have been cleared.")), 'info', self.admin.id) return redirect(self.get_url()) else: if self.is_filtering: search_form = SearchForm(request=request, initial=request.session.get(self.get_token('filter'))) else: search_form = SearchForm(request=request) # See if we sould make and handle tab form table_form = None TableForm = self.get_table_form(request, items) if TableForm: if request.method == 'POST' and request.POST.get('origin') == 'table': table_form = TableForm(request.POST, request=request) if table_form.is_valid(): message, redirect_url = self.table_action(request, items, table_form.cleaned_data) if redirect_url: request.messages.set_flash(message, message.type, self.admin.id) return redirect(redirect_url) else: message = Message(table_form.non_field_errors()[0], 'error') else: table_form = TableForm(request=request) # See if we should make and handle list form list_form = None ListForm = self.get_actions_form(items) if ListForm: if request.method == 'POST' and request.POST.get('origin') == 'list': list_form = ListForm(request.POST, request=request) if list_form.is_valid(): try: form_action = getattr(self, 'action_' + list_form.cleaned_data['list_action']) message, redirect_url = form_action(request, items, list_form.cleaned_data['list_items']) if redirect_url: request.messages.set_flash(message, message.type, self.admin.id) return redirect(redirect_url) except AttributeError: message = Message(_("Action requested is incorrect.")) else: if 'list_items' in list_form.errors: message = Message(self.nothing_checked_message) elif 'list_action' in list_form.errors: message = Message(_("Action requested is incorrect.")) else: message = Message(list_form.non_field_errors()[0]) message.type = 'error' else: list_form = ListForm(request=request) # Render list return request.theme.render_to_response(self.get_template(self.template), { 'admin': self.admin, 'action': self, 'request': request, 'url': self.get_url(), 'messages_log': request.messages.get_messages(self.admin.id), 'message': message, 'sorting': self.sortables, 'sorting_method': sorting_method, 'pagination': paginating_method, 'list_form': FormLayout(list_form) if list_form else None, 'search_form': FormLayout(search_form) if search_form else None, 'table_form': FormFields(table_form).fields if table_form else None, 'items': items, 'items_total': items_total, }, context_instance=RequestContext(request)); class FormWidget(BaseWidget): """ Form page widget """ template = 'form' submit_button = _("Save Changes") form = None layout = None tabbed = False target_name = None original_name = None submit_fallback = False def get_url(self, request, model): return reverse(self.admin.get_action_attr(self.id, 'route')) def get_form(self, request, target): return self.form def get_form_instance(self, form, request, target, initial, post=False): if post: return form(request.POST, request=request, initial=self.get_initial_data(request, target)) return form(request=request, initial=self.get_initial_data(request, target)) def get_layout(self, request, form, model): if self.layout: return self.layout return form.layout def get_initial_data(self, request, model): return {} def submit_form(self, request, form, model): """ Handle form submission, ALWAYS return tuple with model and message """ pass def __call__(self, request, target=None, slug=None): # Fetch target? model = None if target: model = self.get_and_validate_target(request, target) self.original_name = self.get_target_name(model) if not model: return redirect(self.get_fallback_url(request)) original_model = model # Get form type to instantiate FormType = self.get_form(request, model) #Submit form message = None if request.method == 'POST': form = self.get_form_instance(FormType, request, model, self.get_initial_data(request, model), True) if form.is_valid(): try: model, message = self.submit_form(request, form, model) if message.type != 'error': request.messages.set_flash(message, message.type, self.admin.id) # Redirect back to right page try: if 'save_new' in request.POST and self.get_new_url: return redirect(self.get_new_url(request, model)) except AttributeError: pass try: if 'save_edit' in request.POST and self.get_edit_url: return redirect(self.get_edit_url(request, model)) except AttributeError: pass try: if self.get_submit_url: return redirect(self.get_submit_url(request, model)) except AttributeError: pass return redirect(self.get_fallback_url(request)) except ValidationError as e: message = Message(e.messages[0], 'error') else: message = Message(form.non_field_errors()[0], 'error') else: form = self.get_form_instance(FormType, request, model, self.get_initial_data(request, model)) # Render form return request.theme.render_to_response(self.get_template(self.template), { 'admin': self.admin, 'action': self, 'request': request, 'url': self.get_url(request, model), 'fallback': self.get_fallback_url(request), 'messages_log': request.messages.get_messages(self.admin.id), 'message': message, 'tabbed': self.tabbed, 'target': self.get_target_name(original_model), 'target_model': original_model, 'form': FormLayout(form, self.get_layout(request, form, target)), }, context_instance=RequestContext(request)); class ButtonWidget(BaseWidget): """ Button Action Widget This widget handles most basic and common type of admin action - button press: - User presses button on list (for example "delete this user!") - Widget checks if request is CSRF-valid and POST - Widget optionally chcecks if target has been provided and action is allowed at all - Widget does action and redirects us back to fallback url """ def __call__(self, request, target=None, slug=None): # Fetch target? model = None if target: model = self.get_and_validate_target(request, target) if not model: return redirect(self.get_fallback_url(request)) original_model = model # Crash if this is invalid request if not request.csrf.request_secure(request): request.messages.set_flash(Message(_("Action authorization is invalid.")), 'error', self.admin.id) return redirect(self.get_fallback_url(request)) # Do something message, url = self.action(request, model) request.messages.set_flash(message, message.type, self.admin.id) if url: return redirect(url) return redirect(self.get_fallback_url(request)) def action(self, request, target): """ Action to be executed when button is pressed Define custom one in your Admin action. It should return response and message objects """ pass