Quiz import from XML
authorCharles Connell <charles@connells.org>
Tue, 11 Feb 2014 04:20:32 +0000 (23:20 -0500)
committerCharles Connell <charles@connells.org>
Tue, 11 Feb 2014 04:20:32 +0000 (23:20 -0500)
karmaworld/apps/quizzes/admin.py
karmaworld/apps/quizzes/management/__init__.py [new file with mode: 0644]
karmaworld/apps/quizzes/management/commands/__init__.py [new file with mode: 0644]
karmaworld/apps/quizzes/management/commands/import_quiz.py [new file with mode: 0644]
karmaworld/apps/quizzes/migrations/0001_initial.py
karmaworld/apps/quizzes/models.py
karmaworld/apps/quizzes/xml_import.py [new file with mode: 0644]
karmaworld/assets/css/quiz.css [new file with mode: 0644]
karmaworld/templates/quizzes/quiz.html
reqs/common.txt

index 99c588b497908d3ad8548f33c1ac5046ec49b4af..0ef59bf4ef7f167c6c195aceec4ef67477701290 100644 (file)
@@ -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 (file)
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 (file)
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 (file)
index 0000000..4f2bb67
--- /dev/null
@@ -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])
+
+
index 2d89c5b141fba7a912b51d2000b3a0e582a6a3ca..10695cf7b003214dd303f70107e9325c3b60587e 100644 (file)
@@ -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'}),
index 63484369b9e2875ceb533d215b398c9431416503..ac8482b3c3fb5f2a047de26959c324c141f46d95 100644 (file)
@@ -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 (file)
index 0000000..73a7727
--- /dev/null
@@ -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<A>.*)[\n]+B. (?P<B>.*)[\n]+C. (?P<C>.*)[\n]+D. (?P<D>.*)$'
+TRUE_FALSE_CHOICE = r'^A. (?P<A>True|False)[\n]+B. (?P<B>True|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 (file)
index 0000000..48874cb
--- /dev/null
@@ -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
index 000ff20de81bcfe149068ef604ba01b04bbf0a1c..1387955f2b0c1c130f020716da16c16ba949defa 100644 (file)
@@ -2,8 +2,8 @@
 {% load url from future %}
 {% load account %}
 
-{% block pagescripts %}
-  <link rel="stylesheet" type="text/css" media="all" href="{{ STATIC_URL }}css/dashboard.css">
+{% block pagestyle %}
+  <link rel="stylesheet" type="text/css" media="all" href="{{ STATIC_URL }}css/quiz.css">
 {% endblock %}
 
 {% block title %}
@@ -22,7 +22,7 @@
         <div class="small-12 medium-8 large-4 columns small-centered">
           <div class="row">
             <div class="small-6 columns center  stat_lead_in">
-              you've had
+              you've passed
             </div>
             <div class="small-6 columns center  stat_lead_in">
               out of
@@ -30,7 +30,7 @@
           </div>
           <div class="row">
             <div class="small-6 columns center  stat_number stat_earned">
-             3
+              3
             </div>
             <div class="small-6 columns  center stat_number stat_uploaded">
               14
           </div>
           <div class="row">
             <div class="small-6 columns center  stat_object stat_earned">
-              correct answers
+              questions
             </div>
             <div class="small-6 columns center  stat_object stat_uploaded">
-              questions
+              total
             </div>
           </div>
         </div>
       {% for item in questions %}
 
         <div class="row">
-          <div class="small-10 columns small-centered {% cycle '' 'row_alt_on'%}">
-
-            <div class="activity_item">
-              {% if 'MultipleChoiceQuestion' in item.0 %}
-                {% with question=item.1 %}
+          <div class="small-10 columns small-centered quiz-center">
+            {% if 'MultipleChoiceQuestion' in item.0 %}
+              {% with question=item.1 %}
+                <div class="question multiple-choice">
                   <p>{{ question.question_text }}</p>
                   <ul>
                     {% for choice in question.choices.all %}
                       <li>{{ choice.text }}</li>
                     {% endfor %}
                   </ul>
-                {% endwith %}
-
-              {% endif %}
+                </div>
+              {% endwith %}
+            {% endif %}
+
+            {% if 'FlashCardQuestion' in item.0 %}
+              {% with question=item.1 %}
+                <div class="question flash-card">
+                  <p>{{ question.sideA }}</p>
+                  <p>{{ question.sideB }}</p>
+                </div>
+              {% endwith %}
+            {% endif %}
 
 
             </div>
index 183d85b4574d0140e198fe09de0a7e96ef687575..0c045e33d3de088255a144b79a27c9f254134497 100644 (file)
@@ -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