Add a framework for generic views
authorSverre Rabbelier <srabbelier@gmail.com>
Thu, 16 Oct 2008 23:23:16 +0000
changeset 363 d35ffa6ca643
parent 362 d904f4a76f6f
child 364 ab47d3f494b3
Add a framework for generic views This commit does not enable the new code, as such there should be no change in functionality. In order to use the new code the maps.py file should be updated to point at the new views/models/sponsor.py module. Patch by: Sverre Rabbelier Reviewed by: to-be-reviewed
app/soc/logic/dicts.py
app/soc/logic/models/base.py
app/soc/models/group.py
app/soc/views/models/base.py
app/soc/views/models/sponsor.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/app/soc/logic/dicts.py	Thu Oct 16 23:23:16 2008 +0000
@@ -0,0 +1,42 @@
+#!/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.
+
+"""Logic related to handling dictionaries
+"""
+
+__authors__ = [
+  '"Sverre Rabbelier" <sverre@rabbelier.nl>',
+  ]
+
+
+def mergeDicts(target, updates):
+  """Like the builtin 'update' method but does not overwrite existing values
+
+  Args:
+    target: The dictionary that is to be updated, may be None
+    updates: A dictionary containing new values for the original dict
+
+  Returns: the target dictionary 
+  """
+
+  if not target:
+    target = {}
+
+  for key, value in updates.iteritems():
+    if key not in target:
+      target[key] = value
+
+  return target
--- a/app/soc/logic/models/base.py	Thu Oct 16 18:08:35 2008 +0000
+++ b/app/soc/logic/models/base.py	Thu Oct 16 23:23:16 2008 +0000
@@ -118,6 +118,22 @@
 
     return self._keyName(**kwargs)
 
+  def extractKeyFields(self, fields):
+    """Extracts all the fields from that are in the mode's key_fields property
+
+    Args:
+      fields: A dict from which the fields should be extracted
+    """
+
+    key_fields = {}
+    keys = fields.keys()
+
+    for key in keys[:]:
+      if key in self._model.key_fields:
+        key_fields[key] = fields[key]
+
+    return key_fields
+
   def getForLimitAndOffset(self, limit, offset=0):
     """Returns entities for given offset and limit or None if not found.
 
@@ -199,7 +215,15 @@
     key_name  = self.getKeyNameForFields(**kwargs)
 
     return self.updateOrCreateFromKeyName(properties, key_name)
-  
+
+  def isDeletable(self, entity):
+    """Returns whether the specified entity can be deleted
+    """
+
+    # TODO(pawel.solyga): Create specific delete method for Sponsor model
+    # Check if Sponsor can be deleted (has no Hosts and Programs)
+    return True
+
   def delete(self, entity):
     """Delete existing entity from datastore.
     
--- a/app/soc/models/group.py	Thu Oct 16 18:08:35 2008 +0000
+++ b/app/soc/models/group.py	Thu Oct 16 23:23:16 2008 +0000
@@ -37,6 +37,9 @@
   """Common data fields for all groups.
   """
 
+  #: Defines which fields are uses as the key_name
+  key_fields = ['link_name']
+
   #: Required field storing name of the group.
   name = db.StringProperty(required=True,
       verbose_name=ugettext_lazy('Name'))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/app/soc/views/models/base.py	Thu Oct 16 23:23:16 2008 +0000
@@ -0,0 +1,356 @@
+#!/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 functions for displaying views.
+"""
+
+__authors__ = [
+  '"Todd Larsen" <tlarsen@google.com>',
+  '"Sverre Rabbelier" <sverer@rabbelier.nl>',
+  '"Pawel Solyga" <pawel.solyga@gmail.com>',
+  ]
+
+
+from django import http
+from django.utils.translation import ugettext_lazy
+
+import soc.logic
+import soc.logic.out_of_band
+import soc.views.helper.lists
+import soc.views.helper.responses
+import soc.views.out_of_band
+
+from soc.logic import models
+from soc.logic import validate
+from soc.views import simple
+from soc.views import helper
+from soc.views.helper import access
+
+
+class View:
+  """Views for entity classes.
+
+  The View class functions specific to Entity classes by relying
+  on the the child-classes to define the following fields:
+
+  self._logic: the logic singleton for this entity
+
+  Args:
+    rights: This dictionary should be filled with the access check
+            functions that should be called
+
+    params: This dictionary should be filled with the parameters
+            specific to this entity.
+  """
+
+  def __init__(self, params=None, rights=None):
+    """
+    """
+
+    new_rights = {}
+    new_rights['base'] = [access.checkIsLoggedIn]
+
+    self._rights = soc.logic.dicts.mergeDicts(rights, new_rights)
+    self._params = params
+
+    self.DEF_SUBMIT_MSG_PARAM_NAME = 's'
+
+    self.DEF_CREATE_NEW_ENTITY_MSG = ugettext_lazy(
+        ' You can create a new %(model_type)s by visiting'
+        ' <a href="%(create)s">Create '
+        'a New %(Type)s</a> page.')
+
+  def public(self, request, **kwargs):
+    """Displays the public page for the entity specified by **kwargs
+
+    Args:
+      request: the Django request object
+      kwargs: the Key Fields for the specified entity
+    """
+
+    try:
+      self.checkAccess('edit', request)
+    except soc.views.out_of_band.AccessViolationResponse, alt_response:
+      return alt_response.response()
+
+    # create default template context for use with any templates
+    context = helper.responses.getUniversalContext(request)
+    entity = None
+
+    try:
+      entity = self._logic.getIfFields(**kwargs)
+    except soc.logic.out_of_band.ErrorResponse, error:
+      template = soc._params['public_template']
+      return simple.errorResponse(request, error, template, context)
+
+    if not entity:
+      #TODO: Change this into a proper redirect
+      return http.HttpResponseRedirect('/')
+
+    self._makePublic(entity)
+
+    context['entity'] = entity
+    context['entity_type'] = self._params['name']
+
+    template = self._params['public_template']
+
+    return helper.responses.respond(request, template, context)
+
+  def create(self, request, **kwargs):
+    """Displays the create page for this entity type
+
+    request: the django request object
+    kwargs: not used
+    """
+
+    # Create page is an edit page with no key fields
+    kwargs = {}
+    return self.edit(request, **kwargs)
+
+  def edit(self, request, **kwargs):
+    """Displays the public page for the entity specified by **kwargs
+
+    Args:
+      request: The Django request object
+      kwargs: The Key Fields for the specified entity
+    """
+
+    try:
+      self.checkAccess('edit', request)
+    except soc.views.out_of_band.AccessViolationResponse, alt_response:
+      return alt_response.response()
+
+    entity = None
+
+    try:
+      entity = self._logic.getIfFields(**kwargs)
+    except soc.logic.out_of_band.ErrorResponse, error:
+      template = soc._params['public_template']
+      error.message = error.message + self.DEF_CREATE_NEW_ENTITY_MSG % {
+          'entity_type_lower' : self._params['name_lower'],
+          'entity_type_upper' : self._parmas['name_upper'],
+          'create' : self._redirects['create']
+          }
+      return simple.errorResponse(request, error, template, context)
+
+    if request.method == 'POST':
+      return self.editPost(request, entity)
+    else:
+      return self.editGet(request, entity)
+
+  def editPost(self, request, entity):
+    """Same as edit, but on POST
+    """
+
+    context = helper.responses.getUniversalContext(request)
+
+    if entity:
+      form = self._params['edit_form'](request.POST)
+    else:
+      form = self._params['create_form'](request.POST)
+
+    if not form.is_valid():
+      return
+
+    fields = self.collectCleanedFields(form)
+
+    self._editPost(request, entity, fields)
+
+    keys = self._logic.extractKeyFields(fields)
+    entity = self._logic.updateOrCreateFromFields(fields, **keys)
+
+    if not entity:
+      return http.HttpResponseRedirect('/')
+
+    params=self._params['edit_params']
+    #TODO(SRabbelier) Construct a suffix
+    suffix = None
+
+    # redirect to (possibly new) location of the entity
+    # (causes 'Profile saved' message to be displayed)
+    return helper.responses.redirectToChangedSuffix(
+        request, None, suffix,
+        params=params)
+
+  def editGet(self, request, entity):
+    """Same as edit, but on GET
+    """
+
+    context = helper.responses.getUniversalContext(request)
+    #TODO(SRabbelier) Construct a suffix
+    suffix = None
+    is_self_referrer = helper.requests.isReferrerSelf(request, suffix=suffix)
+
+    # Remove the params from the request, this is relevant only if
+    # someone bookmarked a POST page.
+    if request.GET.get(self.DEF_SUBMIT_MSG_PARAM_NAME):
+      if (not entity) or (not is_self_referrer):
+        return http.HttpResponseRedirect(request.path)
+
+    if entity:
+      # Note: no message will be displayed if parameter is not present
+      context['notice'] = helper.requests.getSingleIndexedParamValue(
+              request,
+              self.DEF_SUBMIT_MSG_PARAM_NAME,
+              values=self._params['save_message'])
+
+      # populate form with the existing entity
+      form = self._params['edit_form'](instance=entity)
+    else:
+      form = self._params['create_form']()
+
+    context['form'] = form
+    context['entity'] = entity
+    context['entity_type'] = self._params['name']
+    context['entity_type_plural'] = self._params['name_plural']
+
+    template = self._params['create_template']
+
+    return helper.responses.respond(request, template, context)
+
+  def list(self, request):
+    """Displays the list page for the entity type
+    """
+
+    try:
+      self.checkAccess('list', request)
+    except soc.views.out_of_band.AccessViolationResponse, alt_response:
+      return alt_response.response()
+
+    context = helper.responses.getUniversalContext(request)
+
+    offset, limit = helper.lists.cleanListParameters(
+      offset=request.GET.get('offset'), limit=request.GET.get('limit'))
+
+    # Fetch one more to see if there should be a 'next' link
+    entities = self._logic.getForLimitAndOffset(limit + 1, offset=offset)
+
+    context['pagination_form'] = helper.lists.makePaginationForm(request, limit)
+
+    templates = self._params['lists_template']
+
+    context = helper.lists.setList(request, context, entities, 
+                                 offset, limit, templates)
+
+    context['entity_type'] = self._params['name']
+    context['entity_type_plural'] = self._params['name_plural']
+
+    template = self._params['list_template']
+
+    return helper.responses.respond(request, template, context)
+
+  def delete(self, request, **kwargs):
+    """Shows the delete page for the entity specified by kwargs
+
+    Args:
+      request: The Django request object
+      kwargs: The Key Fields for the specified entity
+    """
+
+    try:
+      self.checkAccess('delete', request)
+    except soc.views.out_of_band.AccessViolationResponse, alt_response:
+      return alt_response.response()
+
+    # create default template context for use with any templates
+    context = helper.responses.getUniversalContext(request)
+    entity = None
+
+    try:
+      entity = models.sponsor.logic.getIfFields(**kwargs)
+    except soc.logic.out_of_band.ErrorResponse, error:
+      template = self._templates['create']
+      error.message = error.message + DEF_CREATE_NEW_ENTITY_MSG % {
+          'entity_type_lower' : self._name,
+          'entity_type_upper' : self._Name,
+           'create' : self._redirects['create']
+           }
+      return simple.errorResponse(request, error, template, context)
+
+    if not entity:
+      #TODO: Create a proper error page for this
+      return http.HttpResponseRedirect('/')
+
+    if not self._logic.isDeletable(entity):
+      # TODO: Direct user to page telling them they can't delete that entity, and why
+      pass
+
+    self._logic.delete(entity)
+    redirect = self._params['delete_redirect']
+
+    return http.HttpResponseRedirect(redirect)
+
+  def _editPost(self, request, entity, fields):
+    """Performs any required processing on the entity to post its edit page
+
+    Args:
+      request: The django request object
+      entity: the entity to make public
+      fields: The new field values
+    """
+
+    pass
+
+  def checkUnspecified(self, access_type, request):
+    """Checks whether an unspecified access_type should be allowed
+
+    Args:
+      access_type: the access type (such as 'list' or 'edit') that was
+                   not present in the _rights dictionary when checking.
+    """
+
+    pass
+
+  def checkAccess(self, access_type, request):
+    """Runs all the defined checks for the specified type
+
+    Args:
+      access_type: the type of request (such as 'list' or 'edit')
+      request: the Django request object
+
+    Returns:
+      True: If all the required access checks have been made successfully
+      False: If a check failed, in this case self._response will contain
+             the response provided by the failed access check.
+    """
+
+    if access_type not in self._rights:
+      self.checkUnspecified(access_type, request)
+      return
+
+    # Call each access checker
+    for check in self._rights['base']:
+      check(request)
+
+    for check in self._rights[access_type]:
+      check(request)
+
+    # All checks were successfull
+    return
+
+  def collectCleanedFields(self, form):
+    """Collects all cleaned fields from form and returns them 
+
+    Args:
+      form: The form from which the cleaned fields should be collected
+    """
+
+    fields = {}
+
+    for field, value in form.cleaned_data.iteritems():
+      fields[field] = value
+
+    return fields
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/app/soc/views/models/sponsor.py	Thu Oct 16 23:23:16 2008 +0000
@@ -0,0 +1,150 @@
+#!/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.
+
+"""Views for Sponsor profiles.
+"""
+
+__authors__ = [
+    '"Sverre Rabbelier" <sverer@rabbelier.nl>',
+    '"Pawel Solyga" <pawel.solyga@gmail.com>',
+  ]
+
+
+from google.appengine.api import users
+
+from django import forms
+from django.utils.translation import ugettext_lazy
+
+from soc.logic import validate 
+from soc.views import helper
+from soc.views.helper import widgets
+from soc.views.models import base
+
+import soc.models.sponsor
+import soc.logic.models.sponsor
+import soc.logic.dicts
+import soc.views.helper
+import soc.views.helper.widgets
+
+
+class CreateForm(helper.forms.DbModelForm):
+  """Django form displayed when creating a Sponsor.
+  """
+  class Meta:
+    """Inner Meta class that defines some behavior for the form.
+    """
+    #: db.Model subclass for which the form will gather information
+    model = soc.models.sponsor.Sponsor
+    
+    #: list of model fields which will *not* be gathered by the form
+    exclude = ['founder', 'inheritance_line']
+  
+  # TODO(pawel.solyga): write validation functions for other fields
+  def clean_link_name(self):
+    link_name = self.cleaned_data.get('link_name')
+    if not validate.isLinkNameFormatValid(link_name):
+      raise forms.ValidationError("This link name is in wrong format.")
+    if models.sponsor.logic.getFromFields(link_name=link_name):
+      raise forms.ValidationError("This link name is already in use.")
+    return link_name
+
+
+class EditForm(CreateForm):
+  """Django form displayed when editing a Sponsor.
+  """
+  link_name = forms.CharField(widget=helper.widgets.ReadOnlyInput())
+  founded_by = forms.CharField(widget=helper.widgets.ReadOnlyInput(),
+                               required=False)
+
+  def clean_link_name(self):
+    link_name = self.cleaned_data.get('link_name')
+    if not validate.isLinkNameFormatValid(link_name):
+      raise forms.ValidationError("This link name is in wrong format.")
+    return link_name
+
+
+class View(base.View):
+  """View methods for the Sponsor model
+  """
+
+  def __init__(self, original_params=None, original_rights=None):
+    """Defines the fields and methods required for the base View class
+    to provide the user with list, public, create, edit and delete views.
+
+    Params:
+      original_params: a dict with params for this View 
+      original_rights: a dict with right definitions for this View
+    """
+
+    self.DEF_SUBMIT_MSG_PARAM_NAME = 's'
+    self.SUBMIT_MSG_PROFILE_SAVED = 0
+    #TODO(TLarsen) Better way to do this?
+    
+    self._logic = soc.logic.models.sponsor.logic
+    
+    params = {}
+    rights = {}
+
+    params['name'] = "Sponsor"
+    params['name_plural'] = "Sponsors"
+       
+    params['edit_form'] = EditForm
+    params['create_form'] = CreateForm
+
+    # TODO(SRabbelier) Add support for Django style template lookup
+    params['create_template'] = 'soc/site/sponsor/profile/edit.html'
+    params['public_template'] = 'soc/group/profile/public.html'
+
+    params['list_template'] = 'soc/group/list/all.html'
+
+    params['lists_template'] = {
+      'list_main': 'soc/list/list_main.html',
+      'list_pagination': 'soc/list/list_pagination.html',
+      'list_row': 'soc/group/list/group_row.html',
+      'list_heading': 'soc/group/list/group_heading.html',
+    }
+    
+    params['delete_redirect'] = '/site/sponsor/list'
+    params['create_redirect'] = '/site/sponsor/profile'
+    
+    params['save_message'] = [ ugettext_lazy('Profile saved.') ]
+    
+    params['edit_params'] = {
+        self.DEF_SUBMIT_MSG_PARAM_NAME:self.SUBMIT_MSG_PROFILE_SAVED,
+        }
+    
+    rights['list'] = [helper.access.checkIsDeveloper]
+    rights['delete'] = [helper.access.checkIsDeveloper]
+
+    params = soc.logic.dicts.mergeDicts(original_params, params)
+    rights = soc.logic.dicts.mergeDicts(original_rights, rights)
+    
+    base.View.__init__(self, rights=rights, params=params)
+
+  def _editPost(self, request, entity, fields):
+    """
+    """
+
+    id = users.get_current_user()
+    user = soc.logic.models.user.logic.getFromFields(email=id.email())
+    fields['founder'] = user
+
+
+view = View()
+public = view.public
+list = view.list
+delete = view.delete
+edit = view.edit