├── books ├── __init__.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── exportbooks.py │ │ ├── addepub.py │ │ ├── addbooks.py │ │ ├── fetch_ia_item.py │ │ └── fetch_ia_items_from_search.py ├── migrations │ ├── __init__.py │ ├── 0002_auto_20171101_0824.py │ └── 0001_initial.py ├── templates │ ├── popupplus.html │ └── popupadd.html ├── selectwithpop.py ├── popuphandler.py ├── tests.py ├── app_settings.py ├── admin.py ├── forms.py ├── uuidfield.py ├── search.py ├── urls.py ├── epubinfo.py ├── epub.py ├── models.py ├── opds.py └── views.py ├── pathagar ├── __init__.py ├── wsgi.py ├── urls.py └── settings.py ├── test-requirements.txt ├── static ├── images │ ├── feed.png │ ├── go-next.png │ ├── go-previous.png │ └── generic_cover.png ├── style │ ├── blueprint │ │ ├── plugins │ │ │ ├── buttons │ │ │ │ ├── icons │ │ │ │ │ ├── key.png │ │ │ │ │ ├── cross.png │ │ │ │ │ └── tick.png │ │ │ │ ├── readme.txt │ │ │ │ └── screen.css │ │ │ ├── link-icons │ │ │ │ ├── icons │ │ │ │ │ ├── doc.png │ │ │ │ │ ├── im.png │ │ │ │ │ ├── pdf.png │ │ │ │ │ ├── xls.png │ │ │ │ │ ├── email.png │ │ │ │ │ ├── epub.png │ │ │ │ │ ├── feed.png │ │ │ │ │ ├── external.png │ │ │ │ │ └── visited.png │ │ │ │ ├── readme.txt │ │ │ │ └── screen.css │ │ │ ├── rtl │ │ │ │ ├── readme.txt │ │ │ │ └── screen.css │ │ │ └── fancy-type │ │ │ │ ├── readme.txt │ │ │ │ └── screen.css │ │ ├── print.css │ │ ├── ie.css │ │ └── screen.css │ └── style.css └── js │ ├── jquery.example.min.js │ └── RelatedObjectLookups.js ├── .gitignore ├── examples └── The Dunwich Horror.epub ├── requirements.txt ├── local_settings.example.py ├── AUTHORS ├── templates ├── admin │ └── base_site.html ├── registration │ └── login.html ├── books │ ├── book_confirm_delete.html │ ├── tag_list.html │ ├── book_form.html │ ├── book_detail.html │ └── book_list.html ├── authors │ └── author_list.html └── base.html ├── .travis.yml ├── scripts └── run-pylint.sh ├── TODO ├── manage.py ├── .circleci └── config.yml ├── README.md ├── pylintrc └── COPYING /books/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pathagar/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /books/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /books/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /books/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | pylint==1.7.4 2 | pylint-django==0.7.2 3 | codecov 4 | coverage 5 | -------------------------------------------------------------------------------- /static/images/feed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathagarBooks/pathagar/HEAD/static/images/feed.png -------------------------------------------------------------------------------- /static/images/go-next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathagarBooks/pathagar/HEAD/static/images/go-next.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | database.db 2 | *pyc 3 | staticfiles/ 4 | static_media/ 5 | local_settings.py 6 | .coverage 7 | htmlcov 8 | -------------------------------------------------------------------------------- /static/images/go-previous.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathagarBooks/pathagar/HEAD/static/images/go-previous.png -------------------------------------------------------------------------------- /examples/The Dunwich Horror.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathagarBooks/pathagar/HEAD/examples/The Dunwich Horror.epub -------------------------------------------------------------------------------- /static/images/generic_cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathagarBooks/pathagar/HEAD/static/images/generic_cover.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django==1.11.20 2 | #django-tagging==0.3.1 3 | django-tagging==0.4.6 4 | django-sendfile==0.3.11 5 | lxml==3.4.4 6 | django-taggit==0.22.1 7 | -------------------------------------------------------------------------------- /static/style/blueprint/plugins/buttons/icons/key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathagarBooks/pathagar/HEAD/static/style/blueprint/plugins/buttons/icons/key.png -------------------------------------------------------------------------------- /static/style/blueprint/plugins/buttons/icons/cross.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathagarBooks/pathagar/HEAD/static/style/blueprint/plugins/buttons/icons/cross.png -------------------------------------------------------------------------------- /static/style/blueprint/plugins/buttons/icons/tick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathagarBooks/pathagar/HEAD/static/style/blueprint/plugins/buttons/icons/tick.png -------------------------------------------------------------------------------- /static/style/blueprint/plugins/link-icons/icons/doc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathagarBooks/pathagar/HEAD/static/style/blueprint/plugins/link-icons/icons/doc.png -------------------------------------------------------------------------------- /static/style/blueprint/plugins/link-icons/icons/im.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathagarBooks/pathagar/HEAD/static/style/blueprint/plugins/link-icons/icons/im.png -------------------------------------------------------------------------------- /static/style/blueprint/plugins/link-icons/icons/pdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathagarBooks/pathagar/HEAD/static/style/blueprint/plugins/link-icons/icons/pdf.png -------------------------------------------------------------------------------- /static/style/blueprint/plugins/link-icons/icons/xls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathagarBooks/pathagar/HEAD/static/style/blueprint/plugins/link-icons/icons/xls.png -------------------------------------------------------------------------------- /local_settings.example.py: -------------------------------------------------------------------------------- 1 | # Copy this file to local_settings.py, then customize settings to your 2 | # setup. 3 | 4 | DEBUG = True 5 | TIME_ZONE = 'America/Los_Angeles' 6 | -------------------------------------------------------------------------------- /static/style/blueprint/plugins/link-icons/icons/email.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathagarBooks/pathagar/HEAD/static/style/blueprint/plugins/link-icons/icons/email.png -------------------------------------------------------------------------------- /static/style/blueprint/plugins/link-icons/icons/epub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathagarBooks/pathagar/HEAD/static/style/blueprint/plugins/link-icons/icons/epub.png -------------------------------------------------------------------------------- /static/style/blueprint/plugins/link-icons/icons/feed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathagarBooks/pathagar/HEAD/static/style/blueprint/plugins/link-icons/icons/feed.png -------------------------------------------------------------------------------- /static/style/blueprint/plugins/link-icons/icons/external.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathagarBooks/pathagar/HEAD/static/style/blueprint/plugins/link-icons/icons/external.png -------------------------------------------------------------------------------- /static/style/blueprint/plugins/link-icons/icons/visited.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PathagarBooks/pathagar/HEAD/static/style/blueprint/plugins/link-icons/icons/visited.png -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Sayamindu Dasgupta 2 | Kushal Das 3 | Seth Wolfwood 4 | Manuel Quiñones 5 | Raj Kumar 6 | Aneesh Dogra 7 | George Hunt 8 | -------------------------------------------------------------------------------- /books/templates/popupplus.html: -------------------------------------------------------------------------------- 1 | 6 | Add Another 7 | 8 | 9 | -------------------------------------------------------------------------------- /templates/admin/base_site.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base.html" %} 2 | {% load i18n %} 3 | 4 | {% block title %}{{ title }} | {% trans 'Pathagar admin' %}{% endblock %} 5 | 6 | {% block branding %} 7 |

{% trans 'Pathagar administration' %}

8 | {% endblock %} 9 | 10 | {% block nav-global %}{% endblock %} 11 | -------------------------------------------------------------------------------- /pathagar/wsgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | path = os.path.realpath(os.path.join(os.path.dirname(__file__), "../")) 5 | 6 | if path not in sys.path: 7 | sys.path.append(path) 8 | 9 | os.environ['DJANGO_SETTINGS_MODULE'] = "pathagar.settings" 10 | 11 | import django.core.handlers.wsgi 12 | application = django.core.handlers.wsgi.WSGIHandler() 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: python 3 | 4 | python: 5 | - 2.7 6 | - 3.5 7 | 8 | cache: pip 9 | 10 | install: 11 | - pip install -r requirements.txt 12 | - pip install -r test-requirements.txt 13 | 14 | script: 15 | - scripts/run-pylint.sh 16 | - coverage run --source='.' manage.py test 17 | 18 | after_script: 19 | - codecov 20 | -------------------------------------------------------------------------------- /books/selectwithpop.py: -------------------------------------------------------------------------------- 1 | from django.template.loader import render_to_string 2 | import django.forms as forms 3 | 4 | class SelectWithPop(forms.Select): 5 | def render(self, name, *args, **kwargs): 6 | html = super(SelectWithPop, self).render(name, *args, **kwargs) 7 | popupplus = render_to_string("popupplus.html", {'field': name}) 8 | 9 | return html+popupplus 10 | -------------------------------------------------------------------------------- /static/style/blueprint/plugins/rtl/readme.txt: -------------------------------------------------------------------------------- 1 | RTL 2 | * Mirrors Blueprint, so it can be used with Right-to-Left languages. 3 | 4 | By Ran Yaniv Hartstein, ranh.co.il 5 | 6 | Usage 7 | ---------------------------------------------------------------- 8 | 9 | 1) Add this line to your HTML: 10 | -------------------------------------------------------------------------------- /static/style/blueprint/plugins/fancy-type/readme.txt: -------------------------------------------------------------------------------- 1 | Fancy Type 2 | 3 | * Gives you classes to use if you'd like some 4 | extra fancy typography. 5 | 6 | Credits and instructions are specified above each class 7 | in the fancy-type.css file in this directory. 8 | 9 | 10 | Usage 11 | ---------------------------------------------------------------- 12 | 13 | 1) Add this plugin to lib/settings.yml. 14 | See compress.rb for instructions. 15 | -------------------------------------------------------------------------------- /static/style/blueprint/plugins/link-icons/readme.txt: -------------------------------------------------------------------------------- 1 | Link Icons 2 | * Icons for links based on protocol or file type. 3 | 4 | This is not supported in IE versions < 7. 5 | 6 | 7 | Credits 8 | ---------------------------------------------------------------- 9 | 10 | * Marc Morgan 11 | * Olav Bjorkoy [bjorkoy.com] 12 | 13 | 14 | Usage 15 | ---------------------------------------------------------------- 16 | 17 | 1) Add this line to your HTML: 18 | -------------------------------------------------------------------------------- /scripts/run-pylint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "========= PYTHON VERSION "$(python --version) 4 | 5 | FILES="$1" 6 | if [ -z $1 ]; then 7 | FILES="$(git ls-files -- '*.py')" 8 | fi 9 | 10 | for file in $FILES; do 11 | echo "==== $file" 12 | python -m pylint --rcfile=pylintrc --load-plugins pylint_django --score=no --reports=no --disable=R,C $file 13 | RET=$? # pylint returns bitfield of found issues 14 | ERR=$(($RET&3)) # Fatal is 1 and Error is 2 15 | if [[ $ERR -ne 0 ]]; then 16 | echo "Aborting" 17 | exit 1 18 | fi 19 | done 20 | 21 | exit 0 22 | -------------------------------------------------------------------------------- /books/migrations/0002_auto_20171101_0824.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.6 on 2017-11-01 08:24 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('books', '0001_initial'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='book', 18 | name='a_status', 19 | field=models.ForeignKey(default='Published', on_delete=django.db.models.deletion.CASCADE, to='books.Status'), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /templates/registration/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Log in{% endblock %} 4 | 5 | {% block content %} 6 | {% if form.errors %} 7 |
8 |

Your username and password didn't match. Please try again.

9 |
10 | {% endif %} 11 | 12 |
{% csrf_token %} 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
{{ form.username.label_tag }}{{ form.username }}
{{ form.password.label_tag }}{{ form.password }}
23 | 24 | 25 | 26 |
27 | 28 | {% endblock %} 29 | 30 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | Export DB in JSON modes 2 | Allow import from calibre 3 | Add responsive mode 4 | (should allow several books on same row ?) 5 | Allow list of author by letter when a lot of authors are available (forced or by config ?) 6 | Compute small thumbnail of cover (with link to real cover if needed ?) (needed for ebook reader or for low bandwidht connection) 7 | Add Catalog.atom link 8 | Add detection of language on summary 9 | Fix validate unique 10 | Tags are not saved form edit webpage 11 | Order Authors list on edit page (it looks like order by their internal ID) 12 | Improve Author check (suppress whitespaces, try to invert between ',', ...) 13 | Add webpage to perform action on multiple items like add tags or updating authors 14 | Add default 'anonymous' author if not fount in EPUB or in JSON import 15 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pathagar.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError: 10 | # The above import may fail for some other reason. Ensure that the 11 | # issue is really that Django is missing to avoid masking other 12 | # exceptions on Python 2. 13 | try: 14 | import django 15 | except ImportError: 16 | raise ImportError( 17 | "Couldn't import Django. Are you sure it's installed and " 18 | "available on your PYTHONPATH environment variable? Did you " 19 | "forget to activate a virtual environment?" 20 | ) 21 | raise 22 | execute_from_command_line(sys.argv) 23 | -------------------------------------------------------------------------------- /books/popuphandler.py: -------------------------------------------------------------------------------- 1 | from django.utils.html import escape 2 | from django.shortcuts import render_to_response 3 | from django.http import HttpResponse 4 | from django import forms 5 | 6 | 7 | def handlePopAdd(request, addForm, field): 8 | if request.method == "POST": 9 | form = addForm(request.POST) 10 | if form.is_valid(): 11 | try: 12 | newObject = form.save() 13 | except forms.ValidationError: 14 | newObject = None 15 | if newObject: 16 | return HttpResponse('' % \ 17 | (escape(newObject._get_pk_val()), escape(newObject))) 18 | 19 | else: 20 | form = addForm() 21 | 22 | pageContext = {'form': form, 'field': field} 23 | return render_to_response("popupadd.html", pageContext) 24 | 25 | 26 | -------------------------------------------------------------------------------- /templates/books/book_confirm_delete.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load static %} 3 | 4 | {% block title %}Are you sure?{% endblock %} 5 | 6 | {% block script %} 7 | $(document).ready(function() 8 | { 9 | $(".hidden_body").hide(); 10 | $(".hidden_head").click(function() 11 | { 12 | $(this).next(".hidden_body").slideToggle(100); 13 | }); 14 | $('#id_dc_identifier').example('ISBN'); 15 | }); 16 | {% endblock %} 17 | 18 | {% block head %}{% endblock %} 19 | 20 | {% block content %} 21 |
{% csrf_token %} 23 |
24 | Are you sure you want to remove {{ book.a_title }}? 25 |
26 | Cancel 27 | 28 |
29 |
30 |
31 | {% endblock %} 32 | -------------------------------------------------------------------------------- /templates/books/tag_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}{{ book.a_title }} :: Pathagar Book Server{% endblock %} 4 | 5 | {% block script %} 6 | {{ block.super }} 7 | $('#search').example('Book Search...'); 8 | {% endblock %} 9 | 10 | {% block content %} 11 | 12 |
13 | 14 |

Tags {% if tag_group %} by {{ tag_group }}{% endif %}

15 |
16 | 17 | {% if tag_list %} 18 |
19 |
20 | {% for tag in tag_list %} 21 | {{ tag.name }} 22 | {% endfor %} 23 |
24 |
25 | {% endif %} 26 | 27 | {% if tag_group_list %} 28 |
29 |
30 | {% for tag_group in tag_group_list %} 31 | {{ tag_group.name }} 32 | {% endfor %} 33 |
34 |
35 | {% endif %} 36 | 37 | {% endblock %} 38 | 39 | -------------------------------------------------------------------------------- /static/style/blueprint/plugins/buttons/readme.txt: -------------------------------------------------------------------------------- 1 | Buttons 2 | 3 | * Gives you great looking CSS buttons, for both and 25 | 26 | 27 | Change Password 28 | 29 | 30 | 31 | Cancel 32 | 33 | -------------------------------------------------------------------------------- /pathagar/urls.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.conf.urls import include, url 4 | #from django.conf.urls import patterns, include, url 5 | from django.contrib import admin 6 | admin.autodiscover() 7 | 8 | from django.conf import settings 9 | from books.app_settings import BOOKS_STATICS_VIA_DJANGO 10 | 11 | from django.contrib.auth import views as auth_views 12 | 13 | urlpatterns = [ 14 | 15 | url(r'', include('books.urls')), 16 | 17 | 18 | # Comments 19 | # FIXME (r'^comments/', include('django.contrib.comments.urls')), 20 | 21 | # Auth login and logout: 22 | url(r'^accounts/login/$', auth_views.login, name='login'), 23 | url(r'^accounts/logout/$', auth_views.logout, name='logout'), 24 | 25 | # Admin: 26 | url(r'^admin/', include(admin.site.urls)), 27 | ] 28 | 29 | 30 | if BOOKS_STATICS_VIA_DJANGO: 31 | from django.views.static import serve 32 | # Serve static media: 33 | # urlpatterns += patterns('', 34 | urlpatterns += [ 35 | url(r'^static_media/(?P.*)$', serve, 36 | {'document_root': settings.MEDIA_ROOT, 'show_indexes': True}), 37 | 38 | # Book covers: 39 | url(r'^covers/(?P.*)$', serve, 40 | {'document_root': os.path.join(settings.MEDIA_ROOT, 'covers')}), 41 | ] 42 | -------------------------------------------------------------------------------- /books/templates/popupadd.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Add {{ field }} 5 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 | Add {{ field }} 19 | 20 | {{ form }} 21 |
22 |

| Cancel

