forked from mirrors/bookwyrm
commit
b4cf193a81
18 changed files with 65 additions and 44 deletions
|
@ -162,7 +162,7 @@ class AbstractConnector(ABC):
|
||||||
def update_book(self, book, data=None):
|
def update_book(self, book, data=None):
|
||||||
''' load new data '''
|
''' load new data '''
|
||||||
if not book.sync and not book.sync_cover:
|
if not book.sync and not book.sync_cover:
|
||||||
return
|
return book
|
||||||
|
|
||||||
if not data:
|
if not data:
|
||||||
key = getattr(book, self.key_name)
|
key = getattr(book, self.key_name)
|
||||||
|
@ -286,7 +286,7 @@ def get_data(url):
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
class SearchResult(object):
|
class SearchResult:
|
||||||
''' standardized search result object '''
|
''' standardized search result object '''
|
||||||
def __init__(self, title, key, author, year):
|
def __init__(self, title, key, author, year):
|
||||||
self.title = title
|
self.title = title
|
||||||
|
@ -299,7 +299,7 @@ class SearchResult(object):
|
||||||
self.key, self.title, self.author)
|
self.key, self.title, self.author)
|
||||||
|
|
||||||
|
|
||||||
class Mapping(object):
|
class Mapping:
|
||||||
''' associate a local database field with a field in an external dataset '''
|
''' associate a local database field with a field in an external dataset '''
|
||||||
def __init__(
|
def __init__(
|
||||||
self, local_field, remote_field=None, formatter=None, model=None):
|
self, local_field, remote_field=None, formatter=None, model=None):
|
||||||
|
|
|
@ -123,15 +123,15 @@ class Connector(AbstractConnector):
|
||||||
return data.get('docs')
|
return data.get('docs')
|
||||||
|
|
||||||
|
|
||||||
def format_search_result(self, doc):
|
def format_search_result(self, search_result):
|
||||||
# build the remote id from the openlibrary key
|
# build the remote id from the openlibrary key
|
||||||
key = self.books_url + doc['key']
|
key = self.books_url + search_result['key']
|
||||||
author = doc.get('author_name') or ['Unknown']
|
author = search_result.get('author_name') or ['Unknown']
|
||||||
return SearchResult(
|
return SearchResult(
|
||||||
doc.get('title'),
|
search_result.get('title'),
|
||||||
key,
|
key,
|
||||||
', '.join(author),
|
', '.join(author),
|
||||||
doc.get('first_publish_year'),
|
search_result.get('first_publish_year'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -7,10 +7,6 @@ from .abstract_connector import AbstractConnector, SearchResult
|
||||||
|
|
||||||
class Connector(AbstractConnector):
|
class Connector(AbstractConnector):
|
||||||
''' instantiate a connector '''
|
''' instantiate a connector '''
|
||||||
def __init__(self, identifier):
|
|
||||||
super().__init__(identifier)
|
|
||||||
|
|
||||||
|
|
||||||
def search(self, query):
|
def search(self, query):
|
||||||
''' right now you can't search bookwyrm sorry, but when
|
''' right now you can't search bookwyrm sorry, but when
|
||||||
that gets implemented it will totally rule '''
|
that gets implemented it will totally rule '''
|
||||||
|
@ -44,18 +40,18 @@ class Connector(AbstractConnector):
|
||||||
return search_results
|
return search_results
|
||||||
|
|
||||||
|
|
||||||
def format_search_result(self, book):
|
def format_search_result(self, search_result):
|
||||||
return SearchResult(
|
return SearchResult(
|
||||||
book.title,
|
search_result.title,
|
||||||
book.local_id,
|
search_result.local_id,
|
||||||
book.author_text,
|
search_result.author_text,
|
||||||
book.published_date.year if book.published_date else None,
|
search_result.published_date.year if \
|
||||||
|
search_result.published_date else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_or_create_book(self, remote_id):
|
def get_or_create_book(self, remote_id):
|
||||||
''' this COULD be semi-implemented but I think it shouldn't be used '''
|
''' this COULD be semi-implemented but I think it shouldn't be used '''
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def is_work_data(self, data):
|
def is_work_data(self, data):
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
''' usin django model forms '''
|
''' using django model forms '''
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.forms import ModelForm, PasswordInput, widgets
|
from django.forms import ModelForm, PasswordInput, widgets
|
||||||
from django import forms
|
from django import forms
|
||||||
|
|
||||||
|
|
|
@ -38,10 +38,8 @@ def shared_inbox(request):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
activity = json.loads(request.body)
|
activity = json.loads(request.body)
|
||||||
except json.decoder.JSONDecodeError:
|
activity_object = activity['object']
|
||||||
return HttpResponseBadRequest()
|
except (json.decoder.JSONDecodeError, KeyError):
|
||||||
|
|
||||||
if not activity.get('object'):
|
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
if not has_valid_signature(request, activity):
|
if not has_valid_signature(request, activity):
|
||||||
|
@ -74,7 +72,7 @@ def shared_inbox(request):
|
||||||
|
|
||||||
handler = handlers.get(activity_type, None)
|
handler = handlers.get(activity_type, None)
|
||||||
if isinstance(handler, dict):
|
if isinstance(handler, dict):
|
||||||
handler = handler.get(activity['object']['type'], None)
|
handler = handler.get(activity_object['type'], None)
|
||||||
|
|
||||||
if not handler:
|
if not handler:
|
||||||
return HttpResponseNotFound()
|
return HttpResponseNotFound()
|
||||||
|
|
|
@ -35,6 +35,7 @@ def construct_search_term(title, author):
|
||||||
|
|
||||||
|
|
||||||
class ImportJob(models.Model):
|
class ImportJob(models.Model):
|
||||||
|
''' entry for a specific request for book data import '''
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
created_date = models.DateTimeField(default=timezone.now)
|
created_date = models.DateTimeField(default=timezone.now)
|
||||||
task_id = models.CharField(max_length=100, null=True)
|
task_id = models.CharField(max_length=100, null=True)
|
||||||
|
@ -42,6 +43,7 @@ class ImportJob(models.Model):
|
||||||
'Status', null=True, on_delete=models.PROTECT)
|
'Status', null=True, on_delete=models.PROTECT)
|
||||||
|
|
||||||
class ImportItem(models.Model):
|
class ImportItem(models.Model):
|
||||||
|
''' a single line of a csv being imported '''
|
||||||
job = models.ForeignKey(
|
job = models.ForeignKey(
|
||||||
ImportJob,
|
ImportJob,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
|
@ -77,6 +79,7 @@ class ImportItem(models.Model):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def isbn(self):
|
def isbn(self):
|
||||||
|
''' pulls out the isbn13 field from the csv line data '''
|
||||||
return unquote_string(self.data['ISBN13'])
|
return unquote_string(self.data['ISBN13'])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -87,24 +90,29 @@ class ImportItem(models.Model):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def review(self):
|
def review(self):
|
||||||
|
''' a user-written review, to be imported with the book data '''
|
||||||
return self.data['My Review']
|
return self.data['My Review']
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def rating(self):
|
def rating(self):
|
||||||
|
''' x/5 star rating for a book '''
|
||||||
return int(self.data['My Rating'])
|
return int(self.data['My Rating'])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def date_added(self):
|
def date_added(self):
|
||||||
|
''' when the book was added to this dataset '''
|
||||||
if self.data['Date Added']:
|
if self.data['Date Added']:
|
||||||
return dateutil.parser.parse(self.data['Date Added'])
|
return dateutil.parser.parse(self.data['Date Added'])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def date_read(self):
|
def date_read(self):
|
||||||
|
''' the date a book was completed '''
|
||||||
if self.data['Date Read']:
|
if self.data['Date Read']:
|
||||||
return dateutil.parser.parse(self.data['Date Read'])
|
return dateutil.parser.parse(self.data['Date Read'])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def reads(self):
|
def reads(self):
|
||||||
|
''' formats a read through dataset for the book in this line '''
|
||||||
if (self.shelf == 'reading'
|
if (self.shelf == 'reading'
|
||||||
and self.date_added and not self.date_read):
|
and self.date_added and not self.date_read):
|
||||||
return [ReadThrough(start_date=self.date_added)]
|
return [ReadThrough(start_date=self.date_added)]
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
|
''' the particulars for this instance of BookWyrm '''
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
from Crypto import Random
|
from Crypto import Random
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
import datetime
|
|
||||||
|
|
||||||
from bookwyrm.settings import DOMAIN
|
from bookwyrm.settings import DOMAIN
|
||||||
from .user import User
|
from .user import User
|
||||||
|
|
||||||
class SiteSettings(models.Model):
|
class SiteSettings(models.Model):
|
||||||
|
''' customized settings for this instance '''
|
||||||
name = models.CharField(default=DOMAIN, max_length=100)
|
name = models.CharField(default=DOMAIN, max_length=100)
|
||||||
instance_description = models.TextField(
|
instance_description = models.TextField(
|
||||||
default="This instance has no description.")
|
default="This instance has no description.")
|
||||||
|
@ -18,6 +19,7 @@ class SiteSettings(models.Model):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get(cls):
|
def get(cls):
|
||||||
|
''' gets the site settings db entry or defaults '''
|
||||||
try:
|
try:
|
||||||
return cls.objects.get(id=1)
|
return cls.objects.get(id=1)
|
||||||
except cls.DoesNotExist:
|
except cls.DoesNotExist:
|
||||||
|
@ -26,9 +28,11 @@ class SiteSettings(models.Model):
|
||||||
return default_settings
|
return default_settings
|
||||||
|
|
||||||
def new_invite_code():
|
def new_invite_code():
|
||||||
|
''' the identifier for a user invite '''
|
||||||
return base64.b32encode(Random.get_random_bytes(5)).decode('ascii')
|
return base64.b32encode(Random.get_random_bytes(5)).decode('ascii')
|
||||||
|
|
||||||
class SiteInvite(models.Model):
|
class SiteInvite(models.Model):
|
||||||
|
''' gives someone access to create an account on the instance '''
|
||||||
code = models.CharField(max_length=32, default=new_invite_code)
|
code = models.CharField(max_length=32, default=new_invite_code)
|
||||||
expiry = models.DateTimeField(blank=True, null=True)
|
expiry = models.DateTimeField(blank=True, null=True)
|
||||||
use_limit = models.IntegerField(blank=True, null=True)
|
use_limit = models.IntegerField(blank=True, null=True)
|
||||||
|
@ -36,10 +40,12 @@ class SiteInvite(models.Model):
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
|
|
||||||
def valid(self):
|
def valid(self):
|
||||||
|
''' make sure it hasn't expired or been used '''
|
||||||
return (
|
return (
|
||||||
(self.expiry is None or self.expiry > timezone.now()) and
|
(self.expiry is None or self.expiry > timezone.now()) and
|
||||||
(self.use_limit is None or self.times_used < self.use_limit))
|
(self.use_limit is None or self.times_used < self.use_limit))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def link(self):
|
def link(self):
|
||||||
|
''' formats the invite link '''
|
||||||
return "https://{}/invite/{}".format(DOMAIN, self.code)
|
return "https://{}/invite/{}".format(DOMAIN, self.code)
|
||||||
|
|
|
@ -2,11 +2,11 @@
|
||||||
from html.parser import HTMLParser
|
from html.parser import HTMLParser
|
||||||
|
|
||||||
class InputHtmlParser(HTMLParser):
|
class InputHtmlParser(HTMLParser):
|
||||||
''' Removes any html that isn't whitelisted from a block '''
|
''' Removes any html that isn't allowed_tagsed from a block '''
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
HTMLParser.__init__(self)
|
HTMLParser.__init__(self)
|
||||||
self.whitelist = ['p', 'b', 'i', 'pre', 'a', 'span']
|
self.allowed_tags = ['p', 'b', 'i', 'pre', 'a', 'span']
|
||||||
self.tag_stack = []
|
self.tag_stack = []
|
||||||
self.output = []
|
self.output = []
|
||||||
# if the html appears invalid, we just won't allow any at all
|
# if the html appears invalid, we just won't allow any at all
|
||||||
|
@ -15,7 +15,7 @@ class InputHtmlParser(HTMLParser):
|
||||||
|
|
||||||
def handle_starttag(self, tag, attrs):
|
def handle_starttag(self, tag, attrs):
|
||||||
''' check if the tag is valid '''
|
''' check if the tag is valid '''
|
||||||
if self.allow_html and tag in self.whitelist:
|
if self.allow_html and tag in self.allowed_tags:
|
||||||
self.output.append(('tag', self.get_starttag_text()))
|
self.output.append(('tag', self.get_starttag_text()))
|
||||||
self.tag_stack.append(tag)
|
self.tag_stack.append(tag)
|
||||||
else:
|
else:
|
||||||
|
@ -24,7 +24,7 @@ class InputHtmlParser(HTMLParser):
|
||||||
|
|
||||||
def handle_endtag(self, tag):
|
def handle_endtag(self, tag):
|
||||||
''' keep the close tag '''
|
''' keep the close tag '''
|
||||||
if not self.allow_html or tag not in self.whitelist:
|
if not self.allow_html or tag not in self.allowed_tags:
|
||||||
self.output.append(('data', ''))
|
self.output.append(('data', ''))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ OL_URL = env('OL_URL')
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
#'django.contrib.admin',
|
'django.contrib.admin',
|
||||||
'django.contrib.auth',
|
'django.contrib.auth',
|
||||||
'django.contrib.contenttypes',
|
'django.contrib.contenttypes',
|
||||||
'django.contrib.sessions',
|
'django.contrib.sessions',
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
''' signs activitypub activities '''
|
||||||
import hashlib
|
import hashlib
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
import datetime
|
import datetime
|
||||||
|
@ -11,6 +12,7 @@ from Crypto.Hash import SHA256
|
||||||
MAX_SIGNATURE_AGE = 300
|
MAX_SIGNATURE_AGE = 300
|
||||||
|
|
||||||
def create_key_pair():
|
def create_key_pair():
|
||||||
|
''' a new public/private key pair, used for creating new users '''
|
||||||
random_generator = Random.new().read
|
random_generator = Random.new().read
|
||||||
key = RSA.generate(1024, random_generator)
|
key = RSA.generate(1024, random_generator)
|
||||||
private_key = key.export_key().decode('utf8')
|
private_key = key.export_key().decode('utf8')
|
||||||
|
@ -20,6 +22,7 @@ def create_key_pair():
|
||||||
|
|
||||||
|
|
||||||
def make_signature(sender, destination, date, digest):
|
def make_signature(sender, destination, date, digest):
|
||||||
|
''' uses a private key to sign an outgoing message '''
|
||||||
inbox_parts = urlparse(destination)
|
inbox_parts = urlparse(destination)
|
||||||
signature_headers = [
|
signature_headers = [
|
||||||
'(request-target): post %s' % inbox_parts.path,
|
'(request-target): post %s' % inbox_parts.path,
|
||||||
|
@ -38,10 +41,15 @@ def make_signature(sender, destination, date, digest):
|
||||||
}
|
}
|
||||||
return ','.join('%s="%s"' % (k, v) for (k, v) in signature.items())
|
return ','.join('%s="%s"' % (k, v) for (k, v) in signature.items())
|
||||||
|
|
||||||
|
|
||||||
def make_digest(data):
|
def make_digest(data):
|
||||||
return 'SHA-256=' + b64encode(hashlib.sha256(data.encode('utf-8')).digest()).decode('utf-8')
|
''' creates a message digest for signing '''
|
||||||
|
return 'SHA-256=' + b64encode(hashlib.sha256(data.encode('utf-8'))\
|
||||||
|
.digest()).decode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
def verify_digest(request):
|
def verify_digest(request):
|
||||||
|
''' checks if a digest is syntactically valid and matches the message '''
|
||||||
algorithm, digest = request.headers['digest'].split('=', 1)
|
algorithm, digest = request.headers['digest'].split('=', 1)
|
||||||
if algorithm == 'SHA-256':
|
if algorithm == 'SHA-256':
|
||||||
hash_function = hashlib.sha256
|
hash_function = hashlib.sha256
|
||||||
|
@ -55,6 +63,7 @@ def verify_digest(request):
|
||||||
raise ValueError("Invalid HTTP Digest header")
|
raise ValueError("Invalid HTTP Digest header")
|
||||||
|
|
||||||
class Signature:
|
class Signature:
|
||||||
|
''' read and validate incoming signatures '''
|
||||||
def __init__(self, key_id, headers, signature):
|
def __init__(self, key_id, headers, signature):
|
||||||
self.key_id = key_id
|
self.key_id = key_id
|
||||||
self.headers = headers
|
self.headers = headers
|
||||||
|
@ -62,6 +71,7 @@ class Signature:
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def parse(cls, request):
|
def parse(cls, request):
|
||||||
|
''' extract and parse a signature from an http request '''
|
||||||
signature_dict = {}
|
signature_dict = {}
|
||||||
for pair in request.headers['Signature'].split(','):
|
for pair in request.headers['Signature'].split(','):
|
||||||
k, v = pair.split('=', 1)
|
k, v = pair.split('=', 1)
|
||||||
|
@ -105,7 +115,9 @@ class Signature:
|
||||||
# raises a ValueError if it fails
|
# raises a ValueError if it fails
|
||||||
signer.verify(digest, self.signature)
|
signer.verify(digest, self.signature)
|
||||||
|
|
||||||
|
|
||||||
def http_date_age(datestr):
|
def http_date_age(datestr):
|
||||||
|
''' age of a signature in seconds '''
|
||||||
parsed = datetime.datetime.strptime(datestr, '%a, %d %b %Y %H:%M:%S GMT')
|
parsed = datetime.datetime.strptime(datestr, '%a, %d %b %Y %H:%M:%S GMT')
|
||||||
delta = datetime.datetime.utcnow() - parsed
|
delta = datetime.datetime.utcnow() - parsed
|
||||||
return delta.total_seconds()
|
return delta.total_seconds()
|
||||||
|
|
|
@ -122,7 +122,7 @@ def get_boosted(boost):
|
||||||
def get_edition_info(book):
|
def get_edition_info(book):
|
||||||
''' paperback, French language, 1982 '''
|
''' paperback, French language, 1982 '''
|
||||||
if not book:
|
if not book:
|
||||||
return
|
return ''
|
||||||
items = [
|
items = [
|
||||||
book.physical_format if isinstance(book, models.Edition) else None,
|
book.physical_format if isinstance(book, models.Edition) else None,
|
||||||
book.languages[0] + ' language' if book.languages and \
|
book.languages[0] + ' language' if book.languages and \
|
||||||
|
@ -184,6 +184,7 @@ def current_shelf(context, book):
|
||||||
|
|
||||||
@register.simple_tag(takes_context=False)
|
@register.simple_tag(takes_context=False)
|
||||||
def latest_read_through(book, user):
|
def latest_read_through(book, user):
|
||||||
|
''' the most recent read activity '''
|
||||||
return models.ReadThrough.objects.filter(
|
return models.ReadThrough.objects.filter(
|
||||||
user=user,
|
user=user,
|
||||||
book=book).order_by('-created_date').first()
|
book=book).order_by('-created_date').first()
|
||||||
|
|
|
@ -7,7 +7,7 @@ from bookwyrm.connectors.abstract_connector import Mapping,\
|
||||||
from bookwyrm.connectors.bookwyrm_connector import Connector
|
from bookwyrm.connectors.bookwyrm_connector import Connector
|
||||||
|
|
||||||
|
|
||||||
class BookWyrmConnector(TestCase):
|
class AbstractConnector(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.book = models.Edition.objects.create(title='Example Edition')
|
self.book = models.Edition.objects.create(title='Example Edition')
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from bookwyrm import models, broadcast
|
from bookwyrm import models, broadcast
|
||||||
from bookwyrm.settings import DOMAIN
|
|
||||||
|
|
||||||
|
|
||||||
class Book(TestCase):
|
class Book(TestCase):
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
''' url routing for the app and api '''
|
''' url routing for the app and api '''
|
||||||
from django.conf.urls.static import static
|
from django.conf.urls.static import static
|
||||||
#from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import path, re_path
|
from django.urls import path, re_path
|
||||||
|
|
||||||
from bookwyrm import incoming, outgoing, views, settings, wellknown
|
from bookwyrm import incoming, outgoing, views, settings, wellknown
|
||||||
|
@ -20,7 +20,7 @@ book_path = r'^book/(?P<book_id>\d+)'
|
||||||
handler404 = 'bookwyrm.views.not_found_page'
|
handler404 = 'bookwyrm.views.not_found_page'
|
||||||
handler500 = 'bookwyrm.views.server_error_page'
|
handler500 = 'bookwyrm.views.server_error_page'
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
|
|
||||||
# federation endpoints
|
# federation endpoints
|
||||||
re_path(r'^inbox/?$', incoming.shared_inbox),
|
re_path(r'^inbox/?$', incoming.shared_inbox),
|
||||||
|
@ -66,15 +66,18 @@ urlpatterns = [
|
||||||
|
|
||||||
# books
|
# books
|
||||||
re_path(r'%s(.json)?/?$' % book_path, views.book_page),
|
re_path(r'%s(.json)?/?$' % book_path, views.book_page),
|
||||||
re_path(r'%s/(?P<tab>friends|local|federated)?$' % book_path, views.book_page),
|
re_path(r'%s/(?P<tab>friends|local|federated)?$' % \
|
||||||
|
book_path, views.book_page),
|
||||||
re_path(r'%s/edit/?$' % book_path, views.edit_book_page),
|
re_path(r'%s/edit/?$' % book_path, views.edit_book_page),
|
||||||
re_path(r'^editions/(?P<work_id>\d+)/?$', views.editions_page),
|
re_path(r'^editions/(?P<work_id>\d+)/?$', views.editions_page),
|
||||||
|
|
||||||
re_path(r'^author/(?P<author_id>[\w\-]+)(.json)?/?$', views.author_page),
|
re_path(r'^author/(?P<author_id>[\w\-]+)(.json)?/?$', views.author_page),
|
||||||
# TODO: tag needs a .json path
|
# TODO: tag needs a .json path
|
||||||
re_path(r'^tag/(?P<tag_id>.+)/?$', views.tag_page),
|
re_path(r'^tag/(?P<tag_id>.+)/?$', views.tag_page),
|
||||||
re_path(r'^%s/shelf/(?P<shelf_identifier>[\w-]+)(.json)?/?$' % user_path, views.shelf_page),
|
re_path(r'^%s/shelf/(?P<shelf_identifier>[\w-]+)(.json)?/?$' % \
|
||||||
re_path(r'^%s/shelf/(?P<shelf_identifier>[\w-]+)(.json)?/?$' % local_user_path, views.shelf_page),
|
user_path, views.shelf_page),
|
||||||
|
re_path(r'^%s/shelf/(?P<shelf_identifier>[\w-]+)(.json)?/?$' % \
|
||||||
|
local_user_path, views.shelf_page),
|
||||||
|
|
||||||
re_path(r'^search/?$', views.search),
|
re_path(r'^search/?$', views.search),
|
||||||
|
|
||||||
|
|
|
@ -435,6 +435,7 @@ def import_data(request):
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def create_invite(request):
|
def create_invite(request):
|
||||||
|
''' creates a user invite database entry '''
|
||||||
form = forms.CreateInviteForm(request.POST)
|
form = forms.CreateInviteForm(request.POST)
|
||||||
if not form.is_valid():
|
if not form.is_valid():
|
||||||
return HttpResponseBadRequest("ERRORS : %s" % (form.errors,))
|
return HttpResponseBadRequest("ERRORS : %s" % (form.errors,))
|
||||||
|
|
|
@ -6,5 +6,3 @@ from __future__ import absolute_import, unicode_literals
|
||||||
from .celery import app as celery_app
|
from .celery import app as celery_app
|
||||||
|
|
||||||
__all__ = ('celery_app',)
|
__all__ = ('celery_app',)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
''' configures celery for task management '''
|
||||||
from __future__ import absolute_import, unicode_literals
|
from __future__ import absolute_import, unicode_literals
|
||||||
from . import settings
|
from . import settings
|
||||||
|
|
||||||
|
@ -22,4 +23,3 @@ app.autodiscover_tasks(['bookwyrm'], related_name='incoming')
|
||||||
app.autodiscover_tasks(['bookwyrm'], related_name='broadcast')
|
app.autodiscover_tasks(['bookwyrm'], related_name='broadcast')
|
||||||
app.autodiscover_tasks(['bookwyrm'], related_name='books_manager')
|
app.autodiscover_tasks(['bookwyrm'], related_name='books_manager')
|
||||||
app.autodiscover_tasks(['bookwyrm'], related_name='goodreads_import')
|
app.autodiscover_tasks(['bookwyrm'], related_name='goodreads_import')
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue