Define the Models for implementing Quizzes (collections of Questions) and their
authorTodd Larsen <tlarsen@google.com>
Wed, 15 Oct 2008 17:10:27 +0000
changeset 339 b9be44e09530
parent 338 0d78f41dde9b
child 340 f1522bafa958
Define the Models for implementing Quizzes (collections of Questions) and their Responses (collections of Answers to those Questions). These Models would form the basis of storage for such items as: Terms of Service (Quiz) Question ("I agree...") Response -> Answer (answer to "I agree..." confirmation) solution ("Yes" Answer to the "I agree..." Question) Mentor and Student surveys (Quiz) Questions (including "Pay this student?") Response -> Answers solution ("Yes" Answer to the "Pay this student?" Question) Organization applications Student Proposal review, comment, and scoring system GHOP task tracking (a specific task list item would be a Quiz) Patch by: Todd Larsen Review by: Pawel Solyga, Sverre Rabbelier, Chen Lunpeng Review URL: http://codereviews.googleopensourceprograms.com/1403
app/soc/models/answer.py
app/soc/models/question.py
app/soc/models/quiz.py
app/soc/models/response.py
app/soc/models/user.py
--- a/app/soc/models/answer.py	Wed Oct 15 14:06:33 2008 +0000
+++ b/app/soc/models/answer.py	Wed Oct 15 17:10:27 2008 +0000
@@ -14,7 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-"""This module contains the Answer Model."""
+"""This module contains the Answer Model"""
 
 __authors__ = [
   '"Todd Larsen" <tlarsen@google.com>',
@@ -22,50 +22,73 @@
 ]
 
 
-from google.appengine.ext import db
+import polymodel
 
-from soc import models
-from soc.models import base
+from google.appengine.ext import db
 
 import soc.models.question
-import soc.models.review
+import soc.models.quiz
+import soc.models.response
 
 
-class Answer(base.ModelWithFieldAttributes):
-  """Model of a specific Answer to a Question in a specific Review."""
+class Answer(polymodel.PolyModel):
+  """Model of a specific Answer to a Question in a specific Response.
+
+  The properties in this Model do not have verbose_name or help_text,
+  because the dynamic nature of the forms required to create, edit, and
+  use entities of this Model make them pretty useless.
+  """
 
   #: A required many:1 relationship, where each of many Answers is
   #: a specific answer to a single Question.  An Answer must always
   #: be associated with a Question in order to be interpreted.
   #: It is currently unclear how useful this back-reference will be,
-  #: since the same question could be used in multiple different
-  #: Review "templates". Given this, 'answers' currently only exists
-  #: for completeness.
-  # TODO: Uncomment when Question model is committed
-  #question = db.ReferenceProperty(reference_class=models.question.Question,
-  #                                required=True, collection_name="answers")
+  #: since the same Question could be used in multiple different
+  #: Quizzes. Given this, 'answers' currently only exists for
+  #: completeness.
+  question = db.ReferenceProperty(
+    reference_class=soc.models.question.Question, required=True,
+    collection_name="answers")
 
-  #: A required many:1 relationship, where each of many Answers to
-  #: different Questions represents the answer set of a specific
-  #: Review. The back-reference in the Review model is a Query named
-  #: 'answers' which represents all of the specific answers to
-  #: questions in that Review.
-  review = db.ReferenceProperty(reference_class=models.review.Review,
-                                required=True, collection_name="answers")
+  #: A many:1 relationship, where each of many Answers to different
+  #: Questions represents the answer set of a specific Response to a Quiz.
+  #: The back-reference in the Response model is a Query named 'answers'
+  #: which represents all of the specific Answers to Questions in that
+  #: Response.
+  #:
+  #: One and only one of the response or quiz ReferenceProperties *must*
+  #: be defined for an Answer entity.
+  response = db.ReferenceProperty(
+      reference_class=soc.models.response.Response, required=False,
+      collection_name="answers")
 
-  #: db.StringProperty storing the "short" answer to the question;
-  #: the interpretation of this value depends on the Question entity
-  #: referred to by 'question'. Answers can be indexed, filtered, and
-  #: sorted by their "short" answer. Depending on the Question type,
-  #: some Answers will use only 'short', some only 'long', some both.
-  short = db.StringProperty()
+  #: A many:1 relationship, where each of many Answers to different
+  #: Questions represents the solution set for the Questions in a Quiz.
+  #: The back-reference in the Quiz model is a Query named 'solutions'
+  #: which represents all of the solutions to Questions in that Quiz.
+  #:
+  #: One and only one of the response or quiz ReferenceProperties *must*
+  #: be defined for an Answer entity.
+  quiz = db.ReferenceProperty(
+      reference_class=soc.models.quiz.Quiz, required=False,
+      collection_name="solutions")
 