23 |
24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /books/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.core.management import call_command, CommandError 3 | 4 | from books.epub import Epub 5 | from books.models import Book, Author 6 | 7 | 8 | class EpubTest(TestCase): 9 | def test_simple_import(self): 10 | epub = Epub("examples/The Dunwich Horror.epub") 11 | info = epub.get_info() 12 | self.assertEqual(info.title, "The Dunwich Horror") 13 | self.assertEqual(info.creator, "H. P. Lovecraft") 14 | epub.close() 15 | 16 | 17 | class AddEpubTest(TestCase): 18 | def test_01_import_commandline(self): 19 | nb_book = len(Book.objects.all()) 20 | self.assertEqual(nb_book, 0) 21 | 22 | args = ["examples/"] 23 | opts = {} 24 | call_command('addepub', *args, **opts) 25 | 26 | nb_book = len(Book.objects.all()) 27 | self.assertEqual(nb_book, 1) 28 | 29 | book = Book.objects.get(pk=1) 30 | self.assertEqual(str(book.a_author), "H. P. Lovecraft") 31 | self.assertEqual(str(book.a_title), "The Dunwich Horror") 32 | 33 | def test_02_import_duplicated(self): 34 | # try to import duplicated epub 35 | args = ["examples/"] 36 | opts = {} 37 | call_command('addepub', *args, **opts) 38 | self.assertRaises(CommandError, call_command, 39 | ('addepub'), opts) 40 | -------------------------------------------------------------------------------- /static/style/blueprint/print.css: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------- 2 | 3 | 4 | Blueprint CSS Framework 0.9 5 | http://blueprintcss.org 6 | 7 | * Copyright (c) 2007-Present. See LICENSE for more info. 8 | * See README for instructions on how to use Blueprint. 9 | * For credits and origins, see AUTHORS. 10 | * This is a compressed file. See the sources in the 'src' directory. 11 | 12 | ----------------------------------------------------------------------- */ 13 | 14 | /* print.css */ 15 | body {line-height:1.5;font-family:"Helvetica Neue", Arial, Helvetica, sans-serif;color:#000;background:none;font-size:10pt;} 16 | .container {background:none;} 17 | hr {background:#ccc;color:#ccc;width:100%;height:2px;margin:2em 0;padding:0;border:none;} 18 | hr.space {background:#fff;color:#fff;visibility:hidden;} 19 | h1, h2, h3, h4, h5, h6 {font-family:"Helvetica Neue", Arial, "Lucida Grande", sans-serif;} 20 | code {font:.9em "Courier New", Monaco, Courier, monospace;} 21 | a img {border:none;} 22 | p img.top {margin-top:0;} 23 | blockquote {margin:1.5em;padding:1em;font-style:italic;font-size:.9em;} 24 | .small {font-size:.9em;} 25 | .large {font-size:1.1em;} 26 | .quiet {color:#999;} 27 | .hide {display:none;} 28 | a:link, a:visited {background:transparent;font-weight:700;text-decoration:underline;} 29 | a:link:after, a:visited:after {content:" (" attr(href) ")";font-size:90%;} -------------------------------------------------------------------------------- /books/app_settings.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010, One Laptop Per Child 2 | # Copyright (C) 2010, Kushal Das 3 | # 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 17 | 18 | from django.conf import settings 19 | 20 | # Number of books shown per page in the OPDS catalogs and in the HTML 21 | # pages: 22 | 23 | BOOKS_PER_PAGE = getattr(settings, 'BOOKS_PER_PAGE', 2) 24 | AUTHORS_PER_PAGE = getattr(settings, 'AUTHORS_PER_PAGE', 10) 25 | 26 | # If True, serve static media via Django. Note that this is not 27 | # recommended for production: 28 | 29 | BOOKS_STATICS_VIA_DJANGO = getattr(settings, 'BOOKS_STATICS_VIA_DJANGO', False) 30 | 31 | # This needs to match the published status 32 | 33 | BOOK_PUBLISHED = getattr(settings, 'BOOK_PUBLISHED', 1) 34 | -------------------------------------------------------------------------------- /static/style/blueprint/plugins/link-icons/screen.css: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------- 2 | 3 | link-icons.css 4 | * Icons for links based on protocol or file type. 5 | 6 | See the Readme file in this folder for additional instructions. 7 | 8 | -------------------------------------------------------------- */ 9 | 10 | /* Use this class if a link gets an icon when it shouldn't. */ 11 | body a.noicon { 12 | background:transparent none !important; 13 | padding:0 !important; 14 | margin:0 !important; 15 | } 16 | 17 | /* Make sure the icons are not cut */ 18 | a[href^="http:"], a[href^="mailto:"], a[href^="http:"]:visited, 19 | a[href$=".pdf"], a[href$=".doc"], a[href$=".xls"], a[href$=".rss"], 20 | a[href$=".rdf"], a[href^="aim:"] { 21 | padding:2px 22px 2px 0; 22 | margin:-2px 0; 23 | background-repeat: no-repeat; 24 | background-position: right center; 25 | } 26 | 27 | /* External links */ 28 | a[href^="http:"] { background-image: url(icons/external.png); } 29 | a[href^="mailto:"] { background-image: url(icons/email.png); } 30 | a[href^="http:"]:visited { background-image: url(icons/visited.png); } 31 | 32 | /* Files */ 33 | a[href$=".pdf"] { background-image: url(icons/pdf.png); } 34 | a[href$=".doc"] { background-image: url(icons/doc.png); } 35 | a[href$=".xls"] { background-image: url(icons/xls.png); } 36 | 37 | /* Misc */ 38 | a[href$=".rss"], 39 | a[href$=".rdf"] { background-image: url(icons/feed.png); } 40 | a[href^="aim:"] { background-image: url(icons/im.png); } -------------------------------------------------------------------------------- /books/admin.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010, One Laptop Per Child 2 | # Copyright (C) 2010, Kushal Das 3 | # 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 17 | 18 | from books.models import Book, Language, Status, TagGroup, Author 19 | from django.contrib import admin 20 | 21 | 22 | class BookAdmin(admin.ModelAdmin): 23 | fieldsets = [ 24 | (None, {'fields': ['book_file']}), 25 | ('Basic Information', {'fields': ['a_title', 'a_author', 'a_status', 'tags']}), 26 | ('Extended information', {'fields': ['a_summary', 'a_category', 'a_rights', 'dc_language', 'dc_publisher', 'dc_issued', 'dc_identifier', 'cover_img'], 'classes': ['collapse']}), 27 | ] 28 | 29 | class AuthorAdmin(admin.ModelAdmin): 30 | fieldsets = [(None, {'fields': ['a_author']})] 31 | 32 | class LanguageAdmin(admin.ModelAdmin): 33 | fieldsets = [(None, {'fields': ['label']})] 34 | 35 | 36 | class TagGroupAdmin(admin.ModelAdmin): 37 | prepopulated_fields = {"slug": ("name",)} 38 | 39 | 40 | admin.site.register(Book, BookAdmin) 41 | admin.site.register(Author, AuthorAdmin) 42 | admin.site.register(Language, LanguageAdmin) 43 | admin.site.register(Status) 44 | admin.site.register(TagGroup, TagGroupAdmin) 45 | -------------------------------------------------------------------------------- /books/forms.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010, One Laptop Per Child 2 | # Copyright (C) 2010, Kushal Das 3 | # 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 17 | 18 | from django.forms import ModelForm 19 | from books.models import Book, Language, Author 20 | 21 | class BookForm(ModelForm): 22 | # dc_language = ModelChoiceField(Language.objects, widget=SelectWithPop) 23 | 24 | class Meta: 25 | model = Book 26 | exclude = ('mimetype', 'file_sha256sum', ) 27 | 28 | def save(self, commit=True): 29 | """ 30 | Store the MIME type of the uploaded book in the database. 31 | 32 | This is given by the browser in the POST request. 33 | 34 | """ 35 | instance = super(BookForm, self).save(commit=False) 36 | book_file = self.cleaned_data['book_file'] 37 | if instance.mimetype is None: 38 | instance.mimetype = book_file.content_type 39 | if commit: 40 | instance.save() 41 | return instance 42 | 43 | 44 | class AuthorForm(ModelForm): 45 | class Meta: 46 | model = Author 47 | exclude = () 48 | 49 | 50 | class AddLanguageForm(ModelForm): 51 | class Meta: 52 | model = Language 53 | exclude = ('code',) 54 | -------------------------------------------------------------------------------- /static/style/blueprint/ie.css: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------- 2 | 3 | 4 | Blueprint CSS Framework 0.9 5 | http://blueprintcss.org 6 | 7 | * Copyright (c) 2007-Present. See LICENSE for more info. 8 | * See README for instructions on how to use Blueprint. 9 | * For credits and origins, see AUTHORS. 10 | * This is a compressed file. See the sources in the 'src' directory. 11 | 12 | ----------------------------------------------------------------------- */ 13 | 14 | /* ie.css */ 15 | body {text-align:center;} 16 | .container {text-align:left;} 17 | * html .column, * html div.span-1, * html div.span-2, * html div.span-3, * html div.span-4, * html div.span-5, * html div.span-6, * html div.span-7, * html div.span-8, * html div.span-9, * html div.span-10, * html div.span-11, * html div.span-12, * html div.span-13, * html div.span-14, * html div.span-15, * html div.span-16, * html div.span-17, * html div.span-18, * html div.span-19, * html div.span-20, * html div.span-21, * html div.span-22, * html div.span-23, * html div.span-24 {display:inline;overflow-x:hidden;} 18 | * html legend {margin:0px -8px 16px 0;padding:0;} 19 | ol {margin-left:2em;} 20 | sup {vertical-align:text-top;} 21 | sub {vertical-align:text-bottom;} 22 | html>body p code {*white-space:normal;} 23 | hr {margin:-8px auto 11px;} 24 | img {-ms-interpolation-mode:bicubic;} 25 | .clearfix, .container {display:inline-block;} 26 | * html .clearfix, * html .container {height:1%;} 27 | fieldset {padding-top:0;} 28 | textarea {overflow:auto;} 29 | input.text, input.title, textarea {background-color:#fff;border:1px solid #bbb;} 30 | input.text:focus, input.title:focus {border-color:#666;} 31 | input.text, input.title, textarea, select {margin:0.5em 0;} 32 | input.checkbox, input.radio {position:relative;top:.25em;} 33 | form.inline div, form.inline p {vertical-align:middle;} 34 | form.inline label {position:relative;top:-0.25em;} 35 | form.inline input.checkbox, form.inline input.radio, form.inline input.button, form.inline button {margin:0.5em 0;} 36 | button, input.button {position:relative;top:0.25em;} -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | build-python2: 3 | working_directory: ~/pathagar 4 | docker: 5 | - image: circleci/python:2.7.14 6 | - image: circleci/postgres:9.6.2 7 | steps: 8 | - checkout 9 | - restore_cache: 10 | key: v1-python2-{{ checksum "requirements.txt" }}-{{ checksum "test-requirements.txt" }} 11 | - run: 12 | command: | 13 | virtualenv venv 14 | . venv/bin/activate 15 | pip install -r requirements.txt 16 | pip install -r test-requirements.txt 17 | - save_cache: 18 | key: v1-python2-{{ checksum "requirements.txt" }}-{{ checksum "test-requirements.txt" }} 19 | paths: 20 | - "venv" 21 | - run: 22 | command: | 23 | . venv/bin/activate 24 | scripts/run-pylint.sh 25 | coverage run --source='.' manage.py test 26 | - run: 27 | command: | 28 | . venv/bin/activate 29 | codecov 30 | 31 | 32 | build-python3: 33 | working_directory: ~/pathagar 34 | docker: 35 | - image: circleci/python:3.5.3 36 | - image: circleci/postgres:9.6.2 37 | steps: 38 | - checkout 39 | - restore_cache: 40 | key: v1-python3-{{ checksum "requirements.txt" }}-{{ checksum "test-requirements.txt" }} 41 | - run: 42 | command: | 43 | python -m venv venv 44 | . venv/bin/activate 45 | pip install -r requirements.txt 46 | pip install -r test-requirements.txt 47 | - save_cache: 48 | key: v1-python3-{{ checksum "requirements.txt" }}-{{ checksum "test-requirements.txt" }} 49 | paths: 50 | - "venv" 51 | - run: 52 | command: | 53 | . venv/bin/activate 54 | scripts/run-pylint.sh 55 | coverage run --source='.' manage.py test 56 | - run: 57 | command: | 58 | . venv/bin/activate 59 | codecov 60 | 61 | workflows: 62 | version: 2 63 | build-and-test: 64 | jobs: 65 | - build-python2 66 | - build-python3 67 | -------------------------------------------------------------------------------- /static/js/jquery.example.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery Form Example Plugin 1.4.2 3 | * Populate form inputs with example text that disappears on focus. 4 | * 5 | * e.g. 6 | * $('input#name').example('Bob Smith'); 7 | * $('input[@title]').example(function() { 8 | * return $(this).attr('title'); 9 | * }); 10 | * $('textarea#message').example('Type your message here', { 11 | * className: 'example_text' 12 | * }); 13 | * 14 | * Copyright (c) Paul Mucur (http://mucur.name), 2007-2008. 15 | * Dual-licensed under the BSD (BSD-LICENSE.txt) and GPL (GPL-LICENSE.txt) 16 | * licenses. 17 | * 18 | * This program is free software; you can redistribute it and/or modify 19 | * it under the terms of the GNU General Public License as published by 20 | * the Free Software Foundation; either version 2 of the License, or 21 | * (at your option) any later version. 22 | * 23 | * This program is distributed in the hope that it will be useful, 24 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 25 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 26 | * GNU General Public License for more details. 27 | */ 28 | (function(a){a.fn.example=function(e,c){var d=a.isFunction(e);var b=a.extend({},c,{example:e});return this.each(function(){var f=a(this);if(a.metadata){var g=a.extend({},a.fn.example.defaults,f.metadata(),b)}else{var g=a.extend({},a.fn.example.defaults,b)}if(!a.fn.example.boundClassNames[g.className]){a(window).unload(function(){a("."+g.className).val("")});a("form").submit(function(){a(this).find("."+g.className).val("")});a.fn.example.boundClassNames[g.className]=true}if(a.browser.msie&&!f.attr("defaultValue")&&(d||f.val()==g.example)){f.val("")}if(f.val()==""&&this!=document.activeElement){f.addClass(g.className);f.val(d?g.example.call(this):g.example)}f.focus(function(){if(a(this).is("."+g.className)){a(this).val("");a(this).removeClass(g.className)}});f.change(function(){if(a(this).is("."+g.className)){a(this).removeClass(g.className)}});f.blur(function(){if(a(this).val()==""){a(this).addClass(g.className);a(this).val(d?g.example.call(this):g.example)}})})};a.fn.example.defaults={className:"example"};a.fn.example.boundClassNames=[]})(jQuery); 29 | -------------------------------------------------------------------------------- /books/uuidfield.py: -------------------------------------------------------------------------------- 1 | from django.db.models import CharField 2 | 3 | try: 4 | import uuid 5 | except ImportError: 6 | from django.utils import uuid 7 | 8 | class UUIDVersionError(Exception): 9 | pass 10 | 11 | class UUIDField(CharField): 12 | """ UUIDField for Django, supports all uuid versions which are natively 13 | suported by the uuid python module. 14 | """ 15 | 16 | def __init__(self, verbose_name=None, name=None, auto=True, version=1, node=None, clock_seq=None, namespace=None, **kwargs): 17 | kwargs['max_length'] = 36 18 | self.auto = auto 19 | if auto: 20 | kwargs['blank'] = True 21 | kwargs['editable'] = kwargs.get('editable', False) 22 | self.version = version 23 | if version==1: 24 | self.node, self.clock_seq = node, clock_seq 25 | elif version==3 or version==5: 26 | self.namespace, self.name = namespace, name 27 | CharField.__init__(self, verbose_name, name, **kwargs) 28 | 29 | def get_internal_type(self): 30 | return CharField.__name__ 31 | 32 | def create_uuid(self): 33 | if not self.version or self.version==4: 34 | return uuid.uuid4() 35 | elif self.version==1: 36 | return uuid.uuid1(self.node, self.clock_seq) 37 | elif self.version==2: 38 | raise UUIDVersionError("UUID version 2 is not supported.") 39 | elif self.version==3: 40 | return uuid.uuid3(self.namespace, self.name) 41 | elif self.version==5: 42 | return uuid.uuid5(self.namespace, self.name) 43 | else: 44 | raise UUIDVersionError("UUID version %s is not valid." % self.version) 45 | 46 | def pre_save(self, model_instance, add): 47 | if self.auto and add: 48 | value = str(self.create_uuid()) 49 | setattr(model_instance, self.attname, value) 50 | return value 51 | else: 52 | value = super(UUIDField, self).pre_save(model_instance, add) 53 | if self.auto and not value: 54 | value = str(self.create_uuid()) 55 | setattr(model_instance, self.attname, value) 56 | return value 57 | -------------------------------------------------------------------------------- /static/style/blueprint/plugins/buttons/screen.css: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------- 2 | 3 | buttons.css 4 | * Gives you some great CSS-only buttons. 5 | 6 | Created by Kevin Hale [particletree.com] 7 | * particletree.com/features/rediscovering-the-button-element 8 | 9 | See Readme.txt in this folder for instructions. 10 | 11 | -------------------------------------------------------------- */ 12 | 13 | a.button, button { 14 | display:block; 15 | float:left; 16 | margin: 0.7em 0.5em 0.7em 0; 17 | padding:5px 10px 5px 7px; /* Links */ 18 | 19 | border:1px solid #dedede; 20 | border-top:1px solid #eee; 21 | border-left:1px solid #eee; 22 | 23 | background-color:#f5f5f5; 24 | font-family:"Lucida Grande", Tahoma, Arial, Verdana, sans-serif; 25 | font-size:100%; 26 | line-height:130%; 27 | text-decoration:none; 28 | font-weight:bold; 29 | color:#565656; 30 | cursor:pointer; 31 | } 32 | button { 33 | width:auto; 34 | overflow:visible; 35 | padding:4px 10px 3px 7px; /* IE6 */ 36 | } 37 | button[type] { 38 | padding:4px 10px 4px 7px; /* Firefox */ 39 | line-height:17px; /* Safari */ 40 | } 41 | *:first-child+html button[type] { 42 | padding:4px 10px 3px 7px; /* IE7 */ 43 | } 44 | button img, a.button img{ 45 | margin:0 3px -3px 0 !important; 46 | padding:0; 47 | border:none; 48 | width:16px; 49 | height:16px; 50 | float:none; 51 | } 52 | 53 | 54 | /* Button colors 55 | -------------------------------------------------------------- */ 56 | 57 | /* Standard */ 58 | button:hover, a.button:hover{ 59 | background-color:#dff4ff; 60 | border:1px solid #c2e1ef; 61 | color:#336699; 62 | } 63 | a.button:active{ 64 | background-color:#6299c5; 65 | border:1px solid #6299c5; 66 | color:#fff; 67 | } 68 | 69 | /* Positive */ 70 | body .positive { 71 | color:#529214; 72 | } 73 | a.positive:hover, button.positive:hover { 74 | background-color:#E6EFC2; 75 | border:1px solid #C6D880; 76 | color:#529214; 77 | } 78 | a.positive:active { 79 | background-color:#529214; 80 | border:1px solid #529214; 81 | color:#fff; 82 | } 83 | 84 | /* Negative */ 85 | body .negative { 86 | color:#d12f19; 87 | } 88 | a.negative:hover, button.negative:hover { 89 | background-color:#fbe3e4; 90 | border:1px solid #fbc2c4; 91 | color:#d12f19; 92 | } 93 | a.negative:active { 94 | background-color:#d12f19; 95 | border:1px solid #d12f19; 96 | color:#fff; 97 | } 98 | -------------------------------------------------------------------------------- /static/style/blueprint/plugins/fancy-type/screen.css: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------- 2 | 3 | fancy-type.css 4 | * Lots of pretty advanced classes for manipulating text. 5 | 6 | See the Readme file in this folder for additional instructions. 7 | 8 | -------------------------------------------------------------- */ 9 | 10 | /* Indentation instead of line shifts for sibling paragraphs. */ 11 | p + p { text-indent:2em; margin-top:-1.5em; } 12 | form p + p { text-indent: 0; } /* Don't want this in forms. */ 13 | 14 | 15 | /* For great looking type, use this code instead of asdf: 16 | asdf 17 | Best used on prepositions and ampersands. */ 18 | 19 | .alt { 20 | color: #666; 21 | font-family: "Warnock Pro", "Goudy Old Style","Palatino","Book Antiqua", Georgia, serif; 22 | font-style: italic; 23 | font-weight: normal; 24 | } 25 | 26 | 27 | /* For great looking quote marks in titles, replace "asdf" with: 28 | asdf” 29 | (That is, when the title starts with a quote mark). 30 | (You may have to change this value depending on your font size). */ 31 | 32 | .dquo { margin-left: -.5em; } 33 | 34 | 35 | /* Reduced size type with incremental leading 36 | (http://www.markboulton.co.uk/journal/comments/incremental_leading/) 37 | 38 | This could be used for side notes. For smaller type, you don't necessarily want to 39 | follow the 1.5x vertical rhythm -- the line-height is too much. 40 | 41 | Using this class, it reduces your font size and line-height so that for 42 | every four lines of normal sized type, there is five lines of the sidenote. eg: 43 | 44 | New type size in em's: 45 | 10px (wanted side note size) / 12px (existing base size) = 0.8333 (new type size in ems) 46 | 47 | New line-height value: 48 | 12px x 1.5 = 18px (old line-height) 49 | 18px x 4 = 72px 50 | 72px / 5 = 14.4px (new line height) 51 | 14.4px / 10px = 1.44 (new line height in em's) */ 52 | 53 | p.incr, .incr p { 54 | font-size: 10px; 55 | line-height: 1.44em; 56 | margin-bottom: 1.5em; 57 | } 58 | 59 | 60 | /* Surround uppercase words and abbreviations with this class. 61 | Based on work by Jørgen Arnor Gårdsø Lom [http://twistedintellect.com/] */ 62 | 63 | .caps { 64 | font-variant: small-caps; 65 | letter-spacing: 1px; 66 | text-transform: lowercase; 67 | font-size:1.2em; 68 | line-height:1%; 69 | font-weight:bold; 70 | padding:0 2px; 71 | } 72 | -------------------------------------------------------------------------------- /books/management/commands/exportbooks.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010, One Laptop Per Child 2 | # Copyright (C) 2017, Michael Bonfils 3 | # 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 17 | 18 | from django.core.management.base import BaseCommand, CommandError 19 | from django.core.exceptions import ValidationError 20 | from django.core.files import File 21 | 22 | from django.db import transaction 23 | from django.db.utils import IntegrityError 24 | 25 | import csv 26 | import json 27 | import logging 28 | import os 29 | import shutil 30 | import sys 31 | from optparse import make_option 32 | 33 | from books.models import Book, Status 34 | 35 | logger = logging.getLogger(__name__) 36 | logging.basicConfig() 37 | logger.setLevel(logging.DEBUG) 38 | 39 | class Command(BaseCommand): 40 | help = "Dump collection in directory (CSV + books)" 41 | args = 'Directory Path' 42 | 43 | 44 | def _handle_csv(self, csvpath): 45 | """ 46 | Export books into directory with CSV catalog 47 | WARN: does not handle tags 48 | 49 | """ 50 | 51 | csvfile = open(csvpath + "/catalog.csv", "w") 52 | writer = csv.DictWriter(csvfile, ['filename', 'title', 'author', 'summary']) 53 | writer.writeheader() 54 | 55 | for book in Book.objects.all(): 56 | shutil.copy(book.book_file.file.name, csvpath) 57 | 58 | entry = {'filename': os.path.basename(book.book_file.name).encode('utf-8'), 59 | 'a_title': book.a_title.encode('utf-8'), 60 | 'a_author': book.a_author.a_author.encode('utf-8'), 61 | 'a_summary': book.a_summary.encode('utf-8')} 62 | writer.writerow(entry) 63 | csvfile.close() 64 | 65 | 66 | def handle(self, filepath='', *args, **options): 67 | if filepath == '' or not os.path.exists(filepath): 68 | raise CommandError("%r is not a valid path" % filepath) 69 | filepath = os.path.abspath(filepath) 70 | 71 | self._handle_csv(filepath) 72 | -------------------------------------------------------------------------------- /books/search.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010, One Laptop Per Child 2 | # Copyright (C) 2010, Kushal Das 3 | # 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 17 | 18 | from django.db.models import Q 19 | from books.models import Book 20 | 21 | 22 | def simple_search(queryset, searchterms, 23 | search_title=False, search_author=False): 24 | q_objects = [] 25 | results = queryset 26 | 27 | subterms = searchterms.split(' ') 28 | for subterm in subterms: 29 | word = subterm 30 | if search_title: 31 | q_objects.append(Q(a_title__icontains = word)) 32 | if search_author: 33 | q_objects.append(Q(a_author__icontains = word)) 34 | 35 | for q_object in q_objects: 36 | results = results.filter(q_object) 37 | return results 38 | 39 | def advanced_search(queryset, searchterms): 40 | """ 41 | Does an advanced search in several fields of the books. 42 | """ 43 | q_objects = [] 44 | results = queryset 45 | 46 | subterms = searchterms.split('AND') 47 | for subterm in subterms: 48 | if ':' in subterm: 49 | key, word = subterm.split(':') 50 | key = key.strip() 51 | if key == 'title': 52 | q_objects.append(Q(a_title__icontains = word)) 53 | if key == 'author': 54 | q_objects.append(Q(a_author__icontains = word)) 55 | if key == 'publisher': 56 | q_objects.append(Q(dc_publisher__icontains = word)) 57 | if key == 'identifier': 58 | q_objects.append(Q(dc_identifier__icontains = word)) 59 | if key == 'summary': 60 | q_objects.append(Q(a_summary__icontains = word)) 61 | else: 62 | word = subterm 63 | try: 64 | results = results.filter(Q(a_title__icontains = word) | \ 65 | Q(a_author__a_author__icontains = word) | \ 66 | Q(dc_publisher__icontains = word) | \ 67 | Q(dc_identifier__icontains = word) | \ 68 | Q(a_summary__icontains = word)) 69 | except Book.DoesNotExist: 70 | results = Book.objects.none() 71 | 72 | for q_object in q_objects: 73 | results = results.filter(q_object) 74 | return results 75 | -------------------------------------------------------------------------------- /books/urls.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.conf.urls import include, url 4 | 5 | from django.conf import settings 6 | from books.app_settings import BOOKS_STATICS_VIA_DJANGO 7 | 8 | from books import views 9 | 10 | 11 | urlpatterns = [ 12 | 13 | # Book list: 14 | url(r'^$', views.home, 15 | kwargs={}, name='home'), 16 | url(r'^latest/$', views.latest, 17 | {}, name='latest'), 18 | url(r'^by-title/$', views.by_title, 19 | {}, 'by_title'), 20 | url(r'^by-author/$', views.by_author, 21 | {}, 'by_author'), 22 | url(r'^by-author/(?P\d+)/$', views.by_title, 23 | {}, 'books_by_author'), 24 | url(r'^tags/(?P.+)/$', views.by_tag, 25 | {}, 'by_tag'), 26 | url(r'^by-popularity/$', views.most_downloaded, 27 | {}, 'most_downloaded'), 28 | 29 | # Tag groups: 30 | url(r'^tags/groups.atom$', views.tags_listgroups, 31 | {}, 'tags_listgroups'), 32 | 33 | # Book list Atom: 34 | url(r'^catalog.atom$', views.root, 35 | {'qtype': u'feed'}, 'root_feed'), 36 | url(r'^latest.atom$', views.latest, 37 | {'qtype': u'feed'}, 'latest_feed'), 38 | url(r'^by-title.atom$', views.by_title, 39 | {'qtype': u'feed'}, 'by_title_feed'), 40 | url(r'^by-author.atom$', views.by_author, 41 | {'qtype': u'feed'}, 'by_author_feed'), 42 | url(r'^by-author/(?P\d+).atom$', views.by_title, 43 | {'qtype': u'feed'}, 'by_author_feed'), 44 | url(r'^tags/(?P.+).atom$', views.by_tag, 45 | {'qtype': u'feed'}, 'by_tag_feed'), 46 | url(r'^by-popularity.atom$', views.most_downloaded, 47 | {'qtype': u'feed'}, 'most_downloaded_feed'), 48 | 49 | # Tag groups: 50 | url(r'^tags/groups/(?P[-\w]+)/$', views.tags, 51 | {}, 'tag_groups'), 52 | 53 | url(r'^tags/groups/(?P[-\w]+).atom$', views.tags, 54 | {'qtype': u'feed'}, 'tag_groups_feed'), 55 | 56 | # Tag list: 57 | url(r'^tags/$', views.tags, {}, 'tags'), 58 | url(r'^tags.atom$', views.tags, 59 | {'qtype': u'feed'}, 'tags_feed'), 60 | 61 | # Add, view, edit and remove books: 62 | url(r'^book/add$', views.BookAddView.as_view(), name='book_add'), 63 | url(r'^book/(?P\d+)/view$', views.BookDetailView.as_view(), name='book_detail'), 64 | url(r'^book/(?P\d+)/edit$', views.BookEditView.as_view(), name='book_edit'), 65 | url(r'^book/(?P\d+)/remove$', views.BookDeleteView.as_view(), name='book_remove'), 66 | url(r'^book/(?P\d+)/download$', views.download_book, name='book_download'), 67 | 68 | # Comments 69 | # FIXME (r'^comments/', include('django.contrib.comments.urls')), 70 | 71 | # Add language: 72 | url(r'^add/dc_language|language/$', views.add_language), 73 | ] 74 | 75 | 76 | if BOOKS_STATICS_VIA_DJANGO: 77 | from django.views.static import serve 78 | # Serve static media: 79 | # urlpatterns += patterns('', 80 | urlpatterns += [ 81 | url(r'^static_media/(?P.*)$', serve, 82 | {'document_root': settings.MEDIA_ROOT, 'show_indexes': True}), 83 | 84 | # Book covers: 85 | url(r'^covers/(?P.*)$', serve, 86 | {'document_root': os.path.join(settings.MEDIA_ROOT, 'covers')}), 87 | ] 88 | -------------------------------------------------------------------------------- /static/js/RelatedObjectLookups.js: -------------------------------------------------------------------------------- 1 | // Handles related-objects functionality: lookup link for raw_id_fields 2 | // and Add Another links. 3 | 4 | function html_unescape(text) { 5 | // Unescape a string that was escaped using django.utils.html.escape. 6 | text = text.replace(/</g, '<'); 7 | text = text.replace(/>/g, '>'); 8 | text = text.replace(/"/g, '"'); 9 | text = text.replace(/'/g, "'"); 10 | text = text.replace(/&/g, '&'); 11 | return text; 12 | } 13 | 14 | // IE doesn't accept periods or dashes in the window name, but the element IDs 15 | // we use to generate popup window names may contain them, therefore we map them 16 | // to allowed characters in a reversible way so that we can locate the correct 17 | // element when the popup window is dismissed. 18 | function id_to_windowname(text) { 19 | text = text.replace(/\./g, '__dot__'); 20 | text = text.replace(/\-/g, '__dash__'); 21 | return text; 22 | } 23 | 24 | function windowname_to_id(text) { 25 | text = text.replace(/__dot__/g, '.'); 26 | text = text.replace(/__dash__/g, '-'); 27 | return text; 28 | } 29 | 30 | function showRelatedObjectLookupPopup(triggeringLink) { 31 | var name = triggeringLink.id.replace(/^lookup_/, ''); 32 | name = id_to_windowname(name); 33 | var href; 34 | if (triggeringLink.href.search(/\?/) >= 0) { 35 | href = triggeringLink.href + '&pop=1'; 36 | } else { 37 | href = triggeringLink.href + '?pop=1'; 38 | } 39 | var win = window.open(href, name, 'height=500,width=800,resizable=yes,scrollbars=yes'); 40 | win.focus(); 41 | return false; 42 | } 43 | 44 | function dismissRelatedLookupPopup(win, chosenId) { 45 | var name = windowname_to_id(win.name); 46 | var elem = document.getElementById(name); 47 | if (elem.className.indexOf('vManyToManyRawIdAdminField') != -1 && elem.value) { 48 | elem.value += ',' + chosenId; 49 | } else { 50 | document.getElementById(name).value = chosenId; 51 | } 52 | win.close(); 53 | } 54 | 55 | function showAddAnotherPopup(triggeringLink) { 56 | var name = triggeringLink.id.replace(/^add_/, ''); 57 | name = id_to_windowname(name); 58 | href = triggeringLink.href 59 | if (href.indexOf('?') == -1) { 60 | href += '?_popup=1'; 61 | } else { 62 | href += '&_popup=1'; 63 | } 64 | var win = window.open(href, name, 'height=500,width=800,resizable=yes,scrollbars=yes'); 65 | win.focus(); 66 | return false; 67 | } 68 | 69 | function dismissAddAnotherPopup(win, newId, newRepr) { 70 | // newId and newRepr are expected to have previously been escaped by 71 | // django.utils.html.escape. 72 | newId = html_unescape(newId); 73 | newRepr = html_unescape(newRepr); 74 | var name = windowname_to_id(win.name); 75 | var elem = document.getElementById(name); 76 | if (elem) { 77 | var elemName = elem.nodeName.toUpperCase(); 78 | if (elemName == 'SELECT') { 79 | var o = new Option(newRepr, newId); 80 | elem.options[elem.options.length] = o; 81 | o.selected = true; 82 | } else if (elemName == 'INPUT') { 83 | if (elem.className.indexOf('vManyToManyRawIdAdminField') != -1 && elem.value) { 84 | elem.value += ',' + newId; 85 | } else { 86 | elem.value = newId; 87 | } 88 | } 89 | } else { 90 | var toId = name + "_to"; 91 | elem = document.getElementById(toId); 92 | var o = new Option(newRepr, newId); 93 | SelectBox.add_to_cache(toId, o); 94 | SelectBox.redisplay(toId); 95 | } 96 | win.close(); 97 | } 98 | -------------------------------------------------------------------------------- /pathagar/settings.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | This settings are for testing Pathagar with the Django development 4 | server. It will use a SQLite database in the current directory and 5 | Pathagar will be available at http://127.0.0.1:8000 loopback address. 6 | 7 | For production, you should use a proper web server to deploy Django, 8 | serve static files, and setup a proper database. 9 | 10 | """ 11 | 12 | 13 | import os 14 | 15 | 16 | # Books settings: 17 | 18 | BOOKS_PER_PAGE = 20 # Number of books shown per page in the OPDS 19 | # catalogs and in the HTML pages. 20 | AUTHORS_PER_PAGE = 40 # Number of books shown per page in the OPDS 21 | # catalogs and in the HTML pages. 22 | 23 | BOOKS_STATICS_VIA_DJANGO = True 24 | DEFAULT_BOOK_STATUS = 'Published' 25 | 26 | # Allow non logued users to upload books 27 | ALLOW_PUBLIC_ADD_BOOKS = False 28 | 29 | # sendfile settings: 30 | 31 | SENDFILE_BACKEND = 'sendfile.backends.development' 32 | 33 | # Get current directory to get media and templates while developing: 34 | BASE_DIR = os.path.dirname(os.path.dirname(__file__)) 35 | 36 | DEBUG = True 37 | # TEMPLATE_DEBUG = DEBUG 38 | 39 | ADMINS = ( 40 | # ('Your Name', 'your_email@domain.com'), 41 | ) 42 | 43 | MANAGERS = ADMINS 44 | 45 | DATABASES = { 46 | 'default': { 47 | 'ENGINE': 'django.db.backends.sqlite3', 48 | 'NAME': os.path.join(BASE_DIR, 'database.db'), 49 | } 50 | } 51 | 52 | TIME_ZONE = 'America/Chicago' 53 | 54 | LANGUAGE_CODE = 'en-us' 55 | 56 | SITE_ID = 1 57 | 58 | USE_I18N = True 59 | 60 | MEDIA_ROOT = os.path.join(BASE_DIR, 'static_media') 61 | 62 | MEDIA_URL = '/static_media/' 63 | 64 | SECRET_KEY = '7ks@b7+gi^c4adff)6ka228#rd4f62v*g_dtmo*@i62k)qn=cs' 65 | 66 | # TEMPLATE_LOADERS = ( 67 | # 'django.template.loaders.filesystem.Loader', 68 | # 'django.template.loaders.app_directories.Loader', 69 | # ) 70 | 71 | MIDDLEWARE_CLASSES = ( 72 | 'django.middleware.common.CommonMiddleware', 73 | 'django.contrib.sessions.middleware.SessionMiddleware', 74 | 'django.middleware.csrf.CsrfViewMiddleware', 75 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 76 | 'django.contrib.messages.middleware.MessageMiddleware', 77 | ) 78 | 79 | ROOT_URLCONF = 'pathagar.urls' 80 | 81 | INTERNAL_IPS = ('127.0.0.1',) 82 | 83 | # TEMPLATE_DIRS = ( 84 | # os.path.join(os.path.dirname(__file__), 'templates'), 85 | # ) 86 | TEMPLATES = [ 87 | { 88 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 89 | 'DIRS': [ 90 | os.path.join(BASE_DIR, 'templates'), 91 | ], 92 | 'APP_DIRS': True, 93 | 'OPTIONS': { 94 | 'context_processors': [ 95 | 'django.template.context_processors.debug', 96 | 'django.template.context_processors.request', 97 | 'django.contrib.auth.context_processors.auth', 98 | 'django.contrib.messages.context_processors.messages', 99 | ], 100 | }, 101 | }, 102 | ] 103 | 104 | STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') 105 | STATIC_URL = '/static/' 106 | STATICFILES_DIRS = ( 107 | os.path.join(BASE_DIR, 'static'), 108 | ) 109 | 110 | ALLOW_USER_COMMENTS = False 111 | 112 | INSTALLED_APPS = ( 113 | 'django.contrib.auth', 114 | 'django.contrib.contenttypes', 115 | 'django.contrib.sessions', 116 | 'django.contrib.sites', 117 | 'django.contrib.staticfiles', 118 | 'django.contrib.admin', 119 | 'tagging', # TODO old 120 | 'taggit', 121 | # 'django.contrib.comments', # DEPRECATED, use https://github.com/django/django-contrib-comments 122 | 'books', 123 | ) 124 | 125 | 126 | try: 127 | from local_settings import * 128 | except ImportError: 129 | pass 130 | -------------------------------------------------------------------------------- /templates/books/book_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load static %} 3 | 4 | {% block title %}{% if action == 'add' %} 5 | Add Book 6 | {% else %} 7 | Edit Book 8 | {% endif %} 9 | {% endblock %} 10 | 11 | {% block script %} 12 | $(document).ready(function() 13 | { 14 | $(".hidden_body").hide(); 15 | $(".hidden_head").click(function() 16 | { 17 | $(this).next(".hidden_body").slideToggle(100); 18 | }); 19 | $('#id_dc_identifier').example('ISBN'); 20 | }); 21 | {% endblock %} 22 | 23 | {% block head %}{% endblock %} 24 | 25 | {% block content %} 26 |
{% csrf_token %} 33 |
34 | 35 | {% if action == 'add' %} 36 | Add Book 37 | {% else %} 38 | Edit Book 39 | {% endif %} 40 | 41 | 42 | {% if form.non_field_errors %} 43 |
44 | {{ form.non_field_errors }} 45 |
46 | {% endif %} 47 | 48 |
49 |
{{ form.book_file }}
50 | {{ form.book_file.errors }} 51 |
52 |
53 |
{{ form.a_title }}
54 | {{ form.a_title.errors }} 55 |
56 |
57 |
{{ form.a_author }}
58 | {{ form.a_author.errors }} 59 |
60 |
61 |
{{ form.a_status }}
62 | {{ form.a_status.errors }} 63 |
64 |
65 |
{{ form.tags }}
66 |
67 |
68 |
{{ form.a_summary }}
69 |
70 |
71 |
72 | {{ form.dc_language }} 73 |
74 |
75 | 76 |
77 |

