got search passably working and a somewhat functional ajax add to list

pull/1/head
Ed Summers 2011-09-29 02:23:50 -04:00
parent 83f553dc36
commit 409df080ce
12 changed files with 259 additions and 18 deletions

View File

@ -9,7 +9,7 @@ from tastypie import fields
from tastypie.constants import ALL, ALL_WITH_RELATIONS
from tastypie.resources import ModelResource, Resource, Bundle
from tastypie.utils import trailing_slash
from tastypie.authentication import ApiKeyAuthentication
from tastypie.authentication import ApiKeyAuthentication, Authentication
from regluit.core import models
@ -17,6 +17,7 @@ from regluit.core import models
logger = logging.getLogger(__name__)
class UserResource(ModelResource):
class Meta:
authentication = ApiKeyAuthentication()
@ -97,6 +98,7 @@ class EditionCoverResource(ModelResource):
class WishlistResource(ModelResource):
user = fields.ToOneField(UserResource, 'user')
works = fields.ToManyField(WorkResource, 'works')
class Meta:
authentication = ApiKeyAuthentication()
queryset = models.Wishlist.objects.all()

View File

@ -1,4 +1,5 @@
from django.db import models
from django.db.models import Q
from django.contrib.auth.models import User
@ -15,16 +16,22 @@ class Campaign(models.Model):
def __unicode__(self):
return u"Campaign for %s" % self.work.title
def cover_image_small(self):
first_isbn = self.work.editions.all()[0].isbn_10
return "http://covers.openlibrary.org/b/isbn/%s-S.jpg" % first_isbn
class Work(models.Model):
created = models.DateTimeField(auto_now_add=True)
title = models.CharField(max_length=1000)
openlibrary_id = models.CharField(max_length=50, null=True)
@classmethod
def get_by_isbn(klass, isbn):
for w in Work.objects.filter(Q(editions__isbn_10=isbn) | Q(editions__isbn_13=isbn)):
return w
return None
def cover_image_small(self):
first_isbn = self.editions.all()[0].isbn_10
return "http://covers.openlibrary.org/b/isbn/%s-S.jpg" % first_isbn
def __unicode__(self):
return self.title
@ -62,6 +69,11 @@ class Edition(models.Model):
def __unicode__(self):
return self.title
@classmethod
def get_by_isbn(klass, isbn):
for e in Edition.objects.filter(Q(isbn_10=isbn) | Q(isbn_13=isbn)):
return e
return None
class EditionCover(models.Model):
openlibrary_id = models.IntegerField()

47
core/search.py Normal file
View File

@ -0,0 +1,47 @@
import json
import requests
def gluejar_search(q):
"""normalizes results from the google books search suitable for gluejar
"""
results = []
for item in googlebooks_search(q)['items']:
# TODO: better to think in terms of editions with titles
# instead of titles with names?
v = item['volumeInfo']
r = {'title': v.get('title', ""),
'description': v.get('description', ""),
'publisher': v.get('publisher', ""),
'google_id': item.get('selfLink')}
# TODO: allow multiple authors
if v.has_key('authors') and len(v['authors']) > 0:
r['author'] = v['authors'][0]
else:
r['author'] = ""
# pull out isbns
for i in v.get('industryIdentifiers', []):
if i['type'] == 'ISBN_13':
r['isbn_13'] = i['identifier']
if i['type'] == 'ISBN_10':
r['isbn_10'] = i['identifier']
# cover image
if v.has_key('imageLinks'):
r['image'] = v['imageLinks'].get('smallThumbnail', "")
else:
r['image'] = ""
results.append(r)
return results
def googlebooks_search(q):
# XXX: need to pass IP address of user in from the frontend
headers = {'X-Forwarded-For': '69.243.24.29'}
r = requests.get('https://www.googleapis.com/books/v1/volumes',
params={'q': q}, headers=headers)
return json.loads(r.content)

View File

@ -1,6 +1,6 @@
from django.test import TestCase
from regluit.core import bookloader, models
from regluit.core import bookloader, models, search
class TestBooks(TestCase):
@ -41,3 +41,26 @@ class TestBooks(TestCase):
self.assertEqual(models.Work.objects.all().count(), 1)
self.assertEqual(models.Subject.objects.all().count(), 18)
class SearchTests(TestCase):
def test_basic_search(self):
results = search.gluejar_search('melville')
self.assertEqual(len(results), 10)
r = results[0]
self.assertTrue(r.has_key('name'))
self.assertTrue(r.has_key('author'))
self.assertTrue(r.has_key('description'))
self.assertTrue(r.has_key('image'))
self.assertTrue(r.has_key('publisher'))
self.assertTrue(r.has_key('identifier'))
for r in results:
for i in r['identifier']:
self.assertTrue(i.has_key('name'))
self.assertTrue(i.has_key('value'))
def test_googlebooks_search(self):
response = search.googlebooks_search('melville')
self.assertEqual(len(response['items']), 10)

View File

@ -8,6 +8,7 @@
<link type="text/css" rel="stylesheet" href="/static/css/book_list.css" />
<script type="text/javascript" src="/static/js/jquery-1.6.3.min.js"></script>
<script type="text/javascript" src="/static/js/book-panel.js"></script>
{% block extra_head %}{% endblock %}
</head>
<body>

View File

@ -6,7 +6,7 @@
<li>
<div class="js-search" style="margin-top: 10px; margin-bottom: 10px;">
<form action="{% url search %}" method="get"> <input type="text" onfocus="if (this.value=='Search for a book...') this.value='';" onblur="if (this.value=='') this.value='Search for a book...';" value="Search for a book..." size="20" class="inputbox" id="ssearchword" name="q">
<form action="{% url search %}" method="get"><input type="text" placeholder="Search for a book..." size="20" class="inputbox" id="ssearchword" name="q" value="{{ q }}">
</form>
<input type="button" onclick="this.form.searchword.focus();" class="button" value="Search">
</div>

View File

@ -0,0 +1,95 @@
{% extends "base.html" %}
{% block extra_head %}
<script type="text/javascript">
$(document).ready(function() {
$(".add-wishlist").each(function (index, element) {
$(element).click(function() {
var span = $(element).find("span");
var isbn = span.attr('id')
if (!isbn) return;
$.post('/wishlist/', {'isbn': isbn}, function(data) {
span.fadeOut();
var newSpan = $("<span>On your wishlist!</span>").hide();
span.replaceWith(newSpan);
newSpan.fadeIn();
newSpan.removeAttr("id");
});
});
});
});
</script>
{% endblock %}
{% block title %}Search Results{% endblock %}
{% block content %}
<div id="main-container">
<div class="js-main">
{% include "explore.html" %}
<div id="js-maincol-fr">
<div class="js-maincol-inner">
<div class="content-block">
<div class="content-block-heading">
<h2 class="content-heading">Search Results</span></h2>
<ul class="book-list-view">
<li>View As:</li>
<li class="view-list">
<a href="#view-list">
<img src="/static/images/booklist/view-list.png" align="view list" title="view list" height="21" width="24" />
</a>
</li>
<li class="view-list">
<a href="#view-icon">
<img src="/static/images/booklist/view-icon.png" align="view icon" title="view icon" height="22" width="22" />
</a>
</li>
<li class="view-list">
<a href="#view-icon-small">
<img src="/static/images/booklist/view-small-icon.png" align="view icon small" title="view icon small" height="22" width="22" />
</a>
</li>
</ul>
</div>
<div class="content-block-content">
{% for result in results %}
<div class="book-list row1">
<div class="book-thumb">
<a href="#"><img src="{{ result.image }}" alt="{{ result.title }}" title="{{ result.title }}" /></a>
</div>
<div class="book-name">
<span>
{{ result.title }}
</span>
</div>
<div class="add-wishlist">
{% if result.on_wishlist %}
<span>On Wishlist!</span>
{% else %}
<span id="{{ result.isbn_10 }}">Add to Wishlist</span>
{% endif %}
</div>
<div class="booklist-status">
<span class="booklist-status-text">Status: In Progress</span>
<span class="booklist-status-img">
<img src="/static/images/booklist/icon1.png" title="book list status" alt="book list status" />
</span>
</div>
<div class="unglue-this none">
<div class="unglue-this-inner1">
<div class="unglue-this-inner2">
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -33,10 +33,33 @@
</ul>
</div>
<div class="content-block-content">
{% for campaign in campaigns %}
{% include "book_list.html" %}
{% for work in wishlist.works.all %}
<div class="book-list row1">
<div class="book-thumb">
<a href="#"><img src="{{ work.cover_image_small }}" alt="Book name" title="book name" /></a>
</div>
<div class="book-name">
<span>
{{ work.title }}
</span>
</div>
<div class="add-wishlist">
<span>Add to Wishlist</span>
</div>
<div class="booklist-status">
<span class="booklist-status-text">Status: In Progress</span>
<span class="booklist-status-img">
<img src="/static/images/booklist/icon1.png" title="book list status" alt="book list status" />
</span>
</div>
<div class="unglue-this none">
<div class="unglue-this-inner1">
<div class="unglue-this-inner2">
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>