-  #: db.TextProperty storing the "long" answer to the question;
-  #: the interpretation of this value depends on the Question entity
-  #: referred to by 'question'.
-  long = db.TextProperty()
-
-  #: db.ListProperty of short strings from the list of possible
-  #: picks in the question.pick_choices list.
-  picks = db.ListProperty(item_type=str)
-
+  #: db.ListProperty of strings representing the answer value or values.
+  #:
+  #: For Questions that are not multiple-choice (see the choice_ids and
+  #: choices properties of soc.models.question.Question), this list will
+  #: contain a single string that is a free-form text answer.
+  #:
+  #: For Questions that *are* multiple-choice, this list will contain one
+  #: or more short, plain-text, "link_name-like" strings representing the
+  #: "encoded" answer choices (see the choice_ids property in
+  #: soc.models.question.Question).  For such multiple-choice Questions,    
+  #: how many strings are stored depends on the max_answers property of
+  #: the soc.models.question.Question entity for which this is an Answer.
+  #:
+  #: If question.is_optional is True, 'answers' may even be None or an
+  #: empty list if no answers were provided.
+  #:
+  #: Answers can be indexed, filtered, and sorted by this list, but only in
+  #: the way that query operators work with a db.ListProperty.
+  answers = db.ListProperty(item_type=str)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/app/soc/models/question.py	Wed Oct 15 17:10:27 2008 +0000
@@ -0,0 +1,168 @@
+#!/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 Question Model."""
+
+__authors__ = [
+  '"Todd Larsen" <tlarsen@google.com>',
+]
+
+
+from google.appengine.ext import db
+
+import soc.models.work
+
+
+class Question(soc.models.work.Work):
+  """Model of a Question, which is a specialized form of Work.
+
+  Specific types of Questions are actually implemented in subclasses.
+
+  The specific way that the properties and relations inherited from
+  Work are used with a Question are described below.
+
+    work.title:  the title of the Question, used for finding the
+      Question in a list of Questions
+
+    work.abstract:  the Question text, asked to the respondent
+
+    work.authors:  the Authors of the Work referred to by this relation
+      are the authors of the Question
+
+    work.reviews:  even Questions can be "reviewed" (possibly commented
+      on during creation or annotated once put into use).
+
+    work.partial_path: used to scope (and, when combined with
+      work.link_name, uniquely identify) a Question in the same way the
+      property are used with Documents, etc.
+
+    work.link_name: used to identify (and, when combined with
+      work.partial_path, *uniquely* identify) a Question in the same way
+      these properties are used with Documents, etc.
+      
+  In addition to any explicit ReferenceProperties in the Question Model
+  and those inherited as described above, a Question entity participates
+  in these relationships:
+
+    answers)  a 1:many relationship, where each Question has many different
+      Answers associated with it as parts of Responses to Quizzes.  This is
+      implemented as the 'answers' back-reference Query of the Answer model
+      'question' reference.  It is currently unclear how useful this
+      back-reference will be, since the same Question could be used in
+      multiple different Quizzes. Given this, 'answers' currently only
+      exists for completeness.
+      
+    quizzes)  a many:many relationship between Questions and the Quizzes
+      that collect them into a set.  This relation is not explicitly
+      implemented, but can be obtained via a query something like:
+      
+        quizzes_with_a_question = db.GqlQuery(
+            "SELECT * FROM Quiz where questions = :1",
+            a_question.key())
+
+      Such queries are probably only needed when a Question might be
+      altered, in order to find which Quizzes will be affected.
+
+  The properties in this Model do not have verbose_name or help_text,
+  because the dynamic nature of the forms required to create, edit, and
+  use entities of this Model make them pretty useless.
+  
+  ######################################################################
+  # TODO(tlarsen): the following verbose comments can be removed later,
+    when these ideas are implemented in the views and controllers; they
+    are here now so that the concepts will not be lost before that time.
+
+  The recommended use for the combination of work.partial_path and
+  work.link_name is to keep the *same* link_name when copying and
+  modifying an existing Question for a new Program (or instance of a
+  Group that is per-Program), while changing the work.partial_path to
+  represent the Program and Group "ownership" of the Question.  For
+  example, if a Question asking about prior GSoC participation needed
+  to have an additional choice (see the choice_ids and choices properties
+  below), it is desirable to keep the same work.link_name (and also
+  simply append new choice_ids and choices to keep the old answer values
+  compatible).  An existing Question in the above example might be identified
+  as something like:
+    Question:google/gsoc2009/gsoc_past_participation
+    <type>:<Sponsor>/<Program>/<link_name> 
+  To make it possible to query for gsoc_past_participation answers regardless
+  of the Program, the next year, new values are added to choice_ids and
+  choices in a new Question copied from the one above, which would then
+  be named something (still unique) like:
+    Question:google/gsoc2010/gsoc_past_participation
+  Care just needs to be taken to keep the existing choice_ids and choices
+  compatible.
+  
+  Other interesting possibilities also exist, such as asking about GSoC
+  participation of the GHOP participants (some GHOP high school students
+  have actually previously been GSoC mentors, for example).  To produce
+  unique statistics for GHOP that could also be aggregated overall in
+  combination with GSoC, the gsoc_past_participation Question would be
+  duplicated (unaltered) to something like:
+    Question:google/ghop2009/gsoc_past_participation
+  To get the combined results, query on a link_name of
+  gsoc_past_participation.  For more targeted results, include the
+  partial_path to make the query more specific.
+
+  Question creation to permit use cases like the one above is going to
+  be a bit of an "advanced" skill, possibly.  "Doing it wrong" the first
+  time a Question is created will make it difficult to implement stuff
+  like multiple-choice Questions that "grow" new choices year-over-year.
+
+  A dynamic form is most definitely going to be needed to implement the
+  Question creation and editing for multiple-choice questions.
+  """
+  #: db.ListProperty of short, plain-text, "link_name-like" strings
+  #: representing the "encoded" answer choices (must be strings compatible
+  #: with being query arguments and being used in HTML controls and POST
+  #: responses).
+  #:
+  #: If empty (None or an empty list), it is assumed that this Question
+  #: is *not* a multiple choice question.  In that case, the UI should
+  #: display the Question as a textarea in forms and accept any plain-text.
+  #:
+  #: If non-empty, max_answers helps determine how the UI should display
+  #: the Question.  Also, controller logic needs to validate if the
+  #: strings in the 'answers' property of the Answer entity come only
+  #: from this list.
+  #:
+  #: Once Answers to this Question have been stored in the Datastore,
+  #: choice_ids and choices should *not* be modified.  An existing
+  #: Question can be duplicated and then modified (but, it will be a
+  #: different question as a result).
+  choice_ids = db.ListProperty(item_type=str)
+
+  #: db.ListProperty of human-readable choice strings, in the same order
+  #: as, and corresponding to, the "encoded" choices in the choice_ids
+  #: db.ListProperty. 
+  choices = db.ListProperty(item_type=str)
+
+  #: db.IntegerProperty indicating the maximum number of answer values
+  #: permitted for this question.  If 'choices' does not contain a list of
+  #: choice strings, this value is ignored (but should still only be 1).
+  #:
+  #: If there are 'choices' and this value is 1, the UI should render the
+  #: Question in forms as a single-choice control ("radio buttons").
+  #:
+  #: If there are 'choices' and this value is greater than 1, the UI should
+  #: render the question as a list of check-boxes.
+  #:
+  #: max_answers greater than 1 combined with choices enables Questions
+  #: like, for example, "...select the three most important...".
+  max_answers = db.IntegerProperty(default=1)
+
+  #: field storing whether the Answer to a Question is optional
+  is_optional = db.BooleanProperty(default=False)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/app/soc/models/quiz.py	Wed Oct 15 17:10:27 2008 +0000
@@ -0,0 +1,104 @@
+#!/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 Quiz Model."""
+
+__authors__ = [
+  '"Todd Larsen" <tlarsen@google.com>',
+]
+
+import reflistprop
+
+from google.appengine.ext import db
+
+from django.utils.translation import ugettext_lazy
+
+import soc.models.answer
+import soc.models.document
+import soc.models.question
+
+
+class Quiz(soc.models.document.Document):
+  """Model of a Quiz, a collection of Questions to be asked.
+  
+  (named Quiz because Questionnaire was too much to type...)
+  
+  A Quiz collects a set of Questions to which Answers are given in the
+  form of a separate Model called a Response.
+
+  Quizzes can even be used as templates for comments and scoring
+  annotations to various Works, such as Documents and Proposals.  A
+  separate Review Model is derived from Quiz for these purposes.
+
+  The specific way that the properties and relations inherited from
+  Document, and also indirectly from Work, are used with a Quiz are
+  described below.
+
+    work.title:  the title of the Quiz
+
+    work.abstract:  summary displayed as a snippet in Quiz list views
+
+    work.authors:  the Authors of the Work referred to by this relation
+      are the authors of the Quiz (but not necessarily the individual
+      Questions themselves, see the Question Model)
+
+    work.reviews:  even Quizzes can be "reviewed" (possibly commented
+      on during creation or annotated once put into use).
+
+    work.partial_path/work.link_name: used to scope and uniquely identify
+      a Quiz in the same way these properties are used with Documents, etc.
+
+    document.content:  the "preface" of the Quiz, displayed before any
+      of the Questions, usually containing instructions for the Quiz
+
+  In addition to any explicit ReferenceProperties in the Quiz Model and
+  those inherited as described above, a Quiz entity participates in these
+  relationships:
+
+    responses)  a 1:many relationship where each Quiz can produce all of
+      its many Response entities that indicate they contain specific
+      Answers to each of the Questions contained in that Quiz. This relation
+      is implemented as the 'responses' back-reference Query of the Response
+      Model 'quiz' reference.
+      
+    solutions)  a 1:many relationship where some (or none, or all) of the
+      Questions in the Quiz have "solutions" or "correct Answers".  The
+      'solutions' back-reference Query of the Answer Model 'quiz' reference
+      is used to point these "correct Answers" at the Quiz to which they
+      apply.  One example of a Quiz having a "correct Answer" is a GSoC
+      mentor survey that has a "pass" Question that gates if the student
+      gets paid.  The desired Answer for this Question would be
+      associated with the Quiz via the 'quiz' property and some controller
+      logic could check if a survey "passed" by querying for these
+      "solution" Answers and seeing if the survey Response had the "right"
+      Answers (to the one Question that matters in this case...).
+  """
+  
+  #: a many:many relationship (many:many because a given Question can be
+  #: reused in more than one Quiz, and each Quiz is made up of one or more
+  #: Questions) between Question entities and, when combined, the Quiz they
+  #: form.
+  #:
+  #: A ReferenceListProperty is used instead of a special many:many
+  #; relation Model for a number of reasons:
+  #:   1) the Questions in a Quiz need to be ordered
+  #:   2) Quizzes will have relatively few Questions, so the performance
+  #:      ReferenceListProperty is not a major concern
+  #:   3) querying a Question for all of the Quizzes that contain it is
+  #:      a rare occurrence, so the expense of a ListProperty query is
+  #:      not a real concern
+  questions = reflistprop.ReferenceListProperty(
+    soc.models.question.Question, default=None) 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/app/soc/models/response.py	Wed Oct 15 17:10:27 2008 +0000
@@ -0,0 +1,63 @@
+#!/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 Response Model."""
+
+__authors__ = [
+  '"Todd Larsen" <tlarsen@google.com>',
+]
+
+
+import polymodel
+
+from google.appengine.ext import db
+
+from django.utils.translation import ugettext_lazy
+
+import soc.models.quiz
+import soc.models.user
+
+
+class Response(polymodel.PolyModel):
+  """Model of a Response to a Quiz.
+
+  A Response is the "collection point" for a set of specific Answers to the
+  Questions that make up a Quiz.
+
+  In addition to the explicit ReferenceProperties in the Response Model, a
+  Response entity participates in these relationships:
+
+    answers)  a 1:many relationship between Answer entities and this
+      Response.  Each Answer points to the Response to which it is a part.
+      The collection of Answers that make up a Response is implemented as
+      the 'answers' back-reference Query of the Answer model 'response'
+      reference.
+  """
+
+  #: a required many:1 relationship between Responses and a Quiz that
+  #: defines what Questions for which each Response collects Answers
+  #: (that is, there can be many Responses to the same Quiz)
+  quiz = db.ReferenceProperty(reference_class=soc.models.quiz.Quiz,
+                              required=True, collection_name="responses")
+
+  #: a required many:1 relationship with a User that indicates which User
+  #: submitted the Response (answered the Questions in the Quiz)
+  respondent = db.ReferenceProperty(
+      reference_class=soc.models.user.User, required=True,
+      collection_name="responses")
+
+  # TODO(tlarsen): should 'respondent' be a ReferenceProperty to some Role
+  #   instead?
--- a/app/soc/models/user.py	Wed Oct 15 14:06:33 2008 +0000
+++ b/app/soc/models/user.py	Wed Oct 15 17:10:27 2008 +0000
@@ -55,6 +55,10 @@
    groups)  a 1:many relationship of Group entities "founded" by the User.
      This relation is implemented as the 'groups' back-reference Query of
      the Group model 'founder' reference.
+
+   responses)  a 1:many relationship of Reponse entities submitted by the
+     User.  This relation is implemented as the 'responses' back-reference
+     Query of the Response model 'respondent' reference.
   """
 
   #: A Google Account, which also provides a "private" email address.