lms.model.quizzes

  1import os
  2import typing
  3
  4import edq.util.dirent
  5import edq.util.json
  6import quizcomp.constants
  7import quizcomp.group
  8import quizcomp.question.base
  9import quizcomp.quiz
 10
 11import lms.model.assignments
 12import lms.model.base
 13import lms.model.query
 14
 15QUESTIONS_DIRNAME: str = 'questions'
 16
 17class QuestionQuery(lms.model.query.BaseQuery):
 18    """
 19    A class for the different ways one can attempt to reference an LMS quiz question.
 20    In general, a quiz question can be queried by:
 21     - LMS Question ID (`id`)
 22     - Full Name (`name`)
 23     - f"{name} ({id})"
 24    """
 25
 26    _include_email = False
 27
 28class ResolvedQuestionQuery(lms.model.query.ResolvedBaseQuery, QuestionQuery):
 29    """
 30    A QuestionQuery that has been resolved (verified) from a real quiz question instance.
 31    """
 32
 33    _include_email = False
 34
 35    def __init__(self,
 36            question: 'Question',
 37            **kwargs: typing.Any) -> None:
 38        super().__init__(id = question.id, name = question.name, **kwargs)
 39
 40class Question(lms.model.base.BaseType):
 41    """
 42    A question within a quiz.
 43    """
 44
 45    CORE_FIELDS = [
 46        'id',
 47        'question_type',
 48        'name',
 49        'prompt',
 50        'points',
 51        'answers',
 52    ]
 53
 54    def __init__(self,
 55            id: typing.Union[str, int, None] = None,
 56            question_type: typing.Union[quizcomp.question.base.QuestionType, None] = None,
 57            name: typing.Union[str, None] = None,
 58            prompt: typing.Union[str, None] = None,
 59            points: typing.Union[float, None] = None,
 60            answers: typing.Union[typing.List[typing.Any], typing.Dict[str, typing.Any], None] = None,
 61            group_id: typing.Union[str, None] = None,
 62            resources: typing.Union[typing.List[str], None] = None,
 63            **kwargs: typing.Any) -> None:
 64        super().__init__(**kwargs)
 65
 66        if (id is None):
 67            raise ValueError("Quiz questions must have an id.")
 68
 69        self.id: str = str(id)
 70        """ The LMS's identifier for this question. """
 71
 72        if (question_type is None):
 73            raise ValueError("Quiz questions must have a type.")
 74
 75        self.question_type: quizcomp.question.base.QuestionType = question_type
 76        """ The type of this question (multiple choice, essay, etc). """
 77
 78        self.name: typing.Union[str, None] = name
 79        """ The display name of this question. """
 80
 81        self.prompt: typing.Union[str, None] = prompt
 82        """ The prompt of this question. """
 83
 84        self.points: typing.Union[float, None] = points
 85        """ The number of points possible for this question. """
 86
 87        if (answers is None):
 88            answers = []
 89
 90        self.answers: typing.Union[typing.List[typing.Any], typing.Dict[str, typing.Any]] = answers
 91        """ Possible answers to this question. """
 92
 93        self.group_id: typing.Union[str, None] = group_id
 94        """ The id of the group this question belongs to (in context of the chosen quiz). """
 95
 96        if (resources is None):
 97            resources = []
 98
 99        self.resources: typing.List[str] = resources
