just merging my local codebase post-internet librarian...

pull/1/head
Andromeda Yelton 2011-10-21 12:51:07 -04:00
commit e41e1e7b6d
10 changed files with 372 additions and 196 deletions

View File

@ -10,7 +10,11 @@ from regluit.core import models
logger = logging.getLogger(__name__)
def add_by_isbn(isbn):
def add_by_isbn(isbn, work=None):
"""add a book to the UnglueIt database based on ISBN. The work parameter
is optional, and if not supplied the edition will be associated with
a stub work.
"""
url = "https://www.googleapis.com/books/v1/volumes"
results = _get_json(url, {"q": "isbn:%s" % isbn})
@ -18,21 +22,27 @@ def add_by_isbn(isbn):
logger.warn("no google hits for %s" % isbn)
return None
return add_by_googlebooks_id(results['items'][0]['id'])
return add_by_googlebooks_id(results['items'][0]['id'], work)
def add_by_googlebooks_id(googlebooks_id):
url = "https://www.googleapis.com/books/v1/volumes/%s" % googlebooks_id
d = _get_json(url)['volumeInfo']
def add_by_googlebooks_id(googlebooks_id, work=None):
"""add a book to the UnglueIt database based on the GoogleBooks ID. The
work parameter is optional, and if not supplied the edition will be
associated with a stub work.
"""
# don't ping google again if we already know about the edition
e, created = models.Edition.objects.get_or_create(googlebooks_id=googlebooks_id)
if not created:
return e
url = "https://www.googleapis.com/books/v1/volumes/%s" % googlebooks_id
d = _get_json(url)['volumeInfo']
e.title = d.get('title')
e.description = d.get('description')
e.publisher = d.get('publisher')
e.publication_date = d.get('publishedDate')
e.language = d.get('language')
for i in d.get('industryIdentifiers', []):
if i['type'] == 'ISBN_10':
@ -48,20 +58,44 @@ def add_by_googlebooks_id(googlebooks_id):
s, created = models.Subject.objects.get_or_create(name=s)
s.editions.add(e)
# add a stub Work for the edition
if e.work == None:
# if we know what work to add the edition to do it
if work:
work.editions.add(e)
# otherwise we need to create a stub work
else:
w = models.Work.objects.create(title=e.title)
w.editions.add(e)
return e
def add_related(isbn):
"""add all books related to a particular ISBN to the UnglueIt database.
The initial seed ISBN will be added if it's not already there.
"""
# make sure the seed edition is there
edition = add_by_isbn(isbn)
# this is the work everything will hang off
work = edition.work
for other_isbn in thingisbn(isbn):
# TODO: if the other book is there already we have some surgery
# to do on the works, and the wishlists
add_by_isbn(other_isbn, work)
def thingisbn(isbn):
"""given an ISBN return a list of related edition ISBNs, according to
Library Thing.
"""
url = "http://www.librarything.com/api/thingISBN/%s" % isbn
xml = requests.get(url, headers={"User-Agent": settings.USER_AGENT}).content
doc = ElementTree.fromstring(xml)
return [e.text for e in doc.findall('isbn')]
def _get_json(url, params={}):
# TODO: should X-Forwarded-For change based on the request from client?
headers = {'User-Agent': settings.USER_AGENT,

View File

@ -0,0 +1,125 @@
# encoding: utf-8
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding field 'Edition.language'
db.add_column('core_edition', 'language', self.gf('django.db.models.fields.CharField')(max_length=2, null=True), keep_default=False)
def backwards(self, orm):
# Deleting field 'Edition.language'
db.delete_column('core_edition', 'language')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'core.author': {
'Meta': {'object_name': 'Author'},
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'editions': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'authors'", 'symmetrical': 'False', 'to': "orm['core.Edition']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '500'})
},
'core.campaign': {
'Meta': {'object_name': 'Campaign'},
'activated': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
'amazon_receiver': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'deadline': ('django.db.models.fields.DateTimeField', [], {}),
'description': ('django.db.models.fields.TextField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '500'}),
'paypal_receiver': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True'}),
'supended_reason': ('django.db.models.fields.TextField', [], {'null': 'True'}),
'suspended': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
'target': ('django.db.models.fields.DecimalField', [], {'max_digits': '14', 'decimal_places': '2'}),
'withdrawn': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
'withdrawn_reason': ('django.db.models.fields.TextField', [], {'null': 'True'}),
'work': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'campaigns'", 'to': "orm['core.Work']"})
},
'core.edition': {
'Meta': {'object_name': 'Edition'},
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True'}),
'googlebooks_id': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'isbn_10': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True'}),
'isbn_13': ('django.db.models.fields.CharField', [], {'max_length': '13', 'null': 'True'}),
'language': ('django.db.models.fields.CharField', [], {'max_length': '2', 'null': 'True'}),
'publication_date': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
'publisher': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'title': ('django.db.models.fields.CharField', [], {'max_length': '1000'}),
'work': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'editions'", 'null': 'True', 'to': "orm['core.Work']"})
},
'core.subject': {
'Meta': {'object_name': 'Subject'},
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'editions': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'subjects'", 'symmetrical': 'False', 'to': "orm['core.Edition']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '500'})
},
'core.userprofile': {
'Meta': {'object_name': 'UserProfile'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'tagline': ('django.db.models.fields.CharField', [], {'max_length': '140', 'blank': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': "orm['auth.User']"})
},
'core.wishlist': {
'Meta': {'object_name': 'Wishlist'},
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'wishlist'", 'unique': 'True', 'to': "orm['auth.User']"}),
'works': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'wishlists'", 'symmetrical': 'False', 'to': "orm['core.Work']"})
},
'core.work': {
'Meta': {'object_name': 'Work'},
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'openlibrary_id': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True'}),
'title': ('django.db.models.fields.CharField', [], {'max_length': '1000'})
}
}
complete_apps = ['core']

View File

@ -152,6 +152,7 @@ class Edition(models.Model):
isbn_10 = models.CharField(max_length=10, null=True)
isbn_13 = models.CharField(max_length=13, null=True)
work = models.ForeignKey("Work", related_name="editions", null=True)
language = models.CharField(max_length=2, null=True)
def __unicode__(self):
return "%s (%s)" % (self.title, self.isbn_13)

View File

@ -21,6 +21,7 @@ class TestBookLoader(TestCase):
self.assertEqual(edition.isbn_10, '0441012035')
self.assertEqual(edition.isbn_13, '9780441012039')
self.assertEqual(edition.googlebooks_id, "2NyiPwAACAAJ")
self.assertEqual(edition.language, "en")
# subjects
subject_names = [subject.name for subject in edition.subjects.all()]
@ -52,6 +53,18 @@ class TestBookLoader(TestCase):
self.assertTrue('0441012035' in isbns)
self.assertTrue('3453313895' in isbns)
def test_add_related(self):
# add one edition
edition = bookloader.add_by_isbn('0441012035')
self.assertEqual(models.Edition.objects.count(), 1)
self.assertEqual(models.Work.objects.count(), 1)
# ask for related editions to be added using the work we just created
bookloader.add_related('0441012035')
self.assertTrue(models.Edition.objects.count() > 20)
self.assertEqual(models.Work.objects.count(), 1)
self.assertTrue(edition.work.editions.count() > 20)
class SearchTests(TestCase):
def test_basic_search(self):
@ -86,10 +99,10 @@ class CampaignTests(TestCase):
c = Campaign(target=D('1000.00'), deadline=datetime(2012, 1, 1))
self.assertRaises(IntegrityError, c.save)
#w = Work()
#w.save()
#c = Campaign(target=D('1000.00'), deadline=datetime(2012, 1, 1), work=w)
#c.save()
w = Work()
w.save()
c = Campaign(target=D('1000.00'), deadline=datetime(2012, 1, 1), work=w)
c.save()
def test_campaign_status(self):

View File

@ -22,6 +22,16 @@
<div class="js-logo">
<a href="/"><img src="/static/images/logo.png" alt="unglue.it" title="unglue.it" /></a>
</div>
{% if not suppress_search_box %}
<div class="js-search" style="float: left; margin-left: 50px;">
<div class="js-search-inner">
<form action="{% url search %}" method="get">
<input type="text" placeholder="Search for a book..." size="30" class="inputbox" name="q" value="{{ q }}">
<input type="button" onclick="this.form.searchword.focus();" class="button" value="Search">
</form>
</div>
</div>
{% endif %}
<div class="js-topmenu">
<ul class="menu">
{% if user.is_authenticated %}

View File

@ -1,24 +1,7 @@
{% 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 gb_id = span.attr('id')
if (!gb_id) return;
$.post('/wishlist/', {'googlebooks_id': gb_id}, function(data) {
span.fadeOut();
var newSpan = $("<span>On Your Wishlist!</span>").hide();
span.replaceWith(newSpan);
newSpan.fadeIn();
newSpan.removeAttr("id");
});
});
});
});
</script>
<script type="text/javascript" src="/static/js/wishlist.js"></script>
{% endblock %}
{% block title %}Search Results{% endblock %}

View File

@ -3,41 +3,9 @@
{% block title %} &#8212; {{ supporter.username }}{% endblock %}
{% 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 gb_id = span.attr('id')
if (!gb_id) return;
$.post('/wishlist/', {'googlebooks_id': gb_id}, function(data) {
span.fadeOut();
var newSpan = $("<span>On Your Wishlist!</span>").hide();
span.replaceWith(newSpan);
newSpan.fadeIn();
newSpan.removeAttr("id");
});
});
});
});
</script>
<script type="text/javascript">
$(document).ready(function() {
$(".remove-wishlist").each(function (index, element) {
$(element).click(function() {
var span = $(element).find("span");
var work_id = span.attr('id')
$.post('/wishlist/', {'remove_work_id': work_id}, function(data) {
var book = $(element).parent();
book.fadeOut();
});
});
});
});
</script>
<script type="text/javascript" src="/static/js/wishlist.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js" type="text/javascript"></script>
<script type="text/javascript" src="/static/js/jquery-ui-1.8.16.custom.min.js"></script>
<script type="text/javascript">
var $j = jQuery.noConflict();
@ -83,24 +51,22 @@ $(document).ready(function() {
/*
To do:
create topsection file for inclusion in multiple contexts, if needed
do we really want to display date joined in profile? if so, with any text? "supporter since (date) is too long"
add support for userpics/avatars/something
figure out how to configure date display
decide if we're even including date joined in profile
add support for userpics
crop/resize? other infrastructure we need? can we pull from twitter/fb?
add support for twitter/fb/other links
add support for twitter/fb/other links?
part of user profile? can I just add to db model?
loosely coupled vs actually integrated with signin
canonicalize -- user enters cut/paste link vs username
Goodreads & Librarything support
If people lack homepage, etc., do we have greyed-out icons or no icons?
if greyed-out, can they add in place by clicking on them?
make sure profile settings do something
make sure we have the profile settings we need
resurrect add/remove functionality
be sure words display correctly
do I need both add-wishlist and remove-wishlist classes? do they differ?
better alignment on I am ungluing & badges
progress indicators
change status text to something more user-friendly
js is not DRY right now -- need to put into scripts folder and reference
make sure backed/backing/wishlist is the order we want the badges to be in
test code with other campaign statuses -- random_campaigns needs to set a variety of statuses!
@ -114,9 +80,6 @@ how do I integrate the your wishlist thing with the tabs thing?
*/
/*
*/
{% block topsection %}
<div id="js-topsection">
@ -143,7 +106,7 @@ how do I integrate the your wishlist thing with the tabs thing?
<div class="social">
<a href="#"><img src="/static/images/header/icon-home.png" alt="Home" title="Home" /></a>
<a href="#"><img src="/static/images/header/icon-facebook.png" alt="Facebook" title="Facebook" /></a>
<a href="#"><img src="/static/images/header/icon-tweeter.png" alt="tweeter" title="tweeter" /></a>
<a href="#"><img src="/static/images/header/icon-twitter.png" alt="twitter" title="twitter" /></a>
<a href="#"><img src="/static/images/header/icon-google.png" alt="google" title="google" /></a>
<a href="#"><img src="/static/images/header/icon-group.png" alt="group" title="group" /></a></div>
</div>
@ -156,29 +119,21 @@ how do I integrate the your wishlist thing with the tabs thing?
<span class="user-status-title">I am ungluing</span>
</div>
</div>
{% ifequal supporter request.user %}
<div class="user-block-hide">
<div class="block block1">
<div class="block-inner">
<h3><a class="profile-edit" href="#">Profile / edit</a></h3>
Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
<div class="profile-save">
<a href="#" class="profile-save">Save setting</a>
</div>
<form method="POST" action="">
{% csrf_token %}
{{ profile_form.as_p }}
<input class="profile-save" type="submit" name="submit" value="Update" id="submit">
</form>
</div>
</div>
<div class="block block2">
<h3 class="title">Pledges</h3>
<div class="check-list">
<input type="checkbox" />
<label>Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</label>
</div>
<div class="check-list">
<input type="checkbox" />
<label>Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</label>
</div>
</div>
<div class="block block3">
<h3 class="title">Links</h3>
<div class="check-list">
<input type="checkbox" />
@ -189,7 +144,7 @@ how do I integrate the your wishlist thing with the tabs thing?
<label>Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</label>
</div>
</div>
<div class="block block4">
<div class="block block3">
<h3 class="title">Privacy</h3>
<div class="check-list">
<input type="checkbox" />
@ -202,6 +157,7 @@ how do I integrate the your wishlist thing with the tabs thing?
</div>
</div>
{% endifequal %}
</div>
</div>
</div>
@ -249,6 +205,21 @@ how do I integrate the your wishlist thing with the tabs thing?
</ul>
</div>
<div class="content-block-content">
{% ifequal wishlist.works.all.count 0 %}
{% ifequal request.user supporter %}
<div class="empty-wishlist">
Your wishlist is currently empty.<br><br>
Go ahead and <span class="bounce-search">find</span> some books to give to the world, and add them to your Wishlist!<br><br>
We double dog dare you...
</div>
{% else %}
<div class="empty-wishlist">
It looks like {{ supporter.username }} is just
getting started, and hasn't added anything to their
wishlist just yet.<br><br>
Nudge, nudge, say no more.
{% endifequal %}
{% else %}
{% for work in wishlist.works.all %}
<div id="tabs-1" class="tabs">
<div class="book-list {% cycle 'row1' 'row2' %}">
@ -257,18 +228,26 @@ how do I integrate the your wishlist thing with the tabs thing?
</div>
<div class="book-name">
<span>
{{ work.title }}
<a href="{% url work work.id %}">{{ work.title }}</a>
</span>
</div>
{% if work in request.user.wishlist.works.all %}
{% ifequal supporter request.user %}
<div class="remove-wishlist">
<a href="#"><span id="{{ work.id }}">Remove from Wishlist</span></a>
<span id="{{ work.id }}">Remove from Wishlist</span>
</div>
{% else %}{% if work in shared_works %}
<div>
<span>On Your Wishlist!</span>
</div>
{% else %}{% if request.user.is_anonymous %}
<div class="create-account">
<span>Add to Wishlist</span>
</div>
{% else %}
<div class="add-wishlist">
<a href="#"><span id="{{ work.id }}">Add to Wishlist</span></a>
<span id="{{ work.editions.all.0.googlebooks_id }}">Add to Wishlist</span>
</div>
{% endif %}
{% endif %}{% endif %}{% endifequal %}
<div class="booklist-status">
<span class="booklist-status-text">{{ work.last_campaign_status }}</span>
<span class="booklist-status-img">
@ -284,6 +263,7 @@ how do I integrate the your wishlist thing with the tabs thing?
</div>
</div>
{% endfor %}
{% endifequal %}
</div>
</div>

View File

@ -19,7 +19,7 @@ def home(request):
if request.user.is_authenticated():
return HttpResponseRedirect(reverse('supporter',
args=[request.user.username]))
return render(request, 'home.html')
return render(request, 'home.html', {'suppress_search_box': True})
def supporter(request, supporter_username):
supporter = get_object_or_404(User, username=supporter_username)

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

30
static/js/wishlist.js Normal file
View File

@ -0,0 +1,30 @@
$(document).ready(function() {
$(".add-wishlist").each(function (index, element) {
$(element).click(function() {
var span = $(element).find("span");
var gb_id = span.attr('id')
if (!gb_id) return;
$.post('/wishlist/', {'googlebooks_id': gb_id}, function(data) {
span.fadeOut();
var newSpan = $("<span>On Your Wishlist!</span>").hide();
span.replaceWith(newSpan);
newSpan.fadeIn();
newSpan.removeAttr("id");
});
});
});
$(".remove-wishlist").each(function (index, element) {
$(element).click(function() {
var span = $(element).find("span");
var work_id = span.attr('id')
$.post('/wishlist/', {'remove_work_id': work_id}, function(data) {
var book = $(element).parent();
book.fadeOut();
});
});
});
});