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
--- 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.