100        """ Paths to additional resources (e.g., images) associated with this question. """
101
102    def to_query(self) -> ResolvedQuestionQuery:
103        """ Get a query representation of this question. """
104
105        return ResolvedQuestionQuery(self)
106
107    def get_label(self) -> str:
108        """ Get the label for this question. """
109
110        return f"{self.name} ({self.id})"
111
112    def write(self, base_dir: str, force: bool = False) -> str:
113        """
114        Write this question to the given directory in Quiz Composer format and return the new directory for this question.
115        If `force` is true, then any existing directory with the same name will be overwritten,
116        otherwise an error will be raised.
117        """
118
119        base_dir = os.path.abspath(base_dir)
120
121        dirname = self.get_label()
122        out_dir = os.path.join(base_dir, dirname)
123
124        if (os.path.exists(out_dir)):
125            if (not force):
126                raise ValueError(f"Path to write quiz question ('{dirname}') already exists: '{out_dir}'.")
127
128            edq.util.dirent.remove(out_dir)
129
130        edq.util.dirent.mkdir(out_dir)
131
132        question = self._to_quizcomp_data()
133        question.to_path(os.path.join(out_dir, quizcomp.constants.QUESTION_FILENAME))
134
135        # Write resources to the same dir.
136        for resource_path in self.resources:
137            edq.util.dirent.move(resource_path, out_dir)
138
139        return out_dir
140
141    def _to_quizcomp_data(self) -> quizcomp.question.base.Question:
142        """ Get a QuizComp representation of this question. """
143
144        data = {
145            'question_type': str(self.question_type),
146            'name': self.name,
147            'prompt': self.prompt,
148            'points': self.points,
149            'ids': {'lms': self.id},
150            'answers': self.answers,
151        }
152
153        question = quizcomp.question.base.Question.from_dict(data)
154        question.validate()
155
156        return question
157
158class QuestionGroupQuery(lms.model.query.BaseQuery):
159    """
160    A class for the different ways one can attempt to reference an LMS quiz question group.
161    In general, a quiz question group can be queried by:
162     - LMS Question Group ID (`id`)
163     - Full Name (`name`)
164     - f"{name} ({id})"
165    """
166
167    _include_email = False
168
169class ResolvedQuestionGroupQuery(lms.model.query.ResolvedBaseQuery, QuestionGroupQuery):
170    """
171    A QuestionGroupQuery that has been resolved (verified) from a real quiz question question instance.
172    """
173
174    _include_email = False
175
176    def __init__(self,
177            group: 'QuestionGroup',
178            **kwargs: typing.Any) -> None:
179        super().__init__(id = group.id, name = group.name, **kwargs)
180
181class QuestionGroup(lms.model.base.BaseType):
182    """
183    A question group within a quiz.
184    This allows a quiz to choose a specific number of questions for each part of the quiz,
185    e.g., the group may have 10 questions, and 3 are chosen when the quiz is given/generated.
186    """
187
188    CORE_FIELDS = [
189        'id',
190        'name',
191        'pick_count',
192        'points',
193    ]
194
195    def __init__(self,
196            id: typing.Union[str, int, None] = None,
197            name: typing.Union[str, None] = None,
198            pick_count: typing.Union[int, None] = None,
199            points: typing.Union[float, None] = None,
200            **kwargs: typing.Any) -> None:
201        super().__init__(**kwargs)
202
203        if (id is None):
204            raise ValueError("Quiz question groups must have an id.")
205
206        self.id: str = str(id)
207        """ The LMS's identifier for this group. """
208
209        self.name: typing.Union[str, None] = name
210        """ The display name of this group. """
211
212        self.pick_count: typing.Union[int, None] = pick_count
213        """ The number of questions to choose for this group. """
214
215        self.points: typing.Union[float, None] = points
216        """ The number of points possible for this queston. """
217
218    def to_query(self) -> ResolvedQuestionGroupQuery:
219        """ Get a query representation of this question group. """
220
221        return ResolvedQuestionGroupQuery(self)
222
223    def get_label(self) -> str:
224        """ Get the label for this group. """
225
226        return f"{self.name} ({self.id})"
227
228    def _to_quizcomp_data(self,
229            all_questions: typing.List[Question],
230            questions_rel_dir: str = QUESTIONS_DIRNAME,
231            ) -> typing.Dict[str, typing.Any]:
232        """
233        Get a QuizComp representation of this group.
234        The path to each question will be based on the base dir and the question's label.
235        """
236
237        group_question_paths = []
238        for question in all_questions:
239            if (self.id == question.group_id):
240                path = os.path.join(questions_rel_dir, question.get_label())
241                group_question_paths.append(path)
242
243        return {
244            'name': self.name,
245            'pick_count': self.pick_count,
246            'points': self.points,
247            'questions': group_question_paths,
248            'ids': {'lms': self.id},
249        }
250
251class QuizQuery(lms.model.query.BaseQuery):
252    """
253    A class for the different ways one can attempt to reference an LMS quiz.
254    In general, a quiz can be queried by:
255     - LMS Quiz ID (`id`)
256     - Full Name (`name`)
257     - f"{name} ({id})"
258    """
259
260    _include_email = False
261
262class ResolvedQuizQuery(lms.model.query.ResolvedBaseQuery, QuizQuery):
263    """
264    A QuizQuery that has been resolved (verified) from a real quiz instance.
265    """
266
267    _include_email = False
268
269    def __init__(self,
270            quiz: 'Quiz',
271            **kwargs: typing.Any) -> None:
272        super().__init__(id = quiz.id, name = quiz.name, **kwargs)
273
274class Quiz(lms.model.assignments.Assignment):
275    """
276    A quiz within a course.
277    """
278
279    def __init__(self,
280            resources: typing.Union[typing.List[str], None] = None,
281            **kwargs: typing.Any) -> None:
282        super().__init__(**kwargs)
283
284        if (resources is None):
285            resources = []
286
287        self.resources: typing.List[str] = resources
288        """ Paths to additional resources (e.g., images) associated with this quiz. """
289
290    def to_query(self) -> ResolvedQuizQuery:  # type: ignore[override]
291        """ Get a query representation of this quiz. """
292
293        return ResolvedQuizQuery(self)
294
295    def write(self, base_dir: str, groups: typing.List[QuestionGroup], questions: typing.List[Question], force: bool = False) -> str:
296        """
297        Write this quiz to the given directory in Quiz Composer format and return the new directory for this quiz.
298        This will also write out all the questions to "questions" dir in the returned directory.
299        If `force` is true, then any existing directory with the same name will be overwritten,
300        otherwise an error will be raised.
301        """
302
303        base_dir = os.path.abspath(base_dir)
304
305        dirname = f"{self.name} ({self.id})"
306        out_dir = os.path.join(base_dir, dirname)
307
308        if (os.path.exists(out_dir)):
309            if (not force):
310                raise ValueError(f"Path to write quiz ('{dirname}') already exists: '{out_dir}'.")
311
312            edq.util.dirent.remove(out_dir)
313
314        edq.util.dirent.mkdir(out_dir)
315
316        quiz = self._to_quizcomp_data(groups, questions)
317        edq.util.json.dump_path(quiz, os.path.join(out_dir, quizcomp.constants.QUIZ_FILENAME), sort_keys = False, indent = 4)
318
319        questions_dir = os.path.join(out_dir, QUESTIONS_DIRNAME)
320        for question in questions:
321            question.write(questions_dir, force = force)
322
323        return out_dir
324
325    def _to_quizcomp_data(self, groups: typing.List[QuestionGroup], questions: typing.List[Question]) -> quizcomp.quiz.Quiz:
326        """ Get a QuizComp representation of this quiz. """
327
328        quizcomp_groups = [group._to_quizcomp_data(questions) for group in groups]
329
330        return {
331            'title': self.name,
332            'description': self.description,
333            'groups': quizcomp_groups,
334            'ids': {'lms': self.id},
335        }
QUESTIONS_DIRNAME: str = 'questions'
class QuestionQuery(lms.model.query.BaseQuery):
18class QuestionQuery(lms.model.query.BaseQuery):
19    """
20    A class for the different ways one can attempt to reference an LMS quiz question.
21    In general, a quiz question can be queried by:
22     - LMS Question ID (`id`)
23     - Full Name (`name`)
24     - f"{name} ({id})"
25    """
26
27    _include_email = False

