Merge branch 'master' of github.com:Gluejar/regluit
commit
b267e7f822
|
@ -0,0 +1,5 @@
|
|||
*.db
|
||||
*.pyc
|
||||
*.log
|
||||
settings/me.py
|
||||
*.dot
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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 %}
|
|
@ -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>
|
||||
|
|
|
@ -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"),
|
||||
)
|
||||
|
|
|
@ -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('/')
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue