diff --git a/bookwyrm/connectors/abstract_connector.py b/bookwyrm/connectors/abstract_connector.py index 486ef394..1137507c 100644 --- a/bookwyrm/connectors/abstract_connector.py +++ b/bookwyrm/connectors/abstract_connector.py @@ -162,7 +162,7 @@ class AbstractConnector(ABC): def update_book(self, book, data=None): ''' load new data ''' if not book.sync and not book.sync_cover: - return + return book if not data: key = getattr(book, self.key_name) @@ -286,7 +286,7 @@ def get_data(url): return data -class SearchResult(object): +class SearchResult: ''' standardized search result object ''' def __init__(self, title, key, author, year): self.title = title @@ -299,7 +299,7 @@ class SearchResult(object): self.key, self.title, self.author) -class Mapping(object): +class Mapping: ''' associate a local database field with a field in an external dataset ''' def __init__( self, local_field, remote_field=None, formatter=None, model=None): diff --git a/bookwyrm/connectors/openlibrary.py b/bookwyrm/connectors/openlibrary.py index f776fa27..d74735d2 100644 --- a/bookwyrm/connectors/openlibrary.py +++ b/bookwyrm/connectors/openlibrary.py @@ -123,15 +123,15 @@ class Connector(AbstractConnector): 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 - key = self.books_url + doc['key'] - author = doc.get('author_name') or ['Unknown'] + key = self.books_url + search_result['key'] + author = search_result.get('author_name') or ['Unknown'] return SearchResult( - doc.get('title'), + search_result.get('title'), key, ', '.join(author), - doc.get('first_publish_year'), + search_result.get('first_publish_year'), ) diff --git a/bookwyrm/connectors/self_connector.py b/bookwyrm/connectors/self_connector.py index 48b49a6d..e1317597 100644 --- a/bookwyrm/connectors/self_connector.py +++ b/bookwyrm/connectors/self_connector.py @@ -7,10 +7,6 @@ from .abstract_connector import AbstractConnector, SearchResult class Connector(AbstractConnector): ''' instantiate a connector ''' - def __init__(self, identifier): - super().__init__(identifier) - - def search(self, query): ''' right now you can't search bookwyrm sorry, but when that gets implemented it will totally rule ''' @@ -44,18 +40,18 @@ class Connector(AbstractConnector): return search_results - def format_search_result(self, book): + def format_search_result(self, search_result): return SearchResult( - book.title, - book.local_id, - book.author_text, - book.published_date.year if book.published_date else None, + search_result.title, + search_result.local_id, + search_result.author_text, + search_result.published_date.year if \ + search_result.published_date else None, ) def get_or_create_book(self, remote_id): ''' this COULD be semi-implemented but I think it shouldn't be used ''' - pass def is_work_data(self, data): diff --git a/bookwyrm/forms.py b/bookwyrm/forms.py index db9a1b69..0419756a 100644 --- a/bookwyrm/forms.py +++ b/bookwyrm/forms.py @@ -1,7 +1,6 @@ -''' usin django model forms ''' +''' using django model forms ''' import datetime -from django.core.exceptions import ValidationError from django.forms import ModelForm, PasswordInput, widgets from django import forms diff --git a/bookwyrm/incoming.py b/bookwyrm/incoming.py index dc6613f2..144400f9 100644 --- a/bookwyrm/incoming.py +++ b/bookwyrm/incoming.py @@ -38,10 +38,8 @@ def shared_inbox(request): try: activity = json.loads(request.body) - except json.decoder.JSONDecodeError: - return HttpResponseBadRequest() - - if not activity.get('object'): + activity_object = activity['object'] + except (json.decoder.JSONDecodeError, KeyError): return HttpResponseBadRequest() if not has_valid_signature(request, activity): @@ -74,7 +72,7 @@ def shared_inbox(request): handler = handlers.get(activity_type, None) if isinstance(handler, dict): - handler = handler.get(activity['object']['type'], None) + handler = handler.get(activity_object['type'], None) if not handler: return HttpResponseNotFound() diff --git a/bookwyrm/models/import_job.py b/bookwyrm/models/import_job.py index c7cea837..b2279bc9 100644 --- a/bookwyrm/models/import_job.py +++ b/bookwyrm/models/import_job.py @@ -35,6 +35,7 @@ def construct_search_term(title, author): class ImportJob(models.Model): + ''' entry for a specific request for book data import ''' user = models.ForeignKey(User, on_delete=models.CASCADE) created_date = models.DateTimeField(default=timezone.now) task_id = models.CharField(max_length=100, null=True) @@ -42,6 +43,7 @@ class ImportJob(models.Model): 'Status', null=True, on_delete=models.PROTECT) class ImportItem(models.Model): + ''' a single line of a csv being imported ''' job = models.ForeignKey( ImportJob, on_delete=models.CASCADE, @@ -77,6 +79,7 @@ class ImportItem(models.Model): @property def isbn(self): + ''' pulls out the isbn13 field from the csv line data ''' return unquote_string(self.data['ISBN13']) @property @@ -87,24 +90,29 @@ class ImportItem(models.Model): @property def review(self): + ''' a user-written review, to be imported with the book data ''' return self.data['My Review'] @property def rating(self): + ''' x/5 star rating for a book ''' return int(self.data['My Rating']) @property def date_added(self): + ''' when the book was added to this dataset ''' if self.data['Date Added']: return dateutil.parser.parse(self.data['Date Added']) @property def date_read(self): + ''' the date a book was completed ''' if self.data['Date Read']: return dateutil.parser.parse(self.data['Date Read']) @property def reads(self): + ''' formats a read through dataset for the book in this line ''' if (self.shelf == 'reading' and self.date_added and not self.date_read): return [ReadThrough(start_date=self.date_added)] diff --git a/bookwyrm/models/site.py b/bookwyrm/models/site.py index 3f7f15f3..1472ab25 100644 --- a/bookwyrm/models/site.py +++ b/bookwyrm/models/site.py @@ -1,14 +1,15 @@ +''' the particulars for this instance of BookWyrm ''' import base64 from Crypto import Random from django.db import models from django.utils import timezone -import datetime from bookwyrm.settings import DOMAIN from .user import User class SiteSettings(models.Model): + ''' customized settings for this instance ''' name = models.CharField(default=DOMAIN, max_length=100) instance_description = models.TextField( default="This instance has no description.") @@ -18,6 +19,7 @@ class SiteSettings(models.Model): @classmethod def get(cls): + ''' gets the site settings db entry or defaults ''' try: return cls.objects.get(id=1) except cls.DoesNotExist: @@ -26,9 +28,11 @@ class SiteSettings(models.Model): return default_settings def new_invite_code(): + ''' the identifier for a user invite ''' return base64.b32encode(Random.get_random_bytes(5)).decode('ascii') class SiteInvite(models.Model): + ''' gives someone access to create an account on the instance ''' code = models.CharField(max_length=32, default=new_invite_code) expiry = models.DateTimeField(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) def valid(self): + ''' make sure it hasn't expired or been used ''' return ( (self.expiry is None or self.expiry > timezone.now()) and (self.use_limit is None or self.times_used < self.use_limit)) @property def link(self): + ''' formats the invite link ''' return "https://{}/invite/{}".format(DOMAIN, self.code) diff --git a/bookwyrm/sanitize_html.py b/bookwyrm/sanitize_html.py index 70f63ed2..9c5ca73a 100644 --- a/bookwyrm/sanitize_html.py +++ b/bookwyrm/sanitize_html.py @@ -2,11 +2,11 @@ from html.parser import 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): HTMLParser.__init__(self) - self.whitelist = ['p', 'b', 'i', 'pre', 'a', 'span'] + self.allowed_tags = ['p', 'b', 'i', 'pre', 'a', 'span'] self.tag_stack = [] self.output = [] # 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): ''' 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.tag_stack.append(tag) else: @@ -24,7 +24,7 @@ class InputHtmlParser(HTMLParser): def handle_endtag(self, 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', '')) return diff --git a/bookwyrm/settings.py b/bookwyrm/settings.py index 8d1276c7..9faba08a 100644 --- a/bookwyrm/settings.py +++ b/bookwyrm/settings.py @@ -31,7 +31,7 @@ OL_URL = env('OL_URL') # Application definition INSTALLED_APPS = [ - #'django.contrib.admin', + 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', diff --git a/bookwyrm/signatures.py b/bookwyrm/signatures.py index c68381a9..0aa74918 100644 --- a/bookwyrm/signatures.py +++ b/bookwyrm/signatures.py @@ -1,3 +1,4 @@ +''' signs activitypub activities ''' import hashlib from urllib.parse import urlparse import datetime @@ -11,6 +12,7 @@ from Crypto.Hash import SHA256 MAX_SIGNATURE_AGE = 300 def create_key_pair(): + ''' a new public/private key pair, used for creating new users ''' random_generator = Random.new().read key = RSA.generate(1024, random_generator) private_key = key.export_key().decode('utf8') @@ -20,6 +22,7 @@ def create_key_pair(): def make_signature(sender, destination, date, digest): + ''' uses a private key to sign an outgoing message ''' inbox_parts = urlparse(destination) signature_headers = [ '(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()) + 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): + ''' checks if a digest is syntactically valid and matches the message ''' algorithm, digest = request.headers['digest'].split('=', 1) if algorithm == 'SHA-256': hash_function = hashlib.sha256 @@ -55,6 +63,7 @@ def verify_digest(request): raise ValueError("Invalid HTTP Digest header") class Signature: + ''' read and validate incoming signatures ''' def __init__(self, key_id, headers, signature): self.key_id = key_id self.headers = headers @@ -62,6 +71,7 @@ class Signature: @classmethod def parse(cls, request): + ''' extract and parse a signature from an http request ''' signature_dict = {} for pair in request.headers['Signature'].split(','): k, v = pair.split('=', 1) @@ -105,7 +115,9 @@ class Signature: # raises a ValueError if it fails signer.verify(digest, self.signature) + def http_date_age(datestr): + ''' age of a signature in seconds ''' parsed = datetime.datetime.strptime(datestr, '%a, %d %b %Y %H:%M:%S GMT') delta = datetime.datetime.utcnow() - parsed return delta.total_seconds() diff --git a/bookwyrm/templatetags/fr_display.py b/bookwyrm/templatetags/fr_display.py index 578ee13e..cf4e46bb 100644 --- a/bookwyrm/templatetags/fr_display.py +++ b/bookwyrm/templatetags/fr_display.py @@ -122,7 +122,7 @@ def get_boosted(boost): def get_edition_info(book): ''' paperback, French language, 1982 ''' if not book: - return + return '' items = [ book.physical_format if isinstance(book, models.Edition) else None, book.languages[0] + ' language' if book.languages and \ @@ -184,6 +184,7 @@ def current_shelf(context, book): @register.simple_tag(takes_context=False) def latest_read_through(book, user): + ''' the most recent read activity ''' return models.ReadThrough.objects.filter( user=user, book=book).order_by('-created_date').first() diff --git a/bookwyrm/tests/connectors/test_abstract_connector.py b/bookwyrm/tests/connectors/test_abstract_connector.py index a7ec9dcb..f4af8a1a 100644 --- a/bookwyrm/tests/connectors/test_abstract_connector.py +++ b/bookwyrm/tests/connectors/test_abstract_connector.py @@ -7,7 +7,7 @@ from bookwyrm.connectors.abstract_connector import Mapping,\ from bookwyrm.connectors.bookwyrm_connector import Connector -class BookWyrmConnector(TestCase): +class AbstractConnector(TestCase): def setUp(self): self.book = models.Edition.objects.create(title='Example Edition') diff --git a/bookwyrm/tests/test_broadcast.py b/bookwyrm/tests/test_broadcast.py index 36e74aba..1112b3fa 100644 --- a/bookwyrm/tests/test_broadcast.py +++ b/bookwyrm/tests/test_broadcast.py @@ -1,7 +1,6 @@ from django.test import TestCase from bookwyrm import models, broadcast -from bookwyrm.settings import DOMAIN class Book(TestCase): diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index e46c8d2c..9f20e233 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -1,6 +1,6 @@ ''' url routing for the app and api ''' 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 bookwyrm import incoming, outgoing, views, settings, wellknown @@ -20,7 +20,7 @@ book_path = r'^book/(?P\d+)' handler404 = 'bookwyrm.views.not_found_page' handler500 = 'bookwyrm.views.server_error_page' urlpatterns = [ -# path('admin/', admin.site.urls), + path('admin/', admin.site.urls), # federation endpoints re_path(r'^inbox/?$', incoming.shared_inbox), @@ -66,15 +66,18 @@ urlpatterns = [ # books re_path(r'%s(.json)?/?$' % book_path, views.book_page), - re_path(r'%s/(?Pfriends|local|federated)?$' % book_path, views.book_page), + re_path(r'%s/(?Pfriends|local|federated)?$' % \ + book_path, views.book_page), re_path(r'%s/edit/?$' % book_path, views.edit_book_page), re_path(r'^editions/(?P\d+)/?$', views.editions_page), re_path(r'^author/(?P[\w\-]+)(.json)?/?$', views.author_page), # TODO: tag needs a .json path re_path(r'^tag/(?P.+)/?$', views.tag_page), - re_path(r'^%s/shelf/(?P[\w-]+)(.json)?/?$' % user_path, views.shelf_page), - re_path(r'^%s/shelf/(?P[\w-]+)(.json)?/?$' % local_user_path, views.shelf_page), + re_path(r'^%s/shelf/(?P[\w-]+)(.json)?/?$' % \ + user_path, views.shelf_page), + re_path(r'^%s/shelf/(?P[\w-]+)(.json)?/?$' % \ + local_user_path, views.shelf_page), re_path(r'^search/?$', views.search), diff --git a/bookwyrm/view_actions.py b/bookwyrm/view_actions.py index c06e3d9f..ad07eaf4 100644 --- a/bookwyrm/view_actions.py +++ b/bookwyrm/view_actions.py @@ -435,6 +435,7 @@ def import_data(request): @login_required def create_invite(request): + ''' creates a user invite database entry ''' form = forms.CreateInviteForm(request.POST) if not form.is_valid(): return HttpResponseBadRequest("ERRORS : %s" % (form.errors,)) diff --git a/celerywyrm/__init__.py b/celerywyrm/__init__.py index 3e6ab9e5..1c8fa15d 100644 --- a/celerywyrm/__init__.py +++ b/celerywyrm/__init__.py @@ -6,5 +6,3 @@ from __future__ import absolute_import, unicode_literals from .celery import app as celery_app __all__ = ('celery_app',) - - diff --git a/celerywyrm/celery.py b/celerywyrm/celery.py index cae546a3..f566bbed 100644 --- a/celerywyrm/celery.py +++ b/celerywyrm/celery.py @@ -1,3 +1,4 @@ +''' configures celery for task management ''' from __future__ import absolute_import, unicode_literals 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='books_manager') app.autodiscover_tasks(['bookwyrm'], related_name='goodreads_import') - diff --git a/fr-dev b/fr-dev index 8fad19dd..15dddd2c 100755 --- a/fr-dev +++ b/fr-dev @@ -45,6 +45,6 @@ case "$1" in docker-compose exec web coverage report ;; *) - echo "Unrecognised command. Try: up, initdb, resetdb,makemigrations, migrate, shell, dbshell, restart_celery, test, test_report" + echo "Unrecognised command. Try: up, initdb, resetdb, makemigrations, migrate, shell, dbshell, restart_celery, test, test_report" ;; esac