A class for the different ways one can attempt to reference an LMS quiz question. In general, a quiz question can be queried by:

  • LMS Question ID (id)
  • Full Name (name)
  • f"{name} ({id})"
class ResolvedQuestionQuery(lms.model.query.ResolvedBaseQuery, QuestionQuery):
29class ResolvedQuestionQuery(lms.model.query.ResolvedBaseQuery, QuestionQuery):
30    """
31    A QuestionQuery that has been resolved (verified) from a real quiz question instance.
32    """
33
34    _include_email = False
35
36    def __init__(self,
37            question: 'Question',
38            **kwargs: typing.Any) -> None:
39        super().__init__(id = question.id, name = question.name, **kwargs)

A QuestionQuery that has been resolved (verified) from a real quiz question instance.

ResolvedQuestionQuery(question: Question, **kwargs: Any)
36    def __init__(self,
37            question: 'Question',
38            **kwargs: typing.Any) -> None:
39        super().__init__(id = question.id, name = question.name, **kwargs)
class Question(lms.model.base.BaseType):
 41class Question(lms.model.base.BaseType):
 42    """
 43    A question within a quiz.
 44    """
 45
 46    CORE_FIELDS = [
 47        'id',
 48        'question_type',
 49        'name',
 50        'prompt',
 51        'points',
 52        'answers',
 53    ]
 54
 55    def __init__(self,
 56            id: typing.Union[str, int, None] = None,
 57            question_type: typing.Union[quizcomp.question.base.QuestionType, None] = None,
 58            name: typing.Union[str, None] = None,
 59            prompt: typing.Union[str, None] = None,
 60            points: typing.Union[float, None] = None,
 61            answers: typing.Union[typing.List[typing.Any], typing.Dict[str, typing.Any], None] = None,
 62            group_id: typing.Union[str, None] = None,
 63            resources: typing.Union[typing.List[str], None] = None,
 64            **kwargs: typing.Any) -> None:
 65        super().__init__(**kwargs)
 66
 67        if (id is None):
 68            raise ValueError("Quiz questions must have an id.")
 69
 70        self.id: str = str(id)
 71        """ The LMS's identifier for this question. """
 72
 73        if (question_type is None):
 74            raise ValueError("Quiz questions must have a type.")
 75
 76        self.question_type: quizcomp.question.base.QuestionType = question_type
 77        """ The type of this question (multiple choice, essay, etc). """
 78
 79        self.name: typing.Union[str, None] = name
 80        """ The display name of this question. """
 81
 82        self.prompt: typing.Union[str, None] = prompt
 83        """ The prompt of this question. """
 84
 85        self.points: typing.Union[float, None] = points
 86        """ The number of points possible for this question. """
 87
 88        if (answers is None):
 89            answers = []
 90
 91        self.answers: typing.Union[typing.List[typing.Any], typing.Dict[str, typing.Any]] = answers
 92        """ Possible answers to this question. """
 93
 94        self.group_id: typing.Union[str, None] = group_id
 95        """ The id of the group this question belongs to (in context of the chosen quiz). """
 96
 97        if (resources is None):
 98            resources = []
 99
