From f014a73dd40db96c87853bfa7b76def4966cd848 Mon Sep 17 00:00:00 2001 From: Charles Connell Date: Fri, 9 May 2014 10:55:41 -0400 Subject: [PATCH] Remove NLP approach to finding keywords, start on quiz interface --- karmaworld/apps/notes/gdrive.py | 6 - karmaworld/apps/notes/models.py | 13 + karmaworld/apps/notes/views.py | 23 +- karmaworld/apps/quizzes/admin.py | 41 +- karmaworld/apps/quizzes/create_quiz.py | 147 +++++++ karmaworld/apps/quizzes/find_keywords.py | 125 ------ karmaworld/apps/quizzes/models.py | 95 ---- karmaworld/assets/css/note_course_pages.css | 54 ++- karmaworld/assets/js/course-list.js | 2 +- karmaworld/assets/js/note-detail.js | 29 ++ karmaworld/templates/courses/course_list.html | 2 +- karmaworld/templates/notes/note_base.html | 285 ++++++++++++ karmaworld/templates/notes/note_detail.html | 406 ++---------------- karmaworld/templates/notes/note_keywords.html | 78 ++++ karmaworld/templates/notes/note_quiz.html | 74 ++++ karmaworld/urls.py | 4 +- reqs/common.txt | 4 - 17 files changed, 734 insertions(+), 654 deletions(-) create mode 100644 karmaworld/apps/quizzes/create_quiz.py delete mode 100644 karmaworld/apps/quizzes/find_keywords.py create mode 100644 karmaworld/templates/notes/note_base.html create mode 100644 karmaworld/templates/notes/note_keywords.html create mode 100644 karmaworld/templates/notes/note_quiz.html diff --git a/karmaworld/apps/notes/gdrive.py b/karmaworld/apps/notes/gdrive.py index 4f4df55..7bb183f 100644 --- a/karmaworld/apps/notes/gdrive.py +++ b/karmaworld/apps/notes/gdrive.py @@ -9,7 +9,6 @@ from django.conf import settings from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned from karmaworld.apps.notes.models import UserUploadMapping from karmaworld.apps.notes.models import NoteMarkdown -from karmaworld.apps.quizzes.find_keywords import find_keywords from karmaworld.apps.quizzes.models import Keyword from karmaworld.apps.users.models import NoteKarmaEvent import os @@ -240,11 +239,6 @@ def convert_raw_document(raw_document, user=None): note_markdown = NoteMarkdown(note=note, markdown=markdown) note_markdown.save() - # Guess some keywords from the note text - keywords = find_keywords(note.text) - for word in keywords: - Keyword.objects.create(word=word, note=note) - # If we know the user who uploaded this, # associate them with the note if user: diff --git a/karmaworld/apps/notes/models.py b/karmaworld/apps/notes/models.py index 3f8e9f0..760b87c 100644 --- a/karmaworld/apps/notes/models.py +++ b/karmaworld/apps/notes/models.py @@ -315,6 +315,19 @@ class Note(Document): # return a url ending in id return reverse('note_keywords', args=[self.course.school.slug, self.course.slug, self.id]) + def get_absolute_quiz_url(self): + """ Resolve note url, use 'note' route and slug if slug + otherwise use note.id + """ + if self.slug is not None: + # return a url ending in slug + if self.course.school: + return reverse('note_quiz', args=[self.course.school.slug, self.course.slug, self.slug]) + else: + return reverse('note_quiz', args=[self.course.department.school.slug, self.course.slug, self.slug]) + else: + # return a url ending in id + return reverse('note_quiz', args=[self.course.school.slug, self.course.slug, self.id]) def filter_html(self, html): """ diff --git a/karmaworld/apps/notes/views.py b/karmaworld/apps/notes/views.py index a0b3d8a..a3c571d 100644 --- a/karmaworld/apps/notes/views.py +++ b/karmaworld/apps/notes/views.py @@ -11,6 +11,7 @@ from django.core.exceptions import ValidationError from django.forms.formsets import formset_factory from karmaworld.apps.courses.models import Course from karmaworld.apps.notes.search import SearchIndex +from karmaworld.apps.quizzes.create_quiz import quiz_from_keywords from karmaworld.apps.quizzes.find_keywords import find_keywords from karmaworld.apps.quizzes.forms import KeywordForm from karmaworld.apps.quizzes.models import Keyword @@ -18,7 +19,7 @@ from karmaworld.apps.users.models import NoteKarmaEvent from karmaworld.utils.ajax_utils import * from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden -from django.views.generic import DetailView, ListView +from django.views.generic import DetailView, ListView, TemplateView from django.views.generic import FormView from django.views.generic import View from django.views.generic.detail import SingleObjectMixin @@ -67,6 +68,7 @@ class NoteDetailView(DetailView): """ Class-based view for the note html page """ model = Note context_object_name = u"note" # name passed to template + template_name = 'notes/note_base.html' def get_context_data(self, **kwargs): """ Generate custom context for the page rendering a Note @@ -87,7 +89,7 @@ class NoteSaveView(FormView, SingleObjectMixin): """ form_class = NoteForm model = Note - template_name = 'notes/note_detail.html' + template_name = 'notes/note_base.html' def post(self, request, *args, **kwargs): self.object = self.get_object() @@ -158,7 +160,7 @@ class NoteKeywordsView(FormView, SingleObjectMixin): model = Note context_object_name = u"note" # name passed to template form_class = formset_factory(KeywordForm) - template_name = 'notes/note_detail.html' + template_name = 'notes/note_base.html' def get_object(self, queryset=None): return Note.objects.get(slug=self.kwargs['slug']) @@ -216,6 +218,21 @@ class NoteKeywordsView(FormView, SingleObjectMixin): keyword_object.save() +class NoteQuizView(TemplateView): + template_name = 'notes/note_base.html' + + def get_context_data(self, **kwargs): + note = Note.objects.get(slug=self.kwargs['slug']) + + note_page_context_helper(note, self.request, kwargs) + + kwargs['note'] = note + kwargs['questions'] = quiz_from_keywords(note) + kwargs['show_quiz'] = True + + return super(NoteQuizView, self).get_context_data(**kwargs) + + class NoteSearchView(ListView): template_name = 'notes/search_results.html' diff --git a/karmaworld/apps/quizzes/admin.py b/karmaworld/apps/quizzes/admin.py index ea1b8b8..de5a7af 100644 --- a/karmaworld/apps/quizzes/admin.py +++ b/karmaworld/apps/quizzes/admin.py @@ -2,45 +2,6 @@ # -*- coding:utf8 -*- # Copyright (C) 2014 FinalsClub Foundation from django.contrib import admin -from karmaworld.apps.quizzes.models import MultipleChoiceQuestion, FlashCardQuestion, MultipleChoiceOption, Quiz, \ - TrueFalseQuestion, Keyword -from nested_inlines.admin import NestedTabularInline, NestedModelAdmin, NestedStackedInline +from karmaworld.apps.quizzes.models import Keyword - -class MultipleChoiceOptionInlineAdmin(NestedTabularInline): - model = MultipleChoiceOption - - -class MultipleChoiceQuestionAdmin(NestedModelAdmin): - model = MultipleChoiceQuestion - inlines = [MultipleChoiceOptionInlineAdmin] - list_display = ('question_text', 'quiz') - - -class MultipleChoiceQuestionInlineAdmin(NestedStackedInline): - model = MultipleChoiceQuestion - list_display = ('question_text', 'quiz') - - -class FlashCardQuestionInlineAdmin(NestedStackedInline): - model = FlashCardQuestion - list_display = ('keyword_side', 'definition_side', '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) admin.site.register(Keyword) diff --git a/karmaworld/apps/quizzes/create_quiz.py b/karmaworld/apps/quizzes/create_quiz.py new file mode 100644 index 0000000..13e76e5 --- /dev/null +++ b/karmaworld/apps/quizzes/create_quiz.py @@ -0,0 +1,147 @@ +import random +from karmaworld.apps.quizzes.models import Keyword + + +class BaseQuizQuestion(object): + + def question_type(self): + return self.__class__.__name__ + + +class MultipleChoiceQuestion(BaseQuizQuestion): + def __init__(self, question_text, choices): + self.question_text = question_text + self.choices = choices + + def __unicode__(self): + return "Multiple choice: {0}: {1}".format(self.question_text, ", ".join(map(str, self.choices))) + + def __str__(self): + return unicode(self) + + def __repr__(self): + return str(self) + + +class MultipleChoiceOption(object): + def __init__(self, text, correct): + self.text = text + self.correct = correct + + def __unicode__(self): + return self.text + + def __str__(self): + return unicode(self) + + def __repr__(self): + return str(self) + + +class FlashCardQuestion(BaseQuizQuestion): + def __init__(self, keyword_side, definition_side): + self.keyword_side = keyword_side + self.definition_side = definition_side + + def __unicode__(self): + return "Flash card: {0} / {1}".format(self.keyword_side, self.definition_side) + + def __str__(self): + return unicode(self) + + def __repr__(self): + return str(self) + + +class TrueFalseQuestion(BaseQuizQuestion): + def __init__(self, question_text, true): + self.question_text = question_text + self.true = true + + def __unicode__(self): + return "True or false: {0}".format(self.question_text) + + def __str__(self): + return unicode(self) + + def __repr__(self): + return str(self) + + +KEYWORD_MULTIPLE_CHOICE = 1 +DEFINITION_MULTIPLE_CHOICE = 2 +KEYWORD_DEFINITION_TRUE_FALSE = 3 +GENERATED_QUESTION_TYPE = ( + KEYWORD_MULTIPLE_CHOICE, + DEFINITION_MULTIPLE_CHOICE, + KEYWORD_DEFINITION_TRUE_FALSE, +) + +MULTIPLE_CHOICE_CHOICES = 4 + + +def _create_keyword_multiple_choice(keyword, keywords): + choices = [MultipleChoiceOption(text=keyword.word, correct=True)] + + for other_keyword in random.sample(keywords.exclude(id=keyword.id), MULTIPLE_CHOICE_CHOICES - 1): + choices.append(MultipleChoiceOption( + text=other_keyword.word, + correct=False)) + + question_text = 'Pick the keyword to match "{d}"'.format(d=keyword.definition) + + return MultipleChoiceQuestion(question_text, choices) + + +def _create_definition_multiple_choice(keyword, keywords): + choices = [MultipleChoiceOption(text=keyword.definition, correct=True)] + + for other_keyword in random.sample(keywords.exclude(id=keyword.id), MULTIPLE_CHOICE_CHOICES - 1): + choices.append(MultipleChoiceOption( + text=other_keyword.definition, + correct=False)) + + question_text = 'Pick the definition to match "{w}"'.format(w=keyword.word) + + return MultipleChoiceQuestion(question_text, choices) + + +def _create_keyword_definition_true_false(keyword, keywords): + true = random.choice((True, False)) + + if true: + definition = keyword.definition + else: + other_keyword = random.choice(keywords.exclude(id=keyword.id)) + definition = other_keyword.definition + + question_text = 'The keyword "{w}" matches the definition "{d}"'. \ + format(w=keyword.word, d=definition) + + return TrueFalseQuestion(question_text, true) + + +def _create_keyword_flashcard_blank(keyword): + return FlashCardQuestion(keyword.definition, keyword.word) + + +def quiz_from_keywords(note): + keywords = Keyword.objects.filter(note=note) + questions = [] + + for keyword in keywords: + if keyword.word and keyword.definition: + question_type = random.choice(GENERATED_QUESTION_TYPE) + + if question_type is KEYWORD_MULTIPLE_CHOICE: + questions.append(_create_keyword_multiple_choice(keyword, keywords)) + + elif question_type is DEFINITION_MULTIPLE_CHOICE: + questions.append(_create_definition_multiple_choice(keyword, keywords)) + + elif question_type is KEYWORD_DEFINITION_TRUE_FALSE: + questions.append(_create_keyword_definition_true_false(keyword, keywords)) + + return questions + + diff --git a/karmaworld/apps/quizzes/find_keywords.py b/karmaworld/apps/quizzes/find_keywords.py deleted file mode 100644 index ea6f340..0000000 --- a/karmaworld/apps/quizzes/find_keywords.py +++ /dev/null @@ -1,125 +0,0 @@ -#!/usr/bin/env python -# -*- coding:utf8 -*- -# Copyright (C) 2014 FinalsClub Foundation -from __future__ import division -from collections import defaultdict -import nltk -import itertools -from operator import itemgetter -from pygraph.classes.digraph import digraph -from pygraph.algorithms.pagerank import pagerank -from pygraph.classes.exceptions import AdditionError - - -def _filter(tagged, tags=('NN', 'JJ', 'NNP')): - pos_filtered = [item[0] for item in tagged if item[1] in tags] - stopwords_filtered = [word.lower() for word in pos_filtered if not word.lower() in nltk.corpus.stopwords.words('english')] - remove_punc = [item.replace('.', '') for item in stopwords_filtered] - return remove_punc - - -def _normalize(words): - lower = [word.lower() for word in words] - remove_punc = [item.replace('.', '') for item in lower] - return remove_punc - - -def _unique_everseen(iterable, key=None): - "List unique elements, preserving order. Remember all elements ever seen." - # unique_everseen('AAAABBBCCDAABBB') --> A B C D - # unique_everseen('ABBCcAD', str.lower) --> A B C D - seen = set() - seen_add = seen.add - if key is None: - for element in itertools.ifilterfalse(seen.__contains__, iterable): - seen_add(element) - yield element - else: - for element in iterable: - k = key(element) - if k not in seen: - seen_add(k) - yield element - - -def _common_ngrams(normalized_words, top_ordered_keywords, n): - ngrams_in_top_keywords = set() - common_ngrams = [] - - for ngram_words in itertools.product(top_ordered_keywords, repeat=n): - target_ngram = list(ngram_words) - for i in range(len(normalized_words)): - ngram = normalized_words[i:i+n] - if target_ngram == ngram: - ngrams_in_top_keywords.add(tuple(target_ngram)) - - for words in ngrams_in_top_keywords: - words_usage_in_ngram = 0 - individual_word_usage = defaultdict(lambda: 0) - for i in range(len(normalized_words)): - for word in words: - if normalized_words[i] == word: - individual_word_usage[word] += 1 - if normalized_words[i:i+n] == list(words): - words_usage_in_ngram += 1 - - for word in words: - ratio = words_usage_in_ngram / individual_word_usage[word] - if ratio > 0.5: - common_ngrams.append(words) - break - - return common_ngrams - - -def find_keywords(document, word_count=10): - """ - Credit to https://gist.github.com/voidfiles/1646117 - and http://acl.ldc.upenn.edu/acl2004/emnlp/pdf/Mihalcea.pdf - """ - sentences = nltk.sent_tokenize(document) - candidate_words = [] - all_words = [] - for sentence in sentences: - words = nltk.word_tokenize(sentence) - all_words.extend(words) - tagged_words = nltk.pos_tag(words) - filtered_words = _filter(tagged_words) - candidate_words.extend(filtered_words) - - unique_word_set = _unique_everseen(candidate_words) - - gr = digraph() - gr.add_nodes(list(unique_word_set)) - - window_start = 0 - window_end = 2 - - while 1: - window_words = candidate_words[window_start:window_end] - if len(window_words) == 2: - try: - gr.add_edge((window_words[0], window_words[1])) - except AdditionError: - pass - else: - break - - window_start += 1 - window_end += 1 - - calculated_page_rank = pagerank(gr) - di = sorted(calculated_page_rank.iteritems(), key=itemgetter(1), reverse=True) - all_ordered_keywords = [w[0] for w in di] - top_ordered_keywords = all_ordered_keywords[:word_count] - - normalized_words = _normalize(all_words) - - common_bigrams = _common_ngrams(normalized_words, top_ordered_keywords, 2) - common_trigrams = _common_ngrams(normalized_words, top_ordered_keywords, 3) - for words in common_bigrams + common_trigrams: - for word in words: - top_ordered_keywords.remove(word) - top_ordered_keywords.insert(0, ' '.join(words)) - - return top_ordered_keywords diff --git a/karmaworld/apps/quizzes/models.py b/karmaworld/apps/quizzes/models.py index 77a86a5..b618209 100644 --- a/karmaworld/apps/quizzes/models.py +++ b/karmaworld/apps/quizzes/models.py @@ -21,98 +21,3 @@ class Keyword(models.Model): def __unicode__(self): return self.word - -class Quiz(models.Model): - name = models.CharField(max_length=512) - note = models.ForeignKey('notes.Note', blank=True, null=True) - timestamp = models.DateTimeField(default=datetime.datetime.utcnow) - - class Meta: - verbose_name_plural = 'quizzes' - - def __unicode__(self): - return self.name - - -class BaseQuizQuestion(models.Model): - quiz = models.ForeignKey(Quiz) - timestamp = models.DateTimeField(default=datetime.datetime.utcnow) - - explanation = models.CharField(max_length=2048, blank=True, null=True) - - EASY = 'EASY' - MEDIUM = 'MEDIUM' - HARD = 'HARD' - DIFFICULTY_CHOICES = ( - (EASY, 'Easy'), - (MEDIUM, 'Medium'), - (HARD, 'Hard'), - ) - - 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, 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 - - -class MultipleChoiceOption(models.Model): - text = models.CharField(max_length=2048) - correct = models.BooleanField() - question = models.ForeignKey(MultipleChoiceQuestion, related_name="choices") - - def __unicode__(self): - return self.text - - -class FlashCardQuestion(BaseQuizQuestion): - keyword_side = models.CharField(max_length=2048, verbose_name='Keyword') - definition_side = models.CharField(max_length=2048, verbose_name='Definition') - - class Meta: - verbose_name = 'Flash card question' - - def __unicode__(self): - return self.keyword_side + ' / ' + self.definition_side - - -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) -ALL_QUESTION_CLASSES_NAMES = { - MultipleChoiceQuestion.__name__: MultipleChoiceQuestion, - FlashCardQuestion.__name__: FlashCardQuestion, - TrueFalseQuestion.__name__: TrueFalseQuestion, -} - diff --git a/karmaworld/assets/css/note_course_pages.css b/karmaworld/assets/css/note_course_pages.css index eca5bbd..1dec4fd 100644 --- a/karmaworld/assets/css/note_course_pages.css +++ b/karmaworld/assets/css/note_course_pages.css @@ -133,11 +133,63 @@ button.add-note-btn { padding: 5px; } -#keyword-intro { +#keyword-intro, +#quiz-intro { font-size: 0.8em; } +#quiz-intro { + margin-bottom: 10px; +} + #note-category { margin-bottom: 10px; } +.quiz-question { + margin-bottom: 20px; +} + +.quiz-question-inner { + padding: 10px 0 10px 0; +} + +.question-text { + margin: 0 0 10px 0; +} + +.quiz-question input[type="radio"]:checked+label { + font-weight: bold; +} + +.correct { + background-color: #e5ffdc; +} + +.incorrect { + background-color: #ffdcdc; +} + +.correct-label, +.incorrect-label { + padding-left: 0; +} + +.correct-label-inner, +.incorrect-label-inner { + color: #ffffff; + font-weight: bold; + padding: 5px 10px 5px 10px; + font-size: 0.8em; + width: intrinsic; /* Safari/WebKit uses a non-standard name */ + width: -moz-max-content; /* Firefox/Gecko */ +} + +.correct-label-inner { + background-color: #4f7342; +} + +.incorrect-label-inner { + background-color: #740b00; +} + diff --git a/karmaworld/assets/js/course-list.js b/karmaworld/assets/js/course-list.js index e526ef3..31d3c14 100644 --- a/karmaworld/assets/js/course-list.js +++ b/karmaworld/assets/js/course-list.js @@ -36,7 +36,7 @@ $(function() { }); // wire up search box - $('input.search-courses').keyup(function() { + $('#search-courses').keyup(function() { dataTable.fnFilter($(this).val()); }); diff --git a/karmaworld/assets/js/note-detail.js b/karmaworld/assets/js/note-detail.js index 81f2b96..1718ee0 100644 --- a/karmaworld/assets/js/note-detail.js +++ b/karmaworld/assets/js/note-detail.js @@ -292,4 +292,33 @@ function initNoteKeywordsPage() { } +function markQuestionCorrect(question) { + question.find('.quiz-question').addClass('correct'); + question.find('.quiz-question').removeClass('incorrect'); + question.find('.correct-label').show(); + question.find('.incorrect-label').hide(); +} + +function markQuestionIncorrect(question) { + question.find('.quiz-question').addClass('incorrect'); + question.find('.quiz-question').removeClass('correct'); + question.find('.incorrect-label').show(); + question.find('.correct-label').hide(); +} + +function initQuizPage() { + $('#check-answers-button').click(function() { + $('.quiz-question-wrapper').each(function() { + var choice = $(this).find('input:checked'); + if (choice.length) { + console.log(choice); + if (choice.data('correct') == true) { + markQuestionCorrect($(this)); + } else { + markQuestionIncorrect($(this)); + } + } + }); + }); +} diff --git a/karmaworld/templates/courses/course_list.html b/karmaworld/templates/courses/course_list.html index 96fd589..c33d718 100644 --- a/karmaworld/templates/courses/course_list.html +++ b/karmaworld/templates/courses/course_list.html @@ -52,7 +52,7 @@
- +
diff --git a/karmaworld/templates/notes/note_base.html b/karmaworld/templates/notes/note_base.html new file mode 100644 index 0000000..bb46f9d --- /dev/null +++ b/karmaworld/templates/notes/note_base.html @@ -0,0 +1,285 @@ +{% extends "base.html" %} +{% load url from future %} +{% load compress %} + +{% block title %} + {{ note.name }} +{% endblock %} + +{% block pagestyle %} + {% compress css %} + + + {% endcompress %} +{% endblock %} + +{% block pagescripts %} + + {% compress js %} + + + + + + {% endcompress %} + +{% endblock %} + +{% block raw_content %} +
+ + + +
+ + + +
+
+ {{ note.name }} + + + {% if user.is_authenticated %} + {% if already_thanked %} + + {% else %} + + + {% endif %} + {% else %} + + {% endif %} + + {% if user.is_authenticated %} + {% if already_flagged %} + + {% else %} + + + {% endif %} + {% else %} + + {% endif %} + + {% if user.is_authenticated %} + + + {% else %} + + {% endif %} + + {% if user.get_profile.can_edit_items and note.user != user %} + + {% endif %} + + {% if note.user == request.user %} +    + {% endif %} + + {% if note.license %} + {{ note.license.html|safe }} {% if note.upstream_link %}{{ note.upstream_link|slice:":80" }}{% endif %} + {% endif %} + + +
+
+ + {% if note.category %} +
+
+ {{ note.get_category_display }} +
+
+ {% endif %} + +
+
+ Tags: + + {% if note.tags.all %} + {% for tag in note.tags.all %} + + {{ tag.name }}{% if not forloop.last %}, {% endif %} + + {% endfor %} + {% else %} + (none defined yet) + {% endif %} + +
+
+ +
+ × +
+
+

Edit this note's tags

+ + +
+
+
+ +
+ × +
+
+

Edit Your Note

+
+
+
+ {% csrf_token %} + {{ note_delete_form }} + +
+
+
+
+
+ {% csrf_token %} +
+ {% with note_edit_form.name as field %} + {{ field.errors|safe }} + + {{ field }} +

{{ field.help_text }}

+ {% endwith %} +
+
+ {% with note_edit_form.tags as field %} + {{ field.errors|safe }} + + {{ field }} +

{{ field.help_text }}

+ {% endwith %} +
+
+ +
+
+
+
+ +
+ +
+
+
+
+ Note +
+
+ Key Terms & Definitions +
+
+ Quiz Questions +
+
+
+
+ {% if show_note_container %} + {% include 'notes/note_detail.html' %} + {% endif %} + {% if show_keywords %} + {% include 'notes/note_keywords.html' %} + {% endif %} + {% if show_quiz %} + {% include 'notes/note_quiz.html' %} + {% endif %} +
+
+ +
+ +
    +
  1. +

    You can highlight important words or phrases in this note to add definitions for them.

    +
  2. +
  3. +

    Keywords you define will appear here, and you can define new ones here too.

    +
  4. +
+ +{% endblock %} + + diff --git a/karmaworld/templates/notes/note_detail.html b/karmaworld/templates/notes/note_detail.html index 7fd36a0..0e8a125 100644 --- a/karmaworld/templates/notes/note_detail.html +++ b/karmaworld/templates/notes/note_detail.html @@ -1,381 +1,33 @@ -{% extends "base.html" %} -{% load url from future %} -{% load compress %} - -{% block title %} - {{ note.name }} -{% endblock %} - -{% block pagestyle %} - {% compress css %} - - - {% endcompress %} -{% endblock %} - -{% block pagescripts %} - - {% compress js %} - - - - - - {% endcompress %} - -{% endblock %} - -{% block raw_content %} -
- - - -
- - - -
-
- {{ note.name }} - - - {% if user.is_authenticated %} - {% if already_thanked %} - - {% else %} - - - {% endif %} - {% else %} - - {% endif %} - - {% if user.is_authenticated %} - {% if already_flagged %} - - {% else %} - - - {% endif %} - {% else %} - - {% endif %} - - {% if user.is_authenticated %} - - - {% else %} - - {% endif %} - - {% if user.get_profile.can_edit_items and note.user != user %} - - {% endif %} - - {% if note.user == request.user %} -    - {% endif %} - - {% if note.license %} - {{ note.license.html|safe }} {% if note.upstream_link %}{{ note.upstream_link|slice:":80" }}{% endif %} - {% endif %} - - -
-
- - {% if note.category %} -
-
- {{ note.get_category_display }} -
+
+ {% if pdf_controls %} +
+
+
- {% endif %} - -
-
- Tags: - - {% if note.tags.all %} - {% for tag in note.tags.all %} - - {{ tag.name }}{% if not forloop.last %}, {% endif %} - - {% endfor %} - {% else %} - (none defined yet) - {% endif %} - -
+
+ Jump to page: +
- -
- × -
-
-

