Fixes linter issues

This commit is contained in:
Mouse Reeve 2020-09-21 10:25:26 -07:00
parent 4d0176a6f7
commit 425da16fd6
16 changed files with 63 additions and 42 deletions

View file

@ -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):

View file

@ -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'),
) )

View file

@ -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):

View file

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

View file

@ -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()

View file

@ -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)]

View file

@ -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)

View file

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

View file

@ -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()

View file

@ -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()

View file

@ -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')

View file

@ -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):

View file

@ -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),

View file

@ -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,))

View file

@ -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',)

View file

@ -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')