100        self.resources: typing.List[str] = resources
101        """ Paths to additional resources (e.g., images) associated with this question. """
102
103    def to_query(self) -> ResolvedQuestionQuery:
104        """ Get a query representation of this question. """
105
106        return ResolvedQuestionQuery(self)
107
108    def get_label(self) -> str:
109        """ Get the label for this question. """
110
111        return f"{self.name} ({self.id})"
112
113    def write(self, base_dir: str, force: bool = False) -> str:
114        """
115        Write this question to the given directory in Quiz Composer format and return the new directory for this question.
116        If `force` is true, then any existing directory with the same name will be overwritten,
117        otherwise an error will be raised.
118        """
119
120        base_dir = os.path.abspath(base_dir)
121
122        dirname = self.get_label()
123        out_dir = os.path.join(base_dir, dirname)
124
125        if (os.path.exists(out_dir)):
126            if (not force):
127                raise ValueError(f"Path to write quiz question ('{dirname}') already exists: '{out_dir}'.")
128
129            edq.util.dirent.remove(out_dir)
130
131        edq.util.dirent.mkdir(out_dir)
132
133        question = self._to_quizcomp_data()
134        question.to_path(os.path.join(out_dir, quizcomp.constants.QUESTION_FILENAME))
135
136        # Write resources to the same dir.
137        for resource_path in self.resources:
138            edq.util.dirent.move(resource_path, out_dir)
139
140        return out_dir
141
142    def _to_quizcomp_data(self) -> quizcomp.question.base.Question:
143        """ Get a QuizComp representation of this question. """
144
145        data = {
146            'question_type': str(self.question_type),
147            'name': self.name,
148            'prompt': self.prompt,
149            'points': self.points,
150            'ids': {'lms': self.id},
151            'answers': self.answers,
152        }
153
154        question = quizcomp.question.base.Question.from_dict(data)
155        question.validate()
156
157        return question

