Added Notifications.
authorLennard de Rijk <ljvderijk@gmail.com>
Fri, 12 Dec 2008 23:28:18 +0000
changeset 726 ba3d399ec9be
parent 725 6180b32d990f
child 727 ddf44af087a0
Added Notifications. Notifications will primarily be used to serve the user information message like "New Request Pending". The system can be expanded later on to a messaging system. If you are a developer you can create message by going to notification/create . This has been done so the system can easily be tested. But will of course be visible in an easy-to-access location when the transition to a message system has been made. Patch by: Lennard de Rijk
app/soc/logic/models/notification.py
app/soc/models/notification.py
app/soc/templates/soc/notification/list/heading.html
app/soc/templates/soc/notification/list/row.html
app/soc/templates/soc/notification/public.html
app/soc/views/helper/access.py
app/soc/views/models/notification.py
app/soc/views/models/user_self.py
app/soc/views/sitemap/build.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/app/soc/logic/models/notification.py	Fri Dec 12 23:28:18 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.
+
+"""Notification (Model) query functions.
+"""
+
+__authors__ = [
+  '"Lennard de Rijk" <ljvderijk@gmail.com>',
+  ]
+
+
+from soc.logic.models import base
+from soc.logic.models import user as user_logic
+
+import soc.models.notification
+
+
+class Logic(base.Logic):
+  """Logic methods for the Notification model.
+  """
+
+  def __init__(self):
+    """Defines the name, key_name and model for this entity.
+    """
+    super(Logic, self).__init__(model=soc.models.notification.Notification,
+         base_model=None, scope_logic=user_logic)
+
+
+logic = Logic()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/app/soc/models/notification.py	Fri Dec 12 23:28:18 2008 +0000
@@ -0,0 +1,58 @@
+#!/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.
+
+"""This module contains the Notification Model.
+"""
+
+__authors__ = [
+  '"Lennard de Rijk" <ljvderijk@gmail.com>',
+]
+
+from google.appengine.ext import db
+
+from django.utils.translation import ugettext_lazy
+
+from soc.models import base
+
+import soc.models.linkable
+import soc.models.user
+
+
+class Notification(soc.models.linkable.Linkable):
+  """Model of a Notification.
+  """
+
+  # a reference to the user this Notification is from
+  # this is a non-required property, None will indicate an Anonymous Admin
+  from_user = db.ReferenceProperty(reference_class=soc.models.user.User,
+      required=False,
+      collection_name="sent_notifications",
+      verbose_name=ugettext_lazy('From User'))
+  
+  subject = db.StringProperty(required=True,
+      verbose_name=ugettext_lazy('Subject'))
+  
+  # the message that is contained within this Notification
+  message = db.TextProperty(required=True,
+      verbose_name=ugettext_lazy('Message'))
+  
+  # date and time on which this Notification was created
+  created_on = db.DateTimeProperty(auto_now_add=True,
+      verbose_name=ugettext_lazy('Created On'))
+  
+  # boolean property that marks if the notification has been read
+  has_been_read = db.BooleanProperty(default=False,
+      verbose_name=ugettext_lazy('Read'))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/app/soc/templates/soc/notification/list/heading.html	Fri Dec 12 23:28:18 2008 +0000
@@ -0,0 +1,5 @@
+<tr align="left">
+  <th>From</th>
+  <th>Subject</th>
+  <th>Received on</th>
+</tr>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/app/soc/templates/soc/notification/list/row.html	Fri Dec 12 23:28:18 2008 +0000
@@ -0,0 +1,6 @@
+<tr class="off" onmouseover="this.className='on'" onmouseout="this.className='off'" 
+onclick="document.location.href='{{ list.redirect }}'" name="name">
+  <td><div class="user_name">{{ list.item.from_user.name }}</div></td>
+  <td><div class="subject">{{ list.item.subject }}</div></td>
+  <td><div class="created_on">{{ list.item.created_on }} </div> </td>
+</tr>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/app/soc/templates/soc/notification/public.html	Fri Dec 12 23:28:18 2008 +0000
@@ -0,0 +1,40 @@
+{% extends "soc/base.html" %}
+{% comment %}
+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.
+{% endcomment %}
+
+{% block page_title %}
+Notification
+{% endblock %}
+
+{% block header_title %}
+Notification about: {{ entity.subject }}
+{% endblock %}
+
+{% block body %}
+From: 
+{% if not entity.from_user  %}
+	Anonymous
+{% else %}
+	{{ entity.from_user.name }}
+{% endif %} <br />
+To: {{ entity.scope.name }} <br />
+Sent On: {{ entity.created_on }} <br />
+Subject: {{ entity.subject }} <br />
+Message: <br/> 
+{{ entity.message }} 
+<p />
+
+<input type="button" onclick="location.href='/{{ entity_type_url|lower }}/delete/{{ entity_suffix }}'" value="Delete"/>
+
+{% endblock %}
--- a/app/soc/views/helper/access.py	Fri Dec 12 00:35:51 2008 +0000
+++ b/app/soc/views/helper/access.py	Fri Dec 12 23:28:18 2008 +0000
@@ -26,6 +26,7 @@
 __authors__ = [
   '"Todd Larsen" <tlarsen@google.com>',
   '"Sverre Rabbelier" <sverre@rabbelier.nl>',
+  '"Lennard de Rijk" <ljvderijk@gmail.com>',
   '"Pawel Solyga" <pawel.solyga@gmail.com>',
   ]
 
@@ -38,6 +39,7 @@
 from soc.logic import accounts
 from soc.logic import dicts
 from soc.logic.models import host as host_logic
+from soc.logic.models import notification as notification_logic
 from soc.logic.models import user as user_logic
 from soc.logic.models import request as request_logic
 from soc.views import helper
@@ -318,6 +320,54 @@
 
   raise out_of_band.LoginRequest(message_fmt=login_message_fmt)
 
+def checkIsMyNotification(request):
+  """Returns an alternate HTTP response if this request is for a Notification belonging
+     to the current user.
+
+  Args:
+    request: a Django HTTP request
+
+   Raises:
+     AccessViolationResponse: if the required authorization is not met
+
+  Returns:
+    None if the current User is allowed to access this Notification.
+  """
+  
+  try:
+    # if the current user is a developer we allow access
+    checkIsDeveloper(request)
+    return
+  except out_of_band.Error:
+    pass
+
+  checkIsUser(request)
+  
+  splitpath = request.path.split('/')
+  splitpath = splitpath[1:] # cut off leading ''
+  
+  # get the notification scope (user link_id) from the request path
+  user_link_id = splitpath[2]
+  # get the notification link_id from the request path
+  notification_link_id = splitpath[3]
+  
+  properties = {
+      'link_id': notification_link_id,
+      'scope_path': user_link_id,
+      }
+  
+  notification = notification_logic.logic.getForFields(properties, unique=True)
+  
+  user = user_logic.logic.getForFields(
+      {'account': users.get_current_user()}, unique=True)
+  
+  # check if the key of the current user matches the key from the scope of the message
+  if user.key() == notification.scope.key():
+    # access granted
+    return None
+  else:
+    # access denied
+    deny(request)  
 
 def checkCanInvite(request):
   """Checks to see if the current user can create an invite
@@ -360,7 +410,6 @@
   # Perform the access check
   helper.access.checkAccess(access_type, request, rights=params['rights'])
 
-
 def checkIsDocumentPublic(request):
   """Checks whether a document is public.
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/app/soc/views/models/notification.py	Fri Dec 12 23:28:18 2008 +0000
@@ -0,0 +1,244 @@
+#!/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.
+
+"""This module contains the view code for Notifications
+"""
+
+__authors__ = [
+  '"Lennard de Rijk" <ljvderijk@gmail.com>',
+]
+
+
+import time
+
+from google.appengine.api import users
+
+from django import forms
+from django import http
+from django.utils.translation import ugettext_lazy
+
+from soc.logic import dicts
+from soc.logic import validate
+from soc.models import notification as notification_model
+from soc.views import helper
+from soc.views import out_of_band
+from soc.views.helper import access
+from soc.views.helper import redirects
+from soc.views.models import base
+from soc.logic.models import notification as notification_logic
+from soc.logic.models import user as user_logic
+
+
+class CreateForm(helper.forms.BaseForm):
+  """Form for creating a Notification.
+  """
+  
+  # to user field
+  to_user = forms.fields.CharField(label='To User')
+  
+  def __init__(self, *args, **kwargs):
+    """ Calls super and then redefines the order in which the fields appear.
+    
+    for parameters see BaseForm.__init__()
+    """    
+    super(CreateForm, self).__init__(*args, **kwargs)
+      
+    # set form fields order
+    self.fields.keyOrder = ['to_user', 'subject', 'message']
+
+  class Meta:
+    model = notification_model.Notification
+    
+    # exclude the necessary fields from the form
+    exclude = ['link_id', 'scope', 'scope_path', 'from_user', 'has_been_read']
+    
+  def clean_to_user(self):
+    """Check if the to_user field has been filled in correctly.
+    """
+    link_id = self.cleaned_data.get('to_user').lower()
+    
+    if not validate.isLinkIdFormatValid(link_id):
+      raise forms.ValidationError("This link ID is in wrong format.")
+    
+    to_user = user_logic.logic.getForFields({'link_id' : link_id}, unique=True)
+    
+    if not to_user:
+      # user does not exist
+      raise forms.ValidationError("This user does not exist")      
+  
+    return link_id
+
+
+class View(base.View):
+  """View methods for the Notification model.
+  """
+  
+  def __init__(self, params=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:
+      params: a dict with params for this View
+    """
+    
+    new_params = {}    
+    new_params['logic'] = notification_logic.logic
+
+    new_params['name'] = "Notification"
+    new_params['name_short'] = "Notification"
+    new_params['name_plural'] = "Notifications"
+    new_params['url_name'] = "notification"
+    new_params['module_name'] = "notification"
+    
+    new_params['create_form'] = CreateForm
+
+    # define the django url patterns    
+    new_params['django_patterns_defaults'] = [
+      (r'^%(url_name)s/(?P<access_type>show)/%(key_fields)s$',
+          'soc.views.models.%(module_name)s.public', 'Show %(name_short)s'),
+      (r'^%(url_name)s/(?P<access_type>create)$',
+          'soc.views.models.%(module_name)s.create', 'Create %(name_short)s'),
+      (r'^%(url_name)s/(?P<access_type>create)/%(scope)s$',
+          'soc.views.models.%(module_name)s.create', 'Create %(name_short)s'),
+      (r'^%(url_name)s/(?P<access_type>delete)/%(key_fields)s$',
+          'soc.views.models.%(module_name)s.delete', 'Delete %(name_short)s'),
+      (r'^%(url_name)s/(?P<access_type>list)$',
+          'soc.views.models.%(module_name)s.list', 'List %(name_plural)s'),
+      ]
+    
+    rights = {}
+    rights['unspecified'] = [access.deny]
+    rights['any_access'] = [access.allow]
+    rights['show'] = [access.checkIsMyNotification]
+    rights['delete'] = [access.checkIsDeveloper]
+    rights['list'] = [access.checkIsUser]
+    # create is developer only for the time being to test functionality
+    rights['create'] = [access.checkIsDeveloper]
+    
+    new_params['rights'] = rights
+    
+    params = dicts.merge(params, new_params)
+
+    super(View, self).__init__(params=params)
+    
+  def create(self, request, access_type,
+             page_name=None, params=None, **kwargs):
+    """On a successful post create redirects the user to the notification list.
+    
+    for parameters see base.create()
+    """
+    
+    if request.method == 'POST':
+      response = super(View, self).create(request, access_type,
+        page_name, params, **kwargs)
+      
+      if (response.__class__ == http.HttpResponseRedirect and
+          response['location'].startswith(
+              '/%s/edit/' %(self._params['url_name']))):             
+        # redirect to list instead of edit view
+        return http.HttpResponseRedirect('/%s/list' %(self._params['url_name']))
+      else:
+        return response
+      
+    else: 
+      # request.method == 'GET' so act normal 
+      return super(View, self).create(request, access_type,
+          page_name, params, **kwargs)
+       
+  def list(self, request, access_type,
+           page_name=None, params=None, seed=None, **kwargs):
+    """Lists all notifications that the current logged in user has stored.
+    
+    for parameters see base.list()
+    """
+    
+    params = dicts.merge(params, self._params)
+      
+    # get the current user
+    properties = {'account': users.get_current_user()}
+    user_entity = user_logic.logic.getForFields(properties, unique=True)
+
+    # only select the notifications for this user so construct a filter
+    filter = {'scope': user_entity}
+    
+    # create the list parameters
+    list_params = params.copy()
+    
+    # define the list redirect action to show the notification
+    list_params['list_action'] = (redirects.getPublicRedirect, params)
+    list_params['list_description'] = ugettext_lazy(
+        "An overview of your received Notifications.")
+    
+    # TODO(Lennard) when list sorting is implemented sort on descending date
+    
+    # use the generic list method with the filter. The access check in this
+    # method will trigger an errorResponse when user_entity is None
+    return super(View, self).list(request, access_type, 
+        page_name, list_params, filter)
+      
+  def _editPost(self, request, entity, fields):
+    """See base.View._editPost().
+    """
+
+    account = users.get_current_user()
+    current_user = user_logic.logic.getForFields({'account': account}, unique=True)
+    
+    to_user = user_logic.logic.getForFields(
+        {'link_id' : fields['to_user']}, unique=True)
+
+    fields['link_id'] = '%i' %(time.time())
+    fields['scope'] = to_user
+    fields['from_user'] = current_user
+    fields['scope_path'] = fields['to_user']
+    
+  def _editSeed(self, request, seed):
+    """Checks if scope_path is seeded and puts it into to_user.
+    
+    for parameters see base._editSeed()
+    """
+    
+    # if scope_path is present    
+    if 'scope_path' in seed.keys():
+      # fill the to_user field with the scope path
+      seed['to_user'] = seed['scope_path']
+
+  def _public(self, request, entity, context):
+    """Marks the Notification as read if that hasn't happened yet.
+    
+    for parameters see base._public()
+    """
+    
+    # if the user viewing is the user for which this notification is meant
+    # and the notification has not been read yet
+    if not entity.has_been_read:
+      # get the current user
+      account = users.get_current_user()
+      user = user_logic.logic.getForFields({'account': account}, unique=True)
+      
+      if entity.scope.key() == user.key():
+      # mark the entity as read
+        self._logic.updateModelProperties(entity, {'has_been_read' : True} )
+    
+    context['entity_type_url'] = self._params['url_name']
+    context['entity_suffix'] = self._logic.getKeySuffix(entity)
+
+
+view = View()
+create = view.create
+edit = view.edit
+delete = view.delete
+list = view.list
+public = view.public
--- a/app/soc/views/models/user_self.py	Fri Dec 12 00:35:51 2008 +0000
+++ b/app/soc/views/models/user_self.py	Fri Dec 12 23:28:18 2008 +0000
@@ -100,6 +100,7 @@
     rights['edit'] = [access.checkIsLoggedIn]
     rights['roles'] = [access.checkIsUser]
     rights['signIn'] = [access.checkNotLoggedIn]
+    rights['notification'] = [access.checkIsUser]
 
     new_params = {}
     new_params['rights'] = rights
@@ -116,6 +117,7 @@
         (users.create_login_url("user/edit"), 'Sign In', 'signIn'),
         ('/' + new_params['url_name'] + '/edit', 'Profile', 'edit'),
         ('/' + new_params['url_name'] + '/roles', 'Roles', 'roles'),
+        ('/' + 'notification/list', 'Notifications', 'notification'),
         ]
 
     patterns = []
@@ -127,9 +129,9 @@
     page_name = "Requests Overview"
     patterns += [(r'^%(url_name)s/(?P<access_type>roles)$',
                    'soc.views.models.request.list_self', page_name)]
-
+    
     new_params['django_patterns_defaults'] = patterns
-
+    
     params = dicts.merge(params, new_params)
 
     super(View, self).__init__(params=params)
--- a/app/soc/views/sitemap/build.py	Fri Dec 12 00:35:51 2008 +0000
+++ b/app/soc/views/sitemap/build.py	Fri Dec 12 23:28:18 2008 +0000
@@ -26,6 +26,7 @@
 
 from soc.views.models import document
 from soc.views.models import host
+from soc.views.models import notification
 from soc.views.models import organization
 from soc.views.models import presence
 from soc.views.models import program
@@ -55,6 +56,7 @@
 sitemap.addPages(site.view.getDjangoURLPatterns())
 sitemap.addPages(user.view.getDjangoURLPatterns())
 sitemap.addPages(user_self.view.getDjangoURLPatterns())
+sitemap.addPages(notification.view.getDjangoURLPatterns())
 sitemap.addPages(document.view.getDjangoURLPatterns())
 sitemap.addPages(sponsor.view.getDjangoURLPatterns())
 sitemap.addPages(host.view.getDjangoURLPatterns())