From 61cb3b67aacbd5efb317f1b33e6872d6bfccfa7d Mon Sep 17 00:00:00 2001 From: Charles Connell Date: Mon, 10 Feb 2014 23:20:32 -0500 Subject: [PATCH] Quiz import from XML --- karmaworld/apps/quizzes/admin.py | 15 +- .../apps/quizzes/management/__init__.py | 0 .../quizzes/management/commands/__init__.py | 0 .../management/commands/import_quiz.py | 16 +++ .../apps/quizzes/migrations/0001_initial.py | 47 ++++++- karmaworld/apps/quizzes/models.py | 45 ++++-- karmaworld/apps/quizzes/xml_import.py | 132 ++++++++++++++++++ karmaworld/assets/css/quiz.css | 76 ++++++++++ karmaworld/templates/quizzes/quiz.html | 36 +++-- reqs/common.txt | 1 + 10 files changed, 334 insertions(+), 34 deletions(-) create mode 100644 karmaworld/apps/quizzes/management/__init__.py create mode 100644 karmaworld/apps/quizzes/management/commands/__init__.py create mode 100644 karmaworld/apps/quizzes/management/commands/import_quiz.py create mode 100644 karmaworld/apps/quizzes/xml_import.py create mode 100644 karmaworld/assets/css/quiz.css diff --git a/karmaworld/apps/quizzes/admin.py b/karmaworld/apps/quizzes/admin.py index 99c588b..0ef59bf 100644 --- a/karmaworld/apps/quizzes/admin.py +++ b/karmaworld/apps/quizzes/admin.py @@ -2,7 +2,8 @@ # -*- coding:utf8 -*- # Copyright (C) 2014 FinalsClub Foundation from django.contrib import admin -from karmaworld.apps.quizzes.models import MultipleChoiceQuestion, FlashCardQuestion, MultipleChoiceOption, Quiz +from karmaworld.apps.quizzes.models import MultipleChoiceQuestion, FlashCardQuestion, MultipleChoiceOption, Quiz, \ + TrueFalseQuestion from nested_inlines.admin import NestedTabularInline, NestedModelAdmin, NestedStackedInline @@ -16,17 +17,29 @@ class MultipleChoiceQuestionAdmin(NestedModelAdmin): list_display = ('question_text', 'quiz') +class MultipleChoiceQuestionInlineAdmin(NestedStackedInline): + model = MultipleChoiceQuestion + list_display = ('question_text', 'quiz') + + class FlashCardQuestionInlineAdmin(NestedStackedInline): model = FlashCardQuestion list_display = ('sideA', 'sideB', 'quiz') +class TrueFalseQuestionInlineAdmin(NestedStackedInline): + model = TrueFalseQuestion + list_display = ('question_text', 'quiz') + + class QuizAdmin(NestedModelAdmin): search_fields = ['name', 'note__name'] list_display = ('name', 'note') + inlines = [MultipleChoiceQuestionInlineAdmin, TrueFalseQuestionInlineAdmin, FlashCardQuestionInlineAdmin] admin.site.register(Quiz, QuizAdmin) admin.site.register(MultipleChoiceQuestion, MultipleChoiceQuestionAdmin) admin.site.register(MultipleChoiceOption) admin.site.register(FlashCardQuestion) +admin.site.register(TrueFalseQuestion) diff --git a/karmaworld/apps/quizzes/management/__init__.py b/karmaworld/apps/quizzes/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/karmaworld/apps/quizzes/management/commands/__init__.py b/karmaworld/apps/quizzes/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/karmaworld/apps/quizzes/management/commands/import_quiz.py b/karmaworld/apps/quizzes/management/commands/import_quiz.py new file mode 100644 index 0000000..4f2bb67 --- /dev/null +++ b/karmaworld/apps/quizzes/management/commands/import_quiz.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python +# -*- coding:utf8 -*- +# Copyright (C) 2014 FinalsClub Foundation +from django.core.management import BaseCommand +from karmaworld.apps.quizzes.xml_import import quiz_from_xml + + +class Command(BaseCommand): + help = """ + Import a quiz from an IQ Metrics XML file + """ + + def handle(self, *args, **kwargs): + quiz_from_xml(args[0]) + + diff --git a/karmaworld/apps/quizzes/migrations/0001_initial.py b/karmaworld/apps/quizzes/migrations/0001_initial.py index 2d89c5b..10695cf 100644 --- a/karmaworld/apps/quizzes/migrations/0001_initial.py +++ b/karmaworld/apps/quizzes/migrations/0001_initial.py @@ -22,10 +22,10 @@ class Migration(SchemaMigration): (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), ('quiz', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['quizzes.Quiz'])), ('timestamp', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.utcnow)), + ('explanation', self.gf('django.db.models.fields.CharField')(max_length=2048, null=True, blank=True)), + ('difficulty', self.gf('django.db.models.fields.CharField')(max_length=50, null=True, blank=True)), + ('category', self.gf('django.db.models.fields.CharField')(max_length=50, null=True, blank=True)), ('question_text', self.gf('django.db.models.fields.CharField')(max_length=2048)), - ('explanation', self.gf('django.db.models.fields.CharField')(max_length=2048)), - ('difficulty', self.gf('django.db.models.fields.CharField')(max_length=50)), - ('category', self.gf('django.db.models.fields.CharField')(max_length=50)), )) db.send_create_signal(u'quizzes', ['MultipleChoiceQuestion']) @@ -43,11 +43,27 @@ class Migration(SchemaMigration): (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), ('quiz', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['quizzes.Quiz'])), ('timestamp', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.utcnow)), + ('explanation', self.gf('django.db.models.fields.CharField')(max_length=2048, null=True, blank=True)), + ('difficulty', self.gf('django.db.models.fields.CharField')(max_length=50, null=True, blank=True)), + ('category', self.gf('django.db.models.fields.CharField')(max_length=50, null=True, blank=True)), ('sideA', self.gf('django.db.models.fields.CharField')(max_length=2048)), ('sideB', self.gf('django.db.models.fields.CharField')(max_length=2048)), )) db.send_create_signal(u'quizzes', ['FlashCardQuestion']) + # Adding model 'TrueFalseQuestion' + db.create_table(u'quizzes_truefalsequestion', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('quiz', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['quizzes.Quiz'])), + ('timestamp', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.utcnow)), + ('explanation', self.gf('django.db.models.fields.CharField')(max_length=2048, null=True, blank=True)), + ('difficulty', self.gf('django.db.models.fields.CharField')(max_length=50, null=True, blank=True)), + ('category', self.gf('django.db.models.fields.CharField')(max_length=50, null=True, blank=True)), + ('text', self.gf('django.db.models.fields.CharField')(max_length=2048)), + ('true', self.gf('django.db.models.fields.BooleanField')(default=False)), + )) + db.send_create_signal(u'quizzes', ['TrueFalseQuestion']) + def backwards(self, orm): # Deleting model 'Quiz' @@ -62,6 +78,9 @@ class Migration(SchemaMigration): # Deleting model 'FlashCardQuestion' db.delete_table(u'quizzes_flashcardquestion') + # Deleting model 'TrueFalseQuestion' + db.delete_table(u'quizzes_truefalsequestion') + models = { u'auth.group': { @@ -101,7 +120,7 @@ class Migration(SchemaMigration): 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) }, u'courses.course': { - 'Meta': {'ordering': "['-file_count', 'school', 'name']", 'unique_together': "(('name', 'department'),)", 'object_name': 'Course'}, + 'Meta': {'ordering': "['-file_count', 'school', 'name']", 'unique_together': "(('name', 'school'),)", 'object_name': 'Course'}, 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 'department': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['courses.Department']", 'null': 'True', 'blank': 'True'}), 'desc': ('django.db.models.fields.TextField', [], {'max_length': '511', 'null': 'True', 'blank': 'True'}), @@ -171,6 +190,9 @@ class Migration(SchemaMigration): }, u'quizzes.flashcardquestion': { 'Meta': {'object_name': 'FlashCardQuestion'}, + 'category': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}), + 'difficulty': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}), + 'explanation': ('django.db.models.fields.CharField', [], {'max_length': '2048', 'null': 'True', 'blank': 'True'}), u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'quiz': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['quizzes.Quiz']"}), 'sideA': ('django.db.models.fields.CharField', [], {'max_length': '2048'}), @@ -186,9 +208,9 @@ class Migration(SchemaMigration): }, u'quizzes.multiplechoicequestion': { 'Meta': {'object_name': 'MultipleChoiceQuestion'}, - 'category': ('django.db.models.fields.CharField', [], {'max_length': '50'}), - 'difficulty': ('django.db.models.fields.CharField', [], {'max_length': '50'}), - 'explanation': ('django.db.models.fields.CharField', [], {'max_length': '2048'}), + 'category': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}), + 'difficulty': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}), + 'explanation': ('django.db.models.fields.CharField', [], {'max_length': '2048', 'null': 'True', 'blank': 'True'}), u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'question_text': ('django.db.models.fields.CharField', [], {'max_length': '2048'}), 'quiz': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['quizzes.Quiz']"}), @@ -201,6 +223,17 @@ class Migration(SchemaMigration): 'note': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['notes.Note']", 'null': 'True', 'blank': 'True'}), 'timestamp': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'}) }, + u'quizzes.truefalsequestion': { + 'Meta': {'object_name': 'TrueFalseQuestion'}, + 'category': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}), + 'difficulty': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}), + 'explanation': ('django.db.models.fields.CharField', [], {'max_length': '2048', 'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'quiz': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['quizzes.Quiz']"}), + 'text': ('django.db.models.fields.CharField', [], {'max_length': '2048'}), + 'timestamp': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'}), + 'true': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, u'taggit.tag': { 'Meta': {'ordering': "['namespace', 'name']", 'object_name': 'Tag'}, u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), diff --git a/karmaworld/apps/quizzes/models.py b/karmaworld/apps/quizzes/models.py index 6348436..ac8482b 100644 --- a/karmaworld/apps/quizzes/models.py +++ b/karmaworld/apps/quizzes/models.py @@ -21,13 +21,7 @@ class BaseQuizQuestion(models.Model): quiz = models.ForeignKey(Quiz) timestamp = models.DateTimeField(default=datetime.datetime.utcnow) - class Meta: - abstract = True - - -class MultipleChoiceQuestion(BaseQuizQuestion): - question_text = models.CharField(max_length=2048) - explanation = models.CharField(max_length=2048) + explanation = models.CharField(max_length=2048, blank=True, null=True) EASY = 'EASY' MEDIUM = 'MEDIUM' @@ -38,18 +32,31 @@ class MultipleChoiceQuestion(BaseQuizQuestion): (HARD, 'Hard'), ) - difficulty = models.CharField(max_length=50, choices=DIFFICULTY_CHOICES) + difficulty = models.CharField(max_length=50, choices=DIFFICULTY_CHOICES, blank=True, null=True) UNDERSTAND = 'UNDERSTAND' REMEMBER = 'REMEMBER' ANALYZE = 'ANALYZE' + KNOWLEDGE = 'KNOWLEDGE' CATEGORY_CHOICES = ( (UNDERSTAND, 'Understand'), (REMEMBER, 'Remember'), (ANALYZE, 'Analyze'), + (KNOWLEDGE, 'Knowledge'), ) - category = models.CharField(max_length=50, choices=CATEGORY_CHOICES) + category = models.CharField(max_length=50, choices=CATEGORY_CHOICES, blank=True, null=True) + + + class Meta: + abstract = True + + +class MultipleChoiceQuestion(BaseQuizQuestion): + question_text = models.CharField(max_length=2048) + + class Meta: + verbose_name = 'Multiple choice question' def __unicode__(self): return self.question_text @@ -65,11 +72,25 @@ class MultipleChoiceOption(models.Model): class FlashCardQuestion(BaseQuizQuestion): - sideA = models.CharField(max_length=2048) - sideB = models.CharField(max_length=2048) + sideA = models.CharField(max_length=2048, verbose_name='Side A') + sideB = models.CharField(max_length=2048, verbose_name='Side B') + + class Meta: + verbose_name = 'Flash card question' def __unicode__(self): return self.sideA + ' / ' + self.sideB -ALL_QUESTION_CLASSES = (MultipleChoiceQuestion, FlashCardQuestion) + +class TrueFalseQuestion(BaseQuizQuestion): + text = models.CharField(max_length=2048) + true = models.BooleanField(verbose_name='True?') + + class Meta: + verbose_name = 'True/False question' + + def __unicode__(self): + return self.text + +ALL_QUESTION_CLASSES = (MultipleChoiceQuestion, FlashCardQuestion, TrueFalseQuestion) diff --git a/karmaworld/apps/quizzes/xml_import.py b/karmaworld/apps/quizzes/xml_import.py new file mode 100644 index 0000000..73a7727 --- /dev/null +++ b/karmaworld/apps/quizzes/xml_import.py @@ -0,0 +1,132 @@ +from StringIO import StringIO +import re +from bs4 import BeautifulSoup +from karmaworld.apps.quizzes.models import MultipleChoiceQuestion, Quiz, TrueFalseQuestion, MultipleChoiceOption +from pyth.plugins.plaintext.writer import PlaintextWriter +from pyth.plugins.rtf15.reader import Rtf15Reader + +FOUR_MULTIPLE_CHOICE = r'^A. (?P.*)[\n]+B. (?P.*)[\n]+C. (?P.*)[\n]+D. (?P.*)$' +TRUE_FALSE_CHOICE = r'^A. (?PTrue|False)[\n]+B. (?PTrue|False)$' + + +def _rtf2plain(str): + if str: + doc = Rtf15Reader.read(StringIO(str)) + return PlaintextWriter.write(doc).getvalue() + else: + return str + + +def _category_from_question(question): + category_string = question.find('Category').string + if category_string == 'Knowledge': + category = MultipleChoiceQuestion.KNOWLEDGE + elif category_string == 'Understand': + category = MultipleChoiceQuestion.UNDERSTAND + elif category_string == 'Remember': + category = MultipleChoiceQuestion.REMEMBER + elif category_string == 'Analyze': + category = MultipleChoiceQuestion.ANALYZE + else: + category = None + + return category + + +def _difficulty_from_question(question): + difficulty_string = question.find('Difficulty').string + if difficulty_string == 'Easy': + difficulty = MultipleChoiceQuestion.EASY + elif difficulty_string == 'Medium': + difficulty = MultipleChoiceQuestion.MEDIUM + elif difficulty_string == 'Hard': + difficulty = MultipleChoiceQuestion.HARD + else: + difficulty = None + + return difficulty + + +def _true_false(question, quiz_object): + question_text = _rtf2plain(question.find('QuestionText').string) + explanation_text = _rtf2plain(question.find('Explanation').string) + category = _category_from_question(question) + difficulty = _difficulty_from_question(question) + + correct_answer_letter = question.find('Answer').string + + options_string = question.find('AnswerOptions').string + options_string = _rtf2plain(options_string) + match_options = re.match(TRUE_FALSE_CHOICE, options_string) + option_a = match_options.group('A') + option_b = match_options.group('B') + + if correct_answer_letter == 'A': + correct_answer_string = option_a + else: + correct_answer_string = option_b + + if correct_answer_string == 'True': + correct_answer = True + else: + correct_answer = False + + TrueFalseQuestion.objects.create(text=question_text, + explanation=explanation_text, + difficulty=difficulty, + category=category, + true=correct_answer, + quiz=quiz_object) + + +def _multiple_choice(question, quiz_object): + question_text = _rtf2plain(question.find('QuestionText').string) + explanation_text = _rtf2plain(question.find('Explanation').string) + category = _category_from_question(question) + difficulty = _difficulty_from_question(question) + + question_object = MultipleChoiceQuestion.objects.create(question_text=question_text, + explanation=explanation_text, + difficulty=difficulty, + category=category, + quiz=quiz_object) + + correct_answer = question.find('Answer').string + + options_string = question.find('AnswerOptions').string + options_string = _rtf2plain(options_string) + match_options = re.match(FOUR_MULTIPLE_CHOICE, options_string) + option_a = match_options.group('A') + option_b = match_options.group('B') + option_c = match_options.group('C') + option_d = match_options.group('D') + + MultipleChoiceOption.objects.create(text=option_a, + correct=(correct_answer == 'A'), + question=question_object) + MultipleChoiceOption.objects.create(text=option_b, + correct=(correct_answer == 'B'), + question=question_object) + MultipleChoiceOption.objects.create(text=option_c, + correct=(correct_answer == 'C'), + question=question_object) + MultipleChoiceOption.objects.create(text=option_d, + correct=(correct_answer == 'D'), + question=question_object) + + +def quiz_from_xml(filename): + with open(filename, 'r') as file: + soup = BeautifulSoup(file.read(), "xml") + + quiz_name = soup.find('EChapterTitle').string + quiz_object = Quiz.objects.create(name=quiz_name) + + questions = soup.find_all('TestBank') + for question in questions: + type_string = question.find('Type').string + if type_string == 'Multiple Choice': + _multiple_choice(question, quiz_object) + + elif type_string == 'True/False': + _true_false(question, quiz_object) diff --git a/karmaworld/assets/css/quiz.css b/karmaworld/assets/css/quiz.css new file mode 100644 index 0000000..48874cb --- /dev/null +++ b/karmaworld/assets/css/quiz.css @@ -0,0 +1,76 @@ +/* DASHBOARD */ + +#dashboard_content +{ + padding-top: 46px; +} + +/* STATS */ +#stats_container +{ + height: 210px; + margin-bottom: 20px; +} + +.stat_lead_in +{ + padding-top: 25px; + font-family: "MuseoSlab-900"; + font-size: 11px; + text-transform: uppercase; +} + +.stat_number +{ + font-family: "MuseoSlab-900"; + font-size: 57px; + padding-top: 5px; +} + +.stat_object +{ + font-family: "MuseoSlab-900"; + font-size: 13px; + text-transform: uppercase; +} + +.stat_earned +{ + color: #f05a28; +} + +.stat_uploaded +{ + color: #9ccf00; +} + +.stat_downloaded +{ + color: #00cf9c; +} + +.stat_liked +{ + color: #009ccf; +} + +/* ACTIVITY */ + +.activity_item +{ + margin: 20px 20px; +} + +#activity_more +{ + text-align: center; + padding: 20px 0; +} + +#no_activity +{ + font-family: "MuseoSlab-900"; + font-size: 10px; + color: #8e8e8e; + margin-bottom: 20px; +} \ No newline at end of file diff --git a/karmaworld/templates/quizzes/quiz.html b/karmaworld/templates/quizzes/quiz.html index 000ff20..1387955 100644 --- a/karmaworld/templates/quizzes/quiz.html +++ b/karmaworld/templates/quizzes/quiz.html @@ -2,8 +2,8 @@ {% load url from future %} {% load account %} -{% block pagescripts %} - +{% block pagestyle %} + {% endblock %} {% block title %} @@ -22,7 +22,7 @@
- you've had + you've passed
out of @@ -30,7 +30,7 @@
- 3 + 3
14 @@ -38,10 +38,10 @@
- correct answers + questions
- questions + total
@@ -62,20 +62,28 @@ {% for item in questions %}
-
- -
- {% if 'MultipleChoiceQuestion' in item.0 %} - {% with question=item.1 %} +
+ {% if 'MultipleChoiceQuestion' in item.0 %} + {% with question=item.1 %} +

{{ question.question_text }}

    {% for choice in question.choices.all %}
  • {{ choice.text }}
  • {% endfor %}
- {% endwith %} - - {% endif %} +
+ {% endwith %} + {% endif %} + + {% if 'FlashCardQuestion' in item.0 %} + {% with question=item.1 %} +
+

{{ question.sideA }}

+

{{ question.sideB }}

+
+ {% endwith %} + {% endif %}
diff --git a/reqs/common.txt b/reqs/common.txt index 183d85b..0c045e3 100644 --- a/reqs/common.txt +++ b/reqs/common.txt @@ -23,3 +23,4 @@ boto==2.6.0 django-storages==1.1.4 django-reversion git+https://github.com/Soaa-/django-nested-inlines.git +pyth -- 2.25.1