A question within a quiz.

Question( id: Union[str, int, NoneType] = None, question_type: Optional[quizcomp.question.base.QuestionType] = None, name: Optional[str] = None, prompt: Optional[str] = None, points: Optional[float] = None, answers: Union[List[Any], Dict[str, Any], NoneType] = None, group_id: Optional[str] = None, resources: Optional[List[str]] = None, **kwargs: Any)
 55    def __init__(self,
 56            id: typing.Union[str, int, None] = None,
 57            question_type: typing.Union[quizcomp.question.base.QuestionType, None] = None,
 58            name: typing.Union[str, None] = None,
 59            prompt: typing.Union[str, None] = None,
 60            points: typing.Union[float, None] = None,
 61            answers: typing.Union[typing.List[typing.Any], typing.Dict[str, typing.Any], None] = None,
 62            group_id: typing.Union[str, None] = None,
 63            resources: typing.Union[typing.List[str], None] = None,
 64            **kwargs: typing.Any) -> None:
 65        super().__init__(**kwargs)
 66
 67        if (id is None):
 68            raise ValueError("Quiz questions must have an id.")
 69
 70        self.id: str = str(id)
 71        """ The LMS's identifier for this question. """
 72
 73        if (question_type is None):
 74            raise ValueError("Quiz questions must have a type.")
 75
 76        self.question_type: quizcomp.question.base.QuestionType = question_type
 77        """ The type of this question (multiple choice, essay, etc). """
 78
 79        self.name: typing.Union[str, None] = name
 80        """ The display name of this question. """
 81
 82        self.prompt: typing.Union[str, None] = prompt
 83        """ The prompt of this question. """
 84
 85        self.points: typing.Union[float, None] = points
 86        """ The number of points possible for this question. """
 87
 88        if (answers is None):
 89            answers = []
 90
 91        self.answers: typing.Union[typing.List[typing.Any], typing.Dict[str, typing.Any]] = answers
 92        """ Possible answers to this question. """
 93
 94        self.group_id: typing.Union[str, None] = group_id
 95        """ The id of the group this question belongs to (in context of the chosen quiz). """
 96
 97        if (resources is None):
 98            resources = []
 99
100        self.resources: typing.List[str] = resources
101        """ Paths to additional resources (e.g., images) associated with this question. """
CORE_FIELDS = ['id', 'question_type', 'name', 'prompt', 'points', 'answers']

The common fields shared across backends for this type that are used for comparison and other operations. Child classes should set this to define how comparisons are made.

id: str

The LMS's identifier for this question.

question_type: quizcomp.question.base.QuestionType

The type of this question (multiple choice, essay, etc).

name: Optional[str]

The display name of this question.

prompt: Optional[str]

The prompt of this question.

points: Optional[float]

The number of points possible for this question.

answers: Union[List[Any], Dict[str, Any]]

Possible answers to this question.

group_id: Optional[str]

The id of the group this question belongs to (in context of the chosen quiz).

resources: List[str]

Paths to additional resources (e.g., images) associated with this question.

def to_query(self) -> ResolvedQuestionQuery:
103    def to_query(self) -> ResolvedQuestionQuery:
104        """ Get a query representation of this question. """
105
106        return ResolvedQuestionQuery(self)

Get a query representation of this question.

def get_label(self) -> str:
108    def get_label(self) -> str:
109        """ Get the label for this question. """
110
111        return f"{self.name} ({self.id})"

Get the label for this question.

def write(self, base_dir: str, force: bool = False) -> str:
113    def write(self, base_dir: str, force: bool = False) -> str:
114        """
115        Write this question to the given directory in Quiz Composer format and return the new directory for this question.
116        If `force` is true, then any existing directory with the same name will be overwritten,
117        otherwise an error will be raised.
118        """
119
120        base_dir = os.path.abspath(base_dir)
121
122        dirname = self.get_label()
123        out_dir = os.path.join(base_dir, dirname)
124
125        if (os.path.exists(out_dir)):
126            if (not force):
127                raise ValueError(f"Path to write quiz question ('{dirname}') already exists: '{out_dir}'.")
128
129            edq.util.dirent.remove(out_dir)
130
131        edq.util.dirent.mkdir(out_dir)
132
133        question = self._to_quizcomp_data()
134        question.to_path(os.path.join(out_dir, quizcomp.constants.QUESTION_FILENAME))
135
136        # Write resources to the same dir.
137        for resource_path in self.resources:
138            edq.util.dirent.move(resource_path, out_dir)
139
140        return out_dir

Write this question to the given directory in Quiz Composer format and return the new directory for this question. If force is true, then any existing directory with the same name will be overwritten, otherwise an error will be raised.

class QuestionGroupQuery(lms.model.query.BaseQuery):
159class QuestionGroupQuery(lms.model.query.BaseQuery):
160    """
161    A class for the different ways one can attempt to reference an LMS quiz question group.
162    In general, a quiz question group can be queried by:
163     - LMS Question Group ID (`id`)
164     - Full Name (`name`)
165     - f"{name} ({id})"
166    """
167
168    _include_email = False

A class for the different ways one can attempt to reference an LMS quiz question group. In general, a quiz question group can be queried by:

  • LMS Question Group ID (id)
  • Full Name (name)
  • f"{name} ({id})"
class ResolvedQuestionGroupQuery(lms.model.query.ResolvedBaseQuery, QuestionGroupQuery):
170class ResolvedQuestionGroupQuery(lms.model.query.ResolvedBaseQuery, QuestionGroupQuery):
171    """
172    A QuestionGroupQuery that has been resolved (verified) from a real quiz question question instance.
173    """
174
175    _include_email = False
176
177    def __init__(self,
178            group: 'QuestionGroup',
179            **kwargs: typing.Any) -> None:
180        super().__init__(id = group.id, name = group.name, **kwargs)

A QuestionGroupQuery that has been resolved (verified) from a real quiz question question instance.

ResolvedQuestionGroupQuery(group: QuestionGroup, **kwargs: Any)
177    def __init__(self,
178            group: 'QuestionGroup',
179            **kwargs: typing.Any) -> None:
180        super().__init__(id = group.id, name = group.name, **kwargs)
class QuestionGroup(lms.model.base.BaseType):
182class QuestionGroup(lms.model.base.BaseType):
183    """
184    A question group within a quiz.
185    This allows a quiz to choose a specific number of questions for each part of the quiz,
186    e.g., the group may have 10 questions, and 3 are chosen when the quiz is given/generated.
187    """
188
189    CORE_FIELDS = [
190        'id',
191        'name',
192        'pick_count',
193        'points',
194    ]
195
196    def __init__(self,
197            id: typing.Union[str, int, None] = None,
198            name: typing.Union[str, None] = None,
199            pick_count: typing.Union[int, None] = None,
200            points: typing.Union[float, None] = None,
201            **kwargs: typing.Any) -> None:
202        super().__init__(**kwargs)
203
204        if (id is None):
205            raise ValueError("Quiz question groups must have an id.")
206
207        self.id: str = str(id)
208        """ The LMS's identifier for this group. """
209
210        self.name: typing.Union[str, None] = name
211        """ The display name of this group. """
212
213        self.pick_count: typing.Union[int, None] = pick_count
214        """ The number of questions to choose for this group. """
215
216        self.points: typing.Union[float, None] = points
217        """ The number of points possible for this queston. """
218
219    def to_query(self) -> ResolvedQuestionGroupQuery:
220        """ Get a query representation of this question group. """
221
222        return ResolvedQuestionGroupQuery(self)
223
224    def get_label(self) -> str:
225        """ Get the label for this group. """
226
227        return f"{self.name} ({self.id})"
228
229    def _to_quizcomp_data(self,
230            all_questions: typing.List[Question],
231            questions_rel_dir: str = QUESTIONS_DIRNAME,
232            ) -> typing.Dict[str, typing.Any]:
233        """
234        Get a QuizComp representation of this group.
235        The path to each question will be based on the base dir and the question's label.
236        """
237
238        group_question_paths = []
239        for question in all_questions:
240            if (self.id == question.group_id):
241                path = os.path.join(questions_rel_dir, question.get_label())
242                group_question_paths.append(path)
243
244        return {
245            'name': self.name,
246            'pick_count': self.pick_count,
247            'points': self.points,
248            'questions': group_question_paths,
249            'ids': {'lms': self.id},
250        }

