Integrate yesterday's AccessChecker progress and tests upgrade.
diff --git a/app/melange/request/access.py b/app/melange/request/access.py
index a44f92c..8c3f191 100644
--- a/app/melange/request/access.py
+++ b/app/melange/request/access.py
@@ -19,9 +19,12 @@
 from melange.request import exception
 from soc.logic import links
 
-_DEF_NOT_HOST = translation.ugettext(
+_MESSAGE_NOT_PROGRAM_ADMINISTRATOR = translation.ugettext(
     'You need to be a program administrator to access this page.')
 
+_MESSAGE_NOT_DEVELOPER = translation.ugettext(
+    'This page is only accessible to developers.')
+
 
 def ensureLoggedIn(self):
   """Ensures that the user is logged in.
@@ -97,6 +100,20 @@
     elif not data.gae_user:
       raise exception.LoginRequired()
     elif not data.is_host:
-      raise exception.Forbidden(message=_DEF_NOT_HOST)
+      raise exception.Forbidden(message=_MESSAGE_NOT_PROGRAM_ADMINISTRATOR)
 
 PROGRAM_ADMINISTRATOR_ACCESS_CHECKER = ProgramAdministratorAccessChecker()
+
+
+# TODO(nathaniel): Eliminate this or make it a
+# "SiteAdministratorAccessChecker" - there should be no aspects of Melange
+# that require developer action or are limited only to developers.
+class DeveloperAccessChecker(AccessChecker):
+  """AccessChecker that ensures that the user is a developer."""
+
+  def checkAccess(self, data, check, mutator):
+    """See AccessChecker.checkAccess for specification."""
+    if not data.is_developer:
+      raise exception.Forbidden(message=_MESSAGE_NOT_DEVELOPER)
+
+DEVELOPER_ACCESS_CHECKER = DeveloperAccessChecker()
diff --git a/app/soc/content/images/gci/logo/banner-gci2013.png b/app/soc/content/images/gci/logo/banner-gci2013.png
new file mode 100644
index 0000000..cd90d1d
--- /dev/null
+++ b/app/soc/content/images/gci/logo/banner-gci2013.png
Binary files differ
diff --git a/app/soc/models/conversation.py b/app/soc/models/conversation.py
new file mode 100644
index 0000000..858a4a8
--- /dev/null
+++ b/app/soc/models/conversation.py
@@ -0,0 +1,122 @@
+# Copyright 2013 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.
+
+"""Module containing the Conversation model."""
+
+from google.appengine.ext import db
+from google.appengine.ext import ndb
+
+from datetime import datetime
+
+from django.utils import translation
+
+from soc.models import user as user_model
+
+#: Constants for specifiying the type of recipients
+PROGRAM       = 'Program'       #: Types of users within the program
+ORGANIZATION  = 'Organization'  #: Types of users within the specified org
+USER          = 'User'          #: Specific users, specified manually
+
+
+class Conversation(ndb.Model):
+  """Model of a conversation: a thread of messages."""
+
+  #: Subject of the conversation
+  subject = ndb.TextProperty(required=True,
+                             verbose_name=translation.ugettext('Subject'))
+
+  #: User who created the conversation.
+  #: Optional; If None, conversation was created by Melange.
+  creator = ndb.KeyProperty(required=False)
+
+  #: When the conversation was created
+  created_on = ndb.DateTimeProperty(required=True, auto_now_add=True)
+
+  #: When the last message was added
+  last_message_on = ndb.DateTimeProperty(
+      default=datetime.min,
+      verbose_name=translation.ugettext('Last Message'))
+
+  #: What type of recipients
+  recipients_type = ndb.StringProperty(required=True,
+                                       choices=[PROGRAM, ORGANIZATION, USER])
+
+  #: Program under which the conversation exists.
+  #: If recipient type is 'Program', this is also the scope of recipients.
+  program = ndb.KeyProperty(required=True,
+                            verbose_name=translation.ugettext('Program'))
+
+  #: Organization to limit to if recipients type is organization.
+  #: Ignored if recipient type is not 'Organization'.
+  organization = ndb.KeyProperty(
+      required=False,
+      verbose_name=translation.ugettext('Organization'))
+
+  #: Include admins as recipients if recipients type is program,
+  #: or organization admins as recipients if recipients type is organization.
+  #: Ignored if recipient type is not 'Program' or 'Organization'.
+  include_admins = ndb.BooleanProperty(required=False)
+
+  #: Include mentors as recipients if recipients type is program,
+  #: or organization mentors as recipients if recipients type is organization.
+  #: Ignored if recipient type is not 'Program' or 'Organization'.
+  include_mentors = ndb.BooleanProperty(required=False)
+
+  #: Include students as recipients if recipients type is program.
+  #: Ignored if recipient type is not 'Program'.
+  include_students = ndb.BooleanProperty(required=False)
+
+  #: Whether users will be automatically added/removed if they start/stop
+  #: matching the criteria defined above. Ignored if recipients type is 'User'.
+  auto_update_users = ndb.BooleanProperty(
+      default=True,
+      verbose_name=translation.ugettext('Automatically Update Users'))
+  auto_update_users.help_text = translation.ugettext(
+      'If set, users will be automatically added and removed from the '
+      'conversation if they no longer fit the criteria.')
+
+
+class ConversationUser(ndb.Model):
+  """Model representing a user's involvement in a conversation.
+
+  An instance of this model is created for every user in every conversation.
+
+  In addition to being a record of a user's involvement in a conversation, it
+  stores the time of the last message seen by the user, and it also store's the
+  user's preferences for this conversation.
+  """
+
+  #: Conversation the preferences apply to
+  conversation = ndb.KeyProperty(kind=Conversation, required=True)
+
+  #: User the preferences are for
+  user = ndb.KeyProperty(required=True)
+
+  #: Conversation's Program, to aid with querying
+  program = ndb.ComputedProperty(lambda self: self.conversation.get().program)
+
+  #: Conversation's last_message_on, to aid with querying
+  last_message_on = ndb.ComputedProperty(
+      lambda self: self.conversation.get().last_message_on)
+
+  #: Time of the last message seen by this user in this conversation
+  last_message_seen_on = ndb.DateTimeProperty(default=datetime.min)
+
+  #: Preference for receiving email notifications for new messages
+  enable_notifications = ndb.BooleanProperty(
+      default=True,
+      verbose_name=translation.ugettext('Get Email Notifications'))
+  enable_notifications.help_test = translation.ugettext(
+      'If set, you will receive email notifications about new '
+      'messages in this conversation.')
diff --git a/app/soc/models/message.py b/app/soc/models/message.py
new file mode 100644
index 0000000..d13e269
--- /dev/null
+++ b/app/soc/models/message.py
@@ -0,0 +1,45 @@
+# Copyright 2013 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.
+
+"""Module containing the Message and MessageSeen models."""
+
+from google.appengine.ext import ndb
+
+from django.utils import translation
+
+from soc.models import conversation as conversation_model
+
+
+class Message(ndb.Model):
+  """Model of a message within a conversation.
+
+  Parent:
+    soc.models.conversation.Conversation
+  """
+
+  #: Conversation the message belongs to
+  conversation = ndb.KeyProperty(kind=conversation_model.Conversation,
+                                 required=True)
+
+  #: User who wrote the message. If None, message was authored by Melange.
+  author = ndb.KeyProperty(required=False,
+                           verbose_name=translation.ugettext('Author'))
+
+  #: Content of the message
+  content = ndb.TextProperty(required=True,
+                             verbose_name=translation.ugettext('Message'))
+
+  #: Time when the message was sent
+  sent_on = ndb.DateTimeProperty(required=True, auto_now_add=True,
+                                 verbose_name=translation.ugettext('Time Sent'))
diff --git a/app/soc/views/org_home.py b/app/soc/views/org_home.py
index 6d07f87..9c7686f 100644
--- a/app/soc/views/org_home.py
+++ b/app/soc/views/org_home.py
@@ -18,6 +18,7 @@
 
 from django import http
 
+from melange.request import access
 from soc.views.helper import url_patterns
 from soc.views.helper.access_checker import isSet
 from soc.views.template import Template
@@ -27,6 +28,8 @@
 class BanOrgPost(object):
   """Handles banning/unbanning of organizations."""
 
+  access_checker = access.PROGRAM_ADMINISTRATOR_ACCESS_CHECKER
+
   def djangoURLPatterns(self):
     return [
          url_patterns.url(
@@ -35,9 +38,6 @@
              self, name=self._getURLName()),
     ]
 
-  def checkAccess(self, data, check, mutator):
-    check.isHost();
-
   def post(self, data, check, mutator):
     """See soc.views.base.RequestHandler.post for specification."""
     assert isSet(data.organization)
diff --git a/app/soc/views/profile_show.py b/app/soc/views/profile_show.py
index 12a5393..ad52fd9 100644
--- a/app/soc/views/profile_show.py
+++ b/app/soc/views/profile_show.py
@@ -18,6 +18,7 @@
 
 from django import http
 
+from melange.request import access
 from soc.models.user import User
 from soc.views import readonly_template
 from soc.views.helper import url_patterns
@@ -88,6 +89,8 @@
 class BanProfilePost(object):
   """Handles banning/unbanning of profiles."""
 
+  access_checker = access.PROGRAM_ADMINISTRATOR_ACCESS_CHECKER
+
   def djangoURLPatterns(self):
     return [
          url_patterns.url(
@@ -96,13 +99,9 @@
              self, name=self._getURLName()),
     ]
 
-  def checkAccess(self, data, check, mutator):
-    """See soc.views.base.RequestHandler.checkAccess for specification."""
-    check.isHost()
-    mutator.profileFromKwargs()
-
   def post(self, data, check, mutator):
     """See soc.views.base.RequestHandler.post for specification."""
+    mutator.profileFromKwargs()
     assert isSet(data.url_profile)
 
     value = data.POST.get('value')
diff --git a/app/soc/views/program.py b/app/soc/views/program.py
index 3ae5eb6..36060d3 100644
--- a/app/soc/views/program.py
+++ b/app/soc/views/program.py
@@ -16,6 +16,8 @@
 
 from google.appengine.ext import db
 
+from melange.request import access
+
 
 class CreateProgramPage(object):
   """View to create a new program.
@@ -24,9 +26,7 @@
   function will be used by inheriting, module-specific classes.
   """
 
-  def checkAccess(self, data, check, mutator):
-    "See soc.views.base.RequestHandler.checkAccess for specification."
-    check.isHost()
+  access_checker = access.PROGRAM_ADMINISTRATOR_ACCESS_CHECKER
 
   def context(self, data, check, mutator):
     """See soc.views.base.RequestHandler.context for specification."""
@@ -120,8 +120,7 @@
 class ProgramMessagesPage(object):
   """View for the content of program specific messages to be sent."""
 
-  def checkAccess(self, data, check, mutator):
-    check.isHost()
+  access_checker = access.PROGRAM_ADMINISTRATOR_ACCESS_CHECKER
 
   def context(self, data, check, mutator):
     """See soc.views.base.RequestHandler.context for specification."""
diff --git a/app/soc/views/site.py b/app/soc/views/site.py
index 7cb0c30..b232e5d 100644
--- a/app/soc/views/site.py
+++ b/app/soc/views/site.py
@@ -24,6 +24,7 @@
 from django.forms import widgets as django_widgets
 from django.utils.translation import ugettext
 
+from melange.request import access
 from melange.request import exception
 from soc.logic import cleaning
 from soc.logic import site as site_logic
@@ -70,6 +71,11 @@
 class EditSitePage(base.RequestHandler):
   """View for the participant profile."""
 
+  # TODO(nathaniel): This page should use something like a "site admin
+  # access checker" - there should be no pages accessible only to
+  # developers.
+  access_checker = access.DEVELOPER_ACCESS_CHECKER
+
   def djangoURLPatterns(self):
     return [
         django_url(r'^site/edit$', self, name='edit_site_settings'),
@@ -85,10 +91,6 @@
 
     return {'data': json_data}
 
-  def checkAccess(self, data, check, mutator):
-    if not data.is_developer:
-      raise exception.Forbidden(message=DEF_NO_DEVELOPER)
-
   def templatePath(self):
     # TODO: make this specific to the current active program
     return 'soc/site/base.html'
diff --git a/tests/app/melange/request/test_access.py b/tests/app/melange/request/test_access.py
index 54c492b..6b2984e 100644
--- a/tests/app/melange/request/test_access.py
+++ b/tests/app/melange/request/test_access.py
@@ -19,6 +19,8 @@
 from nose.plugins import skip
 
 from melange.request import access
+from melange.request import exception
+from soc.views.helper import request_data
 
 
 class Explosive(object):
@@ -65,3 +67,24 @@
   def testAnonymousDeniedAccess(self):
     """Tests that logged-out users are denied access."""
     raise skip.SkipTest()
+
+
+class DeveloperAccessCheckerTest(unittest.TestCase):
+  """Tests the DeveloperAccessChecker class."""
+
+  def testDeveloperAccessAllowed(self):
+    data = request_data.RequestData(None, None, None)
+    # TODO(nathaniel): Reaching around RequestHandler public API.
+    data._is_developer = True
+
+    access_checker = access.DeveloperAccessChecker()
+    access_checker.checkAccess(data, None, None)
+
+  def testNonDeveloperAccessDenied(self):
+    data = request_data.RequestData(None, None, None)
+    # TODO(nathaniel): Reaching around RequestHandler public API.
+    data._is_developer = False
+
+    access_checker = access.DeveloperAccessChecker()
+    with self.assertRaises(exception.UserError):
+      access_checker.checkAccess(data, None, None)