Edit this note's tags

- - -
-
-
- -
- × -
-
-

Edit Your Note

-
-
-
- {% csrf_token %} - {{ note_delete_form }} - -
-
-
-
-
- {% csrf_token %} -
- {% with note_edit_form.name as field %} - {{ field.errors|safe }} - - {{ field }} -

{{ field.help_text }}

- {% endwith %} -
-
- {% with note_edit_form.tags as field %} - {{ field.errors|safe }} - - {{ field }} -

{{ field.help_text }}

- {% endwith %} -
-
- -
-
-
-
- -
- -
-
-
-
- Note -
-
- Key Terms & Definitions -
-
-
-
- {% if show_note_container %} -
- {% if pdf_controls %} -
-
- -
-
- Jump to page: - -
-
- - -
-
- {% endif %} - -
-
-
- {% if note.has_markdown %} - - {% else %} - - - {% endif %} -
-
-
-
- {% endif %} - {% if show_keywords %} -
-
-
- {% if user.is_authenticated %} -

These key terms and definitions have been defined by KarmaNotes users. - You can edit them for accuracy and add more if you like.

-

- {% else %} -

These key terms and definitions have been defined by KarmaNotes users.

- {% endif %} - - - - - - - - {% for keyword in keywords %} - - - - - {% endfor %} -
Key TermsDefinitions
{{ keyword.word }} - {% if keyword.definition %} - {{ keyword.definition }} - {% else %} - (not defined yet) - {% endif %} -
- -
- {% csrf_token %} - {{ keyword_formset.management_form }} -
-
-
- {{ keyword_prototype_form.keyword }} -
-
- {{ keyword_prototype_form.definition }} - {{ keyword_prototype_form.id }} -
-
-
-
-
- {% for form_row in keyword_formset %} -
-
- {{ form_row.keyword }} -
-
- {{ form_row.definition }} - {{ form_row.id }} -
-
-
- {% endfor %} -
-
-
-

(Click or press TAB in the last definition for another row)

-
-
-
-
- -
-
- -
-
-
-
-
-
- {% endif %} +
+ +
- -
- -
    -
  1. -

    You can highlight important words or phrases in this note to add definitions for them.

    -
  2. -
  3. -

    Keywords you define will appear here, and you can define new ones here too.

    -
  4. -
- -{% endblock %} - - + {% endif %} + +
+
+
+ {% if note.has_markdown %} + + {% else %} + + + {% endif %} +
+
+
+ \ No newline at end of file diff --git a/karmaworld/templates/notes/note_keywords.html b/karmaworld/templates/notes/note_keywords.html new file mode 100644 index 0000000..483f3f5 --- /dev/null +++ b/karmaworld/templates/notes/note_keywords.html @@ -0,0 +1,78 @@ + +
+
+
+ {% if user.is_authenticated %} +

These key terms and definitions have been defined by KarmaNotes users. + You can edit them for accuracy and add more if you like.

+

+ {% else %} +

These key terms and definitions have been defined by KarmaNotes users.

+ {% endif %} + + + + + + + + {% for keyword in keywords %} + + + + + {% endfor %} +
Key TermsDefinitions
{{ keyword.word }} + {% if keyword.definition %} + {{ keyword.definition }} + {% else %} + (not defined yet) + {% endif %} +
+ +
+ {% csrf_token %} + {{ keyword_formset.management_form }} +
+
+
+ {{ keyword_prototype_form.keyword }} +
+
+ {{ keyword_prototype_form.definition }} + {{ keyword_prototype_form.id }} +
+
+
+
+
+ {% for form_row in keyword_formset %} +
+
+ {{ form_row.keyword }} +
+
+ {{ form_row.definition }} + {{ form_row.id }} +
+
+
+ {% endfor %} +
+
+
+

(Click or press TAB in the last definition for another row)

+
+
+
+
+ +
+
+ +
+
+
+
+
+
diff --git a/karmaworld/templates/notes/note_quiz.html b/karmaworld/templates/notes/note_quiz.html new file mode 100644 index 0000000..64d7a0c --- /dev/null +++ b/karmaworld/templates/notes/note_quiz.html @@ -0,0 +1,74 @@ +
+
+
+
+ {% if questions %} + These quiz questions have been randomly generated from the keywords and definitions associated + with this note. Refresh to get different questions. + {% else %} + No keywords with definitions have been supplied, so we can't generate any quiz questions. + Provide some key terms with their definitions, and we'll create questions based on them. + {% endif %} +
+
+
+ + {% for question in questions %} +
+
+
+
Correct
+
+
+
+
+
Incorrect
+
+
+
+
+
+
{{ forloop.counter }}. {{ question.question_text }}
+ {% if question.question_type == 'MultipleChoiceQuestion' %} + {% for choice in question.choices %} +
+ + +
+ {% endfor %} + {% endif %} + {% if question.question_type == 'TrueFalseQuestion' %} +
+ + +
+
+ + +
+ {% endif %} +
+
+
+
+ {% endfor %} + +
+
+ +
+
+ +
\ No newline at end of file diff --git a/karmaworld/urls.py b/karmaworld/urls.py index 6df8041..6f6e855 100644 --- a/karmaworld/urls.py +++ b/karmaworld/urls.py @@ -14,7 +14,7 @@ from karmaworld.apps.courses.views import CourseListView from karmaworld.apps.courses.views import school_course_list from karmaworld.apps.courses.views import school_course_instructor_list from karmaworld.apps.notes.views import NoteView, thank_note, NoteSearchView, flag_note, downloaded_note, edit_note_tags, \ - NoteKeywordsView, edit_note, NoteDeleteView + NoteKeywordsView, NoteQuizView, edit_note, NoteDeleteView from karmaworld.apps.moderation import moderator from karmaworld.apps.document_upload.views import save_fp_upload from karmaworld.apps.quizzes.views import set_delete_keyword_annotator, get_keywords_annotator @@ -111,6 +111,8 @@ urlpatterns = patterns('', NoteView.as_view(), name='note_detail'), url(r'^note/' + SLUG.format('school_') + '/' + SLUG.format('course_') +'/'+ SLUG.format('') +'/keywords/$', NoteKeywordsView.as_view(), name='note_keywords'), + url(r'^note/' + SLUG.format('school_') + '/' + SLUG.format('course_') +'/'+ SLUG.format('') +'/quiz/$', + NoteQuizView.as_view(), name='note_quiz'), url(r'note/delete/$', NoteDeleteView.as_view(), name='note_delete'), diff --git a/reqs/common.txt b/reqs/common.txt index c03a7fb..55dbbda 100644 --- a/reqs/common.txt +++ b/reqs/common.txt @@ -25,8 +25,4 @@ django-reversion django-ajax-selects git+https://github.com/btbonval/django-ajax-selects-cascade.git psycopg2 -git+https://github.com/Soaa-/django-nested-inlines.git pyth -http://python-graph.googlecode.com/files/python-graph-core-1.8.2.tar.gz -nltk -numpy -- 2.25.1