A question group within a quiz. This allows a quiz to choose a specific number of questions for each part of the quiz, e.g., the group may have 10 questions, and 3 are chosen when the quiz is given/generated.

QuestionGroup( id: Union[str, int, NoneType] = None, name: Optional[str] = None, pick_count: Optional[int] = None, points: Optional[float] = None, **kwargs: Any)
196    def __init__(self,
197            id: typing.Union[str, int, None] = None,
198            name: typing.Union[str, None] = None,
199            pick_count: typing.Union[int, None] = None,
200            points: typing.Union[float, None] = None,
201            **kwargs: typing.Any) -> None:
202        super().__init__(**kwargs)
203
204        if (id is None):
205            raise ValueError("Quiz question groups must have an id.")
206
207        self.id: str = str(id)
208        """ The LMS's identifier for this group. """
209
210        self.name: typing.Union[str, None] = name
211        """ The display name of this group. """
212
213        self.pick_count: typing.Union[int, None] = pick_count
214        """ The number of questions to choose for this group. """
215
216        self.points: typing.Union[float, None] = points
217        """ The number of points possible for this queston. """
CORE_FIELDS = ['id', 'name', 'pick_count', 'points']

The common fields shared across backends for this type that are used for comparison and other operations. Child classes should set this to define how comparisons are made.

id: str

The LMS's identifier for this group.

name: Optional[str]

The display name of this group.

pick_count: Optional[int]

The number of questions to choose for this group.

points: Optional[float]

The number of points possible for this queston.

def to_query(self) -> ResolvedQuestionGroupQuery:
219    def to_query(self) -> ResolvedQuestionGroupQuery:
220        """ Get a query representation of this question group. """
221
222        return ResolvedQuestionGroupQuery(self)

Get a query representation of this question group.

def get_label(self) -> str:
224    def get_label(self) -> str:
225        """ Get the label for this group. """
226
227        return f"{self.name} ({self.id})"

Get the label for this group.

class QuizQuery(lms.model.query.BaseQuery):
252class QuizQuery(lms.model.query.BaseQuery):
253    """
254    A class for the different ways one can attempt to reference an LMS quiz.
255    In general, a quiz can be queried by:
256     - LMS Quiz ID (`id`)
257     - Full Name (`name`)
258     - f"{name} ({id})"
259    """
260
261    _include_email = False

A class for the different ways one can attempt to reference an LMS quiz. In general, a quiz can be queried by:

  • LMS Quiz ID (id)
  • Full Name (name)
  • f"{name} ({id})"
class ResolvedQuizQuery(lms.model.query.ResolvedBaseQuery, QuizQuery):
263class ResolvedQuizQuery(lms.model.query.ResolvedBaseQuery, QuizQuery):
264    """
265    A QuizQuery that has been resolved (verified) from a real quiz instance.
266    """
267
268    _include_email = False
269
270    def __init__(self,
271            quiz: 'Quiz',
272            **kwargs: typing.Any) -> None:
273        super().__init__(id = quiz.id, name = quiz.name, **kwargs)

A QuizQuery that has been resolved (verified) from a real quiz instance.

