app/soc/views/helper/lists.py
author Sverre Rabbelier <srabbelier@gmail.com>
Mon, 13 Apr 2009 15:31:39 +0000
changeset 2177 e2c193e1f631
parent 2168 ef7222d4847f
child 2196 6b424ec5b0ae
permissions -rw-r--r--
Do not rely on dicts.merge to change target Also make dicts.merge actually not touch target. This is much cleaner than modifying in place, especially since we assign the result of the dicts.merge call to target most of the time anyway. Patch by: Sverre Rabbelier

#!/usr/bin/python2.5
#
# Copyright 2008 the Melange authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Helpers used to render lists.
"""

__authors__ = [
  '"Chen Lunpeng" <forever.clp@gmail.com>',
  '"Pawel Solyga" <pawel.solyga@gmail.com>',
  ]

import logging

from soc.logic import dicts
from soc.logic.models.user import logic as user_logic

import soc.views.helper.forms


DEF_DEFAULT_PAGINATION = 50
DEF_MAX_PAGINATION = 100
DEF_MAX_DEV_PAGINATION = 1000

DEF_PAGINATION_CHOICES = [
    ('10', '10 items per page'),
    ('25', '25 items per page'),
    ('50', '50 items per page'),
    ('100', '100 items per page'),
]

DEF_DEVELOPER_CHOICES = [
    ('500', '500 items per page'),
    ('1000', '1000 items per page'),
    ]


def getPreferredListPagination(user=None):
  """Returns User's preferred list pagination limit.

  Args:
    user: User entity containing the list pagination preference;
      default is None, to use the current logged-in User
  """
  # TODO: eventually this limit should be a User profile preference
  #   (stored in the site-wide User Model) preference
  return DEF_DEFAULT_PAGINATION


OFFSET_KEY = 'offset_%d'
LIMIT_KEY = 'limit_%d'
OFFSET_LINKID_KEY = 'offset_linkid_%d'
REVERSE_DIRECTION_KEY = 'reverse_sort_direction_%d'


def makeOffsetKey(limit_idx):
  return OFFSET_KEY % limit_idx


def makeLimitKey(limit_idx):
  return LIMIT_KEY % limit_idx


def makeOffsetLinkidKey(limit_idx):
  return OFFSET_LINKID_KEY % limit_idx


def makeReverseDirectionKey(limit_idx):
  return REVERSE_DIRECTION_KEY % limit_idx


def getListParameters(request, list_index):
  """Retrieves, converts and validates values for one list

  Args:
    list_index, int: which list to get the values for.
      (there may be multiple lists on one page, which are multiplexed
       by an integer.)

  Returns:
    a dictionary of str -> str.  field name -> field value.
  """

  offset = request.GET.get(makeOffsetKey(list_index))
  limit = request.GET.get(makeLimitKey(list_index))

  if offset is None:
    offset = ''

  if limit is None:
    limit = ''

  try:
    offset = int(offset)
  except ValueError:
    offset = 0

  try:
    limit = int(limit)
  except ValueError:
    limit = getPreferredListPagination()

  offset = max(0, offset)
  limit = max(1, limit)

  if user_logic.isDeveloper():
    limit = min(DEF_MAX_DEV_PAGINATION, limit)
  else:
    limit = min(DEF_MAX_PAGINATION, limit)

  result = dict(limit=limit, offset=offset)
  offset_linkid = request.GET.get(makeOffsetLinkidKey(list_index),
                                  '')
  # TODO(dbentley): URL unescape
  result['offset_linkid'] = offset_linkid

  reverse_direction = makeReverseDirectionKey(list_index) in request.GET
  result['reverse_direction'] = reverse_direction

  return result


class LinkCreator(object):
  """A way to create links for a page.
  """
  def __init__(self, request, list_idx, limit):
    self.path = request.path
    self.base_params = dict(
        i for i in request.GET.iteritems() if
        i[0].startswith('offset_') or i[0].startswith('limit_'))
    self.idx = list_idx
    self.base_params[makeLimitKey(self.idx)] = limit

  def create(self, offset_linkid=None, export=False, reverse_direction=False):
    params = self.base_params.copy()
    if offset_linkid is not None:
      # TODO(dbentley): URL encode
      if offset_linkid == '':
        try:
          del params[makeOffsetLinkidKey(self.idx)]
        except KeyError:
          pass
      else:
        params[makeOffsetLinkidKey(self.idx)]=offset_linkid
    if reverse_direction:
      params[makeReverseDirectionKey(self.idx)]=True
    link_suffix = '&'.join('%s=%s' % (k,v) for k,v in params.iteritems())
    return '%s?%s' % (self.path, link_suffix)


def getListContent(request, params, filter=None, order=None,
                   idx=0, need_content=False):
  """Returns a dict with fields used for rendering lists.

  TODO(dbentley): we need better terminology. List, in this context, can have
    one of two meanings.
    Meaning 1:  the underlying list, which may be very large.
    Meaning 2:  the returned list, which is at most 'limit' items.

  Args:
    request: the Django HTTP request object
    params: a dict with params for the View this list belongs to
    filter: a filter for this list
    order: the order which should be used for the list (in getForFields format)
    idx: the index of this list
    need_content: iff True will return None if there is no data

  Returns:
    A dictionary with the following values set:

    {
      'data': list data to be displayed
      'main': url to list main template
      'pagination': url to list pagination template
      'row': url to list row template
      'heading': url to list heading template
      'limit': max amount of items per page,
      'newest': url to first page of the list
      'prev': url to previous page
      'next': url to next page
      'first': offset of the first item in the list
      'last': offset of the last item in the list
    }
  """
  # TODO(dbentley): this appears to be unnecessary indirection,
  # as we only use this logic for getForFields, which is never overridden
  logic = params['logic']

  limit_key, offset_key, offset_linkid_key, reverse_direction_key = makeLimitKey(idx), makeOffsetKey(idx), makeOffsetLinkidKey(idx), makeReverseDirectionKey(idx)

  list_params = getListParameters(request, idx)
  limit, offset = list_params['limit'], list_params['offset']
  offset_linkid = list_params['offset_linkid']
  reverse_direction = list_params['reverse_direction']
  pagination_form = makePaginationForm(request, list_params['limit'],
                                       limit_key)

  if offset_linkid:
    if filter is None:
      filter = {}

    if reverse_direction:
      filter['link_id <'] = offset_linkid
    else:
      filter['link_id >'] = offset_linkid

    if order is None:
      order = []
    if reverse_direction:
      order.append('-link_id')
    else:
      order.append('link_id')



  # Fetch one more to see if there should be a 'next' link
  data = logic.getForFields(filter=filter, limit=limit+1, offset=offset,
                            order=order)

  if need_content and not data:
    return None

  more = len(data) > limit
  if reverse_direction:
    data.reverse()

  if more:
    if reverse_direction:
      data = data[1:]
    else:
      data = data[:limit]

  should_have_next_link = True
  if not reverse_direction and not more:
    should_have_next_link = False

  # Calculating should_have_previous_link is tricky. It's possible we could
  # be creating a previous link to a page that would have 0 entities.
  # That would be suboptimal; what's a better way?
  should_have_previous_link = False
  if offset_linkid:
    should_have_previous_link = True
  if reverse_direction and not more:
    should_have_previous_link = False

  if data:
    first_displayed_item = data[0]
    last_displayed_item = data[-1]
  else:
    class Dummy(object):
      pass
    first_displayed_item = last_displayed_item = Dummy()
    first_displayed_item.link_id = None
  newest = next = prev = export_link = ''

  link_creator = LinkCreator(request, idx, limit)

  if params.get('list_key_order'):
    export_link = link_creator.create(export=True)

  if should_have_next_link:
    next = link_creator.create(offset_linkid=last_displayed_item.link_id)

  if should_have_previous_link:
    prev = link_creator.create(offset_linkid=first_displayed_item.link_id,
                               reverse_direction=True)

  newest = link_creator.create(offset_linkid='')

  # TODO(dbentley): add a "last" link (which is now possible because we can
  # query with a reverse linkid sorting

  content = {
      'idx': idx,
      'data': data,
      'export': export_link,
      'first': first_displayed_item.link_id,
      'last': last_displayed_item.link_id,
      'logic': logic,
      'limit': limit,
      'newest': newest,
      'next': next,
      'pagination_form': pagination_form,
      'prev': prev,
      }

  updates = dicts.rename(params, params['list_params'])
  content.update(updates)

  return content


def makePaginationForm(
  request, limit, arg_name, choices=DEF_PAGINATION_CHOICES,
  field_name_fmt=soc.views.helper.forms.DEF_SELECT_QUERY_ARG_FIELD_NAME_FMT):
  """Returns a customized pagination limit selection form.

  Args:
    request: the standard Django HTTP request object
    limit: the initial value of the selection control
    arg_name: see soc.views.helper.forms.makeSelectQueryArgForm(); default is 'limit'
    choices: see soc.views.helper.forms.makeSelectQueryArgForm(); default is
      DEF_PAGINATION_CHOICES
    field_name_fmt: see soc.views.helper.forms.makeSelectQueryArgForm()
  """
  choices = makeNewPaginationChoices(limit=limit, choices=choices)

  return soc.views.helper.forms.makeSelectQueryArgForm(
      request, arg_name, limit, choices)


def makeNewPaginationChoices(limit=DEF_DEFAULT_PAGINATION,
                             choices=DEF_PAGINATION_CHOICES):
  """Updates the pagination limit selection form.

  Args:
    limit: the initial value of the selection control;
      default is DEF_DEFAULT_PAGINATION
    choices: see soc.views.helper.forms.makeSelectQueryArgForm();
      default is DEF_PAGINATION_CHOICES

  Returns:
    a new pagination choices list if limit is not in
    DEF_PAGINATION_CHOICES, or DEF_PAGINATION_CHOICES otherwise
  """

  new_choices = []
  new_choice = (str(limit), '%s items per page' % limit)

  new_choices.append(new_choice)
  new_choices.extend(choices)

  if user_logic.isDeveloper():
    new_choices.extend(DEF_DEVELOPER_CHOICES)

  new_choices = set(new_choices)

  return sorted(new_choices, key=lambda (x, y): int(x))