--- /dev/null
+#!/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)
+
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'
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)
--- /dev/null
+#!/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
# 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
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()):
padding-bottom: 3px;
}
+#note_content, #course_content, #school_content, #results_content
+{
+ padding-top: 46px;
+}
+
+
/* INTERFACE ELEMENTS */
.hero_gradient_bar
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;
}
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;
}
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;
--- /dev/null
+
+#results_header {
+ padding-bottom: 20px;
+ margin-bottom: 20px;
+}
--- /dev/null
+
+PRIVATE_URL = ''
+
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 }}
{% 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-->
<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> back to {{ note.course.name }}
</a>
</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>
--- /dev/null
+{% 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 %}
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
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'),
)
pyopenssl
python-twitter
gdshortener
+git+https://github.com/flaptor/indextank-py.git