ResolvedQuizQuery(quiz: Quiz, **kwargs: Any)
270    def __init__(self,
271            quiz: 'Quiz',
272            **kwargs: typing.Any) -> None:
273        super().__init__(id = quiz.id, name = quiz.name, **kwargs)
class Quiz(lms.model.assignments.Assignment):
275class Quiz(lms.model.assignments.Assignment):
276    """
277    A quiz within a course.
278    """
279
280    def __init__(self,
281            resources: typing.Union[typing.List[str], None] = None,
282            **kwargs: typing.Any) -> None:
283        super().__init__(**kwargs)
284
285        if (resources is None):
286            resources = []
287
288        self.resources: typing.List[str] = resources
289        """ Paths to additional resources (e.g., images) associated with this quiz. """
290
291    def to_query(self) -> ResolvedQuizQuery:  # type: ignore[override]
292        """ Get a query representation of this quiz. """
293
294        return ResolvedQuizQuery(self)
295
296    def write(self, base_dir: str, groups: typing.List[QuestionGroup], questions: typing.List[Question], force: bool = False) -> str:
297        """
298        Write this quiz to the given directory in Quiz Composer format and return the new directory for this quiz.
299        This will also write out all the questions to "questions" dir in the returned directory.
300        If `force` is true, then any existing directory with the same name will be overwritten,
301        otherwise an error will be raised.
302        """
303
304        base_dir = os.path.abspath(base_dir)
305
306        dirname = f"{self.name} ({self.id})"
307        out_dir = os.path.join(base_dir, dirname)
308
309        if (os.path.exists(out_dir)):
310            if (not force):
311                raise ValueError(f"Path to write quiz ('{dirname}') already exists: '{out_dir}'.")
312
313            edq.util.dirent.remove(out_dir)
314
315        edq.util.dirent.mkdir(out_dir)
316
317        quiz = self._to_quizcomp_data(groups, questions)
318        edq.util.json.dump_path(quiz, os.path.join(out_dir, quizcomp.constants.QUIZ_FILENAME), sort_keys = False, indent = 4)
319
320        questions_dir = os.path.join(out_dir, QUESTIONS_DIRNAME)
321        for question in questions:
322            question.write(questions_dir, force = force)
323
324        return out_dir
325
326    def _to_quizcomp_data(self, groups: typing.List[QuestionGroup], questions: typing.List[Question]) -> quizcomp.quiz.Quiz:
327        """ Get a QuizComp representation of this quiz. """
328
329        quizcomp_groups = [group._to_quizcomp_data(questions) for group in groups]
330
331        return {
332            'title': self.name,
333            'description': self.description,
334            'groups': quizcomp_groups,
335            'ids': {'lms': self.id},
336        }

A quiz within a course.

Quiz(resources: Optional[List[str]] = None, **kwargs: Any)
280    def __init__(self,
281            resources: typing.Union[typing.List[str], None] = None,
282            **kwargs: typing.Any) -> None:
283        super().__init__(**kwargs)
284
285        if (resources is None):
286            resources = []
287
288        self.resources: typing.List[str] = resources
289        """ Paths to additional resources (e.g., images) associated with this quiz. """
resources: List[str]

Paths to additional resources (e.g., images) associated with this quiz.

def to_query(self) -> ResolvedQuizQuery:
291    def to_query(self) -> ResolvedQuizQuery:  # type: ignore[override]
292        """ Get a query representation of this quiz. """
293
294        return ResolvedQuizQuery(self)

Get a query representation of this quiz.

def write( self, base_dir: str, groups: List[QuestionGroup], questions: List[Question], force: bool = False) -> str:
296    def write(self, base_dir: str, groups: typing.List[QuestionGroup], questions: typing.List[Question], force: bool = False) -> str:
297        """
298        Write this quiz to the given directory in Quiz Composer format and return the new directory for this quiz.
299        This will also write out all the questions to "questions" dir in the returned directory.
300        If `force` is true, then any existing directory with the same name will be overwritten,
301        otherwise an error will be raised.
302        """
303
304        base_dir = os.path.abspath(base_dir)
305
306        dirname = f"{self.name} ({self.id})"
307        out_dir = os.path.join(base_dir, dirname)
308
309        if (os.path.exists(out_dir)):
310            if (not force):
311                raise ValueError(f"Path to write quiz ('{dirname}') already exists: '{out_dir}'.")
312
313            edq.util.dirent.remove(out_dir)
314
315        edq.util.dirent.mkdir(out_dir)
316
317        quiz = self._to_quizcomp_data(groups, questions)
318        edq.util.json.dump_path(quiz, os.path.join(out_dir, quizcomp.constants.QUIZ_FILENAME), sort_keys = False, indent = 4)
319
320        questions_dir = os.path.join(out_dir, QUESTIONS_DIRNAME)
321        for question in questions:
322            question.write(questions_dir, force = force)
323
324        return out_dir

Write this quiz to the given directory in Quiz Composer format and return the new directory for this quiz. This will also write out all the questions to "questions" dir in the returned directory. If force is true, then any existing directory with the same name will be overwritten, otherwise an error will be raised.