Optional information (click to expand)

78 |
79 |
80 |
{{ form.a_category }}
81 |
82 |
83 |
{{ form.a_rights}}
84 |
85 |
86 |
{{ form.dc_publisher }}
87 |
88 |
89 |
{{ form.dc_issued }}
90 |
91 |
92 |
{{ form.dc_identifier }}
93 |
94 |
95 |
{{ form.downloads }}
96 |
97 |
98 |
{{ form.cover_img }}
99 |
100 |
101 |
102 | 103 |
104 | 107 | {% else %} 108 | value="Edit"> 109 | {% endif %} 110 |
111 |
112 |
113 | {% endblock %} 114 | -------------------------------------------------------------------------------- /templates/books/book_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load static %} 3 | {% load tagging_tags %} 4 | {# {% load comments %} #} 5 | 6 | {% block title %}{{ book.a_title }} :: Pathagar Book Server{% endblock %} 7 | 8 | {% block script %} 9 | {{ block.super }} 10 | $('#search').example('Book Search...'); 11 | {% endblock %} 12 | 13 | {% block content %} 14 | 15 |
16 | 17 |

{{ book.a_title }}

18 |

by 19 | 20 | {{ book.a_author }} 21 | 22 |

23 | 24 |
25 | {% if user.is_authenticated %} 26 |
27 | Edit 28 | Remove 29 |
30 |
31 |
32 |
33 |
{{ book.a_status }}
34 |
35 | {% endif %} 36 | 37 | {% if book.tags.count != 0 %} 38 |
39 |
40 | {% for tag in book.tags.all %} 41 | {{ tag.name }} 42 | {% endfor %} 43 |
44 |
45 | {% endif %} 46 |
47 |
48 | {% ifnotequal book.dc_language None %} 49 |
{{ book.dc_language }}
50 | {% else %} 51 |
Unknown
52 | {% endifnotequal %} 53 |
54 |
55 |
{{ book.downloads }}
56 |
57 |
58 |
{{ book.dc_publisher }}
59 |
60 |
61 |
{{ book.dc_issued }}
62 |
63 |
64 |
{{ book.dc_rights }}
65 |
66 |
67 |

68 | {{ book.a_summary }} 69 |

70 |
71 |
72 | {% comment %} 73 | {% if allow_user_comments %} 74 | 75 |
76 | 77 |
78 |
79 | 80 | {% get_comment_list for book as comment_list %} 81 | {% for comment in comment_list %} 82 |
{{ comment.user_name }} said: {{ comment.comment }}
83 |
84 | {% endfor %} 85 | 86 |
87 | 88 |
89 | 90 |
91 | 92 | {% get_comment_form for book as form %} 93 |
94 | {% csrf_token %} 95 | 96 | {{ form.content_type }} 97 | {{ form.object_pk }} 98 | {{ form.timestamp }} 99 | {{ form.security_hash }} 100 | 101 | 102 |
103 | {{ form.name }} 104 |
105 | 106 |
107 | {{ form.comment }} 108 |
109 | 110 | 111 | 112 | 113 | 114 |
115 | 116 | 117 | {% endif %} 118 | {% endcomment %} 119 |
120 | 121 | 130 | 131 | 132 |
133 | 134 | {% endblock %} 135 | 136 | -------------------------------------------------------------------------------- /templates/authors/author_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load static %} 3 | {% load tagging_tags %} 4 | 5 | {% block title %}Welcome to the Pathagar book server{% endblock %} 6 | 7 | {% block script %} 8 | {{ block.super }} 9 | {% if q != None %} 10 | $('#search').val('{{ q }}'); 11 | {% else %} 12 | $('#search').example('Author Search...'); 13 | {% endif %} 14 | {% endblock %} 15 | 16 | {% block feed_link %} 17 | 38 | Feed IconSubscribe to Feed 39 | {% endblock %} 40 | 41 | {% block content %} 42 | 43 |
44 | {% for author in author_list %} 45 | 54 |
55 |

{{ author.a_author }}

56 | 66 |
67 |

{{ book.a_summary }}

68 |
69 |
70 | 71 |
72 | {% endfor %} 73 | 74 | 89 |
90 | 91 | 121 | 122 | {% endblock %} 123 | 124 | -------------------------------------------------------------------------------- /templates/books/book_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load static %} 3 | {% load tagging_tags %} 4 | 5 | {% block title %}Welcome to the Pathagar book server{% endblock %} 6 | 7 | {% block script %} 8 | {{ block.super }} 9 | {% if q != None %} 10 | $('#search').val('{{ q }}'); 11 | {% else %} 12 | $('#search').example('Book Search...'); 13 | {% endif %} 14 | {% endblock %} 15 | 16 | {% block feed_link %} 17 | Feed IconSubscribe to Feed 38 | {% endblock %} 39 | 40 | {% block content %} 41 | 42 |
43 | {% for book in book_list %} 44 |
45 | {% if book.cover_img %} 46 | Cover 47 | {% else %} 48 | Cover 49 | {% endif %} 50 |
51 |
52 |

53 | {{ book.a_title }} 54 |

55 |

56 | 57 | {{ book.a_author }} 58 | 59 |

60 | {% tags_for_object book as tag_list %} 61 | {% if book.tags.count != 0 %} 62 |
63 | {% for tag in book.tags.all %} 64 | {{ tag.name }} 65 | {% endfor %} 66 |
67 | {% endif %} 68 |
69 |

{{ book.a_summary }}

