Search notes using IndexDen
authorCharles Connell <charles@connells.org>
Mon, 30 Dec 2013 22:21:31 +0000 (17:21 -0500)
committerCharles Connell <charles@connells.org>
Tue, 31 Dec 2013 04:20:24 +0000 (23:20 -0500)
14 files changed:
karmaworld/apps/notes/management/commands/populate_indexden.py [new file with mode: 0644]
karmaworld/apps/notes/models.py
karmaworld/apps/notes/search.py [new file with mode: 0644]
karmaworld/apps/notes/views.py
karmaworld/assets/css/global.css
karmaworld/assets/css/note_course_pages.css
karmaworld/assets/css/search.css [new file with mode: 0644]
karmaworld/secret/indexden.py.example [new file with mode: 0644]
karmaworld/templates/courses/course_detail.html
karmaworld/templates/header.html
karmaworld/templates/notes/note_detail.html
karmaworld/templates/notes/search_results.html [new file with mode: 0644]
karmaworld/urls.py
reqs/common.txt

diff --git a/karmaworld/apps/notes/management/commands/populate_indexden.py b/karmaworld/apps/notes/management/commands/populate_indexden.py
new file mode 100644 (file)
index 0000000..4573b8c
--- /dev/null
@@ -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)
+
index 052bdb743101b0ad410e25b7efbba15653f9a6ec..37c20299eb7bf7a779945a7402326ee14a1ed785 100644 (file)
@@ -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 (file)
index 0000000..e47656f
--- /dev/null
@@ -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
index 977eb28118f96eb35961a1b57a0c025766975fbe..b8a33763d1c6fb65c8df1df1aa588a30a1d532aa 100644 (file)
@@ -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()):
index 77e6d4fd84ca78a27928504ae570de7467940ee4..264f7b4ffdc5093b74b271d8bcf6dc2c6c7bb482 100644 (file)
@@ -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;
 }
index a39868c84ab0a914da1803fc4dc32ea62a061caa..70d24e07bb9c9f88510cd56bbab9db56f1346344 100644 (file)
@@ -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 (file)
index 0000000..4eb45e3
--- /dev/null
@@ -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 (file)
index 0000000..1316039
--- /dev/null
@@ -0,0 +1,3 @@
+
+PRIVATE_URL = ''
+
index 8c120171b22ef93c2a0e815ca0ea8b3f942f4d48..b8f14254a258263a72e19180a6f6fae40a922610 100644 (file)
   Share Notes for {{ course.name }} | {{ course.school.name }}
 {% endblock %}
 
-
 {% block content %}
   <section id="course_content">
 
     <div id="course_header" class="hero_gradient_bar">
       <div class="row">
-        <div id="note_name" class="small-12 columns">
+        <div class="small-12 columns header_title">
           {% if course.url %}
           <a rel="nofollow" href="{{ course.url }}">
             {{ course.name }}
index dc07942f6e608d3585415f940af77562355198d1..e47245fe487c74656f2bb5bd4c0a266f023956a3 100644 (file)
@@ -1,30 +1,36 @@
 {% load url from future %}
 
