Merge branch 'master' of github.com:Gluejar/regluit

pull/1/head
Jason 2011-09-29 03:50:27 -04:00
commit b267e7f822
17 changed files with 337 additions and 78 deletions

5
.gitignore~ Normal file
View File

@ -0,0 +1,5 @@
*.db
*.pyc
*.log
settings/me.py
*.dot

View File

@ -29,7 +29,7 @@ to install python-setuptools in step 1:
1. `echo 'export DJANGO_SETTINGS_MODULE=regluit.settings.me' >> ~/.virtualenvs/regluit/bin/postactivate`
1. `deactivate ; workon regluit`
1. `django-admin.py syncdb --migrate --noinput`
1. `django-admin.py testserver --addrport 0.0.0.0:8000` (you can change the port number from the default value of 8000)
1. `django-admin.py runserver 0.0.0.0:8000` (you can change the port number from the default value of 8000)
1. point your browser at http://localhost:8000/
OS X

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

@ -3,6 +3,15 @@
<h3 class="jsmod-title"><span>Explore</span></h3>
<div class="jsmod-content">
<ul class="menu level1">
<li>
<div class="js-search" style="margin-top: 10px; margin-bottom: 10px;">
<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>
</li>
<li class="first parent">
<a href="#"><span>Show me...</span></a>
<ul class="menu level2">

View File

@ -11,8 +11,7 @@
<div class="js-search">
<h2>What book would you give to the world?</h2>
<div class="js-search-inner">
<form action="">
<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="30" class="inputbox" maxlength="200" id="ssearchword" name="searchword">
<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="30" class="inputbox" maxlength="200" id="ssearchword" name="q">
<input type="button" onclick="this.form.searchword.focus();" class="button" value="Search">
</form>
</div>

View File

@ -4,7 +4,7 @@
<h1>Gluejar Privacy Policy</h1>
Date of last revision: September 13, 2011
Date of last revision: September 20, 2011
<h2>Introduction</h2>
@ -12,6 +12,7 @@ Date of last revision: September 13, 2011
<h2>Information we collect</h2>
<p>If you sign up for our mailing list, we will ask for your email address. You may opt out of the mailing list at any time. We include unsubscribe instructions at the bottom of every newsletter.</p>
<p>You will be asked to provide your name and email address if you use our contact form.</p>
@ -42,6 +43,9 @@ Date of last revision: September 13, 2011
<p>Our website, products and services are marketed for and directed towards use and contributions by adults (or with the consent of adults). Individuals under the age of 18 are not permitted to use the GlueJar website to make contributions, offer a license or otherwise post content without the supervision of a parent or legal guardian. Furthermore, we do not knowingly collect or solicit personal information from children under the age of 13 or knowingly allow such persons to register for an online account or to post personal information on our websites. Should we learn that someone under the age of 13 has provided any personal information to or on the GlueJar website, we will remove that information as soon as possible.</p>
<h2>Payment</h2>
<p>We do not collect your credit card information. All financial transactions are processed through Paypal; information is transmitted securely using SSL and the Paypal APIs. You provide your credit card number, address, and other transaction information directly to Paypal; unglue.it does not transmit or store this information. unglue.it does not share your account information with Paypal.</p>
<h2>Changes to this Privacy Policy</h2>

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 Your 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

@ -12,7 +12,7 @@
<div class="js-maincol-inner">
<div class="content-block">
<div class="content-block-heading">
<h2 class="content-heading">Wishlist for {{ supporter.username }}</span></h2>
<h2 class="content-heading">Your Wishlist</span></h2>
<ul class="book-list-view">
<li>View As:</li>
<li class="view-list">
@ -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,8 +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"^privacy$", "textpage", {'page': 'privacy'}, name="privacy"),
url(r"^search/$", "search", name="search"),
url(r"^privacy/$", TemplateView.as_view(template_name="privacy.html"),
name="privacy"),
url(r"^wishlist/$", "wishlist", name="wishlist"),
)

View File

@ -2,30 +2,60 @@ 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.shortcuts import render_to_response, get_object_or_404
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
from regluit.core import models, bookloader
from regluit.core.search import gluejar_search
def home(request):
# if the user is logged in send them to their supporter page
if request.user.is_authenticated():
return HttpResponseRedirect(reverse('supporter',
args=[request.user.username]))
return render_to_response('home.html',
{},
context_instance=RequestContext(request)
)
return render(request, 'home.html')
def supporter(request, supporter_username):
supporter = get_object_or_404(User, username=supporter_username)
campaigns = models.Campaign.objects.all()
return render_to_response('supporter.html',
{"supporter": supporter, "campaigns": campaigns},
context_instance=RequestContext(request)
)
wishlist = supporter.wishlist
context = {
"supporter": supporter,
"wishlist": wishlist,
}
return render(request, 'supporter.html', context)
def textpage(request, page):
return render_to_response(page + '.html',
{},
context_instance=RequestContext(request)
)
def search(request):
q = request.GET.get('q', None)
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:
edition = bookloader.add_book(isbn)
if 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;