70 |
71 |
72 | 73 |
74 | {% endfor %} 75 | 76 | 91 |
92 | 93 | 123 | 124 | {% endblock %} 125 | 126 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | Pathagar is a simple book server for schools where there is little or no 4 | internet. For more information, see [this 5 | presentation](http://www.slideshare.net/sverma/pathagar-a-book-server) by Prof. 6 | Sameer Verma. To get involved, please join our [mailing 7 | list](http://mail.archive.org/cgi-bin/mailman/listinfo/pathagar). For some of 8 | the history of the project, read this [blogpost at 9 | OLPC-SF](http://www.olpcsf.org/node/126). Pathagar is built with the 10 | [Django](https://www.djangoproject.com/) framework. 11 | 12 | 13 | ## OPDS 14 | 15 | It uses the [OPDS spec](http://opds-spec.org). 16 | 17 | 18 | ### Known good clients 19 | 20 | We've successfully used the following OPDS clients with Pathagar. 21 | 22 | - Aldiko Book Reader on Android 23 | 24 | 25 | ## Loading content 26 | 27 | Once installed, Pathagar contains no books. You can upload individual books 28 | through the UI or you can bulk load them by following the instructions below. 29 | 30 | 31 | ### From Internet Archive 32 | 33 | You can download book files from Internet Archive. First you need to create 34 | a user and a bookmarks list. Once created, you'll fetch the book files on your 35 | bookmarks list with the `fetch_ia_item` command. 36 | 37 | python manage.py fetch_ia_item --username= --out=books.json 38 | 39 | This can take a while depending on your internet connection and how many books 40 | you have on your bookmarks list. You might want to go grab a coffee and take 41 | a stretch break. 42 | 43 | Once complete, you'll have a `books.json` file which you can use with the 44 | `addbooks` command. 45 | 46 | python manage.py addbooks --json books.json 47 | 48 | Learn more about the `addbooks` command below. 49 | 50 | 51 | ### addbooks command 52 | 53 | If you have your books already with metadata described in a file, use the 54 | `addbooks` command to import them into Pathagar. 55 | 56 | 57 | #### JSON format 58 | 59 | To add books from a JSON file: 60 | 61 | python manage.py addbooks --json books.json 62 | 63 | The format of the JSON file is like:: 64 | 65 | [ 66 | { 67 | "book_path": "Path to ebook file", 68 | "cover_path": "Path to cover image", 69 | "a_title": "Title", 70 | "a_author": "Author", 71 | "a_status": "Published", 72 | "a_summary": "Description", 73 | "tags": "set, of, tags" 74 | }, 75 | ... 76 | ] 77 | 78 | You can add more fields. Please refer to the Book model. 79 | 80 | 81 | #### CSV format 82 | 83 | To add books from a CSV file: 84 | 85 | python manage.py addbooks books.csv 86 | 87 | The format of the CSV file is like: 88 | 89 | ``` 90 | "Path to ebook file","Title","Author","Description" 91 | ``` 92 | 93 | If you need to add more fields, please use the JSON file. 94 | 95 | 96 | ## Dependencies 97 | 98 | * Python 2.7 or Python 3.5 99 | * libxml2-dev 100 | * libxslt-dev 101 | 102 | 103 | ## Quickstart 104 | 105 | Create a virtualenv and install the dependencies. 106 | 107 | python3 -m venv venv_pathagar 108 | . ./venv_pathagar/bin/activate 109 | pip install -r requirements.pip 110 | 111 | In the Pathagar folder, copy `local_settings.example.py` to `local_settings.py` 112 | and edit it to suite your needs and environment. 113 | 114 | Create the database schema and admin user. 115 | 116 | python manage.py migrate 117 | python manage.py createsuperuser 118 | 119 | You will be asked for an admin username and password during this stage. This is 120 | only for development, so an easy to remember username and password is fine. 121 | 122 | Start the development server. 123 | 124 | python manage.py runserver 125 | 126 | Open your web browser to [http://localhost:8000/](http://localhost:8000/). 127 | 128 | Click on "Add books" in the footer to start adding books. You will be asked for 129 | a username/password. This is the admin username/password you supplied while 130 | running `createsuperuser`. 131 | 132 | 133 | ## Deployment 134 | 135 | To run the server in a production environment, look at [Django deployment 136 | docs](https://docs.djangoproject.com/en/1.11/howto/deployment/). 137 | 138 | [pathagar.info](http://pathagar.info/get-pathagar/) also contains some common 139 | options for deploying Pathagar. 140 | -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | {% load static %} 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | {% block title %}{% endblock %} 18 | 19 | 20 | 21 | {% block head %}{% endblock %} 22 | 23 | 24 | 38 |
39 |

Pathagar Book Server

40 |
61 | 62 | 90 |
91 | 92 |
93 | {% if user.is_authenticated %} 94 | Welcome, {{ user.username }}   95 | Add Book   96 | Log Out 97 | {% else %} 98 | {% if allow_public_add_book %} 99 | Add Book   100 | {% endif %} 101 | Log In 102 | {% endif %} 103 |
104 | 105 | {% block feed_link %}{% endblock %} 106 | 107 |
108 | 109 |
110 | {% block content %}{% endblock %} 111 | {% block footer %}{% endblock %} 112 | 117 |
118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /books/management/commands/addepub.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand, CommandError 2 | from django.core.files import File 3 | from django.db.utils import IntegrityError 4 | 5 | import os 6 | 7 | from books.models import Language, Book, Status, Author, sha256_sum 8 | from books.epub import Epub 9 | from books.langlist import langs 10 | 11 | import sys 12 | 13 | def get_epubs(path): 14 | """Returns a list of EPUB(s)""" 15 | epub_list = [] 16 | 17 | for root, dirs, files in os.walk(path): 18 | for name in files: 19 | if name.lower().endswith('.epub'): 20 | epub_list.append(os.path.join(root,name)) 21 | 22 | return epub_list 23 | 24 | 25 | class Command(BaseCommand): 26 | help = "Adds a book collection (via a directory containing EPUB file(s))" 27 | args = 'Absolute path to directory of EPUB files' 28 | 29 | def add_arguments(self, parser): 30 | parser.add_argument('--ignore-error', 31 | action='store_true', 32 | dest='ignore_error', 33 | default=False, 34 | help='Continue after error') 35 | parser.add_argument('dirpath', 36 | help='PATH') 37 | 38 | def handle(self, *args, **options): 39 | dirpath = options.get('dirpath') 40 | if not dirpath or not os.path.exists(dirpath): 41 | raise CommandError("%r is not a valid path" % dirpath) 42 | 43 | 44 | if os.path.isdir(dirpath): 45 | names = get_epubs(dirpath) 46 | for name in names: 47 | info = None 48 | try: 49 | e = Epub(name) 50 | info = e.get_info() 51 | e.close() 52 | except: 53 | print("%s is not a valid epub file" % name) 54 | continue 55 | lang = Language.objects.filter(code=info.language) 56 | if not lang: 57 | for data in langs: 58 | if data[0] == info.language: 59 | lang = Language() 60 | lang.label = data[1] 61 | lang.save() 62 | break 63 | else: 64 | lang = lang[0] 65 | 66 | #XXX: Hacks below 67 | if not info.title: 68 | info.title = '' 69 | if not info.summary: 70 | info.summary = '' 71 | if not info.creator: 72 | info.creator = '' 73 | if not info.rights: 74 | info.rights = '' 75 | if not info.date: 76 | info.date = '' 77 | if not info.identifier: 78 | info.identifier = {} 79 | if not info.identifier.get('value'): 80 | info.identifier['value'] = '' 81 | 82 | f = open(name, "rb") 83 | sha = sha256_sum(open(name, "rb")) 84 | pub_status = Status.objects.get(status='Published') 85 | author = Author.objects.get_or_create(a_author=info.creator)[0] 86 | book = Book(a_title = info.title, 87 | a_author = author, a_summary = info.summary, 88 | file_sha256sum=sha, 89 | a_rights = info.rights, dc_identifier = info.identifier['value'].strip('urn:uuid:'), 90 | dc_issued = info.date, 91 | a_status = pub_status, mimetype="application/epub+zip") 92 | try: 93 | # Not sure why this errors, book_file.save exists 94 | book.book_file.save(os.path.basename(name), File(f)) #pylint: disable=no-member 95 | book.validate_unique() 96 | book.save() 97 | # FIXME: Find a better way to do this. 98 | except IntegrityError as e: 99 | if str(e) == "column file_sha256sum is not unique": 100 | print("The book (", book.book_file, ") was not saved because the file already exsists in the database.") 101 | else: 102 | if options['ignore_error']: 103 | print('Error adding file %s: %s' % (book.book_file, sys.exc_info()[1])) 104 | continue 105 | raise CommandError('Error adding file %s: %s' % (book.book_file, sys.exc_info()[1])) 106 | except: 107 | if options['ignore_error']: 108 | print('Error adding file %s: %s' % (book.book_file, sys.exc_info()[1])) 109 | continue 110 | raise CommandError('Error adding file %s: %s' % (book.book_file, sys.exc_info()[1])) 111 | 112 | -------------------------------------------------------------------------------- /books/epubinfo.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010, One Laptop Per Child 2 | # Copyright (C) 2010, Kushal Das 3 | # 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 17 | 18 | from lxml import etree 19 | 20 | from django.utils.html import strip_tags 21 | 22 | 23 | class EpubInfo(): #TODO: Cover the entire DC range 24 | def __init__(self, opffile): 25 | self._tree = etree.parse(opffile) 26 | self._root = self._tree.getroot() 27 | self._e_metadata = self._root.find('{http://www.idpf.org/2007/opf}metadata') 28 | self._e_manifest = self._root.find('{http://www.idpf.org/2007/opf}manifest') 29 | 30 | self.title = self._get_title() 31 | self.creator = self._get_creator() 32 | self.date = self._get_date() 33 | self.subject = self._get_subject() 34 | self.source = self._get_source() 35 | self.rights = self._get_rights() 36 | self.identifier = self._get_identifier() 37 | self.language = self._get_language() 38 | self.summary = self._get_description() 39 | self.cover_image = self._get_cover_image() 40 | 41 | def _get_data(self, tagname): 42 | element = self._e_metadata.find(tagname) 43 | return element.text 44 | 45 | def _get_description(self): 46 | try: 47 | ret = self._get_data('.//{http://purl.org/dc/elements/1.1/}description') 48 | except AttributeError: 49 | return None 50 | 51 | return strip_tags(ret) 52 | 53 | def _get_title(self): 54 | try: 55 | ret = self._get_data('.//{http://purl.org/dc/elements/1.1/}title') 56 | except AttributeError: 57 | return None 58 | 59 | return ret 60 | 61 | def _get_creator(self): 62 | try: 63 | ret = self._get_data('.//{http://purl.org/dc/elements/1.1/}creator') 64 | except AttributeError: 65 | return None 66 | return ret 67 | 68 | def _get_date(self): 69 | #TODO: iter 70 | try: 71 | ret = self._get_data('.//{http://purl.org/dc/elements/1.1/}date') 72 | except AttributeError: 73 | return None 74 | 75 | return ret 76 | 77 | def _get_source(self): 78 | try: 79 | ret = self._get_data('.//{http://purl.org/dc/elements/1.1/}source') 80 | except AttributeError: 81 | return None 82 | 83 | return ret 84 | 85 | def _get_rights(self): 86 | try: 87 | ret = self._get_data('.//{http://purl.org/dc/elements/1.1/}rights') 88 | except AttributeError: 89 | return None 90 | 91 | return ret 92 | 93 | def _get_identifier(self): 94 | #TODO: iter 95 | element = self._e_metadata.find('.//{http://purl.org/dc/elements/1.1/}identifier') 96 | 97 | if element is not None: 98 | return {'id':element.get('id'), 'value':element.text} 99 | else: 100 | return None 101 | 102 | def _get_language(self): 103 | try: 104 | ret = self._get_data('.//{http://purl.org/dc/elements/1.1/}language') 105 | except AttributeError: 106 | return None 107 | 108 | return ret 109 | 110 | def _get_subject(self): 111 | try: 112 | subjectlist = [] 113 | for element in self._e_metadata.iterfind('.//{http://purl.org/dc/elements/1.1/}subject'): 114 | subjectlist.append(element.text) 115 | except AttributeError: 116 | return None 117 | 118 | return subjectlist 119 | 120 | def _get_cover_image(self): 121 | # TODO: we we should use xpath 122 | elements = self._e_metadata.findall('{http://www.idpf.org/2007/opf}meta') 123 | if len(elements) == 0: 124 | return None 125 | 126 | element = None 127 | for element in elements: 128 | if element.get('name') == 'cover': 129 | break 130 | if element is not None and element.get('name') == 'cover': 131 | xref = element.get('content') 132 | try: 133 | # FIXME: we should use xpath 134 | for item in self._e_manifest.findall('{http://www.idpf.org/2007/opf}item'): 135 | if item.attrib['id'] == xref: 136 | return item.attrib['href'] 137 | except Exception as ex: 138 | # TODO: add a log 139 | return None 140 | else: 141 | return None 142 | -------------------------------------------------------------------------------- /books/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | import books.uuidfield 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | import taggit.managers 8 | 9 | 10 | def status_insert_status(apps, schema_editor): 11 | Status = apps.get_model("books", "Status") 12 | db_alias = schema_editor.connection.alias 13 | Status.objects.using(db_alias).bulk_create([ 14 | Status(pk=1, status="Published"), 15 | Status(pk=2, status="Draft"), 16 | ]) 17 | 18 | 19 | class Migration(migrations.Migration): 20 | 21 | initial = True 22 | 23 | dependencies = [ 24 | ('taggit', '0002_auto_20150616_2121'), 25 | ] 26 | 27 | operations = [ 28 | migrations.CreateModel( 29 | name='Author', 30 | fields=[ 31 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 32 | ('a_author', models.CharField(max_length=200, unique=True, verbose_name='atom:author')), 33 | ], 34 | ), 35 | migrations.CreateModel( 36 | name='Book', 37 | fields=[ 38 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 39 | ('book_file', models.FileField(upload_to='books')), 40 | ('file_sha256sum', models.CharField(max_length=64, unique=True)), 41 | ('mimetype', models.CharField(max_length=200, null=True)), 42 | ('time_added', models.DateTimeField(auto_now_add=True)), 43 | ('downloads', models.IntegerField(default=0)), 44 | ('a_id', books.uuidfield.UUIDField(blank=True, editable=False, max_length=36, verbose_name='atom:id')), 45 | ('a_title', models.CharField(max_length=200, verbose_name='atom:title')), 46 | ('a_updated', models.DateTimeField(auto_now=True, verbose_name='atom:updated')), 47 | ('a_summary', models.TextField(blank=True, verbose_name='atom:summary')), 48 | ('a_category', models.CharField(blank=True, max_length=200, verbose_name='atom:category')), 49 | ('a_rights', models.CharField(blank=True, max_length=200, verbose_name='atom:rights')), 50 | ('dc_publisher', models.CharField(blank=True, max_length=200, verbose_name='dc:publisher')), 51 | ('dc_issued', models.CharField(blank=True, max_length=100, verbose_name='dc:issued')), 52 | ('dc_identifier', models.CharField(blank=True, help_text='Use ISBN for this', max_length=50, verbose_name='dc:identifier')), 53 | ('cover_img', models.FileField(blank=True, upload_to='covers')), 54 | ('a_author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='books.Author')), 55 | ], 56 | options={ 57 | 'ordering': ('-time_added',), 58 | 'get_latest_by': 'time_added', 59 | }, 60 | ), 61 | migrations.CreateModel( 62 | name='Language', 63 | fields=[ 64 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 65 | ('label', models.CharField(max_length=50, unique=True, verbose_name='language name')), 66 | ('code', models.CharField(blank=True, max_length=4)), 67 | ], 68 | ), 69 | migrations.CreateModel( 70 | name='Status', 71 | fields=[ 72 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 73 | ('status', models.CharField(max_length=200, blank=False)), 74 | ], 75 | options={ 76 | 'verbose_name_plural': 'Status', 77 | }, 78 | ), 79 | migrations.RunPython(status_insert_status), 80 | migrations.CreateModel( 81 | name='TagGroup', 82 | fields=[ 83 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 84 | ('name', models.CharField(max_length=200)), 85 | ('slug', models.SlugField(max_length=200)), 86 | ], 87 | options={ 88 | 'verbose_name': 'Tag group', 89 | 'verbose_name_plural': 'Tag groups', 90 | }, 91 | ), 92 | migrations.AddField( 93 | model_name='book', 94 | name='a_status', 95 | field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='books.Status'), 96 | ), 97 | migrations.AddField( 98 | model_name='book', 99 | name='dc_language', 100 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='books.Language'), 101 | ), 102 | migrations.AddField( 103 | model_name='book', 104 | name='tags', 105 | field=taggit.managers.TaggableManager(blank=True, help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'), 106 | ), 107 | ] 108 | -------------------------------------------------------------------------------- /static/style/blueprint/plugins/rtl/screen.css: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------- 2 | 3 | rtl.css 4 | * Mirrors Blueprint for left-to-right languages 5 | 6 | By Ran Yaniv Hartstein [ranh.co.il] 7 | 8 | -------------------------------------------------------------- */ 9 | 10 | body .container { direction: rtl; } 11 | body .column, body div.span-1, body div.span-2, body div.span-3, body div.span-4, body div.span-5, body div.span-6, body div.span-7, body div.span-8, body div.span-9, body div.span-10, body div.span-11, body div.span-12, body div.span-13, body div.span-14, body div.span-15, body div.span-16, body div.span-17, body div.span-18, body div.span-19, body div.span-20, body div.span-21, body div.span-22, body div.span-23, body div.span-24 { 12 | float: right; 13 | margin-right: 0; 14 | margin-left: 10px; 15 | text-align:right; 16 | } 17 | 18 | body div.last { margin-left: 0; } 19 | body table .last { padding-left: 0; } 20 | 21 | body .append-1 { padding-right: 0; padding-left: 40px; } 22 | body .append-2 { padding-right: 0; padding-left: 80px; } 23 | body .append-3 { padding-right: 0; padding-left: 120px; } 24 | body .append-4 { padding-right: 0; padding-left: 160px; } 25 | body .append-5 { padding-right: 0; padding-left: 200px; } 26 | body .append-6 { padding-right: 0; padding-left: 240px; } 27 | body .append-7 { padding-right: 0; padding-left: 280px; } 28 | body .append-8 { padding-right: 0; padding-left: 320px; } 29 | body .append-9 { padding-right: 0; padding-left: 360px; } 30 | body .append-10 { padding-right: 0; padding-left: 400px; } 31 | body .append-11 { padding-right: 0; padding-left: 440px; } 32 | body .append-12 { padding-right: 0; padding-left: 480px; } 33 | body .append-13 { padding-right: 0; padding-left: 520px; } 34 | body .append-14 { padding-right: 0; padding-left: 560px; } 35 | body .append-15 { padding-right: 0; padding-left: 600px; } 36 | body .append-16 { padding-right: 0; padding-left: 640px; } 37 | body .append-17 { padding-right: 0; padding-left: 680px; } 38 | body .append-18 { padding-right: 0; padding-left: 720px; } 39 | body .append-19 { padding-right: 0; padding-left: 760px; } 40 | body .append-20 { padding-right: 0; padding-left: 800px; } 41 | body .append-21 { padding-right: 0; padding-left: 840px; } 42 | body .append-22 { padding-right: 0; padding-left: 880px; } 43 | body .append-23 { padding-right: 0; padding-left: 920px; } 44 | 45 | body .prepend-1 { padding-left: 0; padding-right: 40px; } 46 | body .prepend-2 { padding-left: 0; padding-right: 80px; } 47 | body .prepend-3 { padding-left: 0; padding-right: 120px; } 48 | body .prepend-4 { padding-left: 0; padding-right: 160px; } 49 | body .prepend-5 { padding-left: 0; padding-right: 200px; } 50 | body .prepend-6 { padding-left: 0; padding-right: 240px; } 51 | body .prepend-7 { padding-left: 0; padding-right: 280px; } 52 | body .prepend-8 { padding-left: 0; padding-right: 320px; } 53 | body .prepend-9 { padding-left: 0; padding-right: 360px; } 54 | body .prepend-10 { padding-left: 0; padding-right: 400px; } 55 | body .prepend-11 { padding-left: 0; padding-right: 440px; } 56 | body .prepend-12 { padding-left: 0; padding-right: 480px; } 57 | body .prepend-13 { padding-left: 0; padding-right: 520px; } 58 | body .prepend-14 { padding-left: 0; padding-right: 560px; } 59 | body .prepend-15 { padding-left: 0; padding-right: 600px; } 60 | body .prepend-16 { padding-left: 0; padding-right: 640px; } 61 | body .prepend-17 { padding-left: 0; padding-right: 680px; } 62 | body .prepend-18 { padding-left: 0; padding-right: 720px; } 63 | body .prepend-19 { padding-left: 0; padding-right: 760px; } 64 | body .prepend-20 { padding-left: 0; padding-right: 800px; } 65 | body .prepend-21 { padding-left: 0; padding-right: 840px; } 66 | body .prepend-22 { padding-left: 0; padding-right: 880px; } 67 | body .prepend-23 { padding-left: 0; padding-right: 920px; } 68 | 69 | body .border { 70 | padding-right: 0; 71 | padding-left: 4px; 72 | margin-right: 0; 73 | margin-left: 5px; 74 | border-right: none; 75 | border-left: 1px solid #eee; 76 | } 77 | 78 | body .colborder { 79 | padding-right: 0; 80 | padding-left: 24px; 81 | margin-right: 0; 82 | margin-left: 25px; 83 | border-right: none; 84 | border-left: 1px solid #eee; 85 | } 86 | 87 | body .pull-1 { margin-left: 0; margin-right: -40px; } 88 | body .pull-2 { margin-left: 0; margin-right: -80px; } 89 | body .pull-3 { margin-left: 0; margin-right: -120px; } 90 | body .pull-4 { margin-left: 0; margin-right: -160px; } 91 | 92 | body .push-0 { margin: 0 18px 0 0; } 93 | body .push-1 { margin: 0 18px 0 -40px; } 94 | body .push-2 { margin: 0 18px 0 -80px; } 95 | body .push-3 { margin: 0 18px 0 -120px; } 96 | body .push-4 { margin: 0 18px 0 -160px; } 97 | body .push-0, body .push-1, body .push-2, 98 | body .push-3, body .push-4 { float: left; } 99 | 100 | 101 | /* Typography with RTL support */ 102 | body h1,body h2,body h3, 103 | body h4,body h5,body h6 { font-family: Arial, sans-serif; } 104 | html body { font-family: Arial, sans-serif; } 105 | body pre,body code,body tt { font-family: monospace; } 106 | 107 | /* Mirror floats and margins on typographic elements */ 108 | body p img { float: right; margin: 1.5em 0 1.5em 1.5em; } 109 | body dd, body ul, body ol { margin-left: 0; margin-right: 1.5em;} 110 | body td, body th { text-align:right; } -------------------------------------------------------------------------------- /static/style/style.css: -------------------------------------------------------------------------------- 1 | 2 | .cover { 3 | margin-top:5em 4 | } 5 | 6 | a[href$=".epub"] { 7 | background-image: url(blueprint/plugins/link-icons/icons/epub.png); 8 | padding-left: 20px; 9 | background-repeat: no-repeat; 10 | } 11 | 12 | .footer { 13 | text-align:center; 14 | padding-top: 20px; 15 | } 16 | 17 | .pagination { 18 | text-align:center; 19 | padding: 5px; 20 | } 21 | 22 | .current-page { 23 | font-size: 180%; 24 | color: #A9A4A4; 25 | position: relative; 26 | top: -10px; 27 | } 28 | 29 | .header { 30 | width:100%; 31 | height: 80px; 32 | background: gray; 33 | margin-bottom: 20px; 34 | } 35 | 36 | .header h1 { 37 | color: white; 38 | font-size: 200%; 39 | font-weight: bold; 40 | position: relative; 41 | top: 15px; 42 | left: 20px; 43 | display: inline; 44 | } 45 | 46 | .header .feed-link { 47 | position: absolute; 48 | top: 45px; 49 | left: 20px; 50 | font-size: 130%; 51 | padding: 0.2em 0.4em; 52 | -moz-border-radius: 5px; 53 | border-radius: 5px; 54 | color: white; 55 | font-weight: bold; 56 | text-decoration: none; 57 | } 58 | 59 | .header .feed-link:hover { 60 | color: gray; 61 | background: white; 62 | } 63 | 64 | .header .feed-link { 65 | position: absolute; 66 | top: 45px; 67 | left: 20px; 68 | font-size: 130%; 69 | padding: 2px 4px 2px 30px; 70 | -moz-border-radius: 5px; 71 | border-radius: 5px; 72 | color: white; 73 | font-weight: bold; 74 | text-decoration: none; 75 | } 76 | 77 | .header .feed-link img { 78 | position: absolute; 79 | top: 4px; 80 | left: 4px; 81 | } 82 | 83 | .header .navbar { 84 | position: absolute; 85 | right: 0px; 86 | top: 25px; 87 | } 88 | 89 | .header .navbar > li { 90 | display: inline; 91 | } 92 | 93 | .header .navbar a { 94 | font-size: 130%; 95 | padding: 2px 4px; 96 | -moz-border-radius: 5px; 97 | border-radius: 5px; 98 | color: white; 99 | font-weight: bold; 100 | text-decoration: none; 101 | } 102 | 103 | .header .navbar a.active { 104 | background: #aaa; 105 | } 106 | 107 | .header .navbar a:hover { 108 | color: gray; 109 | background: white; 110 | } 111 | 112 | .header .loginbox { 113 | position: absolute; 114 | right: 25px; 115 | top: 5px; 116 | color: white; 117 | } 118 | 119 | .header .loginbox a { 120 | color: white; 121 | font-weight: bold; 122 | text-decoration: none; 123 | } 124 | 125 | .header .loginbox a:hover { 126 | text-decoration: underline; 127 | } 128 | 129 | .header a { 130 | text-decoration: none; 131 | } 132 | 133 | .header a:hover { 134 | outline: none; 135 | } 136 | 137 | .searchbox #search { 138 | padding: 2px 4px; 139 | -moz-border-radius: 5px; 140 | border-radius: 5px; 141 | color: gray; 142 | } 143 | 144 | .searchbox #submit { 145 | display: none; 146 | } 147 | 148 | .search-ops { 149 | display: none; 150 | margin: 0; 151 | padding: 12px; 152 | color: white; 153 | background-color: gray; 154 | position: absolute; 155 | right: 0; 156 | top: 35px; 157 | width: 150px; 158 | -moz-border-radius-bottomright: 10px; 159 | border-bottom-right-radius: 10px; 160 | -moz-border-radius-bottomleft: 10px; 161 | border-bottom-left-radius: 10px; 162 | } 163 | 164 | .search-ops label { 165 | cursor: pointer; 166 | } 167 | 168 | .search-ops li { 169 | list-style-type: none; 170 | } 171 | 172 | p.hidden_head { 173 | cursor: pointer; 174 | position: relative; 175 | background-color:#A9A4A4; 176 | margin:1px; 177 | } 178 | div.hidden_body { 179 | padding: 5px 10px 15px; 180 | background-color:#BFBFBF; 181 | } 182 | 183 | .bookname a { 184 | display: inline-block; 185 | margin-left: -0.4em; 186 | padding: 5px 10px; 187 | -moz-border-radius: 10px; 188 | border-radius: 10px; 189 | font-weight:normal; 190 | color:gray; 191 | line-height:1; 192 | text-decoration: none; 193 | font-size: 90%; 194 | font-family: serif; 195 | } 196 | 197 | .bookname a:hover { 198 | color: white; 199 | background: gray; 200 | } 201 | 202 | .authorname a { 203 | -moz-border-radius: 9px; 204 | border-radius: 9px; 205 | line-height:1; 206 | text-decoration: none; 207 | } 208 | 209 | .authorname a:hover { 210 | color: white; 211 | background: gray; 212 | } 213 | 214 | .button { 215 | padding: 2px 4px; 216 | -moz-border-radius: 5px; 217 | border-radius: 5px; 218 | font-weight:normal; 219 | line-height:1; 220 | text-decoration: none; 221 | font-family: serif; 222 | color: white; 223 | background: gray; 224 | } 225 | 226 | 227 | 228 | .button:hover { 229 | color: white; 230 | background: #555; 231 | } 232 | 233 | div.required:after { 234 | content: '*'; 235 | color: red; 236 | font-size: 120%; 237 | font-weight: bold; 238 | padding-left: 5px; 239 | } 240 | 241 | .download { 242 | display: inline-block; 243 | padding: 5px 10px; 244 | -moz-border-radius: 10px; 245 | border-radius: 10px; 246 | font-weight:normal; 247 | background-color: lightgray; 248 | color:black; 249 | line-height:1; 250 | text-decoration: none; 251 | font-size: 150%; 252 | font-family: serif; 253 | margin: 5px; 254 | } 255 | 256 | .download:hover { 257 | color: white; 258 | background: gray; 259 | } 260 | -------------------------------------------------------------------------------- /books/epub.py: -------------------------------------------------------------------------------- 1 | # Copyright 2009 One Laptop Per Child 2 | # Author: Sayamindu Dasgupta 3 | # 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 17 | 18 | import zipfile 19 | import tempfile 20 | import os, os.path 21 | from lxml import etree 22 | import shutil 23 | 24 | from books import epubinfo 25 | 26 | 27 | class Epub(object): 28 | def __init__(self, _file): 29 | """ 30 | _file: can be either a path to a file (a string) or a file-like object. 31 | """ 32 | self._file = _file 33 | self._zobject = None 34 | self._opfpath = None 35 | self._ncxpath = None 36 | self._basepath = None 37 | self._tempdir = tempfile.mkdtemp() 38 | 39 | if not self._verify(): 40 | print('Warning: This does not seem to be a valid epub file') 41 | 42 | self._get_opf() 43 | self._get_ncx() 44 | 45 | opffile = self._zobject.open(self._opfpath) 46 | self._info = epubinfo.EpubInfo(opffile) 47 | 48 | self._unzip() 49 | 50 | def __del__(self): 51 | if self._tempdir: 52 | raise Exception("you should call close method to clean up stuff") 53 | 54 | def _unzip(self): 55 | #self._zobject.extractall(path = self._tempdir) # This is broken upto python 2.7 56 | for name in self._zobject.namelist(): 57 | # Some weird zip file entries start with a slash, and we don't want to write to the root directory 58 | if name.startswith(os.path.sep): 59 | name = name[1:] 60 | if name.endswith(os.path.sep) or name.endswith('\\'): 61 | os.makedirs(os.path.join(self._tempdir, name)) 62 | else: 63 | self._zobject.extract(name, self._tempdir) 64 | 65 | def _get_opf(self): 66 | containerfile = self._zobject.open('META-INF/container.xml') 67 | 68 | tree = etree.parse(containerfile) 69 | root = tree.getroot() 70 | 71 | for element in root.iterfind('.//{urn:oasis:names:tc:opendocument:xmlns:container}rootfile'): 72 | if element.get('media-type') == 'application/oebps-package+xml': 73 | self._opfpath = element.get('full-path') 74 | 75 | if self._opfpath.rpartition('/')[0]: 76 | self._basepath = self._opfpath.rpartition('/')[0] + '/' 77 | else: 78 | self._basepath = '' 79 | 80 | containerfile.close() 81 | 82 | 83 | def _get_ncx(self): 84 | opffile = self._zobject.open(self._opfpath) 85 | 86 | tree = etree.parse(opffile) 87 | root = tree.getroot() 88 | 89 | spine = root.find('.//{http://www.idpf.org/2007/opf}spine') 90 | tocid = spine.get('toc') 91 | 92 | for element in root.iterfind('.//{http://www.idpf.org/2007/opf}item'): 93 | if element.get('id') == tocid: 94 | self._ncxpath = self._basepath + element.get('href') 95 | 96 | opffile.close() 97 | 98 | def _verify(self): 99 | ''' 100 | Method to crudely check to verify that what we 101 | are dealing with is a epub file or not 102 | ''' 103 | if isinstance(self._file, str): 104 | self._file = os.path.abspath(self._file) 105 | if not os.path.exists(self._file): 106 | return False 107 | 108 | self._zobject = zipfile.ZipFile(self._file) 109 | 110 | if not 'mimetype' in self._zobject.namelist(): 111 | return False 112 | 113 | mtypefile = self._zobject.open('mimetype') 114 | mimetype = mtypefile.readline().decode('utf-8') 115 | 116 | if not mimetype.startswith('application/epub+zip'): # Some files seem to have trailing characters 117 | return False 118 | 119 | return True 120 | 121 | def get_basedir(self): 122 | ''' 123 | Returns the base directory where the contents of the 124 | epub has been unzipped 125 | ''' 126 | return self._tempdir 127 | 128 | def get_info(self): 129 | ''' 130 | Returns a EpubInfo object for the open Epub file 131 | ''' 132 | return self._info 133 | 134 | def get_cover_image_path(self): 135 | if self._info.cover_image is None: 136 | return None 137 | for subdir in [self._basepath, '']: 138 | path = os.path.join(self._tempdir, subdir, self._info.cover_image) 139 | if os.path.exists(path): 140 | return path 141 | return None 142 | 143 | def close(self): 144 | ''' 145 | Cleans up (closes open zip files and deletes uncompressed content of Epub. 146 | Please call this when a file is being closed or during application exit. 147 | ''' 148 | self._zobject.close() 149 | shutil.rmtree(self._tempdir) 150 | self._tempdir = None 151 | -------------------------------------------------------------------------------- /books/models.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010, One Laptop Per Child 2 | # Copyright (C) 2010, Kushal Das 3 | # 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 17 | 18 | import os 19 | import os.path 20 | 21 | from django.db import models 22 | from django.core.files import File 23 | from django.core.exceptions import ValidationError 24 | from django.conf import settings 25 | from django.forms.forms import NON_FIELD_ERRORS 26 | 27 | from hashlib import sha256 28 | 29 | from taggit.managers import TaggableManager #NEW 30 | 31 | from books.uuidfield import UUIDField 32 | from books.langlist import langs 33 | from books.epub import Epub 34 | 35 | def sha256_sum(_file): # used to generate sha256 sum of book files 36 | s = sha256() 37 | for chunk in _file: 38 | s.update(chunk) 39 | return s.hexdigest() 40 | 41 | class Language(models.Model): 42 | label = models.CharField('language name', max_length=50, blank=False, unique=True) 43 | code = models.CharField(max_length=4, blank=True) 44 | 45 | def __unicode__(self): 46 | return self.label 47 | 48 | def __str__(self): 49 | return self.label 50 | 51 | def save(self, *args, **kwargs): 52 | ''' 53 | This method automatically tries to assign the right language code 54 | to the specified language. If a code cannot be found, it assigns 55 | 'xx' 56 | ''' 57 | code = 'xx' 58 | for lang in langs: 59 | if self.label.lower() == lang[1].lower(): 60 | code = lang[0] 61 | break 62 | self.code = code 63 | super(Language, self).save(*args, **kwargs) 64 | 65 | 66 | class TagGroup(models.Model): 67 | name = models.CharField(max_length=200, blank=False) 68 | slug = models.SlugField(max_length=200, blank=False) 69 | #tags = TagableManager() 70 | 71 | class Meta: 72 | verbose_name = "Tag group" 73 | verbose_name_plural = "Tag groups" 74 | 75 | def __unicode__(self): 76 | return self.name 77 | 78 | def __str__(self): 79 | return self.name 80 | 81 | 82 | class Status(models.Model): 83 | status = models.CharField(max_length=200, blank=False) 84 | 85 | class Meta: 86 | verbose_name_plural = "Status" 87 | 88 | def __unicode__(self): 89 | return self.status 90 | 91 | def __str__(self): 92 | return self.status 93 | 94 | 95 | class Author(models.Model): 96 | a_author = models.CharField('atom:author', max_length=200, unique=True) 97 | 98 | def __unicode__(self): 99 | return self.a_author 100 | 101 | def __str__(self): 102 | return self.a_author 103 | 104 | 105 | class Book(models.Model): 106 | """ 107 | This model stores the book file, and all the metadata that is 108 | needed to publish it in a OPDS atom feed. 109 | 110 | It also stores other information, like tags and downloads, so the 111 | book can be listed in OPDS catalogs. 112 | 113 | """ 114 | book_file = models.FileField(upload_to='books') 115 | file_sha256sum = models.CharField(max_length=64, unique=True) 116 | mimetype = models.CharField(max_length=200, null=True) 117 | time_added = models.DateTimeField(auto_now_add=True) 118 | tags = TaggableManager(blank=True) 119 | downloads = models.IntegerField(default=0) 120 | a_id = UUIDField('atom:id') 121 | a_status = models.ForeignKey(Status, blank=False, null=False, default=settings.DEFAULT_BOOK_STATUS) 122 | a_title = models.CharField('atom:title', max_length=200) 123 | a_author = models.ForeignKey(Author, blank=False, null=False) 124 | a_updated = models.DateTimeField('atom:updated', auto_now=True) 125 | a_summary = models.TextField('atom:summary', blank=True) 126 | a_category = models.CharField('atom:category', max_length=200, blank=True) 127 | a_rights = models.CharField('atom:rights', max_length=200, blank=True) 128 | dc_language = models.ForeignKey(Language, blank=True, null=True) 129 | dc_publisher = models.CharField('dc:publisher', max_length=200, blank=True) 130 | dc_issued = models.CharField('dc:issued', max_length=100, blank=True) 131 | dc_identifier = models.CharField('dc:identifier', max_length=50, \ 132 | help_text='Use ISBN for this', blank=True) 133 | cover_img = models.FileField(blank=True, upload_to='covers') 134 | 135 | def validate_unique(self, *args, **kwargs): 136 | if not self.file_sha256sum: 137 | self.file_sha256sum = sha256_sum(self.book_file) 138 | unicity = self.__class__.objects.filter(file_sha256sum=self.file_sha256sum) 139 | if self.pk is not None: 140 | unicity = unicity.exclude(pk=self.pk) 141 | if unicity.exists(): 142 | raise ValidationError({ 143 | NON_FIELD_ERRORS:['The book already exists in the server.',]}) 144 | 145 | def save(self, *args, **kwargs): 146 | assert self.file_sha256sum 147 | if not self.cover_img: 148 | # FIXME: we should use mimetype 149 | if self.book_file.name.endswith('.epub'): 150 | # get the cover path from the epub file 151 | epub_file = Epub(self.book_file) 152 | cover_path = epub_file.get_cover_image_path() 153 | if cover_path is not None and os.path.exists(cover_path): 154 | cover_file = File(open(cover_path, "rb")) 155 | self.cover_img.save(os.path.basename(cover_path), # pylint: disable=no-member 156 | cover_file) 157 | epub_file.close() 158 | 159 | super(Book, self).save(*args, **kwargs) 160 | 161 | class Meta: 162 | ordering = ('-time_added',) 163 | get_latest_by = "time_added" 164 | 165 | def __unicode__(self): 166 | return self.a_title 167 | 168 | def __str__(self): 169 | return self.a_title 170 | 171 | @models.permalink 172 | def get_absolute_url(self): 173 | return ('book_detail', [self.pk]) 174 | -------------------------------------------------------------------------------- /books/management/commands/addbooks.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010, One Laptop Per Child 2 | # 3 | # This program is free software; you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation; either version 2 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software 15 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 16 | 17 | from django.core.management.base import BaseCommand, CommandError 18 | from django.core.exceptions import ValidationError 19 | from django.core.files import File 20 | 21 | from django.db import transaction 22 | from django.db.utils import IntegrityError 23 | 24 | import csv 25 | import json 26 | import logging 27 | import os 28 | import sys 29 | from optparse import make_option 30 | 31 | from books.models import Author, Book, Status 32 | 33 | logger = logging.getLogger(__name__) 34 | logging.basicConfig() 35 | logger.setLevel(logging.DEBUG) 36 | 37 | class Command(BaseCommand): 38 | help = "Adds a book collection (via a CSV file)" 39 | args = 'Absolute path to CSV file' 40 | 41 | def add_arguments(self, parser): 42 | parser.add_argument('--json', 43 | action='store_true', 44 | dest='is_json_format', 45 | default=False, 46 | help='The file is in JSON format') 47 | parser.add_argument('filepath', 48 | help='PATH') 49 | 50 | def _handle_csv(self, csvpath): 51 | """ 52 | Store books from a file in CSV format. 53 | WARN: does not handle tags 54 | 55 | """ 56 | 57 | csvfile = open(csvpath) 58 | # Sniffer fais to detect a CSV created with DictWriter with default Dialect (excel) ! 59 | # dialect = csv.Sniffer().sniff(csvfile.read(32000)) 60 | # csvfile.seek(0) 61 | dialect = 'excel' 62 | reader = csv.reader(csvfile) #, dialect) 63 | 64 | # TODO: Figure out if this is a valid CSV file 65 | 66 | status_published = Status.objects.get(status='Published') 67 | 68 | for row in reader: 69 | path = row[0] 70 | title = row[1] 71 | author = row[2] 72 | summary = row[3] 73 | 74 | if not os.path.exists(path): 75 | # check if file is located in same directory as CSV 76 | path = os.path.join(os.path.dirname(csvpath), path) 77 | 78 | if os.path.exists(path): 79 | 80 | f = open(path, 'rb') 81 | 82 | a_author = Author.objects.get_or_create(a_author=author)[0] 83 | book = Book(book_file=File(f), a_title=title, a_author=a_author, 84 | a_summary=summary, a_status=status_published) 85 | try: 86 | book.validate_unique() 87 | book.save() 88 | except: 89 | print("EXCEPTION SAVING FILE '%s': %s" % ( 90 | path, sys.exc_info()[0])) 91 | else: 92 | print("FILE NOT FOUND '%s'" % path) 93 | 94 | def _handle_json(self, jsonpath): 95 | """ 96 | Store books from a file in JSON format. 97 | 98 | """ 99 | jsonfile = open(jsonpath) 100 | data_list = json.loads(jsonfile.read()) 101 | 102 | status_published = Status.objects.get(status='Published') 103 | stats = dict(total=0, errors=0, skipped=0, imported=0) 104 | 105 | for d in data_list: 106 | stats['total'] += 1 107 | logger.debug('read item %s' % json.dumps(d)) 108 | 109 | # Skip unless there is book content 110 | if 'book_path' not in d: 111 | stats['skipped'] += 1 112 | continue 113 | 114 | # Skip unless there is book content 115 | if not os.path.exists(d['book_path']): 116 | stats['skipped'] += 1 117 | continue 118 | 119 | # Get a Django File from the given path: 120 | f = open(d['book_path'], 'rb') 121 | d['book_file'] = File(f) 122 | del d['book_path'] 123 | 124 | if 'cover_path' in d: 125 | f_cover = open(d['cover_path'], 'rb') 126 | d['cover_img'] = File(f_cover) 127 | del d['cover_path'] 128 | 129 | if 'a_status' in d: 130 | d['a_status'] = Status.objects.get(status=d['a_status']) 131 | else: 132 | d['a_status'] = status_published 133 | 134 | a_author = None 135 | if 'a_author' in d: 136 | d['a_author'] = Author.objects.get_or_create(a_author=d['a_author'])[0] 137 | 138 | tags = d.get('tags', []) 139 | if 'tags' in d: 140 | del d['tags'] 141 | 142 | book = Book(**d) 143 | try: 144 | book.validate_unique() # Throws ValidationError if not unique 145 | 146 | with transaction.atomic(): 147 | book.save() # must save item to generate Book.id before creating tags 148 | [book.tags.add(tag) for tag in tags if tag] 149 | book.save() # save again after tags are generated 150 | stats['imported'] += 1 151 | except ValidationError as e: 152 | stats['skipped'] += 1 153 | logger.info('Book already imported, skipping title="%s"' % book.a_title) 154 | except Exception as e: 155 | stats['errors'] += 1 156 | # Likely a bug 157 | logger.warn('Error adding book title="%s": %s' % ( 158 | book.a_title, e)) 159 | 160 | logger.info("addbooks complete total=%(total)d imported=%(imported)d skipped=%(skipped)d errors=%(errors)d" % stats) 161 | 162 | 163 | def handle(self, *args, **options): 164 | filepath = options.get('filepath') 165 | if not os.path.exists(filepath): 166 | raise CommandError("%r is not a valid path" % filepath) 167 | 168 | filepath = os.path.abspath(filepath) 169 | if options['is_json_format']: 170 | self._handle_json(filepath) 171 | else: 172 | self._handle_csv(filepath) 173 | -------------------------------------------------------------------------------- /books/management/commands/fetch_ia_item.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Initial standalone script by rajbot 3 | # Ported to django management by Aneesh Dogra (aneesh@activitycentral.com) 4 | 5 | """ 6 | This script will download all of an user's bookmarked items from archive.org. 7 | """ 8 | 9 | from django.core.management.base import BaseCommand, CommandError 10 | from optparse import make_option 11 | from django.conf import settings 12 | 13 | import re 14 | import os 15 | import sys 16 | import json 17 | import time 18 | 19 | try: 20 | # python 2 21 | from urllib import urlopen 22 | except ImportError: 23 | # python 3 24 | from urllib.request import urlopen 25 | 26 | import subprocess 27 | 28 | 29 | # Customize this script by editing global variables below 30 | # uncomment formats below to download more data 31 | # formats are listed in order of preference, i.e. prefer 'Text' over 'DjVuTXT' 32 | requested_formats = {'pdf': ['Text PDF', 'Additional Text PDF', 'Image Container PDF'], 33 | 'epub': ['EPUB'], 34 | 'meta': ['Metadata'], 35 | 'text': ['Text', 'DjVuTXT'], 36 | 'jpeg': ['JPEG'], 37 | #'djvu': ['DjVu'], 38 | } 39 | 40 | download_directory = os.path.join(settings.MEDIA_ROOT, "books") 41 | 42 | should_download_cover = True 43 | 44 | def load_user_bookmarks(user): 45 | """Return an array of bookmarked items for a given user. 46 | An example of user bookmarks: http://archive.org/bookmarks/sverma""" 47 | 48 | print(user) 49 | url = 'http://archive.org/bookmarks/%s?output=json' % user 50 | f = urlopen(url) 51 | data = f.read().decode('utf-8') 52 | return json.loads(data) 53 | 54 | def get_item_metadata(item_id): 55 | """Returns an object from the archive.org Metadata API""" 56 | 57 | url = 'http://archive.org/metadata/%s' % item_id 58 | f = urlopen(url) 59 | data = f.read().decode('utf-8') 60 | return json.loads(data) 61 | 62 | def get_download_url(item_id, file): 63 | 64 | prefix = 'http://archive.org/download/' 65 | return prefix + os.path.join(item_id, file) 66 | 67 | def download_files(item_id, matching_files, item_dir): 68 | 69 | for file in matching_files: 70 | download_path = os.path.join(item_dir, file) 71 | 72 | if os.path.exists(download_path): 73 | print(" Already downloaded", file) 74 | continue 75 | 76 | parent_dir = os.path.dirname(download_path) 77 | if not os.path.exists(parent_dir): 78 | os.makedirs(parent_dir) 79 | 80 | print(" Downloading", file, "to", download_path) 81 | download_url= get_download_url(item_id, file) 82 | ret = subprocess.call(['wget', download_url, '-O', download_path, 83 | '--limit-rate=1000k', '--user-agent=fetch_ia_item.py', '-q']) 84 | 85 | if 0 != ret: 86 | print(" ERROR DOWNLOADING", download_path) 87 | sys.exit(-1) 88 | 89 | time.sleep(0.5) 90 | 91 | def download_item(item_id, mediatype, metadata, out_dir, formats): 92 | """Download an archive.org item into the specified directory""" 93 | 94 | print("Downloading", item_id) 95 | 96 | item_dir = os.path.join(out_dir, item_id) 97 | 98 | if not os.path.exists(item_dir): 99 | os.mkdir(item_dir) 100 | 101 | files_list = metadata['files'] 102 | 103 | if 'gutenberg' == metadata['metadata']['collection']: 104 | #For Project Gutenberg books, download entire directory 105 | matching_files = [x['name'] for x in files_list] 106 | download_files(item_id, matching_files, item_dir) 107 | return 108 | 109 | for key, format_list in formats.items(): 110 | for format in format_list: 111 | matching_files = [x['name'] for x in files_list if x['format']==format] 112 | download_files(item_id, matching_files, item_dir) 113 | 114 | #if we found some matching files in for this format, move on to next format 115 | #(i.e. if we downloaded a Text, no need to download DjVuTXT as well) 116 | if len(matching_files) > 0: 117 | break 118 | 119 | def download_cover(item_id, metadata, download_directory): 120 | files_list = metadata['files'] 121 | 122 | item_dir = os.path.join(download_directory, item_id) 123 | cover_formats = set(['JPEG Thumb', 'JPEG', 'Animated GIF']) 124 | 125 | covers = [x['name'] for x in files_list if x['format'] in cover_formats] 126 | 127 | if covers: 128 | download_files(item_id, [covers[0]], item_dir) 129 | return covers[0] 130 | 131 | #no JPEG Thumbs, JPEGs, or AGIFs, return None 132 | return None 133 | 134 | def add_to_pathagar(pathagar_books, mdata, cover_image): 135 | pathagar_formats = [] 136 | if 'epub' in requested_formats: 137 | pathagar_formats += requested_formats['epub'] 138 | 139 | if 'pdf' in requested_formats: 140 | pathagar_formats += requested_formats['pdf'] 141 | 142 | if not pathagar_formats: 143 | return 144 | 145 | metadata = mdata['metadata'] 146 | files_list = mdata['files'] 147 | book_paths = [x['name'] for x in files_list if x['format'] in pathagar_formats] 148 | 149 | if not book_paths: 150 | return 151 | 152 | item_dir = os.path.join(download_directory, metadata['identifier']) 153 | book_path = os.path.abspath(os.path.join(item_dir, book_paths[0])) 154 | # Some fields are not required 155 | if 'description' in metadata: 156 | summary = metadata['description'] 157 | else: 158 | summary = metadata['title'] 159 | 160 | if 'creator' in metadata: 161 | author = metadata['creator'] 162 | else: 163 | author = '' 164 | 165 | book = { 166 | "book_path": os.path.abspath(book_path), 167 | "a_title": metadata['title'], 168 | "a_author": author, 169 | "a_status": "Published", 170 | "a_summary": summary, 171 | } 172 | 173 | if cover_image: 174 | book['cover_path'] = os.path.abspath(os.path.join(item_dir, cover_image)) 175 | 176 | 177 | if 'subject' in metadata: 178 | if isinstance(metadata['subject'], list): 179 | tags = metadata['subject'] 180 | else: 181 | tags = re.split(';\s*', metadata['subject']) 182 | 183 | book['tags'] = tags 184 | 185 | pathagar_books.append(book) 186 | 187 | 188 | class Command(BaseCommand): 189 | help = "A script to download all of an user's bookmarked items from archive.org" 190 | args = "<--username ... --out ...>" 191 | 192 | def add_arguments(self, parser): 193 | parser.add_argument('--username', 194 | dest='username', 195 | default=False, 196 | help='The username at archive.org') 197 | parser.add_argument('--out', 198 | dest='out_json_path', 199 | default=False, 200 | help='The json file to write the output to') 201 | 202 | def handle(self, *args, **options): 203 | if not options['username']: 204 | raise CommandError("Option '--username ...' must be specified.") 205 | 206 | if not os.path.exists(download_directory): 207 | os.mkdir(download_directory, 0o755) 208 | 209 | bookmarks = load_user_bookmarks(options['username']) 210 | pathagar_books = [] 211 | for item in bookmarks: 212 | item_id = item['identifier'] 213 | metadata = get_item_metadata(item_id) 214 | 215 | download_item(item_id, item['mediatype'], metadata, download_directory, requested_formats) 216 | 217 | if should_download_cover: 218 | cover_image = download_cover(item_id, metadata, download_directory) 219 | else: 220 | cover_image = None 221 | 222 | add_to_pathagar(pathagar_books, metadata, cover_image) 223 | 224 | if pathagar_books: 225 | if options['out_json_path']: 226 | fh = open(options['out_json_path'], 'w') 227 | json.dump(pathagar_books, fh, indent=4) 228 | else: 229 | print(json.dumps(pathagar_books, indent = 4)) 230 | -------------------------------------------------------------------------------- /books/opds.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010, One Laptop Per Child 2 | # Copyright (C) 2010, Kushal Das 3 | # 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 17 | 18 | # from cStringIO import StringIO 19 | from io import StringIO 20 | 21 | 22 | from django.core.urlresolvers import reverse 23 | 24 | from books.atom import AtomFeed 25 | import mimetypes 26 | 27 | import datetime 28 | 29 | ATTRS = {} 30 | ATTRS[u'xmlns:dcterms'] = u'http://purl.org/dc/terms/' 31 | ATTRS[u'xmlns:opds'] = u'http://opds-spec.org/' 32 | ATTRS[u'xmlns:dc'] = u'http://purl.org/dc/elements/1.1/' 33 | ATTRS[u'xmlns:opensearch'] = 'http://a9.com/-/spec/opensearch/1.1/' 34 | 35 | 36 | def __get_mimetype(item): 37 | if item.mimetype is not None: 38 | return item.mimetype 39 | 40 | # The MIME Type was not stored in the database, try to guess it 41 | # from the filename: 42 | mimetype, encoding = mimetypes.guess_type(item.book_file.url) 43 | if mimetype is not None: 44 | return mimetype 45 | else: 46 | return 'Unknown' 47 | 48 | def page_qstring(request, page_number=None): 49 | """ 50 | Return the query string for the URL. 51 | 52 | If page_number is given, modify the query for that page. 53 | """ 54 | qdict = dict(request.GET.items()) 55 | if page_number is not None: 56 | qdict['page'] = str(page_number) 57 | 58 | if len(qdict) > 0: 59 | qstring = '?'+'&'.join(('%s=%s' % (k, v) for k, v in qdict.items())) 60 | else: 61 | qstring = '' 62 | 63 | return qstring 64 | 65 | def generate_nav_catalog(subsections, is_root=False): 66 | links = [] 67 | 68 | if is_root: 69 | links.append({'type': 'application/atom+xml', 70 | 'rel': 'self', 71 | 'href': reverse('root_feed')}) 72 | 73 | links.append({'title': 'Home', 'type': 'application/atom+xml', 74 | 'rel': 'start', 75 | 'href': reverse('root_feed')}) 76 | 77 | feed = AtomFeed(title = 'Pathagar Bookserver OPDS feed', \ 78 | atom_id = 'pathagar:full-catalog', subtitle = \ 79 | 'OPDS catalog for the Pathagar book server', \ 80 | extra_attrs = ATTRS, hide_generator=True, links=links) 81 | 82 | 83 | for subsec in subsections: 84 | feed.add_item(subsec['id'], subsec['title'], 85 | subsec['updated'], links=subsec['links']) 86 | 87 | s = StringIO() 88 | feed.write(s, 'UTF-8') 89 | return s.getvalue() 90 | 91 | def generate_root_catalog(): 92 | subsections = [ 93 | {'id': 'latest', 'title': 'Latest', 'updated': datetime.datetime.now(), 94 | 'links': [{'rel': 'subsection', 'type': 'application/atom+xml', \ 95 | 'href': reverse('latest_feed')}]}, 96 | {'id': 'by-title', 'title': 'By Title', 'updated': datetime.datetime.now(), 97 | 'links': [{'rel': 'subsection', 'type': 'application/atom+xml', \ 98 | 'href': reverse('by_title_feed')}]}, 99 | {'id': 'by-author', 'title': 'By Author', 'updated': datetime.datetime.now(), 100 | 'links': [{'rel': 'subsection', 'type': 'application/atom+xml', \ 101 | 'href': reverse('by_author_feed')}]}, 102 | {'id': 'by-popularity', 'title': 'Most downloaded', 'updated': datetime.datetime.now(), 103 | 'links': [{'rel': 'subsection', 'type': 'application/atom+xml', \ 104 | 'href': reverse('most_downloaded_feed')}]}, 105 | {'id': 'tags', 'title': 'Tags', 'updated': datetime.datetime.now(), 106 | 'links': [{'rel': 'subsection', 'type': 'application/atom+xml', \ 107 | 'href': reverse('tags_feed')}]}, 108 | {'id': 'tag-groups', 'title': 'Tag groups', 'updated': datetime.datetime.now(), 109 | 'links': [{'rel': 'subsection', 'type': 'application/atom+xml', \ 110 | 'href': reverse('tags_listgroups')}]}, 111 | ] 112 | return generate_nav_catalog(subsections) 113 | 114 | def generate_tags_catalog(tags): 115 | def convert_tag(tag): 116 | return {'id': tag.name, 'title': tag.name, 'updated': datetime.datetime.now(), 117 | 'links': [{'rel': 'subsection', 'type': 'application/atom+xml', \ 118 | 'href': reverse('by_tag_feed', kwargs=dict(tag=tag.name))}]} 119 | 120 | tags_subsections = map(convert_tag, tags) 121 | return generate_nav_catalog(tags_subsections) 122 | 123 | def generate_taggroups_catalog(tag_groups): 124 | def convert_group(group): 125 | return {'id': group.slug, 'title': group.name, 'updated': datetime.datetime.now(), 126 | 'links': [{'rel': 'subsection', 'type': 'application/atom+xml', \ 127 | 'href': reverse('tag_groups_feed', kwargs=dict(group_slug=group.slug))}]} 128 | 129 | tags_subsections = map(convert_group, tag_groups) 130 | return generate_nav_catalog(tags_subsections) 131 | 132 | 133 | def generate_catalog(request, page_obj): 134 | links = [] 135 | links.append({'title': 'Home', 'type': 'application/atom+xml', 136 | 'rel': 'start', 137 | 'href': reverse('root_feed')}) 138 | 139 | if page_obj.has_previous(): 140 | previous_page = page_obj.previous_page_number() 141 | links.append({'title': 'Previous results', 'type': 'application/atom+xml', 142 | 'rel': 'previous', 143 | 'href': page_qstring(request, previous_page)}) 144 | 145 | if page_obj.has_next(): 146 | next_page = page_obj.next_page_number() 147 | links.append({'title': 'Next results', 'type': 'application/atom+xml', 148 | 'rel': 'next', 149 | 'href': page_qstring(request, next_page)}) 150 | 151 | feed = AtomFeed(title = 'Pathagar Bookserver OPDS feed', \ 152 | atom_id = 'pathagar:full-catalog', subtitle = \ 153 | 'OPDS catalog for the Pathagar book server', \ 154 | extra_attrs = ATTRS, hide_generator=True, links=links) 155 | 156 | for book in page_obj.object_list: 157 | if book.cover_img: 158 | linklist = [{'rel': \ 159 | 'http://opds-spec.org/acquisition', 'href': \ 160 | reverse('book_download', 161 | kwargs=dict(book_id=book.pk)), 162 | 'type': __get_mimetype(book)}, {'rel': \ 163 | 'http://opds-spec.org/cover', 'href': \ 164 | book.cover_img.url }] 165 | else: 166 | linklist = [{'rel': \ 167 | 'http://opds-spec.org/acquisition', 'href': \ 168 | reverse('book_download', 169 | kwargs=dict(book_id=book.pk)), 170 | 'type': __get_mimetype(book)}] 171 | add_kwargs = { 172 | 'content': book.a_summary, 173 | 'links': linklist, 174 | 'authors': [{'name' : str(book.a_author)}], 175 | 'dc_publisher': book.dc_publisher, 176 | 'dc_issued': book.dc_issued, 177 | 'dc_identifier': book.dc_identifier, 178 | } 179 | 180 | if book.dc_language is not None: 181 | add_kwargs['dc_language'] = book.dc_language.code 182 | 183 | feed.add_item(book.a_id, book.a_title, book.a_updated, **add_kwargs) 184 | 185 | s = StringIO() 186 | feed.write(s, 'UTF-8') 187 | return s.getvalue() 188 | 189 | def generate_author_catalog(request, page_obj): 190 | nav = 'application/atom+xml' #;profile=opds-catalog;kind=navigation' 191 | links = [] 192 | links.append({'title': 'Home', 'type': nav, #'application/atom+xml', 193 | 'rel': 'start', 194 | 'href': reverse('root_feed')}) 195 | 196 | if page_obj.has_previous(): 197 | previous_page = page_obj.previous_page_number() 198 | links.append({'title': 'Previous results', 'type': nav, #'application/atom+xml', 199 | 'rel': 'previous', 200 | 'href': page_qstring(request, previous_page)}) 201 | 202 | if page_obj.has_next(): 203 | next_page = page_obj.next_page_number() 204 | links.append({'title': 'Next results', 'type': nav, #'application/atom+xml', 205 | 'rel': 'next', 206 | 'href': page_qstring(request, next_page)}) 207 | 208 | feed = AtomFeed(title = 'Pathagar Bookserver OPDS feed', \ 209 | atom_id = 'pathagar:full-catalog', subtitle = \ 210 | 'OPDS catalog for the Pathagar book server', \ 211 | extra_attrs = ATTRS, hide_generator=True, links=links) 212 | 213 | print("send ", len(page_obj.object_list)) 214 | for author in page_obj.object_list: 215 | linklist = [{'rel': 'subsection', 216 | 'href': reverse('by_title', 217 | kwargs=dict(author_id=author.pk)), 218 | 'type': nav}] 219 | add_kwargs = { 220 | 'content': '', #book.a_summary, 221 | 'links': linklist, 222 | } 223 | 224 | feed.add_item(str(author.pk), author.a_author, datetime.datetime.now(), **add_kwargs) 225 | 226 | s = StringIO() 227 | feed.write(s, 'UTF-8') 228 | return s.getvalue() 229 | -------------------------------------------------------------------------------- /books/management/commands/fetch_ia_items_from_search.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env 2 | # Initial standalone script by rajbot 3 | # Ported to django management by Aneesh Dogra (aneesh@activitycentral.com) 4 | 5 | """ 6 | This script will download results matching a search term. . 7 | """ 8 | 9 | from django.core.management.base import BaseCommand, CommandError 10 | from optparse import make_option 11 | from django.conf import settings 12 | 13 | import re 14 | import os 15 | import sys 16 | import json 17 | import time 18 | import urllib 19 | import subprocess 20 | 21 | 22 | # Customize this script by editing global variables below 23 | # uncomment formats below to download more data 24 | # formats are listed in order of preference, i.e. prefer 'Text' over 'DjVuTXT' 25 | requested_formats = {'pdf': ['Text PDF', 'Additional Text PDF', 'Image Container PDF'], 26 | 'epub': ['EPUB'], 27 | 'meta': ['Metadata'], 28 | 'text': ['Text', 'DjVuTXT'], 29 | 'jpeg': ['JPEG'], 30 | 'mpeg': ['MPEG4'], 31 | #'djvu': ['DjVu'], 32 | } 33 | 34 | download_directory = os.path.join(settings.MEDIA_ROOT, "books") 35 | 36 | should_download_cover = True 37 | 38 | def load_search_results(searchterm, page, rows): 39 | """Return an array of results for a given search term. 40 | An example for a search term is: kahnacademy 41 | """ 42 | 43 | print searchterm 44 | 45 | url = 'https://archive.org/advancedsearch.php?q={searchterm}&fl%5B%5D=description&fl%5B%5D=contributor&fl%5B%5D=coverage&fl%5B%5D=creator&fl%5B%5D=date&fl%5B%5D=description&fl%5B%5D=item&fl%5B%5D=format&fl%5B%5D=identifier&fl%5B%5D=mediatype&fl%5B%5D=subject&fl%5B%5D=description&fl%5B%5D=title&fl%5B%5D=media:title&fl%5B%5D=type&fl%5B%5D=volume&fl%5B%5D=week&fl%5B%5D=year&sort%5B%5D=&sort%5B%5D=&sort%5B%5D=&rows={rows}&page={page}&output=json'.format(searchterm=searchterm, page=str(page), rows=str(rows)) 46 | 47 | f = urllib.urlopen(url) 48 | return json.load(f) 49 | 50 | def get_item_meatadata(item_id): 51 | """Returns an object from the archive.org Metadata API""" 52 | if isinstance(item_id, list): 53 | item_id = item_id[0] 54 | 55 | url = 'http://archive.org/metadata/%s' % item_id 56 | f = urllib.urlopen(url) 57 | return json.load(f) 58 | 59 | def get_download_url(item_id, file): 60 | 61 | prefix = 'http://archive.org/download/' 62 | return prefix + os.path.join(item_id, file) 63 | 64 | def download_files(item_id, matching_files, item_dir): 65 | 66 | for file in matching_files: 67 | download_path = os.path.join(item_dir, file) 68 | print("Download_path: %s" % download_path) 69 | if os.path.exists(download_path): 70 | print " Already downloaded", file 71 | continue 72 | 73 | parent_dir = os.path.dirname(download_path) 74 | if not os.path.exists(parent_dir): 75 | os.makedirs(parent_dir) 76 | 77 | print " Downloading", file, "to", download_path 78 | download_url= get_download_url(item_id, file) 79 | ret = subprocess.call(['wget', download_url, '-O', download_path, 80 | '--limit-rate=1000k', '--user-agent=fetch_ia_items_from_search.py', '-q']) 81 | 82 | if 0 != ret: 83 | print " ERROR DOWNLOADING", download_path 84 | sys.exit(-1) 85 | 86 | time.sleep(0.5) 87 | 88 | def download_item(item_id, mediatype, metadata, out_dir, formats): 89 | """Download an archive.org item into the specified directory""" 90 | if isinstance(item_id, list): 91 | item_id = item_id[0] 92 | print "Downloading", item_id 93 | item_dir = os.path.join(out_dir, item_id) 94 | 95 | if not os.path.exists(item_dir): 96 | os.mkdir(item_dir) 97 | 98 | print("metadata") 99 | print(metadata) 100 | files_list = metadata['files'] 101 | 102 | if 'gutenberg' == metadata['metadata']['collection']: 103 | #For Project Gutenberg books, download entire directory 104 | matching_files = [x['name'] for x in files_list] 105 | download_files(item_id, matching_files, item_dir) 106 | return 107 | 108 | for key, format_list in formats.iteritems(): 109 | for format in format_list: 110 | matching_files = [x['name'] for x in files_list if x['format']==format] 111 | download_files(item_id, matching_files, item_dir) 112 | 113 | #if we found some matching files in for this format, move on to next format 114 | #(i.e. if we downloaded a Text, no need to download DjVuTXT as well) 115 | if len(matching_files) > 0: 116 | break 117 | 118 | def download_cover(item_id, metadata, download_directory): 119 | files_list = metadata['files'] 120 | 121 | if isinstance(item_id, list): 122 | item_id = item_id[0] 123 | 124 | item_dir = os.path.join(download_directory, item_id) 125 | cover_formats = set(['JPEG Thumb', 'JPEG', 'Animated GIF']) 126 | 127 | covers = [x['name'] for x in files_list if x['format'] in cover_formats] 128 | 129 | if covers: 130 | download_files(item_id, [covers[0]], item_dir) 131 | return covers[0] 132 | 133 | #no JPEG Thumbs, JPEGs, or AGIFs, return None 134 | return None 135 | 136 | def add_to_pathagar(pathagar_books, mdata, cover_image): 137 | pathagar_formats = [] 138 | if 'epub' in requested_formats: 139 | pathagar_formats += requested_formats['epub'] 140 | 141 | if 'pdf' in requested_formats: 142 | pathagar_formats += requested_formats['pdf'] 143 | 144 | if 'mpeg' in requested_formats: 145 | pathagar_formats += requested_formats['mpeg'] 146 | 147 | if not pathagar_formats: 148 | return 149 | 150 | metadata = mdata['metadata'] 151 | files_list = mdata['files'] 152 | book_paths = [x['name'] for x in files_list if x['format'] in pathagar_formats] 153 | 154 | if not book_paths: 155 | return 156 | 157 | item_dir = os.path.join(download_directory, metadata['identifier']) 158 | print(item_dir) 159 | if isinstance(book_paths, list): 160 | book_path = os.path.abspath(os.path.join(item_dir, book_paths[0])) 161 | else: 162 | book_path = os.path.abspath(os.path.join(item_dir, book_paths)) 163 | print(book_path) 164 | print('') 165 | # Some fields are not required 166 | if 'description' in metadata: 167 | summary = metadata['description'] 168 | else: 169 | summary = metadata['title'] 170 | 171 | if 'creator' in metadata: 172 | author = metadata['creator'] 173 | else: 174 | author = '' 175 | 176 | book = { 177 | "book_path": os.path.abspath(book_path), 178 | "a_title": metadata['title'], 179 | "a_author": author, 180 | "a_status": "Published", 181 | "a_summary": summary, 182 | } 183 | 184 | if cover_image: 185 | book['cover_path'] = os.path.abspath(os.path.join(item_dir, cover_image)) 186 | 187 | 188 | if 'subject' in metadata: 189 | if isinstance(metadata['subject'], list): 190 | tags = metadata['subject'] 191 | else: 192 | tags = re.split(';\s*', metadata['subject']) 193 | 194 | book['tags'] = tags 195 | 196 | pathagar_books.append(book) 197 | 198 | 199 | class Command(BaseCommand): 200 | help = "A script to download all of an user's bookmarked items from archive.org" 201 | args = "<--searchterm ... --out ...>" 202 | 203 | option_list = BaseCommand.option_list + ( 204 | make_option('--searchterm', 205 | dest='searchterm', 206 | default=False, 207 | help='The search term with which to search at archive.org'), 208 | make_option('--maxnumresults', 209 | dest='maxnumresults', 210 | default=False, 211 | help='The maximum number of items to fetch from archive.org'), 212 | make_option('--out', 213 | dest='out_json_path', 214 | default=False, 215 | help='The json file to write the output to'), 216 | ) 217 | def handle(self, *args, **options): 218 | if not options['searchterm']: 219 | raise CommandError("Option '--searchterm ...' must be specified.") 220 | 221 | if not os.path.exists(download_directory): 222 | os.mkdir(download_directory, 0o755) 223 | 224 | max_num_results = None 225 | if options['maxnumresults']: 226 | max_num_results = int(options['maxnumresults']) 227 | 228 | page = 1 229 | rows = 50 230 | if max_num_results is not None: 231 | rows = min(50, max_num_results) 232 | row_count = 0 233 | pathagar_books = [] 234 | while True: 235 | if max_num_results is not None: 236 | rows = min(50, max_num_results - row_count) 237 | 238 | response = load_search_results(options['searchterm'], page, rows)['response'] 239 | bookmarks = response['docs'] 240 | numFound = response['numFound'] 241 | row_count += len(bookmarks) 242 | 243 | for item in bookmarks: 244 | print(item) 245 | item_id = item['identifier'] 246 | metadata = get_item_meatadata(item_id) 247 | 248 | download_item(item_id, item['mediatype'], metadata, download_directory, requested_formats) 249 | 250 | if should_download_cover: 251 | cover_image = download_cover(item_id, metadata, download_directory) 252 | else: 253 | cover_image = None 254 | 255 | add_to_pathagar(pathagar_books, metadata, cover_image) 256 | page += 1 257 | if row_count >= numFound or row_count >= max_num_results: 258 | break 259 | 260 | if pathagar_books: 261 | if options['out_json_path']: 262 | fh = open(options['out_json_path'], 'w') 263 | json.dump(pathagar_books, fh, indent=4) 264 | else: 265 | print json.dumps(pathagar_books, indent = 4) 266 | -------------------------------------------------------------------------------- /books/views.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2010, One Laptop Per Child 2 | # Copyright (C) 2010, Kushal Das 3 | # 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 17 | 18 | import os 19 | 20 | from django.conf import settings 21 | from django.core.urlresolvers import reverse 22 | from django.http import HttpResponse 23 | from django.http import Http404 24 | from django.shortcuts import render 25 | from django.shortcuts import get_object_or_404 26 | from django.shortcuts import redirect 27 | from django.contrib.auth.decorators import login_required 28 | from django.core.paginator import Paginator, InvalidPage, EmptyPage 29 | 30 | from django.views.generic.detail import DetailView 31 | from django.views.generic.edit import UpdateView 32 | from django.views.generic.edit import CreateView 33 | from django.views.generic.edit import FormView 34 | from django.views.generic.edit import DeleteView 35 | 36 | from pathagar.settings import BOOKS_PER_PAGE, AUTHORS_PER_PAGE 37 | from django.conf import settings 38 | 39 | # OLD --------------- 40 | from tagging.models import Tag 41 | # --------------- OLD 42 | from taggit.models import Tag as tTag 43 | 44 | from sendfile import sendfile 45 | 46 | from books.search import simple_search, advanced_search 47 | from books.forms import BookForm, AddLanguageForm 48 | from books.models import TagGroup, Book, Author 49 | from books.popuphandler import handlePopAdd 50 | # FIXME: move opds in dedicated app 51 | from books.opds import page_qstring 52 | from books.opds import generate_catalog 53 | from books.opds import generate_author_catalog 54 | from books.opds import generate_root_catalog 55 | from books.opds import generate_tags_catalog 56 | from books.opds import generate_taggroups_catalog 57 | 58 | from books.app_settings import BOOK_PUBLISHED 59 | 60 | @login_required 61 | def add_language(request): 62 | return handlePopAdd(request, AddLanguageForm, 'language') 63 | 64 | class BookDetailView(DetailView): 65 | model = Book 66 | form_class = BookForm 67 | 68 | class BookEditView(UpdateView): 69 | model = Book 70 | form_class = BookForm 71 | 72 | def get_context_data(self, **kwargs): 73 | context = super(BookEditView, self).get_context_data(**kwargs) 74 | context['action'] = 'edit' 75 | return context 76 | 77 | class BookAddView(CreateView): 78 | model = Book 79 | form_class = BookForm 80 | 81 | def get_context_data(self, **kwargs): 82 | context = super(BookAddView, self).get_context_data(**kwargs) 83 | context['action'] = 'add' 84 | return context 85 | 86 | class BookDeleteView(DeleteView): 87 | model = Book 88 | form_class = BookForm 89 | success_url = '/' 90 | 91 | def download_book(request, book_id): 92 | book = get_object_or_404(Book, pk=book_id) 93 | filename = os.path.join(settings.MEDIA_ROOT, book.book_file.name) 94 | 95 | # TODO, currently the downloads counter is incremented when the 96 | # download is requested, without knowing if the file sending was 97 | # successfull: 98 | book.downloads += 1 99 | book.save() 100 | return sendfile(request, filename, attachment=True) 101 | 102 | def tags(request, qtype=None, group_slug=None): 103 | context = {'list_by': 'by-tag'} 104 | 105 | if group_slug is not None: 106 | tag_group = get_object_or_404(TagGroup, slug=group_slug) 107 | context.update({'tag_group': tag_group}) 108 | context.update({'tag_list': Tag.objects.get_for_object(tag_group)}) 109 | else: 110 | context.update({'tag_list': tTag.objects.all()}) 111 | 112 | tag_groups = TagGroup.objects.all() 113 | context.update({'tag_group_list': tag_groups}) 114 | 115 | 116 | # Return OPDS Atom Feed: 117 | if qtype == 'feed': 118 | catalog = generate_tags_catalog(context['tag_list']) 119 | return HttpResponse(catalog, content_type='application/atom+xml') 120 | 121 | # Return HTML page: 122 | return render(request, 'books/tag_list.html', 123 | context) 124 | 125 | def tags_listgroups(request): 126 | tag_groups = TagGroup.objects.all() 127 | catalog = generate_taggroups_catalog(tag_groups) 128 | return HttpResponse(catalog, content_type='application/atom+xml') 129 | 130 | def _book_list(request, queryset, qtype=None, list_by='latest', **kwargs): 131 | """ 132 | Filter the books, paginate the result, and return either a HTML 133 | book list, or a atom+xml OPDS catalog. 134 | 135 | """ 136 | q = request.GET.get('q') 137 | search_all = request.GET.get('search-all') == 'on' 138 | search_title = request.GET.get('search-title') == 'on' 139 | search_author = request.GET.get('search-author') == 'on' 140 | 141 | user = request.user 142 | if not user.is_authenticated(): 143 | queryset = queryset.filter(a_status = BOOK_PUBLISHED) 144 | 145 | published_books_count = Book.objects.filter(a_status = BOOK_PUBLISHED).count() 146 | unpublished_books_count = Book.objects.exclude(a_status = BOOK_PUBLISHED).count() 147 | 148 | # If no search options are specified, assumes search all, the 149 | # advanced search will be used: 150 | if not search_all and not search_title and not search_author: 151 | search_all = True 152 | 153 | # If search queried, modify the queryset with the result of the 154 | # search: 155 | if q is not None: 156 | if search_all: 157 | queryset = advanced_search(queryset, q) 158 | else: 159 | queryset = simple_search(queryset, q, 160 | search_title, search_author) 161 | 162 | paginator = Paginator(queryset, BOOKS_PER_PAGE) 163 | page = int(request.GET.get('page', '1')) 164 | 165 | try: 166 | page_obj = paginator.page(page) 167 | except (EmptyPage, InvalidPage): 168 | page_obj = paginator.page(paginator.num_pages) 169 | 170 | # Build the query string: 171 | qstring = page_qstring(request) 172 | 173 | # Return OPDS Atom Feed: 174 | if qtype == 'feed': 175 | catalog = generate_catalog(request, page_obj) 176 | return HttpResponse(catalog, content_type='application/atom+xml') 177 | 178 | # Return HTML page: 179 | extra_context = dict(kwargs) 180 | extra_context.update({ 181 | 'book_list': page_obj.object_list, 182 | 'published_books': published_books_count, 183 | 'unpublished_books': unpublished_books_count, 184 | 'q': q, 185 | 'paginator': paginator, 186 | 'page_obj': page_obj, 187 | 'search_title': search_title, 188 | 'search_author': search_author, 'list_by': list_by, 189 | 'qstring': qstring, 190 | 'allow_public_add_book': settings.ALLOW_PUBLIC_ADD_BOOKS 191 | }) 192 | return render(request, 'books/book_list.html', 193 | context=extra_context) 194 | 195 | def _author_list(request, queryset, qtype=None, list_by='latest', **kwargs): 196 | """ 197 | Filter the books, paginate the result, and return either a HTML 198 | book list, or a atom+xml OPDS catalog. 199 | 200 | """ 201 | q = request.GET.get('q') 202 | # search_all = request.GET.get('search-all') == 'on' 203 | # search_title = request.GET.get('search-title') == 'on' 204 | search_author = request.GET.get('search-author') == 'on' 205 | 206 | # context_instance = RequestContext(request) 207 | # user = resolve_variable('user', context_instance) 208 | # if not user.is_authenticated(): 209 | # queryset = queryset.filter(a_status = BOOK_PUBLISHED) 210 | 211 | # published_books_count = Book.objects.filter(a_status = BOOK_PUBLISHED).count() 212 | # unpublished_books_count = Book.objects.exclude(a_status = BOOK_PUBLISHED).count() 213 | 214 | # If no search options are specified, assumes search all, the 215 | # advanced search will be used: 216 | # if not search_all and not search_title and not search_author: 217 | # search_all = True 218 | 219 | # FIXME 220 | # If search queried, modify the queryset with the result of the 221 | # search: 222 | # if q is not None: 223 | # if search_all: 224 | # queryset = advanced_search(queryset, q) 225 | # else: 226 | # queryset = simple_search(queryset, q, 227 | # search_title, search_author) 228 | 229 | paginator = Paginator(queryset, BOOKS_PER_PAGE) 230 | page = int(request.GET.get('page', '1')) 231 | 232 | try: 233 | page_obj = paginator.page(page) 234 | except (EmptyPage, InvalidPage): 235 | page_obj = paginator.page(paginator.num_pages) 236 | 237 | # Build the query string: 238 | qstring = page_qstring(request) 239 | 240 | # Return OPDS Atom Feed: 241 | if qtype == 'feed': 242 | catalog = generate_author_catalog(request, page_obj) 243 | return HttpResponse(catalog, content_type='application/atom+xml') 244 | 245 | # Return HTML page: 246 | extra_context = dict(kwargs) 247 | extra_context.update({ 248 | 'author_list': page_obj.object_list, 249 | # 'published_books': published_books_count, 250 | # 'unpublished_books': unpublished_books_count, 251 | 'q': q, 252 | 'paginator': paginator, 253 | 'page_obj': page_obj, 254 | # 'search_title': search_title, 255 | 'search_author': search_author, 256 | 'list_by': list_by, 257 | 'qstring': qstring, 258 | # 'allow_public_add_book': settings.ALLOW_PUBLIC_ADD_BOOKS 259 | }) 260 | return render(request, 'authors/author_list.html', 261 | context=extra_context) 262 | 263 | def home(request): 264 | return redirect('latest') 265 | 266 | def root(request, qtype=None): 267 | """Return the root catalog for navigation""" 268 | root_catalog = generate_root_catalog() 269 | return HttpResponse(root_catalog, content_type='application/atom+xml') 270 | 271 | def latest(request, qtype=None): 272 | queryset = Book.objects.all() 273 | return _book_list(request, queryset, qtype, list_by='latest') 274 | 275 | def by_title(request, qtype=None, author_id=None): 276 | queryset = Book.objects.all().order_by('a_title') 277 | if author_id: 278 | queryset = queryset.filter(a_author=author_id) 279 | return _book_list(request, queryset, qtype, list_by='by-title') 280 | 281 | def by_author(request, qtype=None): 282 | #queryset = Book.objects.all().order_by('a_author') 283 | #return _book_list(request, queryset, qtype, list_by='by-author') 284 | queryset = Author.objects.all().order_by('a_author') 285 | print("stuff") 286 | return _author_list(request, queryset, qtype, list_by='by-author') 287 | 288 | def by_tag(request, tag, qtype=None): 289 | """ displays a book list by the tag argument """ 290 | # get the Tag object 291 | tag_instance = tTag.objects.get(name=tag) # TODO replace as Tag when django-tagging is removed 292 | 293 | # if the tag does not exist, return 404 294 | if tag_instance is None: 295 | raise Http404() 296 | 297 | # Get a list of books that have the requested tag 298 | queryset = Book.objects.filter(tags=tag_instance) 299 | return _book_list(request, queryset, qtype, list_by='by-tag', 300 | tag=tag_instance) 301 | 302 | def most_downloaded(request, qtype=None): 303 | queryset = Book.objects.all().order_by('-downloads') 304 | return _book_list(request, queryset, qtype, list_by='most-downloaded') 305 | 306 | -------------------------------------------------------------------------------- /static/style/blueprint/screen.css: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------- 2 | 3 | 4 | Blueprint CSS Framework 0.9 5 | http://blueprintcss.org 6 | 7 | * Copyright (c) 2007-Present. See LICENSE for more info. 8 | * See README for instructions on how to use Blueprint. 9 | * For credits and origins, see AUTHORS. 10 | * This is a compressed file. See the sources in the 'src' directory. 11 | 12 | ----------------------------------------------------------------------- */ 13 | 14 | /* reset.css */ 15 | html, body, div, span, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, code, del, dfn, em, img, q, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td {margin:0;padding:0;border:0;font-weight:inherit;font-style:inherit;font-size:100%;font-family:inherit;vertical-align:baseline;} 16 | body {line-height:1.5;} 17 | table {border-collapse:separate;border-spacing:0;} 18 | caption, th, td {text-align:left;font-weight:normal;} 19 | table, td, th {vertical-align:middle;} 20 | blockquote:before, blockquote:after, q:before, q:after {content:"";} 21 | blockquote, q {quotes:"" "";} 22 | a img {border:none;} 23 | 24 | /* typography.css */ 25 | html {font-size:100.01%;} 26 | body {font-size:75%;color:#222;background:#fff;font-family:"Helvetica Neue", Arial, Helvetica, sans-serif;} 27 | h1, h2, h3, h4, h5, h6 {font-weight:normal;color:#111;} 28 | h1 {font-size:3em;line-height:1;margin-bottom:0.5em;} 29 | h2 {font-size:2em;margin-bottom:0.75em;} 30 | h3 {font-size:1.5em;line-height:1;margin-bottom:1em;} 31 | h4 {font-size:1.2em;line-height:1.25;margin-bottom:1.25em;} 32 | h5 {font-size:1em;font-weight:bold;margin-bottom:1.5em;} 33 | h6 {font-size:1em;font-weight:bold;} 34 | h1 img, h2 img, h3 img, h4 img, h5 img, h6 img {margin:0;} 35 | p {margin:0 0 1.5em;} 36 | p img.left {float:left;margin:1.5em 1.5em 1.5em 0;padding:0;} 37 | p img.right {float:right;margin:1.5em 0 1.5em 1.5em;} 38 | a:focus, a:hover {color:#000;} 39 | a {color:#009;text-decoration:underline;} 40 | blockquote {margin:1.5em;color:#666;font-style:italic;} 41 | strong {font-weight:bold;} 42 | em, dfn {font-style:italic;} 43 | dfn {font-weight:bold;} 44 | sup, sub {line-height:0;} 45 | abbr, acronym {border-bottom:1px dotted #666;} 46 | address {margin:0 0 1.5em;font-style:italic;} 47 | del {color:#666;} 48 | pre {margin:1.5em 0;white-space:pre;} 49 | pre, code, tt {font:1em 'andale mono', 'lucida console', monospace;line-height:1.5;} 50 | li ul, li ol {margin:0 1.5em;} 51 | ul, ol {margin:0 1.5em 1.5em 1.5em;} 52 | ul {list-style-type:disc;} 53 | ol {list-style-type:decimal;} 54 | dl {margin:0 0 1.5em 0;} 55 | dl dt {font-weight:bold;} 56 | dd {margin-left:1.5em;} 57 | table {margin-bottom:1.4em;width:100%;} 58 | th {font-weight:bold;} 59 | thead th {background:#c3d9ff;} 60 | th, td, caption {padding:4px 10px 4px 5px;} 61 | tr.even td {background:#e5ecf9;} 62 | tfoot {font-style:italic;} 63 | caption {background:#eee;} 64 | .small {font-size:.8em;margin-bottom:1.875em;line-height:1.875em;} 65 | .large {font-size:1.2em;line-height:2.5em;margin-bottom:1.25em;} 66 | .hide {display:none;} 67 | .quiet {color:#666;} 68 | .loud {color:#000;} 69 | .highlight {background:#ff0;} 70 | .added {background:#060;color:#fff;} 71 | .removed {background:#900;color:#fff;} 72 | .first {margin-left:0;padding-left:0;} 73 | .last {margin-right:0;padding-right:0;} 74 | .top {margin-top:0;padding-top:0;} 75 | .bottom {margin-bottom:0;padding-bottom:0;} 76 | 77 | /* forms.css */ 78 | label {font-weight:bold;} 79 | fieldset {padding:1.4em;margin:0 0 1.5em 0;border:1px solid #ccc;} 80 | legend {font-weight:bold;font-size:1.2em;} 81 | input[type=text], input[type=password], input.text, input.title, textarea, select {background-color:#fff;border:1px solid #bbb;} 82 | input[type=text]:focus, input[type=password]:focus, input.text:focus, input.title:focus, textarea:focus, select:focus {border-color:#666;} 83 | input.text, input.title {width:300px;padding:5px;} 84 | input.title {font-size:1.5em;} 85 | textarea {width:390px;height:250px;padding:5px;} 86 | input[type=checkbox], input[type=radio], input.checkbox, input.radio {position:relative;top:.25em;} 87 | form.inline {line-height:3;} 88 | form.inline p {margin-bottom:0;} 89 | .error, .notice, .success {padding:.8em;margin-bottom:1em;border:2px solid #ddd;} 90 | .error {background:#FBE3E4;color:#8a1f11;border-color:#FBC2C4;} 91 | .notice {background:#FFF6BF;color:#514721;border-color:#FFD324;} 92 | .success {background:#E6EFC2;color:#264409;border-color:#C6D880;} 93 | .error a {color:#8a1f11;} 94 | .notice a {color:#514721;} 95 | .success a {color:#264409;} 96 | .errorlist { 97 | margin-left: 400px; 98 | margin-bottom: 0px; 99 | color: red; 100 | } 101 | 102 | /* grid.css */ 103 | .container {width:950px;margin:0 auto;} 104 | .showgrid {background:url(src/grid.png);} 105 | .column, div.span-1, div.span-2, div.span-3, div.span-4, div.span-5, div.span-6, div.span-7, div.span-8, div.span-9, div.span-10, div.span-11, div.span-12, div.span-13, div.span-14, div.span-15, div.span-16, div.span-17, div.span-18, div.span-19, div.span-20, div.span-21, div.span-22, div.span-23, div.span-24 {float:left;margin-right:10px;} 106 | .last, div.last {margin-right:0;} 107 | .span-1 {width:30px;} 108 | .span-2 {width:70px;} 109 | .span-3 {width:110px;} 110 | .span-4 {width:150px;} 111 | .span-5 {width:190px;} 112 | .span-6 {width:230px;} 113 | .span-7 {width:270px;} 114 | .span-8 {width:310px;} 115 | .span-9 {width:350px;} 116 | .span-10 {width:390px;} 117 | .span-11 {width:430px;} 118 | .span-12 {width:470px;} 119 | .span-13 {width:510px;} 120 | .span-14 {width:550px;} 121 | .span-15 {width:590px;} 122 | .span-16 {width:630px;} 123 | .span-17 {width:670px;} 124 | .span-18 {width:710px;} 125 | .span-19 {width:750px;} 126 | .span-20 {width:790px;} 127 | .span-21 {width:830px;} 128 | .span-22 {width:870px;} 129 | .span-23 {width:910px;} 130 | .span-24, div.span-24 {width:950px;margin-right:0;} 131 | input.span-1, textarea.span-1, input.span-2, textarea.span-2, input.span-3, textarea.span-3, input.span-4, textarea.span-4, input.span-5, textarea.span-5, input.span-6, textarea.span-6, input.span-7, textarea.span-7, input.span-8, textarea.span-8, input.span-9, textarea.span-9, input.span-10, textarea.span-10, input.span-11, textarea.span-11, input.span-12, textarea.span-12, input.span-13, textarea.span-13, input.span-14, textarea.span-14, input.span-15, textarea.span-15, input.span-16, textarea.span-16, input.span-17, textarea.span-17, input.span-18, textarea.span-18, input.span-19, textarea.span-19, input.span-20, textarea.span-20, input.span-21, textarea.span-21, input.span-22, textarea.span-22, input.span-23, textarea.span-23, input.span-24, textarea.span-24 {border-left-width:1px!important;border-right-width:1px!important;padding-left:5px!important;padding-right:5px!important;} 132 | input.span-1, textarea.span-1 {width:18px!important;} 133 | input.span-2, textarea.span-2 {width:58px!important;} 134 | input.span-3, textarea.span-3 {width:98px!important;} 135 | input.span-4, textarea.span-4 {width:138px!important;} 136 | input.span-5, textarea.span-5 {width:178px!important;} 137 | input.span-6, textarea.span-6 {width:218px!important;} 138 | input.span-7, textarea.span-7 {width:258px!important;} 139 | input.span-8, textarea.span-8 {width:298px!important;} 140 | input.span-9, textarea.span-9 {width:338px!important;} 141 | input.span-10, textarea.span-10 {width:378px!important;} 142 | input.span-11, textarea.span-11 {width:418px!important;} 143 | input.span-12, textarea.span-12 {width:458px!important;} 144 | input.span-13, textarea.span-13 {width:498px!important;} 145 | input.span-14, textarea.span-14 {width:538px!important;} 146 | input.span-15, textarea.span-15 {width:578px!important;} 147 | input.span-16, textarea.span-16 {width:618px!important;} 148 | input.span-17, textarea.span-17 {width:658px!important;} 149 | input.span-18, textarea.span-18 {width:698px!important;} 150 | input.span-19, textarea.span-19 {width:738px!important;} 151 | input.span-20, textarea.span-20 {width:778px!important;} 152 | input.span-21, textarea.span-21 {width:818px!important;} 153 | input.span-22, textarea.span-22 {width:858px!important;} 154 | input.span-23, textarea.span-23 {width:898px!important;} 155 | input.span-24, textarea.span-24 {width:938px!important;} 156 | .append-1 {padding-right:40px;} 157 | .append-2 {padding-right:80px;} 158 | .append-3 {padding-right:120px;} 159 | .append-4 {padding-right:160px;} 160 | .append-5 {padding-right:200px;} 161 | .append-6 {padding-right:240px;} 162 | .append-7 {padding-right:280px;} 163 | .append-8 {padding-right:320px;} 164 | .append-9 {padding-right:360px;} 165 | .append-10 {padding-right:400px;} 166 | .append-11 {padding-right:440px;} 167 | .append-12 {padding-right:480px;} 168 | .append-13 {padding-right:520px;} 169 | .append-14 {padding-right:560px;} 170 | .append-15 {padding-right:600px;} 171 | .append-16 {padding-right:640px;} 172 | .append-17 {padding-right:680px;} 173 | .append-18 {padding-right:720px;} 174 | .append-19 {padding-right:760px;} 175 | .append-20 {padding-right:800px;} 176 | .append-21 {padding-right:840px;} 177 | .append-22 {padding-right:880px;} 178 | .append-23 {padding-right:920px;} 179 | .prepend-1 {padding-left:40px;} 180 | .prepend-2 {padding-left:80px;} 181 | .prepend-3 {padding-left:120px;} 182 | .prepend-4 {padding-left:160px;} 183 | .prepend-5 {padding-left:200px;} 184 | .prepend-6 {padding-left:240px;} 185 | .prepend-7 {padding-left:280px;} 186 | .prepend-8 {padding-left:320px;} 187 | .prepend-9 {padding-left:360px;} 188 | .prepend-10 {padding-left:400px;} 189 | .prepend-11 {padding-left:440px;} 190 | .prepend-12 {padding-left:480px;} 191 | .prepend-13 {padding-left:520px;} 192 | .prepend-14 {padding-left:560px;} 193 | .prepend-15 {padding-left:600px;} 194 | .prepend-16 {padding-left:640px;} 195 | .prepend-17 {padding-left:680px;} 196 | .prepend-18 {padding-left:720px;} 197 | .prepend-19 {padding-left:760px;} 198 | .prepend-20 {padding-left:800px;} 199 | .prepend-21 {padding-left:840px;} 200 | .prepend-22 {padding-left:880px;} 201 | .prepend-23 {padding-left:920px;} 202 | div.border {padding-right:4px;margin-right:5px;border-right:1px solid #eee;} 203 | div.colborder {padding-right:24px;margin-right:25px;border-right:1px solid #eee;} 204 | .pull-1 {margin-left:-40px;} 205 | .pull-2 {margin-left:-80px;} 206 | .pull-3 {margin-left:-120px;} 207 | .pull-4 {margin-left:-160px;} 208 | .pull-5 {margin-left:-200px;} 209 | .pull-6 {margin-left:-240px;} 210 | .pull-7 {margin-left:-280px;} 211 | .pull-8 {margin-left:-320px;} 212 | .pull-9 {margin-left:-360px;} 213 | .pull-10 {margin-left:-400px;} 214 | .pull-11 {margin-left:-440px;} 215 | .pull-12 {margin-left:-480px;} 216 | .pull-13 {margin-left:-520px;} 217 | .pull-14 {margin-left:-560px;} 218 | .pull-15 {margin-left:-600px;} 219 | .pull-16 {margin-left:-640px;} 220 | .pull-17 {margin-left:-680px;} 221 | .pull-18 {margin-left:-720px;} 222 | .pull-19 {margin-left:-760px;} 223 | .pull-20 {margin-left:-800px;} 224 | .pull-21 {margin-left:-840px;} 225 | .pull-22 {margin-left:-880px;} 226 | .pull-23 {margin-left:-920px;} 227 | .pull-24 {margin-left:-960px;} 228 | .pull-1, .pull-2, .pull-3, .pull-4, .pull-5, .pull-6, .pull-7, .pull-8, .pull-9, .pull-10, .pull-11, .pull-12, .pull-13, .pull-14, .pull-15, .pull-16, .pull-17, .pull-18, .pull-19, .pull-20, .pull-21, .pull-22, .pull-23, .pull-24 {float:left;position:relative;} 229 | .push-1 {margin:0 -40px 1.5em 40px;} 230 | .push-2 {margin:0 -80px 1.5em 80px;} 231 | .push-3 {margin:0 -120px 1.5em 120px;} 232 | .push-4 {margin:0 -160px 1.5em 160px;} 233 | .push-5 {margin:0 -200px 1.5em 200px;} 234 | .push-6 {margin:0 -240px 1.5em 240px;} 235 | .push-7 {margin:0 -280px 1.5em 280px;} 236 | .push-8 {margin:0 -320px 1.5em 320px;} 237 | .push-9 {margin:0 -360px 1.5em 360px;} 238 | .push-10 {margin:0 -400px 1.5em 400px;} 239 | .push-11 {margin:0 -440px 1.5em 440px;} 240 | .push-12 {margin:0 -480px 1.5em 480px;} 241 | .push-13 {margin:0 -520px 1.5em 520px;} 242 | .push-14 {margin:0 -560px 1.5em 560px;} 243 | .push-15 {margin:0 -600px 1.5em 600px;} 244 | .push-16 {margin:0 -640px 1.5em 640px;} 245 | .push-17 {margin:0 -680px 1.5em 680px;} 246 | .push-18 {margin:0 -720px 1.5em 720px;} 247 | .push-19 {margin:0 -760px 1.5em 760px;} 248 | .push-20 {margin:0 -800px 1.5em 800px;} 249 | .push-21 {margin:0 -840px 1.5em 840px;} 250 | .push-22 {margin:0 -880px 1.5em 880px;} 251 | .push-23 {margin:0 -920px 1.5em 920px;} 252 | .push-24 {margin:0 -960px 1.5em 960px;} 253 | .push-1, .push-2, .push-3, .push-4, .push-5, .push-6, .push-7, .push-8, .push-9, .push-10, .push-11, .push-12, .push-13, .push-14, .push-15, .push-16, .push-17, .push-18, .push-19, .push-20, .push-21, .push-22, .push-23, .push-24 {float:right;position:relative;} 254 | .prepend-top {margin-top:1.5em;} 255 | .append-bottom {margin-bottom:1.5em;} 256 | .box {padding:1.5em;margin-bottom:1.5em;background:#E5ECF9;} 257 | hr {background:#ddd;color:#ddd;clear:both;float:none;width:100%;height:.1em;margin:0 0 1.45em;border:none;} 258 | hr.space {background:#fff;color:#fff;visibility:hidden;} 259 | .clearfix:after, .container:after {content:"\0020";display:block;height:0;clear:both;visibility:hidden;overflow:hidden;} 260 | .clearfix, .container {display:block;} 261 | .clear {clear:both;} 262 | -------------------------------------------------------------------------------- /pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # Specify a configuration file. 4 | #rcfile= 5 | 6 | # Python code to execute, usually for sys.path manipulation such as 7 | # pygtk.require(). 8 | #init-hook= 9 | 10 | # Add files or directories to the blacklist. They should be base names, not 11 | # paths. 12 | ignore=CVS 13 | 14 | # Add files or directories matching the regex patterns to the blacklist. The 15 | # regex matches against base names, not paths. 16 | ignore-patterns= 17 | 18 | # Pickle collected data for later comparisons. 19 | persistent=yes 20 | 21 | # List of plugins (as comma separated values of python modules names) to load, 22 | # usually to register additional checkers. 23 | load-plugins= 24 | 25 | # Use multiple processes to speed up Pylint. 26 | jobs=1 27 | 28 | # Allow loading of arbitrary C extensions. Extensions are imported into the 29 | # active Python interpreter and may run arbitrary code. 30 | unsafe-load-any-extension=no 31 | 32 | # A comma-separated list of package or module names from where C extensions may 33 | # be loaded. Extensions are loading into the active Python interpreter and may 34 | # run arbitrary code 35 | extension-pkg-whitelist=lxml 36 | 37 | # Allow optimization of some AST trees. This will activate a peephole AST 38 | # optimizer, which will apply various small optimizations. For instance, it can 39 | # be used to obtain the result of joining multiple strings with the addition 40 | # operator. Joining a lot of strings can lead to a maximum recursion error in 41 | # Pylint and this flag can prevent that. It has one side effect, the resulting 42 | # AST will be different than the one from reality. This option is deprecated 43 | # and it will be removed in Pylint 2.0. 44 | optimize-ast=no 45 | 46 | 47 | [MESSAGES CONTROL] 48 | 49 | # Only show warnings with the listed confidence levels. Leave empty to show 50 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED 51 | confidence= 52 | 53 | # Enable the message, report, category or checker with the given id(s). You can 54 | # either give multiple identifier separated by comma (,) or put this option 55 | # multiple time (only on the command line, not in the configuration file where 56 | # it should appear only once). See also the "--disable" option for examples. 57 | #enable= 58 | 59 | # Disable the message, report, category or checker with the given id(s). You 60 | # can either give multiple identifiers separated by comma (,) or put this 61 | # option multiple times (only on the command line, not in the configuration 62 | # file where it should appear only once).You can also use "--disable=all" to 63 | # disable everything first and then reenable specific checks. For example, if 64 | # you want to run only the similarities checker, you can use "--disable=all 65 | # --enable=similarities". If you want to run only the classes checker, but have 66 | # no Warning level messages displayed, use"--disable=all --enable=classes 67 | # --disable=W" 68 | disable=import-star-module-level,old-octal-literal,oct-method,print-statement,unpacking-in-except,parameter-unpacking,backtick,old-raise-syntax,old-ne-operator,long-suffix,dict-view-method,dict-iter-method,metaclass-assignment,next-method-called,raising-string,indexing-exception,raw_input-builtin,long-builtin,file-builtin,execfile-builtin,coerce-builtin,cmp-builtin,buffer-builtin,basestring-builtin,apply-builtin,filter-builtin-not-iterating,using-cmp-argument,useless-suppression,range-builtin-not-iterating,suppressed-message,no-absolute-import,old-division,cmp-method,reload-builtin,zip-builtin-not-iterating,intern-builtin,unichr-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,input-builtin,round-builtin,hex-method,nonzero-method,map-builtin-not-iterating,locally-disabled 69 | 70 | 71 | [REPORTS] 72 | 73 | # Set the output format. Available formats are text, parseable, colorized, msvs 74 | # (visual studio) and html. You can also give a reporter class, eg 75 | # mypackage.mymodule.MyReporterClass. 76 | output-format=text 77 | 78 | # Put messages in a separate file for each module / package specified on the 79 | # command line instead of printing them on stdout. Reports (if any) will be 80 | # written in a file name "pylint_global.[txt|html]". This option is deprecated 81 | # and it will be removed in Pylint 2.0. 82 | files-output=no 83 | 84 | # Tells whether to display a full report or only the messages 85 | reports=no 86 | 87 | # Python expression which should return a note less than 10 (10 is the highest 88 | # note). You have access to the variables errors warning, statement which 89 | # respectively contain the number of errors / warnings messages and the total 90 | # number of statements analyzed. This is used by the global evaluation report 91 | # (RP0004). 92 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 93 | 94 | # Template used to display messages. This is a python new-style format string 95 | # used to format the message information. See doc for all details 96 | #msg-template= 97 | 98 | 99 | [BASIC] 100 | 101 | # Good variable names which should always be accepted, separated by a comma 102 | good-names=i,j,k,ex,Run,_ 103 | 104 | # Bad variable names which should always be refused, separated by a comma 105 | bad-names=foo,bar,baz,toto,tutu,tata 106 | 107 | # Colon-delimited sets of names that determine each other's naming style when 108 | # the name regexes allow several styles. 109 | name-group= 110 | 111 | # Include a hint for the correct naming format with invalid-name 112 | include-naming-hint=no 113 | 114 | # List of decorators that produce properties, such as abc.abstractproperty. Add 115 | # to this list to register other decorators that produce valid properties. 116 | property-classes=abc.abstractproperty 117 | 118 | # Regular expression matching correct function names 119 | function-rgx=[a-z_][a-z0-9_]{2,30}$ 120 | 121 | # Naming hint for function names 122 | function-name-hint=[a-z_][a-z0-9_]{2,30}$ 123 | 124 | # Regular expression matching correct variable names 125 | variable-rgx=[a-z_][a-z0-9_]{2,30}$ 126 | 127 | # Naming hint for variable names 128 | variable-name-hint=[a-z_][a-z0-9_]{2,30}$ 129 | 130 | # Regular expression matching correct constant names 131 | const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 132 | 133 | # Naming hint for constant names 134 | const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 135 | 136 | # Regular expression matching correct attribute names 137 | attr-rgx=[a-z_][a-z0-9_]{2,30}$ 138 | 139 | # Naming hint for attribute names 140 | attr-name-hint=[a-z_][a-z0-9_]{2,30}$ 141 | 142 | # Regular expression matching correct argument names 143 | argument-rgx=[a-z_][a-z0-9_]{2,30}$ 144 | 145 | # Naming hint for argument names 146 | argument-name-hint=[a-z_][a-z0-9_]{2,30}$ 147 | 148 | # Regular expression matching correct class attribute names 149 | class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 150 | 151 | # Naming hint for class attribute names 152 | class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 153 | 154 | # Regular expression matching correct inline iteration names 155 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 156 | 157 | # Naming hint for inline iteration names 158 | inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ 159 | 160 | # Regular expression matching correct class names 161 | class-rgx=[A-Z_][a-zA-Z0-9]+$ 162 | 163 | # Naming hint for class names 164 | class-name-hint=[A-Z_][a-zA-Z0-9]+$ 165 | 166 | # Regular expression matching correct module names 167 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 168 | 169 | # Naming hint for module names 170 | module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 171 | 172 | # Regular expression matching correct method names 173 | method-rgx=[a-z_][a-z0-9_]{2,30}$ 174 | 175 | # Naming hint for method names 176 | method-name-hint=[a-z_][a-z0-9_]{2,30}$ 177 | 178 | # Regular expression which should only match function or class names that do 179 | # not require a docstring. 180 | no-docstring-rgx=^_ 181 | 182 | # Minimum line length for functions/classes that require docstrings, shorter 183 | # ones are exempt. 184 | docstring-min-length=-1 185 | 186 | 187 | [ELIF] 188 | 189 | # Maximum number of nested blocks for function / method body 190 | max-nested-blocks=5 191 | 192 | 193 | [SPELLING] 194 | 195 | # Spelling dictionary name. Available dictionaries: none. To make it working 196 | # install python-enchant package. 197 | spelling-dict= 198 | 199 | # List of comma separated words that should not be checked. 200 | spelling-ignore-words= 201 | 202 | # A path to a file that contains private dictionary; one word per line. 203 | spelling-private-dict-file= 204 | 205 | # Tells whether to store unknown words to indicated private dictionary in 206 | # --spelling-private-dict-file option instead of raising a message. 207 | spelling-store-unknown-words=no 208 | 209 | 210 | [FORMAT] 211 | 212 | # Maximum number of characters on a single line. 213 | max-line-length=100 214 | 215 | # Regexp for a line that is allowed to be longer than the limit. 216 | ignore-long-lines=^\s*(# )??$ 217 | 218 | # Allow the body of an if to be on the same line as the test if there is no 219 | # else. 220 | single-line-if-stmt=no 221 | 222 | # List of optional constructs for which whitespace checking is disabled. `dict- 223 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. 224 | # `trailing-comma` allows a space between comma and closing bracket: (a, ). 225 | # `empty-line` allows space-only lines. 226 | no-space-check=trailing-comma,dict-separator 227 | 228 | # Maximum number of lines in a module 229 | max-module-lines=1000 230 | 231 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 232 | # tab). 233 | indent-string=' ' 234 | 235 | # Number of spaces of indent required inside a hanging or continued line. 236 | indent-after-paren=4 237 | 238 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 239 | expected-line-ending-format= 240 | 241 | 242 | [TYPECHECK] 243 | 244 | # Tells whether missing members accessed in mixin class should be ignored. A 245 | # mixin class is detected if its name ends with "mixin" (case insensitive). 246 | ignore-mixin-members=yes 247 | 248 | # List of module names for which member attributes should not be checked 249 | # (useful for modules/projects where namespaces are manipulated during runtime 250 | # and thus existing member attributes cannot be deduced by static analysis. It 251 | # supports qualified module names, as well as Unix pattern matching. 252 | ignored-modules=distutils 253 | 254 | # List of class names for which member attributes should not be checked (useful 255 | # for classes with dynamically set attributes). This supports the use of 256 | # qualified names. 257 | ignored-classes=optparse.Values,thread._local,_thread._local 258 | 259 | # List of members which are set dynamically and missed by pylint inference 260 | # system, and so shouldn't trigger E1101 when accessed. Python regular 261 | # expressions are accepted. 262 | generated-members= 263 | 264 | # List of decorators that produce context managers, such as 265 | # contextlib.contextmanager. Add to this list to register other decorators that 266 | # produce valid context managers. 267 | contextmanager-decorators=contextlib.contextmanager 268 | 269 | 270 | [SIMILARITIES] 271 | 272 | # Minimum lines number of a similarity. 273 | min-similarity-lines=4 274 | 275 | # Ignore comments when computing similarities. 276 | ignore-comments=yes 277 | 278 | # Ignore docstrings when computing similarities. 279 | ignore-docstrings=yes 280 | 281 | # Ignore imports when computing similarities. 282 | ignore-imports=no 283 | 284 | 285 | [LOGGING] 286 | 287 | # Logging modules to check that the string format arguments are in logging 288 | # function parameter format 289 | logging-modules=logging 290 | 291 | 292 | [MISCELLANEOUS] 293 | 294 | # List of note tags to take in consideration, separated by a comma. 295 | notes=FIXME,XXX,TODO 296 | 297 | 298 | [VARIABLES] 299 | 300 | # Tells whether we should check for unused import in __init__ files. 301 | init-import=no 302 | 303 | # A regular expression matching the name of dummy variables (i.e. expectedly 304 | # not used). 305 | dummy-variables-rgx=(_+[a-zA-Z0-9]*?$)|dummy 306 | 307 | # List of additional names supposed to be defined in builtins. Remember that 308 | # you should avoid to define new builtins when possible. 309 | additional-builtins= 310 | 311 | # List of strings which can identify a callback function by name. A callback 312 | # name must start or end with one of those strings. 313 | callbacks=cb_,_cb 314 | 315 | # List of qualified module names which can have objects that can redefine 316 | # builtins. 317 | redefining-builtins-modules=six.moves,future.builtins 318 | 319 | 320 | [CLASSES] 321 | 322 | # List of method names used to declare (i.e. assign) instance attributes. 323 | defining-attr-methods=__init__,__new__,setUp 324 | 325 | # List of valid names for the first argument in a class method. 326 | valid-classmethod-first-arg=cls 327 | 328 | # List of valid names for the first argument in a metaclass class method. 329 | valid-metaclass-classmethod-first-arg=mcs 330 | 331 | # List of member names, which should be excluded from the protected access 332 | # warning. 333 | exclude-protected=_asdict,_fields,_replace,_source,_make 334 | 335 | 336 | [DESIGN] 337 | 338 | # Maximum number of arguments for function / method 339 | max-args=5 340 | 341 | # Argument names that match this expression will be ignored. Default to name 342 | # with leading underscore 343 | ignored-argument-names=_.* 344 | 345 | # Maximum number of locals for function / method body 346 | max-locals=15 347 | 348 | # Maximum number of return / yield for function / method body 349 | max-returns=6 350 | 351 | # Maximum number of branch for function / method body 352 | max-branches=12 353 | 354 | # Maximum number of statements in function / method body 355 | max-statements=50 356 | 357 | # Maximum number of parents for a class (see R0901). 358 | max-parents=7 359 | 360 | # Maximum number of attributes for a class (see R0902). 361 | max-attributes=7 362 | 363 | # Minimum number of public methods for a class (see R0903). 364 | min-public-methods=2 365 | 366 | # Maximum number of public methods for a class (see R0904). 367 | max-public-methods=20 368 | 369 | # Maximum number of boolean expressions in a if statement 370 | max-bool-expr=5 371 | 372 | 373 | [IMPORTS] 374 | 375 | # Deprecated modules which should not be used, separated by a comma 376 | deprecated-modules=regsub,TERMIOS,Bastion,rexec 377 | 378 | # Create a graph of every (i.e. internal and external) dependencies in the 379 | # given file (report RP0402 must not be disabled) 380 | import-graph= 381 | 382 | # Create a graph of external dependencies in the given file (report RP0402 must 383 | # not be disabled) 384 | ext-import-graph= 385 | 386 | # Create a graph of internal dependencies in the given file (report RP0402 must 387 | # not be disabled) 388 | int-import-graph= 389 | 390 | # Force import order to recognize a module as part of the standard 391 | # compatibility libraries. 392 | known-standard-library= 393 | 394 | # Force import order to recognize a module as part of a third party library. 395 | known-third-party=enchant 396 | 397 | # Analyse import fallback blocks. This can be used to support both Python 2 and 398 | # 3 compatible code, which means that the block might have code that exists 399 | # only in one or another interpreter, leading to false positives when analysed. 400 | analyse-fallback-blocks=no 401 | 402 | 403 | [EXCEPTIONS] 404 | 405 | # Exceptions that will emit a warning when being caught. Defaults to 406 | # "Exception" 407 | overgeneral-exceptions=Exception 408 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | --------------------------------------------------------------------------------