-{# Logged out #}
-  <header id="global_header">
-    <div class="row">
-      <div id="logo_container" class="small-6 columns">
-        <a href="/">
-          <img src="{{ STATIC_URL }}img/global_header_logo.png" alt="global_header_logo" width="144" height="17" />
-        </a>
-      </div><!--/logo container-->
+<header id="global_header">
+  <div class="row">
+    <div id="logo_container" class="small-6 large-3 columns">
+      <a href="/">
+        <img src="{{ STATIC_URL }}img/global_header_logo.png" alt="global_header_logo" width="144" height="17" />
+      </a>
+    </div><!--/logo container-->
 
-      {% if display_add_course %}
-        <div class="small-2 columns small-offset-3 show-for-medium-up">
-          <a id="add_course_header_button" href="#"><img src="{{ STATIC_URL }}img/global_header_pluscourse.png" /></a>
+    <div class="small-4 columns show-for-medium-up">
+      {% if course.id %}
+        <div id="search_container">
+          <form action="{% url 'note_search' %}" method="GET">
+            <input type="text" id="search_txt" name="query" placeholder="Search in this course">
+            <input type="hidden" name="course_id" value="{{ course.id }}">
+          </form>
         </div>
       {% endif %}
+    </div>
 
-      {% if display_add_note %}
-        <div class="small-2 columns small-offset-3 show-for-medium-up">
-          <a id="add_note_header_button" href="#" onclick="$('.add-note-btn').click();">
-            <img src="{{ STATIC_URL }}img/global_header_plusnote.png" /></a>
-        </div>
-      {% endif %}
+    {% if display_add_course %}
+      <div class="small-2 columns small-offset-3 show-for-medium-up">
+        <a id="add_course_header_button" href="#"><img src="{{ STATIC_URL }}img/global_header_pluscourse.png" /></a>
+      </div>
+    {% endif %}
 
-      <div id="login_container" class="small-1 columns">
-        <a class=white href="{% url 'about' %}">About</a>
+    {% if display_add_note %}
+      <div class="small-2 columns small-offset-3 show-for-medium-up">
+        <a id="add_note_header_button" href="#" onclick="$('.add-note-btn').click();">
+          <img src="{{ STATIC_URL }}img/global_header_plusnote.png" /></a>
       </div>
+    {% endif %}
 
-    </div>
-  </header><!--/global header-->
+  </div>
+</header><!--/global header-->
index e494c3f1a6b862293be92db320419fc7b96d7d85..f617e8ec9e4ac9db2ebe530d9e012b459ddb248c 100644 (file)
@@ -24,7 +24,7 @@
 
     <div id="note_header" class="hero_gradient_bar">
       <div class="row">
-        <div id="note_back_to_course" class="twelve columns">
+        <div class="twelve columns header_subhead">
           <a href="{{note.course.get_absolute_url}}">
             <i class="fa fa-arrow-left"></i>&nbsp;back to {{ note.course.name }}
           </a>
@@ -32,7 +32,7 @@
       </div>
 
       <div class="row">
-        <div id="note_name" class="small-12 columns">
+        <div class="small-12 columns header_title">
           {{ note.name }}
         </div><!-- /note_name -->
       </div>
diff --git a/karmaworld/templates/notes/search_results.html b/karmaworld/templates/notes/search_results.html
new file mode 100644 (file)
index 0000000..7792f8e
--- /dev/null
@@ -0,0 +1,55 @@
+{% extends "base.html" %}
+{% load url from future %}
+
+{% block title %}
+  Search Results
+{% endblock %}
+
+{% block pagestyle %}
+  <link rel="stylesheet" type="text/css" media="all" href="{{ STATIC_URL }}css/search.css">
+{% endblock %}
+
+{% block content %}
+<section id="results_content">
+
+  <div id="results_header" class="hero_gradient_bar">
+    <div class="row">
+      <div class="small-12 columns header_subhead">
+        YOU SEARCHED FOR
+      </div>
+    </div>
+
+    <div class="row">
+      <div class="twelve columns header_title">
+        {{ query }}
+      </div>
+    </div>
+
+    {% if course %}
+      <div class="row">
+        <div class="small-12 columns header_subhead">
+          IN COURSE
+        </div>
+      </div>
+
+      <div class="row">
+        <div class="twelve columns header_title">
+          {{ course.name }}
+        </div>
+      </div>
+    {% endif %}
+  </div>
+
+  <div id="results_container">
+    <div class="row">
+      <div class="small-12 columns large-10 large-offset-1">
+        {% for note in object_list %}
+          {% include 'notes/note_list_entry.html' with note=note %}
+        {% endfor %}
+      </div>
+    </div>
+  </div><!-- /course_container -->
+
+</section>
+
+{% endblock %}
index fd827216572e43622a6cadff2718ee660b123973..4d6846c6713abd5f4013a76bb8e227991000c307 100644 (file)
@@ -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'),
 )
index bc168eef7adc8776aa1f58df62597eb82f4ac6c1..bb5b20c89d129ffa79b0adc6c29eaa2e98b1206b 100644 (file)
@@ -18,3 +18,4 @@ beautifulsoup4
 pyopenssl
 python-twitter
 gdshortener
+git+https://github.com/flaptor/indextank-py.git