From 161dbc15a3e104e86760d1cc6d094e121dab7852 Mon Sep 17 00:00:00 2001 From: Charles Connell Date: Mon, 30 Dec 2013 17:21:31 -0500 Subject: [PATCH] Search notes using IndexDen --- .../management/commands/populate_indexden.py | 21 +++++++ karmaworld/apps/notes/models.py | 30 ++++++++-- karmaworld/apps/notes/search.py | 47 ++++++++++++++++ karmaworld/apps/notes/views.py | 32 ++++++++++- karmaworld/assets/css/global.css | 23 +++++++- karmaworld/assets/css/note_course_pages.css | 22 -------- karmaworld/assets/css/search.css | 5 ++ karmaworld/secret/indexden.py.example | 3 + .../templates/courses/course_detail.html | 3 +- karmaworld/templates/header.html | 48 +++++++++------- karmaworld/templates/notes/note_detail.html | 4 +- .../templates/notes/search_results.html | 55 +++++++++++++++++++ karmaworld/urls.py | 5 +- reqs/common.txt | 1 + 14 files changed, 240 insertions(+), 59 deletions(-) create mode 100644 karmaworld/apps/notes/management/commands/populate_indexden.py create mode 100644 karmaworld/apps/notes/search.py create mode 100644 karmaworld/assets/css/search.css create mode 100644 karmaworld/secret/indexden.py.example create mode 100644 karmaworld/templates/notes/search_results.html diff --git a/karmaworld/apps/notes/management/commands/populate_indexden.py b/karmaworld/apps/notes/management/commands/populate_indexden.py new file mode 100644 index 0000000..4573b8c --- /dev/null +++ b/karmaworld/apps/notes/management/commands/populate_indexden.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python +# -*- coding:utf8 -*- +# Copyright (C) 2013 FinalsClub Foundation + +from django.core.management.base import BaseCommand +from karmaworld.apps.notes.models import Note +from karmaworld.apps.notes.search import * + +class Command(BaseCommand): + args = 'none' + help = "Populate the search index in IndexDen with all the notes" \ + "in the database. Will not clear the index beforehand, so notes" \ + "in the index that are not overwritten will still be around." + + def handle(self, *args, **kwargs): + notes = Note.objects.all() + + for note in notes: + print "Indexing {n}".format(n=note) + add_document(note) + diff --git a/karmaworld/apps/notes/models.py b/karmaworld/apps/notes/models.py index 052bdb7..37c2029 100644 --- a/karmaworld/apps/notes/models.py +++ b/karmaworld/apps/notes/models.py @@ -20,14 +20,13 @@ from django.db import models from django.template import defaultfilters import django_filepicker from lxml.html import fromstring, tostring -from oauth2client.client import Credentials from taggit.managers import TaggableManager from karmaworld.apps.courses.models import Course -from karmaworld.apps.users.models import KarmaUser +import karmaworld.apps.notes.search as search try: - from secrets.drive import GOOGLE_USER + from karmaworld.secrets.drive import GOOGLE_USER except: GOOGLE_USER = u'admin@karmanotes.org' @@ -230,10 +229,29 @@ def update_note_counts(note_instance): note_instance.course.school.update_note_count() @receiver(post_save, sender=Note, weak=False) -def note_receiver(sender, **kwargs): +def note_save_receiver(sender, **kwargs): + if not 'instance' in kwargs: + return + note = kwargs['instance'] + + # Update course and school counts of how + # many notes they have if kwargs['created']: - update_note_counts(kwargs['instance']) + update_note_counts(note) + + # Add or update document in search index + search.add_document(note) + @receiver(post_delete, sender=Note, weak=False) -def note_receiver(sender, **kwargs): +def note_delete_receiver(sender, **kwargs): + if not 'instance' in kwargs: + return + note = kwargs['instance'] + + # Update course and school counts of how + # many notes they have update_note_counts(kwargs['instance']) + + # Remove document from search index + search.remove_document(note) diff --git a/karmaworld/apps/notes/search.py b/karmaworld/apps/notes/search.py new file mode 100644 index 0000000..e47656f --- /dev/null +++ b/karmaworld/apps/notes/search.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python +# -*- coding:utf8 -*- +# Copyright (C) 2013 FinalsClub Foundation + +import indextank.client as itc +import karmaworld.secret.indexden as secret + +api_client = itc.ApiClient(secret.PRIVATE_URL) +index = api_client.get_index('karmanotes') + + +def note_to_dict(note): + d = { + 'name': note.name, + } + + if note.text: + d['text'] = note.text + + if note.tags.exists(): + d['tags'] = [str(tag) for tag in note.tags.all()] + + if note.desc: + d['desc'] = note.desc + + if note.course: + d['course_id'] = note.course.id + + return d + +def add_document(note): + index.add_document(note.id, note_to_dict(note)) + +def remove_document(note): + index.delete_document(note.id) + +def search(query, course_id=None): + """Returns note IDs matching the given query, + filtered by course ID if given""" + if course_id: + results = index.search('(text:"%s" OR name:"%s") AND course_id:%s' % (query, query, course_id)) + else: + results = index.search('text:"%s" OR name:"%s"' % (query, query)) + + matching_note_ids = [r['docid'] for r in results['results']] + + return matching_note_ids \ No newline at end of file diff --git a/karmaworld/apps/notes/views.py b/karmaworld/apps/notes/views.py index 977eb28..b8a3376 100644 --- a/karmaworld/apps/notes/views.py +++ b/karmaworld/apps/notes/views.py @@ -3,18 +3,20 @@ # Copyright (C) 2012 FinalsClub Foundation import json from django.core.exceptions import ObjectDoesNotExist +from karmaworld.apps.courses.models import Course +from karmaworld.apps.notes import search import os from django.conf import settings from django.contrib.sites.models import Site from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseNotFound -from django.views.generic import DetailView +from django.views.generic import DetailView, ListView from django.views.generic import FormView from django.views.generic import View from django.views.generic import TemplateView from django.views.generic.detail import SingleObjectMixin -from django.shortcuts import get_object_or_404 +from django.shortcuts import get_object_or_404, render_to_response from karmaworld.apps.notes.models import Note from karmaworld.apps.notes.forms import NoteForm @@ -147,6 +149,32 @@ class PDFView(DetailView): return super(PDFView, self).get_context_data(**kwargs) + +class NoteSearchView(ListView): + template_name = 'notes/search_results.html' + + def get_queryset(self): + if not 'query' in self.request.GET: + return Note.objects.none() + + if 'course_id' in self.request.GET: + matching_note_ids = search.search(self.request.GET['query'], + self.request.GET['course_id']) + else: + matching_note_ids = search.search(self.request.GET['query']) + + return Note.objects.filter(id__in=matching_note_ids) + + def get_context_data(self, **kwargs): + if 'query' in self.request.GET: + kwargs['query'] = self.request.GET['query'] + + if 'course_id' in self.request.GET: + kwargs['course'] = Course.objects.get(id=self.request.GET['course_id']) + + return super(NoteSearchView, self).get_context_data(**kwargs) + + def thank_note(request, pk): """Record that somebody has thanked a note.""" if not (request.method == 'POST' and request.is_ajax()): diff --git a/karmaworld/assets/css/global.css b/karmaworld/assets/css/global.css index 77e6d4f..264f7b4 100644 --- a/karmaworld/assets/css/global.css +++ b/karmaworld/assets/css/global.css @@ -304,6 +304,12 @@ input[type="text"]:focus, textarea:focus padding-bottom: 3px; } +#note_content, #course_content, #school_content, #results_content +{ + padding-top: 46px; +} + + /* INTERFACE ELEMENTS */ .hero_gradient_bar @@ -657,6 +663,16 @@ ul#course_menu li a height: 42px; } +.header_subhead +{ + font-family: "MuseoSans-900"; + font-size: 10px; + text-align: center; + text-transform: uppercase; + padding-top: 20px; + padding-bottom: 1px; +} + #course_list_filter { margin: 0; } @@ -759,14 +775,15 @@ p.text a background-color: white; } -#note_name, #course_name, #school_name +.header_title { margin-top: 1em; text-align: center; - font-family: "MuseoSlab-300"; - font-size: 22px; + font-family: "MuseoSlab-500"; + font-size: 24px; } + #course_name a { color: black; } diff --git a/karmaworld/assets/css/note_course_pages.css b/karmaworld/assets/css/note_course_pages.css index a39868c..70d24e0 100644 --- a/karmaworld/assets/css/note_course_pages.css +++ b/karmaworld/assets/css/note_course_pages.css @@ -6,34 +6,12 @@ margin-bottom: 20px; } -#note_content, #course_content, #school_content -{ - padding-top: 46px; -} - -#note_back_to_course, #course_subhead, #school_subhead -{ - font-family: "MuseoSans-900"; - font-size: 10px; - text-align: center; - text-transform: uppercase; - padding-top: 20px; - padding-bottom: 1px; -} - #note_back_to_course img { margin-bottom: -1px; margin-right: 3px; } -#note_name, #school_name -{ - text-align: center; - font-family: "MuseoSlab-500"; - font-size: 24px; -} - #note_status { text-align: center; diff --git a/karmaworld/assets/css/search.css b/karmaworld/assets/css/search.css new file mode 100644 index 0000000..4eb45e3 --- /dev/null +++ b/karmaworld/assets/css/search.css @@ -0,0 +1,5 @@ + +#results_header { + padding-bottom: 20px; + margin-bottom: 20px; +} diff --git a/karmaworld/secret/indexden.py.example b/karmaworld/secret/indexden.py.example new file mode 100644 index 0000000..1316039 --- /dev/null +++ b/karmaworld/secret/indexden.py.example @@ -0,0 +1,3 @@ + +PRIVATE_URL = '' + diff --git a/karmaworld/templates/courses/course_detail.html b/karmaworld/templates/courses/course_detail.html index 8c12017..b8f1425 100644 --- a/karmaworld/templates/courses/course_detail.html +++ b/karmaworld/templates/courses/course_detail.html @@ -19,13 +19,12 @@ Share Notes for {{ course.name }} | {{ course.school.name }} {% endblock %} - {% block content %}
-
+
{% if course.url %} {{ course.name }} diff --git a/karmaworld/templates/header.html b/karmaworld/templates/header.html index dc07942..e47245f 100644 --- a/karmaworld/templates/header.html +++ b/karmaworld/templates/header.html @@ -1,30 +1,36 @@ {% load url from future %} -{# Logged out #} - diff --git a/karmaworld/templates/notes/note_detail.html b/karmaworld/templates/notes/note_detail.html index e494c3f..f617e8e 100644 --- a/karmaworld/templates/notes/note_detail.html +++ b/karmaworld/templates/notes/note_detail.html @@ -24,7 +24,7 @@
-
+
-
+
{{ note.name }}
diff --git a/karmaworld/templates/notes/search_results.html b/karmaworld/templates/notes/search_results.html new file mode 100644 index 0000000..7792f8e --- /dev/null +++ b/karmaworld/templates/notes/search_results.html @@ -0,0 +1,55 @@ +{% extends "base.html" %} +{% load url from future %} + +{% block title %} + Search Results +{% endblock %} + +{% block pagestyle %} + +{% endblock %} + +{% block content %} +
+ +
+
+
+ YOU SEARCHED FOR +
+
+ +
+
+ {{ query }} +
+
+ + {% if course %} +
+
+ IN COURSE +
+
+ +
+
+ {{ course.name }} +
+
+ {% endif %} +
+ +
+
+
+ {% for note in object_list %} + {% include 'notes/note_list_entry.html' with note=note %} + {% endfor %} +
+
+
+ +
+ +{% endblock %} diff --git a/karmaworld/urls.py b/karmaworld/urls.py index fd82721..4d6846c 100644 --- a/karmaworld/urls.py +++ b/karmaworld/urls.py @@ -15,7 +15,8 @@ from karmaworld.apps.courses.views import CourseListView from karmaworld.apps.courses.views import school_list 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 +from karmaworld.apps.notes.models import Note +from karmaworld.apps.notes.views import NoteView, thank_note, NoteSearchView from karmaworld.apps.notes.views import RawNoteDetailView from karmaworld.apps.notes.views import PDFView from karmaworld.apps.moderation import moderator @@ -100,5 +101,7 @@ urlpatterns = patterns('', url(r'^school/course/instructors/list/$', school_course_instructor_list, name='json_school_course_instructor_list'), # ---- end JSON views ----# + url(r'^search/$', NoteSearchView.as_view(), name='note_search'), + url(r'^$', CourseListView.as_view(model=Course), name='home'), ) diff --git a/reqs/common.txt b/reqs/common.txt index bc168ee..bb5b20c 100644 --- a/reqs/common.txt +++ b/reqs/common.txt @@ -18,3 +18,4 @@ beautifulsoup4 pyopenssl python-twitter gdshortener +git+https://github.com/flaptor/indextank-py.git -- 2.25.1