View File

@ -1,11 +1,13 @@
from django.conf.urls.defaults import *
from django.views.generic.simple import direct_to_template
from django.views.generic.base import TemplateView
urlpatterns = patterns(
"regluit.frontend.views",
url(r"^$", "home", name="home"),
url(r"^supporter/(?P<supporter_username>.+)/$", "supporter", name="supporter"),
url(r"^search/$", "search", name="search"),
url(r"^privacy/$", direct_to_template, {"template": "privacy.html"},
url(r"^privacy/$", TemplateView.as_view(template_name="privacy.html"),
name="privacy"),
url(r"^wishlist/$", "wishlist", name="wishlist"),
)

View File

@ -2,9 +2,13 @@ from django.template import RequestContext
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from django.contrib.auth.decorators import login_required
from django.shortcuts import render, render_to_response, get_object_or_404
from regluit.core import models, search
from regluit.core import models, bookloader
from regluit.core.search import gluejar_search
def home(request):
if request.user.is_authenticated():
@ -14,18 +18,46 @@ def home(request):
def supporter(request, supporter_username):
supporter = get_object_or_404(User, username=supporter_username)
campaigns = models.Campaign.objects.all()
wishlist = supporter.wishlist
context = {
"supporter": supporter,
"campaigns": campaigns,
"wishlist": wishlist,
}
return render(request, 'supporter.html', context)
def search(request):
q = request.GET.get('q', None)
results = search.gluejar_search(q)
results = gluejar_search(q)
# flag search result as on wishlist
# TODO: make this better and faster
if request.user:
for result in results:
if not result.has_key('isbn_10'):
continue
work = models.Work.get_by_isbn(result['isbn_10'])
if work and work in request.user.wishlist.works.all():
result['on_wishlist'] = True
else:
result['on_wishlist'] = False
context = {
"q": q,
"results": results,
}
return render(request, 'search.html', context)
@csrf_exempt
@require_POST
@login_required
def wishlist(request):
isbn = request.POST.get('isbn', None)
edition = models.Edition.get_by_isbn(isbn)
if not edition:
print "loading book"
edition = bookloader.add_book(isbn)
if edition:
print "adding edition"
request.user.wishlist.works.add(edition.work)
# TODO: redirect to work page, when it exists
return HttpResponseRedirect('/')

View File

@ -22,7 +22,7 @@ div.book-list div.book-thumb{ margin-right:5px;}
div.book-list div.book-name{ width:260px; margin-right:10px; background:url(../images/booklist/booklist-vline.png) right center no-repeat;}
div.book-list div.book-name span{ display:block; line-height:normal; height:43px; line-height:43px;}
div.book-list div.add-wishlist{ margin-right:10px; padding-right:10px;background:url(../images/booklist/booklist-vline.png) right center no-repeat;}
div.book-list div.add-wishlist a{ font-weight:normal; color:#3d4e53; text-transform: none; background:url(../images/booklist/add-wishlist.png) left center no-repeat; padding-left:20px;}
div.book-list div.add-wishlist span{ font-weight:normal; color:#3d4e53; text-transform: none; background:url(../images/booklist/add-wishlist.png) left center no-repeat; padding-left:20px;}
div.book-list div.booklist-status{ margin-right:7px;}
span.booklist-status-text{ float:left; display:block; padding-right:5px;}
@ -49,3 +49,7 @@ ul.navigation li.arrow-l a{ background:url(../images/booklist/bg.png) 0 -168px n
ul.navigation li.arrow-r a{ background:url(../images/booklist/bg.png) -1px -185px no-repeat;width:10px; height:15px; display:block; text-indent:-10000px;}
.unglue-button { display: block; border: 0;}
.book-thumb img {
height: 50px;
}

View File

@ -91,7 +91,7 @@ div.content-block-content .cols3 .column{ width:33.33%; float:left;}
.column-left .item{ margin:0 10px 10px 0;}
.column-center .item{ margin:0 5px 10px 5px;}
.column-right .item{ margin:0 0 10px 10px;}
.column .item{ border:7px solid #edf3f4; padding:10px;}
.column .item{ border:8px solid #edf3f4; padding:10px;}
.book-image{ padding:0 0 10px 0;}
.book-info{ padding:0 0 10px 0; line-height:125%; position:relative;}
.book-info span.book-new{ background:url(/static/images/icon-new.png) 0 0 no-repeat; width:38px; height:36px; display:block; position:absolute;
@ -150,4 +150,4 @@ a{ font-weight:bold; font-size:13px; text-decoration:none; cursor:pointer;}
#footer a{
color:#3d4e53;
}
}