Merge branch 'main' into frontend-book-cover

This commit is contained in:
Fabien Basmaison 2021-04-27 09:16:10 +02:00
commit 56d821970a
163 changed files with 11795 additions and 4501 deletions

View file

@ -27,5 +27,5 @@ activity_objects = {c[0]: c[1] for c in cls_members if hasattr(c[1], "to_model")
def parse(activity_json):
""" figure out what activity this is and parse it """
"""figure out what activity this is and parse it"""
return naive_parse(activity_objects, activity_json)

View file

@ -10,11 +10,11 @@ from bookwyrm.tasks import app
class ActivitySerializerError(ValueError):
""" routine problems serializing activitypub json """
"""routine problems serializing activitypub json"""
class ActivityEncoder(JSONEncoder):
""" used to convert an Activity object into json """
"""used to convert an Activity object into json"""
def default(self, o):
return o.__dict__
@ -22,7 +22,7 @@ class ActivityEncoder(JSONEncoder):
@dataclass
class Link:
""" for tagging a book in a status """
"""for tagging a book in a status"""
href: str
name: str
@ -31,14 +31,14 @@ class Link:
@dataclass
class Mention(Link):
""" a subtype of Link for mentioning an actor """
"""a subtype of Link for mentioning an actor"""
type: str = "Mention"
@dataclass
class Signature:
""" public key block """
"""public key block"""
creator: str
created: str
@ -47,7 +47,7 @@ class Signature:
def naive_parse(activity_objects, activity_json, serializer=None):
""" this navigates circular import issues """
"""this navigates circular import issues"""
if not serializer:
if activity_json.get("publicKeyPem"):
# ugh
@ -67,7 +67,7 @@ def naive_parse(activity_objects, activity_json, serializer=None):
@dataclass(init=False)
class ActivityObject:
""" actor activitypub json """
"""actor activitypub json"""
id: str
type: str
@ -106,7 +106,7 @@ class ActivityObject:
setattr(self, field.name, value)
def to_model(self, model=None, instance=None, allow_create=True, save=True):
""" convert from an activity to a model instance """
"""convert from an activity to a model instance"""
model = model or get_model_from_type(self.type)
# only reject statuses if we're potentially creating them
@ -181,7 +181,7 @@ class ActivityObject:
return instance
def serialize(self):
""" convert to dictionary with context attr """
"""convert to dictionary with context attr"""
data = self.__dict__.copy()
# recursively serialize
for (k, v) in data.items():
@ -200,7 +200,7 @@ class ActivityObject:
def set_related_field(
model_name, origin_model_name, related_field_name, related_remote_id, data
):
""" load reverse related fields (editions, attachments) without blocking """
"""load reverse related fields (editions, attachments) without blocking"""
model = apps.get_model("bookwyrm.%s" % model_name, require_ready=True)
origin_model = apps.get_model("bookwyrm.%s" % origin_model_name, require_ready=True)
@ -236,7 +236,7 @@ def set_related_field(
def get_model_from_type(activity_type):
""" given the activity, what type of model """
"""given the activity, what type of model"""
models = apps.get_models()
model = [
m
@ -255,7 +255,7 @@ def get_model_from_type(activity_type):
def resolve_remote_id(
remote_id, model=None, refresh=False, save=True, get_activity=False
):
""" take a remote_id and return an instance, creating if necessary """
"""take a remote_id and return an instance, creating if necessary"""
if model: # a bonus check we can do if we already know the model
result = model.find_existing_by_remote_id(remote_id)
if result and not refresh:

View file

@ -8,7 +8,7 @@ from .image import Document
@dataclass(init=False)
class Book(ActivityObject):
""" serializes an edition or work, abstract """
"""serializes an edition or work, abstract"""
title: str
lastEditedBy: str = None
@ -35,7 +35,7 @@ class Book(ActivityObject):
@dataclass(init=False)
class Edition(Book):
""" Edition instance of a book object """
"""Edition instance of a book object"""
work: str
isbn10: str = ""
@ -52,7 +52,7 @@ class Edition(Book):
@dataclass(init=False)
class Work(Book):
""" work instance of a book object """
"""work instance of a book object"""
lccn: str = ""
defaultEdition: str = ""
@ -62,7 +62,7 @@ class Work(Book):
@dataclass(init=False)
class Author(ActivityObject):
""" author of a book """
"""author of a book"""
name: str
lastEditedBy: str = None

View file

@ -5,7 +5,7 @@ from .base_activity import ActivityObject
@dataclass(init=False)
class Document(ActivityObject):
""" a document """
"""a document"""
url: str
name: str = ""
@ -15,6 +15,6 @@ class Document(ActivityObject):
@dataclass(init=False)
class Image(Document):
""" an image """
"""an image"""
type: str = "Image"

View file

@ -9,19 +9,19 @@ from .image import Document
@dataclass(init=False)
class Tombstone(ActivityObject):
""" the placeholder for a deleted status """
"""the placeholder for a deleted status"""
type: str = "Tombstone"
def to_model(self, *args, **kwargs): # pylint: disable=unused-argument
""" this should never really get serialized, just searched for """
"""this should never really get serialized, just searched for"""
model = apps.get_model("bookwyrm.Status")
return model.find_existing_by_remote_id(self.id)
@dataclass(init=False)
class Note(ActivityObject):
""" Note activity """
"""Note activity"""
published: str
attributedTo: str
@ -39,7 +39,7 @@ class Note(ActivityObject):
@dataclass(init=False)
class Article(Note):
""" what's an article except a note with more fields """
"""what's an article except a note with more fields"""
name: str
type: str = "Article"
@ -47,14 +47,14 @@ class Article(Note):
@dataclass(init=False)
class GeneratedNote(Note):
""" just a re-typed note """
"""just a re-typed note"""
type: str = "GeneratedNote"
@dataclass(init=False)
class Comment(Note):
""" like a note but with a book """
"""like a note but with a book"""
inReplyToBook: str
type: str = "Comment"
@ -62,7 +62,7 @@ class Comment(Note):
@dataclass(init=False)
class Quotation(Comment):
""" a quote and commentary on a book """
"""a quote and commentary on a book"""
quote: str
type: str = "Quotation"
@ -70,7 +70,7 @@ class Quotation(Comment):
@dataclass(init=False)
class Review(Comment):
""" a full book review """
"""a full book review"""
name: str = None
rating: int = None
@ -79,7 +79,7 @@ class Review(Comment):
@dataclass(init=False)
class Rating(Comment):
""" just a star rating """
"""just a star rating"""
rating: int
content: str = None

View file

@ -7,7 +7,7 @@ from .base_activity import ActivityObject
@dataclass(init=False)
class OrderedCollection(ActivityObject):
""" structure of an ordered collection activity """
"""structure of an ordered collection activity"""
totalItems: int
first: str
@ -19,7 +19,7 @@ class OrderedCollection(ActivityObject):
@dataclass(init=False)
class OrderedCollectionPrivate(OrderedCollection):
""" an ordered collection with privacy settings """
"""an ordered collection with privacy settings"""
to: List[str] = field(default_factory=lambda: [])
cc: List[str] = field(default_factory=lambda: [])
@ -27,14 +27,14 @@ class OrderedCollectionPrivate(OrderedCollection):
@dataclass(init=False)
class Shelf(OrderedCollectionPrivate):
""" structure of an ordered collection activity """
"""structure of an ordered collection activity"""
type: str = "Shelf"
@dataclass(init=False)
class BookList(OrderedCollectionPrivate):
""" structure of an ordered collection activity """
"""structure of an ordered collection activity"""
summary: str = None
curation: str = "closed"
@ -43,7 +43,7 @@ class BookList(OrderedCollectionPrivate):
@dataclass(init=False)
class OrderedCollectionPage(ActivityObject):
""" structure of an ordered collection activity """
"""structure of an ordered collection activity"""
partOf: str
orderedItems: List
@ -54,7 +54,7 @@ class OrderedCollectionPage(ActivityObject):
@dataclass(init=False)
class CollectionItem(ActivityObject):
""" an item in a collection """
"""an item in a collection"""
actor: str
type: str = "CollectionItem"
@ -62,7 +62,7 @@ class CollectionItem(ActivityObject):
@dataclass(init=False)
class ListItem(CollectionItem):
""" a book on a list """
"""a book on a list"""
book: str
notes: str = None
@ -73,7 +73,7 @@ class ListItem(CollectionItem):
@dataclass(init=False)
class ShelfItem(CollectionItem):
""" a book on a list """
"""a book on a list"""
book: str
type: str = "ShelfItem"

View file

@ -8,7 +8,7 @@ from .image import Image
@dataclass(init=False)
class PublicKey(ActivityObject):
""" public key block """
"""public key block"""
owner: str
publicKeyPem: str
@ -17,7 +17,7 @@ class PublicKey(ActivityObject):
@dataclass(init=False)
class Person(ActivityObject):
""" actor activitypub json """
"""actor activitypub json"""
preferredUsername: str
inbox: str

View file

@ -9,13 +9,13 @@ from .ordered_collection import CollectionItem
@dataclass(init=False)
class Verb(ActivityObject):
"""generic fields for activities """
"""generic fields for activities"""
actor: str
object: ActivityObject
def action(self):
""" usually we just want to update and save """
"""usually we just want to update and save"""
# self.object may return None if the object is invalid in an expected way
# ie, Question type
if self.object:
@ -24,7 +24,7 @@ class Verb(ActivityObject):
@dataclass(init=False)
class Create(Verb):
""" Create activity """
"""Create activity"""
to: List[str]
cc: List[str] = field(default_factory=lambda: [])
@ -34,14 +34,14 @@ class Create(Verb):
@dataclass(init=False)
class Delete(Verb):
""" Create activity """
"""Create activity"""
to: List[str]
cc: List[str] = field(default_factory=lambda: [])
type: str = "Delete"
def action(self):
""" find and delete the activity object """
"""find and delete the activity object"""
if not self.object:
return
@ -59,25 +59,25 @@ class Delete(Verb):
@dataclass(init=False)
class Update(Verb):
""" Update activity """
"""Update activity"""
to: List[str]
type: str = "Update"
def action(self):
""" update a model instance from the dataclass """
"""update a model instance from the dataclass"""
if self.object:
self.object.to_model(allow_create=False)
@dataclass(init=False)
class Undo(Verb):
""" Undo an activity """
"""Undo an activity"""
type: str = "Undo"
def action(self):
""" find and remove the activity object """
"""find and remove the activity object"""
if isinstance(self.object, str):
# it may be that sometihng should be done with these, but idk what
# this seems just to be coming from pleroma
@ -103,64 +103,64 @@ class Undo(Verb):
@dataclass(init=False)
class Follow(Verb):
""" Follow activity """
"""Follow activity"""
object: str
type: str = "Follow"
def action(self):
""" relationship save """
"""relationship save"""
self.to_model()
@dataclass(init=False)
class Block(Verb):
""" Block activity """
"""Block activity"""
object: str
type: str = "Block"
def action(self):
""" relationship save """
"""relationship save"""
self.to_model()
@dataclass(init=False)
class Accept(Verb):
""" Accept activity """
"""Accept activity"""
object: Follow
type: str = "Accept"
def action(self):
""" find and remove the activity object """
"""find and remove the activity object"""
obj = self.object.to_model(save=False, allow_create=False)
obj.accept()
@dataclass(init=False)
class Reject(Verb):
""" Reject activity """
"""Reject activity"""
object: Follow
type: str = "Reject"
def action(self):
""" find and remove the activity object """
"""find and remove the activity object"""
obj = self.object.to_model(save=False, allow_create=False)
obj.reject()
@dataclass(init=False)
class Add(Verb):
"""Add activity """
"""Add activity"""
target: ActivityObject
object: CollectionItem
type: str = "Add"
def action(self):
""" figure out the target to assign the item to a collection """
"""figure out the target to assign the item to a collection"""
target = resolve_remote_id(self.target)
item = self.object.to_model(save=False)
setattr(item, item.collection_field, target)
@ -169,12 +169,12 @@ class Add(Verb):
@dataclass(init=False)
class Remove(Add):
"""Remove activity """
"""Remove activity"""
type: str = "Remove"
def action(self):
""" find and remove the activity object """
"""find and remove the activity object"""
obj = self.object.to_model(save=False, allow_create=False)
if obj:
obj.delete()
@ -182,19 +182,19 @@ class Remove(Add):
@dataclass(init=False)
class Like(Verb):
""" a user faving an object """
"""a user faving an object"""
object: str
type: str = "Like"
def action(self):
""" like """
"""like"""
self.to_model()
@dataclass(init=False)
class Announce(Verb):
""" boosting a status """
"""boosting a status"""
published: str
to: List[str] = field(default_factory=lambda: [])
@ -203,5 +203,5 @@ class Announce(Verb):
type: str = "Announce"
def action(self):
""" boost """
"""boost"""
self.to_model()

View file

@ -8,22 +8,22 @@ from bookwyrm.views.helpers import privacy_filter
class ActivityStream(RedisStore):
""" a category of activity stream (like home, local, federated) """
"""a category of activity stream (like home, local, federated)"""
def stream_id(self, user):
""" the redis key for this user's instance of this stream """
"""the redis key for this user's instance of this stream"""
return "{}-{}".format(user.id, self.key)
def unread_id(self, user):
""" the redis key for this user's unread count for this stream """
"""the redis key for this user's unread count for this stream"""
return "{}-unread".format(self.stream_id(user))
def get_rank(self, obj): # pylint: disable=no-self-use
""" statuses are sorted by date published """
"""statuses are sorted by date published"""
return obj.published_date.timestamp()
def add_status(self, status):
""" add a status to users' feeds """
"""add a status to users' feeds"""
# the pipeline contains all the add-to-stream activities
pipeline = self.add_object_to_related_stores(status, execute=False)
@ -35,19 +35,19 @@ class ActivityStream(RedisStore):
pipeline.execute()
def add_user_statuses(self, viewer, user):
""" add a user's statuses to another user's feed """
"""add a user's statuses to another user's feed"""
# only add the statuses that the viewer should be able to see (ie, not dms)
statuses = privacy_filter(viewer, user.status_set.all())
self.bulk_add_objects_to_store(statuses, self.stream_id(viewer))
def remove_user_statuses(self, viewer, user):
""" remove a user's status from another user's feed """
"""remove a user's status from another user's feed"""
# remove all so that followers only statuses are removed
statuses = user.status_set.all()
self.bulk_remove_objects_from_store(statuses, self.stream_id(viewer))
def get_activity_stream(self, user):
""" load the statuses to be displayed """
"""load the statuses to be displayed"""
# clear unreads for this feed
r.set(self.unread_id(user), 0)
@ -59,15 +59,15 @@ class ActivityStream(RedisStore):
)
def get_unread_count(self, user):
""" get the unread status count for this user's feed """
"""get the unread status count for this user's feed"""
return int(r.get(self.unread_id(user)) or 0)
def populate_streams(self, user):
""" go from zero to a timeline """
"""go from zero to a timeline"""
self.populate_store(self.stream_id(user))
def get_audience(self, status): # pylint: disable=no-self-use
""" given a status, what users should see it """
"""given a status, what users should see it"""
# direct messages don't appeard in feeds, direct comments/reviews/etc do
if status.privacy == "direct" and status.status_type == "Note":
return []
@ -98,7 +98,7 @@ class ActivityStream(RedisStore):
return [self.stream_id(u) for u in self.get_audience(obj)]
def get_statuses_for_user(self, user): # pylint: disable=no-self-use
""" given a user, what statuses should they see on this stream """
"""given a user, what statuses should they see on this stream"""
return privacy_filter(
user,
models.Status.objects.select_subclasses(),
@ -111,7 +111,7 @@ class ActivityStream(RedisStore):
class HomeStream(ActivityStream):
""" users you follow """
"""users you follow"""
key = "home"
@ -134,7 +134,7 @@ class HomeStream(ActivityStream):
class LocalStream(ActivityStream):
""" users you follow """
"""users you follow"""
key = "local"
@ -154,7 +154,7 @@ class LocalStream(ActivityStream):
class FederatedStream(ActivityStream):
""" users you follow """
"""users you follow"""
key = "federated"
@ -182,7 +182,7 @@ streams = {
@receiver(signals.post_save)
# pylint: disable=unused-argument
def add_status_on_create(sender, instance, created, *args, **kwargs):
""" add newly created statuses to activity feeds """
"""add newly created statuses to activity feeds"""
# we're only interested in new statuses
if not issubclass(sender, models.Status):
return
@ -203,7 +203,7 @@ def add_status_on_create(sender, instance, created, *args, **kwargs):
@receiver(signals.post_delete, sender=models.Boost)
# pylint: disable=unused-argument
def remove_boost_on_delete(sender, instance, *args, **kwargs):
""" boosts are deleted """
"""boosts are deleted"""
# we're only interested in new statuses
for stream in streams.values():
stream.remove_object_from_related_stores(instance)
@ -212,7 +212,7 @@ def remove_boost_on_delete(sender, instance, *args, **kwargs):
@receiver(signals.post_save, sender=models.UserFollows)
# pylint: disable=unused-argument
def add_statuses_on_follow(sender, instance, created, *args, **kwargs):
""" add a newly followed user's statuses to feeds """
"""add a newly followed user's statuses to feeds"""
if not created or not instance.user_subject.local:
return
HomeStream().add_user_statuses(instance.user_subject, instance.user_object)
@ -221,7 +221,7 @@ def add_statuses_on_follow(sender, instance, created, *args, **kwargs):
@receiver(signals.post_delete, sender=models.UserFollows)
# pylint: disable=unused-argument
def remove_statuses_on_unfollow(sender, instance, *args, **kwargs):
""" remove statuses from a feed on unfollow """
"""remove statuses from a feed on unfollow"""
if not instance.user_subject.local:
return
HomeStream().remove_user_statuses(instance.user_subject, instance.user_object)
@ -230,7 +230,7 @@ def remove_statuses_on_unfollow(sender, instance, *args, **kwargs):
@receiver(signals.post_save, sender=models.UserBlocks)
# pylint: disable=unused-argument
def remove_statuses_on_block(sender, instance, *args, **kwargs):
""" remove statuses from all feeds on block """
"""remove statuses from all feeds on block"""
# blocks apply ot all feeds
if instance.user_subject.local:
for stream in streams.values():
@ -245,7 +245,7 @@ def remove_statuses_on_block(sender, instance, *args, **kwargs):
@receiver(signals.post_delete, sender=models.UserBlocks)
# pylint: disable=unused-argument
def add_statuses_on_unblock(sender, instance, *args, **kwargs):
""" remove statuses from all feeds on block """
"""remove statuses from all feeds on block"""
public_streams = [LocalStream(), FederatedStream()]
# add statuses back to streams with statuses from anyone
if instance.user_subject.local:
@ -261,7 +261,7 @@ def add_statuses_on_unblock(sender, instance, *args, **kwargs):
@receiver(signals.post_save, sender=models.User)
# pylint: disable=unused-argument
def populate_streams_on_account_create(sender, instance, created, *args, **kwargs):
""" build a user's feeds when they join """
"""build a user's feeds when they join"""
if not created or not instance.local:
return

View file

@ -16,7 +16,7 @@ logger = logging.getLogger(__name__)
class AbstractMinimalConnector(ABC):
""" just the bare bones, for other bookwyrm instances """
"""just the bare bones, for other bookwyrm instances"""
def __init__(self, identifier):
# load connector settings
@ -39,7 +39,7 @@ class AbstractMinimalConnector(ABC):
setattr(self, field, getattr(info, field))
def search(self, query, min_confidence=None):
""" free text search """
"""free text search"""
params = {}
if min_confidence:
params["min_confidence"] = min_confidence
@ -55,7 +55,7 @@ class AbstractMinimalConnector(ABC):
return results
def isbn_search(self, query):
""" isbn search """
"""isbn search"""
params = {}
data = get_data(
"%s%s" % (self.isbn_search_url, query),
@ -70,27 +70,27 @@ class AbstractMinimalConnector(ABC):
@abstractmethod
def get_or_create_book(self, remote_id):
""" pull up a book record by whatever means possible """
"""pull up a book record by whatever means possible"""
@abstractmethod
def parse_search_data(self, data):
""" turn the result json from a search into a list """
"""turn the result json from a search into a list"""
@abstractmethod
def format_search_result(self, search_result):
""" create a SearchResult obj from json """
"""create a SearchResult obj from json"""
@abstractmethod
def parse_isbn_search_data(self, data):
""" turn the result json from a search into a list """
"""turn the result json from a search into a list"""
@abstractmethod
def format_isbn_search_result(self, search_result):
""" create a SearchResult obj from json """
"""create a SearchResult obj from json"""
class AbstractConnector(AbstractMinimalConnector):
""" generic book data connector """
"""generic book data connector"""
def __init__(self, identifier):
super().__init__(identifier)
@ -99,14 +99,14 @@ class AbstractConnector(AbstractMinimalConnector):
self.book_mappings = []
def is_available(self):
""" check if you're allowed to use this connector """
"""check if you're allowed to use this connector"""
if self.max_query_count is not None:
if self.connector.query_count >= self.max_query_count:
return False
return True
def get_or_create_book(self, remote_id):
""" translate arbitrary json into an Activitypub dataclass """
"""translate arbitrary json into an Activitypub dataclass"""
# first, check if we have the origin_id saved
existing = models.Edition.find_existing_by_remote_id(
remote_id
@ -151,7 +151,7 @@ class AbstractConnector(AbstractMinimalConnector):
return edition
def create_edition_from_data(self, work, edition_data):
""" if we already have the work, we're ready """
"""if we already have the work, we're ready"""
mapped_data = dict_from_mappings(edition_data, self.book_mappings)
mapped_data["work"] = work.remote_id
edition_activity = activitypub.Edition(**mapped_data)
@ -171,7 +171,7 @@ class AbstractConnector(AbstractMinimalConnector):
return edition
def get_or_create_author(self, remote_id):
""" load that author """
"""load that author"""
existing = models.Author.find_existing_by_remote_id(remote_id)
if existing:
return existing
@ -189,23 +189,23 @@ class AbstractConnector(AbstractMinimalConnector):
@abstractmethod
def is_work_data(self, data):
""" differentiate works and editions """
"""differentiate works and editions"""
@abstractmethod
def get_edition_from_work_data(self, data):
""" every work needs at least one edition """
"""every work needs at least one edition"""
@abstractmethod
def get_work_from_edition_data(self, data):
""" every edition needs a work """
"""every edition needs a work"""
@abstractmethod
def get_authors_from_data(self, data):
""" load author data """
"""load author data"""
@abstractmethod
def expand_book_data(self, book):
""" get more info on a book """
"""get more info on a book"""
def dict_from_mappings(data, mappings):
@ -218,7 +218,7 @@ def dict_from_mappings(data, mappings):
def get_data(url, params=None):
""" wrapper for request.get """
"""wrapper for request.get"""
# check if the url is blocked
if models.FederatedServer.is_blocked(url):
raise ConnectorException(
@ -250,7 +250,7 @@ def get_data(url, params=None):
def get_image(url):
""" wrapper for requesting an image """
"""wrapper for requesting an image"""
try:
resp = requests.get(
url,
@ -268,7 +268,7 @@ def get_image(url):
@dataclass
class SearchResult:
""" standardized search result object """
"""standardized search result object"""
title: str
key: str
@ -284,14 +284,14 @@ class SearchResult:
)
def json(self):
""" serialize a connector for json response """
"""serialize a connector for json response"""
serialized = asdict(self)
del serialized["connector"]
return serialized
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__(self, local_field, remote_field=None, formatter=None):
noop = lambda x: x
@ -301,7 +301,7 @@ class Mapping:
self.formatter = formatter or noop
def get_value(self, data):
""" pull a field from incoming json and return the formatted version """
"""pull a field from incoming json and return the formatted version"""
value = data.get(self.remote_field)
if not value:
return None

View file

@ -4,7 +4,7 @@ from .abstract_connector import AbstractMinimalConnector, SearchResult
class Connector(AbstractMinimalConnector):
""" this is basically just for search """
"""this is basically just for search"""
def get_or_create_book(self, remote_id):
edition = activitypub.resolve_remote_id(remote_id, model=models.Edition)

View file

@ -16,11 +16,11 @@ logger = logging.getLogger(__name__)
class ConnectorException(HTTPError):
""" when the connector can't do what was asked """
"""when the connector can't do what was asked"""
def search(query, min_confidence=0.1):
""" find books based on arbitary keywords """
"""find books based on arbitary keywords"""
if not query:
return []
results = []
@ -67,20 +67,22 @@ def search(query, min_confidence=0.1):
return results
def local_search(query, min_confidence=0.1, raw=False):
""" only look at local search results """
def local_search(query, min_confidence=0.1, raw=False, filters=None):
"""only look at local search results"""
connector = load_connector(models.Connector.objects.get(local=True))
return connector.search(query, min_confidence=min_confidence, raw=raw)
return connector.search(
query, min_confidence=min_confidence, raw=raw, filters=filters
)
def isbn_local_search(query, raw=False):
""" only look at local search results """
"""only look at local search results"""
connector = load_connector(models.Connector.objects.get(local=True))
return connector.isbn_search(query, raw=raw)
def first_search_result(query, min_confidence=0.1):
""" search until you find a result that fits """
"""search until you find a result that fits"""
for connector in get_connectors():
result = connector.search(query, min_confidence=min_confidence)
if result:
@ -89,13 +91,13 @@ def first_search_result(query, min_confidence=0.1):
def get_connectors():
""" load all connectors """
"""load all connectors"""
for info in models.Connector.objects.order_by("priority").all():
yield load_connector(info)
def get_or_create_connector(remote_id):
""" get the connector related to the object's server """
"""get the connector related to the object's server"""
url = urlparse(remote_id)
identifier = url.netloc
if not identifier:
@ -119,7 +121,7 @@ def get_or_create_connector(remote_id):
@app.task
def load_more_data(connector_id, book_id):
""" background the work of getting all 10,000 editions of LoTR """
"""background the work of getting all 10,000 editions of LoTR"""
connector_info = models.Connector.objects.get(id=connector_id)
connector = load_connector(connector_info)
book = models.Book.objects.select_subclasses().get(id=book_id)
@ -127,7 +129,7 @@ def load_more_data(connector_id, book_id):
def load_connector(connector_info):
""" instantiate the connector class """
"""instantiate the connector class"""
connector = importlib.import_module(
"bookwyrm.connectors.%s" % connector_info.connector_file
)
@ -137,6 +139,6 @@ def load_connector(connector_info):
@receiver(signals.post_save, sender="bookwyrm.FederatedServer")
# pylint: disable=unused-argument
def create_connector(sender, instance, created, *args, **kwargs):
""" create a connector to an external bookwyrm server """
"""create a connector to an external bookwyrm server"""
if instance.application_type == "bookwyrm":
get_or_create_connector("https://{:s}".format(instance.server_name))

View file

@ -9,7 +9,7 @@ from .openlibrary_languages import languages
class Connector(AbstractConnector):
""" instantiate a connector for OL """
"""instantiate a connector for OL"""
def __init__(self, identifier):
super().__init__(identifier)
@ -59,7 +59,7 @@ class Connector(AbstractConnector):
]
def get_remote_id_from_data(self, data):
""" format a url from an openlibrary id field """
"""format a url from an openlibrary id field"""
try:
key = data["key"]
except KeyError:
@ -87,7 +87,7 @@ class Connector(AbstractConnector):
return get_data(url)
def get_authors_from_data(self, data):
""" parse author json and load or create authors """
"""parse author json and load or create authors"""
for author_blob in data.get("authors", []):
author_blob = author_blob.get("author", author_blob)
# this id is "/authors/OL1234567A"
@ -99,7 +99,7 @@ class Connector(AbstractConnector):
yield author
def get_cover_url(self, cover_blob, size="L"):
""" ask openlibrary for the cover """
"""ask openlibrary for the cover"""
if not cover_blob:
return None
cover_id = cover_blob[0]
@ -141,7 +141,7 @@ class Connector(AbstractConnector):
)
def load_edition_data(self, olkey):
""" query openlibrary for editions of a work """
"""query openlibrary for editions of a work"""
url = "%s/works/%s/editions" % (self.books_url, olkey)
return get_data(url)
@ -166,7 +166,7 @@ class Connector(AbstractConnector):
def ignore_edition(edition_data):
""" don't load a million editions that have no metadata """
"""don't load a million editions that have no metadata"""
# an isbn, we love to see it
if edition_data.get("isbn_13") or edition_data.get("isbn_10"):
return False
@ -185,19 +185,19 @@ def ignore_edition(edition_data):
def get_description(description_blob):
""" descriptions can be a string or a dict """
"""descriptions can be a string or a dict"""
if isinstance(description_blob, dict):
return description_blob.get("value")
return description_blob
def get_openlibrary_key(key):
""" convert /books/OL27320736M into OL27320736M """
"""convert /books/OL27320736M into OL27320736M"""
return key.split("/")[-1]
def get_languages(language_blob):
""" /language/eng -> English """
"""/language/eng -> English"""
langs = []
for lang in language_blob:
langs.append(languages.get(lang.get("key", ""), None))
@ -205,7 +205,7 @@ def get_languages(language_blob):
def pick_default_edition(options):
""" favor physical copies with covers in english """
"""favor physical copies with covers in english"""
if not options:
return None
if len(options) == 1:

View file

@ -10,18 +10,19 @@ from .abstract_connector import AbstractConnector, SearchResult
class Connector(AbstractConnector):
""" instantiate a connector """
"""instantiate a connector"""
# pylint: disable=arguments-differ
def search(self, query, min_confidence=0.1, raw=False):
""" search your local database """
def search(self, query, min_confidence=0.1, raw=False, filters=None):
"""search your local database"""
filters = filters or []
if not query:
return []
# first, try searching unqiue identifiers
results = search_identifiers(query)
results = search_identifiers(query, *filters)
if not results:
# then try searching title/author
results = search_title_author(query, min_confidence)
results = search_title_author(query, min_confidence, *filters)
search_results = []
for result in results:
if raw:
@ -35,7 +36,7 @@ class Connector(AbstractConnector):
return search_results
def isbn_search(self, query, raw=False):
""" search your local database """
"""search your local database"""
if not query:
return []
@ -87,26 +88,26 @@ class Connector(AbstractConnector):
return None
def parse_isbn_search_data(self, data):
""" it's already in the right format, don't even worry about it """
"""it's already in the right format, don't even worry about it"""
return data
def parse_search_data(self, data):
""" it's already in the right format, don't even worry about it """
"""it's already in the right format, don't even worry about it"""
return data
def expand_book_data(self, book):
pass
def search_identifiers(query):
""" tries remote_id, isbn; defined as dedupe fields on the model """
filters = [
def search_identifiers(query, *filters):
"""tries remote_id, isbn; defined as dedupe fields on the model"""
or_filters = [
{f.name: query}
for f in models.Edition._meta.get_fields()
if hasattr(f, "deduplication_field") and f.deduplication_field
]
results = models.Edition.objects.filter(
reduce(operator.or_, (Q(**f) for f in filters))
*filters, reduce(operator.or_, (Q(**f) for f in or_filters))
).distinct()
# when there are multiple editions of the same work, pick the default.
@ -114,8 +115,8 @@ def search_identifiers(query):
return results.filter(parent_work__default_edition__id=F("id")) or results
def search_title_author(query, min_confidence):
""" searches for title and author """
def search_title_author(query, min_confidence, *filters):
"""searches for title and author"""
vector = (
SearchVector("title", weight="A")
+ SearchVector("subtitle", weight="B")
@ -126,7 +127,7 @@ def search_title_author(query, min_confidence):
results = (
models.Edition.objects.annotate(search=vector)
.annotate(rank=SearchRank(vector, query))
.filter(rank__gt=min_confidence)
.filter(*filters, rank__gt=min_confidence)
.order_by("-rank")
)

View file

@ -3,5 +3,5 @@ from bookwyrm import models
def site_settings(request): # pylint: disable=unused-argument
""" include the custom info about the site """
"""include the custom info about the site"""
return {"site": models.SiteSettings.objects.get()}

View file

@ -8,7 +8,7 @@ from bookwyrm.settings import DOMAIN
def email_data():
""" fields every email needs """
"""fields every email needs"""
site = models.SiteSettings.objects.get()
if site.logo_small:
logo_path = "/images/{}".format(site.logo_small.url)
@ -24,14 +24,14 @@ def email_data():
def invite_email(invite_request):
""" send out an invite code """
"""send out an invite code"""
data = email_data()
data["invite_link"] = invite_request.invite.link
send_email.delay(invite_request.email, *format_email("invite", data))
def password_reset_email(reset_code):
""" generate a password reset email """
"""generate a password reset email"""
data = email_data()
data["reset_link"] = reset_code.link
data["user"] = reset_code.user.display_name
@ -39,7 +39,7 @@ def password_reset_email(reset_code):
def format_email(email_name, data):
""" render the email templates """
"""render the email templates"""
subject = (
get_template("email/{}/subject.html".format(email_name)).render(data).strip()
)
@ -58,7 +58,7 @@ def format_email(email_name, data):
@app.task
def send_email(recipient, subject, html_content, text_content):
""" use a task to send the email """
"""use a task to send the email"""
email = EmailMultiAlternatives(
subject, text_content, settings.DEFAULT_FROM_EMAIL, [recipient]
)

View file

@ -12,7 +12,7 @@ from bookwyrm import models
class CustomForm(ModelForm):
""" add css classes to the forms """
"""add css classes to the forms"""
def __init__(self, *args, **kwargs):
css_classes = defaultdict(lambda: "")
@ -198,7 +198,7 @@ class ImportForm(forms.Form):
class ExpiryWidget(widgets.Select):
def value_from_datadict(self, data, files, name):
""" human-readable exiration time buckets """
"""human-readable exiration time buckets"""
selected_string = super().value_from_datadict(data, files, name)
if selected_string == "day":
@ -217,7 +217,7 @@ class ExpiryWidget(widgets.Select):
class InviteRequestForm(CustomForm):
def clean(self):
""" make sure the email isn't in use by a registered user """
"""make sure the email isn't in use by a registered user"""
cleaned_data = super().clean()
email = cleaned_data.get("email")
if email and models.User.objects.filter(email=email).exists():

View file

@ -9,7 +9,7 @@ class GoodreadsImporter(Importer):
service = "GoodReads"
def parse_fields(self, entry):
""" handle the specific fields in goodreads csvs """
"""handle the specific fields in goodreads csvs"""
entry.update({"import_source": self.service})
# add missing 'Date Started' field
entry.update({"Date Started": None})

View file

@ -10,7 +10,7 @@ logger = logging.getLogger(__name__)
class Importer:
""" Generic class for csv data import from an outside service """
"""Generic class for csv data import from an outside service"""
service = "Unknown"
delimiter = ","
@ -18,7 +18,7 @@ class Importer:
mandatory_fields = ["Title", "Author"]
def create_job(self, user, csv_file, include_reviews, privacy):
""" check over a csv and creates a database entry for the job"""
"""check over a csv and creates a database entry for the job"""
job = ImportJob.objects.create(
user=user, include_reviews=include_reviews, privacy=privacy
)
@ -32,16 +32,16 @@ class Importer:
return job
def save_item(self, job, index, data): # pylint: disable=no-self-use
""" creates and saves an import item """
"""creates and saves an import item"""
ImportItem(job=job, index=index, data=data).save()
def parse_fields(self, entry):
""" updates csv data with additional info """
"""updates csv data with additional info"""
entry.update({"import_source": self.service})
return entry
def create_retry_job(self, user, original_job, items):
""" retry items that didn't import """
"""retry items that didn't import"""
job = ImportJob.objects.create(
user=user,
include_reviews=original_job.include_reviews,
@ -53,7 +53,7 @@ class Importer:
return job
def start_import(self, job):
""" initalizes a csv import job """
"""initalizes a csv import job"""
result = import_data.delay(self.service, job.id)
job.task_id = result.id
job.save()
@ -61,7 +61,7 @@ class Importer:
@app.task
def import_data(source, job_id):
""" does the actual lookup work in a celery task """
"""does the actual lookup work in a celery task"""
job = ImportJob.objects.get(id=job_id)
try:
for item in job.items.all():
@ -89,7 +89,7 @@ def import_data(source, job_id):
def handle_imported_book(source, user, item, include_reviews, privacy):
""" process a csv and then post about it """
"""process a csv and then post about it"""
if isinstance(item.book, models.Work):
item.book = item.book.default_edition
if not item.book:
@ -116,24 +116,33 @@ def handle_imported_book(source, user, item, include_reviews, privacy):
read.save()
if include_reviews and (item.rating or item.review):
review_title = (
"Review of {!r} on {!r}".format(
item.book.title,
source,
)
if item.review
else ""
)
# we don't know the publication date of the review,
# but "now" is a bad guess
published_date_guess = item.date_read or item.date_added
models.Review.objects.create(
user=user,
book=item.book,
name=review_title,
content=item.review,
rating=item.rating,
published_date=published_date_guess,
privacy=privacy,
)
if item.review:
review_title = (
"Review of {!r} on {!r}".format(
item.book.title,
source,
)
if item.review
else ""
)
models.Review.objects.create(
user=user,
book=item.book,
name=review_title,
content=item.review,
rating=item.rating,
published_date=published_date_guess,
privacy=privacy,
)
else:
# just a rating
models.ReviewRating.objects.create(
user=user,
book=item.book,
rating=item.rating,
published_date=published_date_guess,
privacy=privacy,
)

View file

@ -6,7 +6,7 @@ from . import Importer
class LibrarythingImporter(Importer):
""" csv downloads from librarything """
"""csv downloads from librarything"""
service = "LibraryThing"
delimiter = "\t"
@ -15,7 +15,7 @@ class LibrarythingImporter(Importer):
mandatory_fields = ["Title", "Primary Author"]
def parse_fields(self, entry):
""" custom parsing for librarything """
"""custom parsing for librarything"""
data = {}
data["import_source"] = self.service
data["Book Id"] = entry["Book Id"]

View file

@ -6,7 +6,7 @@ from bookwyrm import models
def update_related(canonical, obj):
""" update all the models with fk to the object being removed """
"""update all the models with fk to the object being removed"""
# move related models to canonical
related_models = [
(r.remote_field.name, r.related_model) for r in canonical._meta.related_objects
@ -24,7 +24,7 @@ def update_related(canonical, obj):
def copy_data(canonical, obj):
""" try to get the most data possible """
"""try to get the most data possible"""
for data_field in obj._meta.get_fields():
if not hasattr(data_field, "activitypub_field"):
continue
@ -38,7 +38,7 @@ def copy_data(canonical, obj):
def dedupe_model(model):
""" combine duplicate editions and update related models """
"""combine duplicate editions and update related models"""
fields = model._meta.get_fields()
dedupe_fields = [
f for f in fields if hasattr(f, "deduplication_field") and f.deduplication_field
@ -68,12 +68,12 @@ def dedupe_model(model):
class Command(BaseCommand):
""" dedplucate allllll the book data models """
"""dedplucate allllll the book data models"""
help = "merges duplicate book data"
# pylint: disable=no-self-use,unused-argument
def handle(self, *args, **options):
""" run deudplications """
"""run deudplications"""
dedupe_model(models.Edition)
dedupe_model(models.Work)
dedupe_model(models.Author)

View file

@ -10,15 +10,15 @@ r = redis.Redis(
def erase_streams():
""" throw the whole redis away """
"""throw the whole redis away"""
r.flushall()
class Command(BaseCommand):
""" delete activity streams for all users """
"""delete activity streams for all users"""
help = "Delete all the user streams"
# pylint: disable=no-self-use,unused-argument
def handle(self, *args, **options):
""" flush all, baby """
"""flush all, baby"""
erase_streams()

View file

@ -108,7 +108,7 @@ def init_connectors():
def init_federated_servers():
""" big no to nazis """
"""big no to nazis"""
built_in_blocks = ["gab.ai", "gab.com"]
for server in built_in_blocks:
FederatedServer.objects.create(

View file

@ -10,7 +10,7 @@ r = redis.Redis(
def populate_streams():
""" build all the streams for all the users """
"""build all the streams for all the users"""
users = models.User.objects.filter(
local=True,
is_active=True,
@ -21,10 +21,10 @@ def populate_streams():
class Command(BaseCommand):
""" start all over with user streams """
"""start all over with user streams"""
help = "Populate streams for all users"
# pylint: disable=no-self-use,unused-argument
def handle(self, *args, **options):
""" run feed builder """
"""run feed builder"""
populate_streams()

View file

@ -5,7 +5,7 @@ from bookwyrm import models
def remove_editions():
""" combine duplicate editions and update related models """
"""combine duplicate editions and update related models"""
# not in use
filters = {
"%s__isnull" % r.name: True for r in models.Edition._meta.related_objects
@ -33,10 +33,10 @@ def remove_editions():
class Command(BaseCommand):
""" dedplucate allllll the book data models """
"""dedplucate allllll the book data models"""
help = "merges duplicate book data"
# pylint: disable=no-self-use,unused-argument
def handle(self, *args, **options):
""" run deudplications """
"""run deudplications"""
remove_editions()

View file

@ -8,7 +8,7 @@ from psycopg2.extras import execute_values
def convert_review_rating(app_registry, schema_editor):
""" take rating type Reviews and convert them to ReviewRatings """
"""take rating type Reviews and convert them to ReviewRatings"""
db_alias = schema_editor.connection.alias
reviews = (
@ -29,7 +29,7 @@ VALUES %s""",
def unconvert_review_rating(app_registry, schema_editor):
""" undo the conversion from ratings back to reviews"""
"""undo the conversion from ratings back to reviews"""
# All we need to do to revert this is drop the table, which Django will do
# on its own, as long as we have a valid reverse function. So, this is a
# no-op function so Django will do its thing

View file

@ -31,18 +31,18 @@ PropertyField = namedtuple("PropertyField", ("set_activity_from_field"))
def set_activity_from_property_field(activity, obj, field):
""" assign a model property value to the activity json """
"""assign a model property value to the activity json"""
activity[field[1]] = getattr(obj, field[0])
class ActivitypubMixin:
""" add this mixin for models that are AP serializable """
"""add this mixin for models that are AP serializable"""
activity_serializer = lambda: {}
reverse_unfurl = False
def __init__(self, *args, **kwargs):
""" collect some info on model fields """
"""collect some info on model fields"""
self.image_fields = []
self.many_to_many_fields = []
self.simple_fields = [] # "simple"
@ -85,7 +85,7 @@ class ActivitypubMixin:
@classmethod
def find_existing_by_remote_id(cls, remote_id):
""" look up a remote id in the db """
"""look up a remote id in the db"""
return cls.find_existing({"id": remote_id})
@classmethod
@ -126,7 +126,7 @@ class ActivitypubMixin:
return match.first()
def broadcast(self, activity, sender, software=None):
""" send out an activity """
"""send out an activity"""
broadcast_task.delay(
sender.id,
json.dumps(activity, cls=activitypub.ActivityEncoder),
@ -134,7 +134,7 @@ class ActivitypubMixin:
)
def get_recipients(self, software=None):
""" figure out which inbox urls to post to """
"""figure out which inbox urls to post to"""
# first we have to figure out who should receive this activity
privacy = self.privacy if hasattr(self, "privacy") else "public"
# is this activity owned by a user (statuses, lists, shelves), or is it
@ -182,20 +182,20 @@ class ActivitypubMixin:
return list(set(recipients))
def to_activity_dataclass(self):
""" convert from a model to an activity """
"""convert from a model to an activity"""
activity = generate_activity(self)
return self.activity_serializer(**activity)
def to_activity(self, **kwargs): # pylint: disable=unused-argument
""" convert from a model to a json activity """
"""convert from a model to a json activity"""
return self.to_activity_dataclass().serialize()
class ObjectMixin(ActivitypubMixin):
""" add this mixin for object models that are AP serializable """
"""add this mixin for object models that are AP serializable"""
def save(self, *args, created=None, **kwargs):
""" broadcast created/updated/deleted objects as appropriate """
"""broadcast created/updated/deleted objects as appropriate"""
broadcast = kwargs.get("broadcast", True)
# this bonus kwarg would cause an error in the base save method
if "broadcast" in kwargs:
@ -254,7 +254,7 @@ class ObjectMixin(ActivitypubMixin):
self.broadcast(activity, user)
def to_create_activity(self, user, **kwargs):
""" returns the object wrapped in a Create activity """
"""returns the object wrapped in a Create activity"""
activity_object = self.to_activity_dataclass(**kwargs)
signature = None
@ -280,7 +280,7 @@ class ObjectMixin(ActivitypubMixin):
).serialize()
def to_delete_activity(self, user):
""" notice of deletion """
"""notice of deletion"""
return activitypub.Delete(
id=self.remote_id + "/activity",
actor=user.remote_id,
@ -290,7 +290,7 @@ class ObjectMixin(ActivitypubMixin):
).serialize()
def to_update_activity(self, user):
""" wrapper for Updates to an activity """
"""wrapper for Updates to an activity"""
activity_id = "%s#update/%s" % (self.remote_id, uuid4())
return activitypub.Update(
id=activity_id,
@ -306,13 +306,13 @@ class OrderedCollectionPageMixin(ObjectMixin):
@property
def collection_remote_id(self):
""" this can be overriden if there's a special remote id, ie outbox """
"""this can be overriden if there's a special remote id, ie outbox"""
return self.remote_id
def to_ordered_collection(
self, queryset, remote_id=None, page=False, collection_only=False, **kwargs
):
""" an ordered collection of whatevers """
"""an ordered collection of whatevers"""
if not queryset.ordered:
raise RuntimeError("queryset must be ordered")
@ -341,11 +341,11 @@ class OrderedCollectionPageMixin(ObjectMixin):
class OrderedCollectionMixin(OrderedCollectionPageMixin):
""" extends activitypub models to work as ordered collections """
"""extends activitypub models to work as ordered collections"""
@property
def collection_queryset(self):
""" usually an ordered collection model aggregates a different model """
"""usually an ordered collection model aggregates a different model"""
raise NotImplementedError("Model must define collection_queryset")
activity_serializer = activitypub.OrderedCollection
@ -354,24 +354,24 @@ class OrderedCollectionMixin(OrderedCollectionPageMixin):
return self.to_ordered_collection(self.collection_queryset, **kwargs)
def to_activity(self, **kwargs):
""" an ordered collection of the specified model queryset """
"""an ordered collection of the specified model queryset"""
return self.to_ordered_collection(
self.collection_queryset, **kwargs
).serialize()
class CollectionItemMixin(ActivitypubMixin):
""" for items that are part of an (Ordered)Collection """
"""for items that are part of an (Ordered)Collection"""
activity_serializer = activitypub.CollectionItem
def broadcast(self, activity, sender, software="bookwyrm"):
""" only send book collection updates to other bookwyrm instances """
"""only send book collection updates to other bookwyrm instances"""
super().broadcast(activity, sender, software=software)
@property
def privacy(self):
""" inherit the privacy of the list, or direct if pending """
"""inherit the privacy of the list, or direct if pending"""
collection_field = getattr(self, self.collection_field)
if self.approved:
return collection_field.privacy
@ -379,7 +379,7 @@ class CollectionItemMixin(ActivitypubMixin):
@property
def recipients(self):
""" the owner of the list is a direct recipient """
"""the owner of the list is a direct recipient"""
collection_field = getattr(self, self.collection_field)
if collection_field.user.local:
# don't broadcast to yourself
@ -387,7 +387,7 @@ class CollectionItemMixin(ActivitypubMixin):
return [collection_field.user]
def save(self, *args, broadcast=True, **kwargs):
""" broadcast updated """
"""broadcast updated"""
# first off, we want to save normally no matter what
super().save(*args, **kwargs)
@ -400,14 +400,14 @@ class CollectionItemMixin(ActivitypubMixin):
self.broadcast(activity, self.user)
def delete(self, *args, broadcast=True, **kwargs):
""" broadcast a remove activity """
"""broadcast a remove activity"""
activity = self.to_remove_activity(self.user)
super().delete(*args, **kwargs)
if self.user.local and broadcast:
self.broadcast(activity, self.user)
def to_add_activity(self, user):
""" AP for shelving a book"""
"""AP for shelving a book"""
collection_field = getattr(self, self.collection_field)
return activitypub.Add(
id="{:s}#add".format(collection_field.remote_id),
@ -417,7 +417,7 @@ class CollectionItemMixin(ActivitypubMixin):
).serialize()
def to_remove_activity(self, user):
""" AP for un-shelving a book"""
"""AP for un-shelving a book"""
collection_field = getattr(self, self.collection_field)
return activitypub.Remove(
id="{:s}#remove".format(collection_field.remote_id),
@ -428,24 +428,24 @@ class CollectionItemMixin(ActivitypubMixin):
class ActivityMixin(ActivitypubMixin):
""" add this mixin for models that are AP serializable """
"""add this mixin for models that are AP serializable"""
def save(self, *args, broadcast=True, **kwargs):
""" broadcast activity """
"""broadcast activity"""
super().save(*args, **kwargs)
user = self.user if hasattr(self, "user") else self.user_subject
if broadcast and user.local:
self.broadcast(self.to_activity(), user)
def delete(self, *args, broadcast=True, **kwargs):
""" nevermind, undo that activity """
"""nevermind, undo that activity"""
user = self.user if hasattr(self, "user") else self.user_subject
if broadcast and user.local:
self.broadcast(self.to_undo_activity(), user)
super().delete(*args, **kwargs)
def to_undo_activity(self):
""" undo an action """
"""undo an action"""
user = self.user if hasattr(self, "user") else self.user_subject
return activitypub.Undo(
id="%s#undo" % self.remote_id,
@ -455,7 +455,7 @@ class ActivityMixin(ActivitypubMixin):
def generate_activity(obj):
""" go through the fields on an object """
"""go through the fields on an object"""
activity = {}
for field in obj.activity_fields:
field.set_activity_from_field(activity, obj)
@ -478,7 +478,7 @@ def generate_activity(obj):
def unfurl_related_field(related_field, sort_field=None):
""" load reverse lookups (like public key owner or Status attachment """
"""load reverse lookups (like public key owner or Status attachment"""
if sort_field and hasattr(related_field, "all"):
return [
unfurl_related_field(i) for i in related_field.order_by(sort_field).all()
@ -494,7 +494,7 @@ def unfurl_related_field(related_field, sort_field=None):
@app.task
def broadcast_task(sender_id, activity, recipients):
""" the celery task for broadcast """
"""the celery task for broadcast"""
user_model = apps.get_model("bookwyrm.User", require_ready=True)
sender = user_model.objects.get(id=sender_id)
for recipient in recipients:
@ -505,7 +505,7 @@ def broadcast_task(sender_id, activity, recipients):
def sign_and_send(sender, data, destination):
""" crpyto whatever and http junk """
"""crpyto whatever and http junk"""
now = http_date()
if not sender.key_pair.private_key:
@ -534,7 +534,7 @@ def sign_and_send(sender, data, destination):
def to_ordered_collection_page(
queryset, remote_id, id_only=False, page=1, pure=False, **kwargs
):
""" serialize and pagiante a queryset """
"""serialize and pagiante a queryset"""
paginated = Paginator(queryset, PAGE_LENGTH)
activity_page = paginated.get_page(page)

View file

@ -8,7 +8,7 @@ from . import fields
class Attachment(ActivitypubMixin, BookWyrmModel):
""" an image (or, in the future, video etc) associated with a status """
"""an image (or, in the future, video etc) associated with a status"""
status = models.ForeignKey(
"Status", on_delete=models.CASCADE, related_name="attachments", null=True
@ -16,13 +16,13 @@ class Attachment(ActivitypubMixin, BookWyrmModel):
reverse_unfurl = True
class Meta:
""" one day we'll have other types of attachments besides images """
"""one day we'll have other types of attachments besides images"""
abstract = True
class Image(Attachment):
""" an image attachment """
"""an image attachment"""
image = fields.ImageField(
upload_to="status/",

View file

@ -9,7 +9,7 @@ from . import fields
class Author(BookDataModel):
""" basic biographic info """
"""basic biographic info"""
wikipedia_link = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True
@ -24,7 +24,7 @@ class Author(BookDataModel):
bio = fields.HtmlField(null=True, blank=True)
def get_remote_id(self):
""" editions and works both use "book" instead of model_name """
"""editions and works both use "book" instead of model_name"""
return "https://%s/author/%s" % (DOMAIN, self.id)
activity_serializer = activitypub.Author

View file

@ -7,14 +7,14 @@ from .fields import RemoteIdField
class BookWyrmModel(models.Model):
""" shared fields """
"""shared fields"""
created_date = models.DateTimeField(auto_now_add=True)
updated_date = models.DateTimeField(auto_now=True)
remote_id = RemoteIdField(null=True, activitypub_field="id")
def get_remote_id(self):
""" generate a url that resolves to the local object """
"""generate a url that resolves to the local object"""
base_path = "https://%s" % DOMAIN
if hasattr(self, "user"):
base_path = "%s%s" % (base_path, self.user.local_path)
@ -22,17 +22,17 @@ class BookWyrmModel(models.Model):
return "%s/%s/%d" % (base_path, model_name, self.id)
class Meta:
""" this is just here to provide default fields for other models """
"""this is just here to provide default fields for other models"""
abstract = True
@property
def local_path(self):
""" how to link to this object in the local app """
"""how to link to this object in the local app"""
return self.get_remote_id().replace("https://%s" % DOMAIN, "")
def visible_to_user(self, viewer):
""" is a user authorized to view an object? """
"""is a user authorized to view an object?"""
# make sure this is an object with privacy owned by a user
if not hasattr(self, "user") or not hasattr(self, "privacy"):
return None
@ -65,7 +65,7 @@ class BookWyrmModel(models.Model):
@receiver(models.signals.post_save)
# pylint: disable=unused-argument
def set_remote_id(sender, instance, created, *args, **kwargs):
""" set the remote_id after save (when the id is available) """
"""set the remote_id after save (when the id is available)"""
if not created or not hasattr(instance, "get_remote_id"):
return
if not instance.remote_id:

View file

@ -13,7 +13,7 @@ from . import fields
class BookDataModel(ObjectMixin, BookWyrmModel):
""" fields shared between editable book data (books, works, authors) """
"""fields shared between editable book data (books, works, authors)"""
origin_id = models.CharField(max_length=255, null=True, blank=True)
openlibrary_key = fields.CharField(
@ -33,12 +33,12 @@ class BookDataModel(ObjectMixin, BookWyrmModel):
)
class Meta:
""" can't initialize this model, that wouldn't make sense """
"""can't initialize this model, that wouldn't make sense"""
abstract = True
def save(self, *args, **kwargs):
""" ensure that the remote_id is within this instance """
"""ensure that the remote_id is within this instance"""
if self.id:
self.remote_id = self.get_remote_id()
else:
@ -47,12 +47,12 @@ class BookDataModel(ObjectMixin, BookWyrmModel):
return super().save(*args, **kwargs)
def broadcast(self, activity, sender, software="bookwyrm"):
""" only send book data updates to other bookwyrm instances """
"""only send book data updates to other bookwyrm instances"""
super().broadcast(activity, sender, software=software)
class Book(BookDataModel):
""" a generic book, which can mean either an edition or a work """
"""a generic book, which can mean either an edition or a work"""
connector = models.ForeignKey("Connector", on_delete=models.PROTECT, null=True)
@ -83,17 +83,17 @@ class Book(BookDataModel):
@property
def author_text(self):
""" format a list of authors """
"""format a list of authors"""
return ", ".join(a.name for a in self.authors.all())
@property
def latest_readthrough(self):
""" most recent readthrough activity """
"""most recent readthrough activity"""
return self.readthrough_set.order_by("-updated_date").first()
@property
def edition_info(self):
""" properties of this edition, as a string """
"""properties of this edition, as a string"""
items = [
self.physical_format if hasattr(self, "physical_format") else None,
self.languages[0] + " language"
@ -106,20 +106,20 @@ class Book(BookDataModel):
@property
def alt_text(self):
""" image alt test """
"""image alt test"""
text = "%s" % self.title
if self.edition_info:
text += " (%s)" % self.edition_info
return text
def save(self, *args, **kwargs):
""" can't be abstract for query reasons, but you shouldn't USE it """
"""can't be abstract for query reasons, but you shouldn't USE it"""
if not isinstance(self, Edition) and not isinstance(self, Work):
raise ValueError("Books should be added as Editions or Works")
return super().save(*args, **kwargs)
def get_remote_id(self):
""" editions and works both use "book" instead of model_name """
"""editions and works both use "book" instead of model_name"""
return "https://%s/book/%d" % (DOMAIN, self.id)
def __repr__(self):
@ -131,7 +131,7 @@ class Book(BookDataModel):
class Work(OrderedCollectionPageMixin, Book):
""" a work (an abstract concept of a book that manifests in an edition) """
"""a work (an abstract concept of a book that manifests in an edition)"""
# library of congress catalog control number
lccn = fields.CharField(
@ -143,19 +143,19 @@ class Work(OrderedCollectionPageMixin, Book):
)
def save(self, *args, **kwargs):
""" set some fields on the edition object """
"""set some fields on the edition object"""
# set rank
for edition in self.editions.all():
edition.save()
return super().save(*args, **kwargs)
def get_default_edition(self):
""" in case the default edition is not set """
"""in case the default edition is not set"""
return self.default_edition or self.editions.order_by("-edition_rank").first()
@transaction.atomic()
def reset_default_edition(self):
""" sets a new default edition based on computed rank """
"""sets a new default edition based on computed rank"""
self.default_edition = None
# editions are re-ranked implicitly
self.save()
@ -163,11 +163,11 @@ class Work(OrderedCollectionPageMixin, Book):
self.save()
def to_edition_list(self, **kwargs):
""" an ordered collection of editions """
"""an ordered collection of editions"""
return self.to_ordered_collection(
self.editions.order_by("-edition_rank").all(),
remote_id="%s/editions" % self.remote_id,
**kwargs
**kwargs,
)
activity_serializer = activitypub.Work
@ -176,7 +176,7 @@ class Work(OrderedCollectionPageMixin, Book):
class Edition(Book):
""" an edition of a book """
"""an edition of a book"""
# these identifiers only apply to editions, not works
isbn_10 = fields.CharField(
@ -215,7 +215,7 @@ class Edition(Book):
name_field = "title"
def get_rank(self, ignore_default=False):
""" calculate how complete the data is on this edition """
"""calculate how complete the data is on this edition"""
if (
not ignore_default
and self.parent_work
@ -235,7 +235,7 @@ class Edition(Book):
return rank
def save(self, *args, **kwargs):
""" set some fields on the edition object """
"""set some fields on the edition object"""
# calculate isbn 10/13
if self.isbn_13 and self.isbn_13[:3] == "978" and not self.isbn_10:
self.isbn_10 = isbn_13_to_10(self.isbn_13)
@ -249,7 +249,7 @@ class Edition(Book):
def isbn_10_to_13(isbn_10):
""" convert an isbn 10 into an isbn 13 """
"""convert an isbn 10 into an isbn 13"""
isbn_10 = re.sub(r"[^0-9X]", "", isbn_10)
# drop the last character of the isbn 10 number (the original checkdigit)
converted = isbn_10[:9]
@ -271,7 +271,7 @@ def isbn_10_to_13(isbn_10):
def isbn_13_to_10(isbn_13):
""" convert isbn 13 to 10, if possible """
"""convert isbn 13 to 10, if possible"""
if isbn_13[:3] != "978":
return None

View file

@ -9,7 +9,7 @@ ConnectorFiles = models.TextChoices("ConnectorFiles", CONNECTORS)
class Connector(BookWyrmModel):
""" book data source connectors """
"""book data source connectors"""
identifier = models.CharField(max_length=255, unique=True)
priority = models.IntegerField(default=2)
@ -32,7 +32,7 @@ class Connector(BookWyrmModel):
query_count_expiry = models.DateTimeField(auto_now_add=True, blank=True)
class Meta:
""" check that there's code to actually use this connector """
"""check that there's code to actually use this connector"""
constraints = [
models.CheckConstraint(

View file

@ -11,7 +11,7 @@ from .status import Status
class Favorite(ActivityMixin, BookWyrmModel):
""" fav'ing a post """
"""fav'ing a post"""
user = fields.ForeignKey(
"User", on_delete=models.PROTECT, activitypub_field="actor"
@ -24,11 +24,11 @@ class Favorite(ActivityMixin, BookWyrmModel):
@classmethod
def ignore_activity(cls, activity):
""" don't bother with incoming favs of unknown statuses """
"""don't bother with incoming favs of unknown statuses"""
return not Status.objects.filter(remote_id=activity.object).exists()
def save(self, *args, **kwargs):
""" update user active time """
"""update user active time"""
self.user.last_active_date = timezone.now()
self.user.save(broadcast=False)
super().save(*args, **kwargs)
@ -45,7 +45,7 @@ class Favorite(ActivityMixin, BookWyrmModel):
)
def delete(self, *args, **kwargs):
""" delete and delete notifications """
"""delete and delete notifications"""
# check for notification
if self.status.user.local:
notification_model = apps.get_model(
@ -62,6 +62,6 @@ class Favorite(ActivityMixin, BookWyrmModel):
super().delete(*args, **kwargs)
class Meta:
""" can't fav things twice """
"""can't fav things twice"""
unique_together = ("user", "status")

View file

@ -13,7 +13,7 @@ FederationStatus = models.TextChoices(
class FederatedServer(BookWyrmModel):
""" store which servers we federate with """
"""store which servers we federate with"""
server_name = models.CharField(max_length=255, unique=True)
status = models.CharField(
@ -25,7 +25,7 @@ class FederatedServer(BookWyrmModel):
notes = models.TextField(null=True, blank=True)
def block(self):
""" block a server """
"""block a server"""
self.status = "blocked"
self.save()
@ -35,7 +35,7 @@ class FederatedServer(BookWyrmModel):
)
def unblock(self):
""" unblock a server """
"""unblock a server"""
self.status = "federated"
self.save()
@ -45,7 +45,7 @@ class FederatedServer(BookWyrmModel):
@classmethod
def is_blocked(cls, url):
""" look up if a domain is blocked """
"""look up if a domain is blocked"""
url = urlparse(url)
domain = url.netloc
return cls.objects.filter(server_name=domain, status="blocked").exists()

View file

@ -18,7 +18,7 @@ from bookwyrm.settings import DOMAIN
def validate_remote_id(value):
""" make sure the remote_id looks like a url """
"""make sure the remote_id looks like a url"""
if not value or not re.match(r"^http.?:\/\/[^\s]+$", value):
raise ValidationError(
_("%(value)s is not a valid remote_id"),
@ -27,7 +27,7 @@ def validate_remote_id(value):
def validate_localname(value):
""" make sure localnames look okay """
"""make sure localnames look okay"""
if not re.match(r"^[A-Za-z\-_\.0-9]+$", value):
raise ValidationError(
_("%(value)s is not a valid username"),
@ -36,7 +36,7 @@ def validate_localname(value):
def validate_username(value):
""" make sure usernames look okay """
"""make sure usernames look okay"""
if not re.match(r"^[A-Za-z\-_\.0-9]+@[A-Za-z\-_\.0-9]+\.[a-z]{2,}$", value):
raise ValidationError(
_("%(value)s is not a valid username"),
@ -45,7 +45,7 @@ def validate_username(value):
class ActivitypubFieldMixin:
""" make a database field serializable """
"""make a database field serializable"""
def __init__(
self,
@ -64,7 +64,7 @@ class ActivitypubFieldMixin:
super().__init__(*args, **kwargs)
def set_field_from_activity(self, instance, data):
""" helper function for assinging a value to the field """
"""helper function for assinging a value to the field"""
try:
value = getattr(data, self.get_activitypub_field())
except AttributeError:
@ -78,7 +78,7 @@ class ActivitypubFieldMixin:
setattr(instance, self.name, formatted)
def set_activity_from_field(self, activity, instance):
""" update the json object """
"""update the json object"""
value = getattr(instance, self.name)
formatted = self.field_to_activity(value)
if formatted is None:
@ -94,19 +94,19 @@ class ActivitypubFieldMixin:
activity[key] = formatted
def field_to_activity(self, value):
""" formatter to convert a model value into activitypub """
"""formatter to convert a model value into activitypub"""
if hasattr(self, "activitypub_wrapper"):
return {self.activitypub_wrapper: value}
return value
def field_from_activity(self, value):
""" formatter to convert activitypub into a model value """
"""formatter to convert activitypub into a model value"""
if value and hasattr(self, "activitypub_wrapper"):
value = value.get(self.activitypub_wrapper)
return value
def get_activitypub_field(self):
""" model_field_name to activitypubFieldName """
"""model_field_name to activitypubFieldName"""
if self.activitypub_field:
return self.activitypub_field
name = self.name.split(".")[-1]
@ -115,7 +115,7 @@ class ActivitypubFieldMixin:
class ActivitypubRelatedFieldMixin(ActivitypubFieldMixin):
""" default (de)serialization for foreign key and one to one """
"""default (de)serialization for foreign key and one to one"""
def __init__(self, *args, load_remote=True, **kwargs):
self.load_remote = load_remote
@ -146,7 +146,7 @@ class ActivitypubRelatedFieldMixin(ActivitypubFieldMixin):
class RemoteIdField(ActivitypubFieldMixin, models.CharField):
""" a url that serves as a unique identifier """
"""a url that serves as a unique identifier"""
def __init__(self, *args, max_length=255, validators=None, **kwargs):
validators = validators or [validate_remote_id]
@ -156,7 +156,7 @@ class RemoteIdField(ActivitypubFieldMixin, models.CharField):
class UsernameField(ActivitypubFieldMixin, models.CharField):
""" activitypub-aware username field """
"""activitypub-aware username field"""
def __init__(self, activitypub_field="preferredUsername", **kwargs):
self.activitypub_field = activitypub_field
@ -172,7 +172,7 @@ class UsernameField(ActivitypubFieldMixin, models.CharField):
)
def deconstruct(self):
""" implementation of models.Field deconstruct """
"""implementation of models.Field deconstruct"""
name, path, args, kwargs = super().deconstruct()
del kwargs["verbose_name"]
del kwargs["max_length"]
@ -191,7 +191,7 @@ PrivacyLevels = models.TextChoices(
class PrivacyField(ActivitypubFieldMixin, models.CharField):
""" this maps to two differente activitypub fields """
"""this maps to two differente activitypub fields"""
public = "https://www.w3.org/ns/activitystreams#Public"
@ -236,7 +236,7 @@ class PrivacyField(ActivitypubFieldMixin, models.CharField):
class ForeignKey(ActivitypubRelatedFieldMixin, models.ForeignKey):
""" activitypub-aware foreign key field """
"""activitypub-aware foreign key field"""
def field_to_activity(self, value):
if not value:
@ -245,7 +245,7 @@ class ForeignKey(ActivitypubRelatedFieldMixin, models.ForeignKey):
class OneToOneField(ActivitypubRelatedFieldMixin, models.OneToOneField):
""" activitypub-aware foreign key field """
"""activitypub-aware foreign key field"""
def field_to_activity(self, value):
if not value:
@ -254,14 +254,14 @@ class OneToOneField(ActivitypubRelatedFieldMixin, models.OneToOneField):
class ManyToManyField(ActivitypubFieldMixin, models.ManyToManyField):
""" activitypub-aware many to many field """
"""activitypub-aware many to many field"""
def __init__(self, *args, link_only=False, **kwargs):
self.link_only = link_only
super().__init__(*args, **kwargs)
def set_field_from_activity(self, instance, data):
""" helper function for assinging a value to the field """
"""helper function for assinging a value to the field"""
value = getattr(data, self.get_activitypub_field())
formatted = self.field_from_activity(value)
if formatted is None or formatted is MISSING:
@ -293,7 +293,7 @@ class ManyToManyField(ActivitypubFieldMixin, models.ManyToManyField):
class TagField(ManyToManyField):
""" special case of many to many that uses Tags """
"""special case of many to many that uses Tags"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@ -333,7 +333,7 @@ class TagField(ManyToManyField):
def image_serializer(value, alt):
""" helper for serializing images """
"""helper for serializing images"""
if value and hasattr(value, "url"):
url = value.url
else:
@ -343,7 +343,7 @@ def image_serializer(value, alt):
class ImageField(ActivitypubFieldMixin, models.ImageField):
""" activitypub-aware image field """
"""activitypub-aware image field"""
def __init__(self, *args, alt_field=None, **kwargs):
self.alt_field = alt_field
@ -351,7 +351,7 @@ class ImageField(ActivitypubFieldMixin, models.ImageField):
# pylint: disable=arguments-differ
def set_field_from_activity(self, instance, data, save=True):
""" helper function for assinging a value to the field """
"""helper function for assinging a value to the field"""
value = getattr(data, self.get_activitypub_field())
formatted = self.field_from_activity(value)
if formatted is None or formatted is MISSING:
@ -397,7 +397,7 @@ class ImageField(ActivitypubFieldMixin, models.ImageField):
class DateTimeField(ActivitypubFieldMixin, models.DateTimeField):
""" activitypub-aware datetime field """
"""activitypub-aware datetime field"""
def field_to_activity(self, value):
if not value:
@ -416,7 +416,7 @@ class DateTimeField(ActivitypubFieldMixin, models.DateTimeField):
class HtmlField(ActivitypubFieldMixin, models.TextField):
""" a text field for storing html """
"""a text field for storing html"""
def field_from_activity(self, value):
if not value or value == MISSING:
@ -427,30 +427,30 @@ class HtmlField(ActivitypubFieldMixin, models.TextField):
class ArrayField(ActivitypubFieldMixin, DjangoArrayField):
""" activitypub-aware array field """
"""activitypub-aware array field"""
def field_to_activity(self, value):
return [str(i) for i in value]
class CharField(ActivitypubFieldMixin, models.CharField):
""" activitypub-aware char field """
"""activitypub-aware char field"""
class TextField(ActivitypubFieldMixin, models.TextField):
""" activitypub-aware text field """
"""activitypub-aware text field"""
class BooleanField(ActivitypubFieldMixin, models.BooleanField):
""" activitypub-aware boolean field """
"""activitypub-aware boolean field"""
class IntegerField(ActivitypubFieldMixin, models.IntegerField):
""" activitypub-aware boolean field """
"""activitypub-aware boolean field"""
class DecimalField(ActivitypubFieldMixin, models.DecimalField):
""" activitypub-aware boolean field """
"""activitypub-aware boolean field"""
def field_to_activity(self, value):
if not value:

View file

@ -20,7 +20,7 @@ GOODREADS_SHELVES = {
def unquote_string(text):
""" resolve csv quote weirdness """
"""resolve csv quote weirdness"""
match = re.match(r'="([^"]*)"', text)
if match:
return match.group(1)
@ -28,7 +28,7 @@ def unquote_string(text):
def construct_search_term(title, author):
""" formulate a query for the data connector """
"""formulate a query for the data connector"""
# Strip brackets (usually series title from search term)
title = re.sub(r"\s*\([^)]*\)\s*", "", title)
# Open library doesn't like including author initials in search term.
@ -38,7 +38,7 @@ def construct_search_term(title, author):
class ImportJob(models.Model):
""" entry for a specific request for book data import """
"""entry for a specific request for book data import"""
user = models.ForeignKey(User, on_delete=models.CASCADE)
created_date = models.DateTimeField(default=timezone.now)
@ -51,7 +51,7 @@ class ImportJob(models.Model):
retry = models.BooleanField(default=False)
def save(self, *args, **kwargs):
""" save and notify """
"""save and notify"""
super().save(*args, **kwargs)
if self.complete:
notification_model = apps.get_model(
@ -65,7 +65,7 @@ class ImportJob(models.Model):
class ImportItem(models.Model):
""" a single line of a csv being imported """
"""a single line of a csv being imported"""
job = models.ForeignKey(ImportJob, on_delete=models.CASCADE, related_name="items")
index = models.IntegerField()
@ -74,11 +74,11 @@ class ImportItem(models.Model):
fail_reason = models.TextField(null=True)
def resolve(self):
""" try various ways to lookup a book """
"""try various ways to lookup a book"""
self.book = self.get_book_from_isbn() or self.get_book_from_title_author()
def get_book_from_isbn(self):
""" search by isbn """
"""search by isbn"""
search_result = connector_manager.first_search_result(
self.isbn, min_confidence=0.999
)
@ -88,7 +88,7 @@ class ImportItem(models.Model):
return None
def get_book_from_title_author(self):
""" search by title and author """
"""search by title and author"""
search_term = construct_search_term(self.title, self.author)
search_result = connector_manager.first_search_result(
search_term, min_confidence=0.999
@ -100,60 +100,60 @@ class ImportItem(models.Model):
@property
def title(self):
""" get the book title """
"""get the book title"""
return self.data["Title"]
@property
def author(self):
""" get the book title """
"""get the book title"""
return self.data["Author"]
@property
def isbn(self):
""" pulls out the isbn13 field from the csv line data """
"""pulls out the isbn13 field from the csv line data"""
return unquote_string(self.data["ISBN13"])
@property
def shelf(self):
""" the goodreads shelf field """
"""the goodreads shelf field"""
if self.data["Exclusive Shelf"]:
return GOODREADS_SHELVES.get(self.data["Exclusive Shelf"])
return None
@property
def review(self):
""" a user-written review, to be imported with the book data """
"""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 """
"""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 """
"""when the book was added to this dataset"""
if self.data["Date Added"]:
return timezone.make_aware(dateutil.parser.parse(self.data["Date Added"]))
return None
@property
def date_started(self):
""" when the book was started """
"""when the book was started"""
if "Date Started" in self.data and self.data["Date Started"]:
return timezone.make_aware(dateutil.parser.parse(self.data["Date Started"]))
return None
@property
def date_read(self):
""" the date a book was completed """
"""the date a book was completed"""
if self.data["Date Read"]:
return timezone.make_aware(dateutil.parser.parse(self.data["Date Read"]))
return None
@property
def reads(self):
""" formats a read through dataset for the book in this line """
"""formats a read through dataset for the book in this line"""
start_date = self.date_started
# Goodreads special case (no 'date started' field)

View file

@ -21,7 +21,7 @@ CurationType = models.TextChoices(
class List(OrderedCollectionMixin, BookWyrmModel):
""" a list of books """
"""a list of books"""
name = fields.CharField(max_length=100)
user = fields.ForeignKey(
@ -41,22 +41,22 @@ class List(OrderedCollectionMixin, BookWyrmModel):
activity_serializer = activitypub.BookList
def get_remote_id(self):
""" don't want the user to be in there in this case """
"""don't want the user to be in there in this case"""
return "https://%s/list/%d" % (DOMAIN, self.id)
@property
def collection_queryset(self):
""" list of books for this shelf, overrides OrderedCollectionMixin """
"""list of books for this shelf, overrides OrderedCollectionMixin"""
return self.books.filter(listitem__approved=True).order_by("listitem")
class Meta:
""" default sorting """
"""default sorting"""
ordering = ("-updated_date",)
class ListItem(CollectionItemMixin, BookWyrmModel):
""" ok """
"""ok"""
book = fields.ForeignKey(
"Edition", on_delete=models.PROTECT, activitypub_field="book"
@ -74,7 +74,7 @@ class ListItem(CollectionItemMixin, BookWyrmModel):
collection_field = "book_list"
def save(self, *args, **kwargs):
""" create a notification too """
"""create a notification too"""
created = not bool(self.id)
super().save(*args, **kwargs)
# tick the updated date on the parent list

View file

@ -10,7 +10,7 @@ NotificationType = models.TextChoices(
class Notification(BookWyrmModel):
""" you've been tagged, liked, followed, etc """
"""you've been tagged, liked, followed, etc"""
user = models.ForeignKey("User", on_delete=models.CASCADE)
related_book = models.ForeignKey("Edition", on_delete=models.CASCADE, null=True)
@ -29,7 +29,7 @@ class Notification(BookWyrmModel):
)
def save(self, *args, **kwargs):
""" save, but don't make dupes """
"""save, but don't make dupes"""
# there's probably a better way to do this
if self.__class__.objects.filter(
user=self.user,
@ -45,7 +45,7 @@ class Notification(BookWyrmModel):
super().save(*args, **kwargs)
class Meta:
""" checks if notifcation is in enum list for valid types """
"""checks if notifcation is in enum list for valid types"""
constraints = [
models.CheckConstraint(

View file

@ -7,14 +7,14 @@ from .base_model import BookWyrmModel
class ProgressMode(models.TextChoices):
""" types of prgress available """
"""types of prgress available"""
PAGE = "PG", "page"
PERCENT = "PCT", "percent"
class ReadThrough(BookWyrmModel):
""" Store a read through a book in the database. """
"""Store a read through a book in the database."""
user = models.ForeignKey("User", on_delete=models.PROTECT)
book = models.ForeignKey("Edition", on_delete=models.PROTECT)
@ -28,13 +28,13 @@ class ReadThrough(BookWyrmModel):
finish_date = models.DateTimeField(blank=True, null=True)
def save(self, *args, **kwargs):
""" update user active time """
"""update user active time"""
self.user.last_active_date = timezone.now()
self.user.save(broadcast=False)
super().save(*args, **kwargs)
def create_update(self):
""" add update to the readthrough """
"""add update to the readthrough"""
if self.progress:
return self.progressupdate_set.create(
user=self.user, progress=self.progress, mode=self.progress_mode
@ -43,7 +43,7 @@ class ReadThrough(BookWyrmModel):
class ProgressUpdate(BookWyrmModel):
""" Store progress through a book in the database. """
"""Store progress through a book in the database."""
user = models.ForeignKey("User", on_delete=models.PROTECT)
readthrough = models.ForeignKey("ReadThrough", on_delete=models.CASCADE)
@ -53,7 +53,7 @@ class ProgressUpdate(BookWyrmModel):
)
def save(self, *args, **kwargs):
""" update user active time """
"""update user active time"""
self.user.last_active_date = timezone.now()
self.user.save(broadcast=False)
super().save(*args, **kwargs)

View file

@ -11,7 +11,7 @@ from . import fields
class UserRelationship(BookWyrmModel):
""" many-to-many through table for followers """
"""many-to-many through table for followers"""
user_subject = fields.ForeignKey(
"User",
@ -28,16 +28,16 @@ class UserRelationship(BookWyrmModel):
@property
def privacy(self):
""" all relationships are handled directly with the participants """
"""all relationships are handled directly with the participants"""
return "direct"
@property
def recipients(self):
""" the remote user needs to recieve direct broadcasts """
"""the remote user needs to recieve direct broadcasts"""
return [u for u in [self.user_subject, self.user_object] if not u.local]
class Meta:
""" relationships should be unique """
"""relationships should be unique"""
abstract = True
constraints = [
@ -51,22 +51,22 @@ class UserRelationship(BookWyrmModel):
]
def get_remote_id(self):
""" use shelf identifier in remote_id """
"""use shelf identifier in remote_id"""
base_path = self.user_subject.remote_id
return "%s#follows/%d" % (base_path, self.id)
class UserFollows(ActivityMixin, UserRelationship):
""" Following a user """
"""Following a user"""
status = "follows"
def to_activity(self): # pylint: disable=arguments-differ
""" overrides default to manually set serializer """
"""overrides default to manually set serializer"""
return activitypub.Follow(**generate_activity(self))
def save(self, *args, **kwargs):
""" really really don't let a user follow someone who blocked them """
"""really really don't let a user follow someone who blocked them"""
# blocking in either direction is a no-go
if UserBlocks.objects.filter(
Q(
@ -85,7 +85,7 @@ class UserFollows(ActivityMixin, UserRelationship):
@classmethod
def from_request(cls, follow_request):
""" converts a follow request into a follow relationship """
"""converts a follow request into a follow relationship"""
return cls.objects.create(
user_subject=follow_request.user_subject,
user_object=follow_request.user_object,
@ -94,13 +94,13 @@ class UserFollows(ActivityMixin, UserRelationship):
class UserFollowRequest(ActivitypubMixin, UserRelationship):
""" following a user requires manual or automatic confirmation """
"""following a user requires manual or automatic confirmation"""
status = "follow_request"
activity_serializer = activitypub.Follow
def save(self, *args, broadcast=True, **kwargs):
""" make sure the follow or block relationship doesn't already exist """
"""make sure the follow or block relationship doesn't already exist"""
# if there's a request for a follow that already exists, accept it
# without changing the local database state
if UserFollows.objects.filter(
@ -141,13 +141,13 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship):
)
def get_accept_reject_id(self, status):
""" get id for sending an accept or reject of a local user """
"""get id for sending an accept or reject of a local user"""
base_path = self.user_object.remote_id
return "%s#%s/%d" % (base_path, status, self.id or 0)
def accept(self, broadcast_only=False):
""" turn this request into the real deal"""
"""turn this request into the real deal"""
user = self.user_object
if not self.user_subject.local:
activity = activitypub.Accept(
@ -164,7 +164,7 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship):
self.delete()
def reject(self):
""" generate a Reject for this follow request """
"""generate a Reject for this follow request"""
if self.user_object.local:
activity = activitypub.Reject(
id=self.get_accept_reject_id(status="rejects"),
@ -177,13 +177,13 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship):
class UserBlocks(ActivityMixin, UserRelationship):
""" prevent another user from following you and seeing your posts """
"""prevent another user from following you and seeing your posts"""
status = "blocks"
activity_serializer = activitypub.Block
def save(self, *args, **kwargs):
""" remove follow or follow request rels after a block is created """
"""remove follow or follow request rels after a block is created"""
super().save(*args, **kwargs)
UserFollows.objects.filter(

View file

@ -6,7 +6,7 @@ from .base_model import BookWyrmModel
class Report(BookWyrmModel):
""" reported status or user """
"""reported status or user"""
reporter = models.ForeignKey(
"User", related_name="reporter", on_delete=models.PROTECT
@ -17,7 +17,7 @@ class Report(BookWyrmModel):
resolved = models.BooleanField(default=False)
def save(self, *args, **kwargs):
""" notify admins when a report is created """
"""notify admins when a report is created"""
super().save(*args, **kwargs)
user_model = apps.get_model("bookwyrm.User", require_ready=True)
# moderators and superusers should be notified
@ -34,7 +34,7 @@ class Report(BookWyrmModel):
)
class Meta:
""" don't let users report themselves """
"""don't let users report themselves"""
constraints = [
models.CheckConstraint(check=~Q(reporter=F("user")), name="self_report")
@ -43,13 +43,13 @@ class Report(BookWyrmModel):
class ReportComment(BookWyrmModel):
""" updates on a report """
"""updates on a report"""
user = models.ForeignKey("User", on_delete=models.PROTECT)
note = models.TextField()
report = models.ForeignKey(Report, on_delete=models.PROTECT)
class Meta:
""" sort comments """
"""sort comments"""
ordering = ("-created_date",)

View file

@ -9,7 +9,7 @@ from . import fields
class Shelf(OrderedCollectionMixin, BookWyrmModel):
""" a list of books owned by a user """
"""a list of books owned by a user"""
TO_READ = "to-read"
READING = "reading"
@ -34,36 +34,36 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel):
activity_serializer = activitypub.Shelf
def save(self, *args, **kwargs):
""" set the identifier """
"""set the identifier"""
super().save(*args, **kwargs)
if not self.identifier:
self.identifier = self.get_identifier()
super().save(*args, **kwargs, broadcast=False)
def get_identifier(self):
""" custom-shelf-123 for the url """
"""custom-shelf-123 for the url"""
slug = re.sub(r"[^\w]", "", self.name).lower()
return "{:s}-{:d}".format(slug, self.id)
@property
def collection_queryset(self):
""" list of books for this shelf, overrides OrderedCollectionMixin """
"""list of books for this shelf, overrides OrderedCollectionMixin"""
return self.books.order_by("shelfbook")
def get_remote_id(self):
""" shelf identifier instead of id """
"""shelf identifier instead of id"""
base_path = self.user.remote_id
identifier = self.identifier or self.get_identifier()
return "%s/books/%s" % (base_path, identifier)
class Meta:
""" user/shelf unqiueness """
"""user/shelf unqiueness"""
unique_together = ("user", "identifier")
class ShelfBook(CollectionItemMixin, BookWyrmModel):
""" many to many join table for books and shelves """
"""many to many join table for books and shelves"""
book = fields.ForeignKey(
"Edition", on_delete=models.PROTECT, activitypub_field="book"

View file

@ -12,7 +12,7 @@ from .user import User
class SiteSettings(models.Model):
""" customized settings for this instance """
"""customized settings for this instance"""
name = models.CharField(default="BookWyrm", max_length=100)
instance_tagline = models.CharField(
@ -35,7 +35,7 @@ class SiteSettings(models.Model):
@classmethod
def get(cls):
""" gets the site settings db entry or defaults """
"""gets the site settings db entry or defaults"""
try:
return cls.objects.get(id=1)
except cls.DoesNotExist:
@ -45,12 +45,12 @@ class SiteSettings(models.Model):
def new_access_code():
""" the identifier for a user invite """
"""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 """
"""gives someone access to create an account on the instance"""
created_date = models.DateTimeField(auto_now_add=True)
code = models.CharField(max_length=32, default=new_access_code)
@ -61,19 +61,19 @@ class SiteInvite(models.Model):
invitees = models.ManyToManyField(User, related_name="invitees")
def valid(self):
""" make sure it hasn't expired or been used """
"""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 """
"""formats the invite link"""
return "https://{}/invite/{}".format(DOMAIN, self.code)
class InviteRequest(BookWyrmModel):
""" prospective users can request an invite """
"""prospective users can request an invite"""
email = models.EmailField(max_length=255, unique=True)
invite = models.ForeignKey(
@ -83,30 +83,30 @@ class InviteRequest(BookWyrmModel):
ignored = models.BooleanField(default=False)
def save(self, *args, **kwargs):
""" don't create a request for a registered email """
"""don't create a request for a registered email"""
if not self.id and User.objects.filter(email=self.email).exists():
raise IntegrityError()
super().save(*args, **kwargs)
def get_passowrd_reset_expiry():
""" give people a limited time to use the link """
"""give people a limited time to use the link"""
now = timezone.now()
return now + datetime.timedelta(days=1)
class PasswordReset(models.Model):
""" gives someone access to create an account on the instance """
"""gives someone access to create an account on the instance"""
code = models.CharField(max_length=32, default=new_access_code)
expiry = models.DateTimeField(default=get_passowrd_reset_expiry)
user = models.OneToOneField(User, on_delete=models.CASCADE)
def valid(self):
""" make sure it hasn't expired or been used """
"""make sure it hasn't expired or been used"""
return self.expiry > timezone.now()
@property
def link(self):
""" formats the invite link """
"""formats the invite link"""
return "https://{}/password-reset/{}".format(DOMAIN, self.code)

View file

@ -19,7 +19,7 @@ from . import fields
class Status(OrderedCollectionPageMixin, BookWyrmModel):
""" any post, like a reply to a review, etc """
"""any post, like a reply to a review, etc"""
user = fields.ForeignKey(
"User", on_delete=models.PROTECT, activitypub_field="attributedTo"
@ -59,12 +59,12 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
deserialize_reverse_fields = [("attachments", "attachment")]
class Meta:
""" default sorting """
"""default sorting"""
ordering = ("-published_date",)
def save(self, *args, **kwargs):
""" save and notify """
"""save and notify"""
super().save(*args, **kwargs)
notification_model = apps.get_model("bookwyrm.Notification", require_ready=True)
@ -98,7 +98,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
)
def delete(self, *args, **kwargs): # pylint: disable=unused-argument
""" "delete" a status """
""" "delete" a status"""
if hasattr(self, "boosted_status"):
# okay but if it's a boost really delete it
super().delete(*args, **kwargs)
@ -109,7 +109,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
@property
def recipients(self):
""" tagged users who definitely need to get this status in broadcast """
"""tagged users who definitely need to get this status in broadcast"""
mentions = [u for u in self.mention_users.all() if not u.local]
if (
hasattr(self, "reply_parent")
@ -121,7 +121,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
@classmethod
def ignore_activity(cls, activity): # pylint: disable=too-many-return-statements
""" keep notes if they are replies to existing statuses """
"""keep notes if they are replies to existing statuses"""
if activity.type == "Announce":
try:
boosted = activitypub.resolve_remote_id(
@ -163,16 +163,16 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
@property
def status_type(self):
""" expose the type of status for the ui using activity type """
"""expose the type of status for the ui using activity type"""
return self.activity_serializer.__name__
@property
def boostable(self):
""" you can't boost dms """
"""you can't boost dms"""
return self.privacy in ["unlisted", "public"]
def to_replies(self, **kwargs):
""" helper function for loading AP serialized replies to a status """
"""helper function for loading AP serialized replies to a status"""
return self.to_ordered_collection(
self.replies(self),
remote_id="%s/replies" % self.remote_id,
@ -181,7 +181,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
).serialize()
def to_activity_dataclass(self, pure=False): # pylint: disable=arguments-differ
""" return tombstone if the status is deleted """
"""return tombstone if the status is deleted"""
if self.deleted:
return activitypub.Tombstone(
id=self.remote_id,
@ -210,16 +210,16 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
return activity
def to_activity(self, pure=False): # pylint: disable=arguments-differ
""" json serialized activitypub class """
"""json serialized activitypub class"""
return self.to_activity_dataclass(pure=pure).serialize()
class GeneratedNote(Status):
""" these are app-generated messages about user activity """
"""these are app-generated messages about user activity"""
@property
def pure_content(self):
""" indicate the book in question for mastodon (or w/e) users """
"""indicate the book in question for mastodon (or w/e) users"""
message = self.content
books = ", ".join(
'<a href="%s">"%s"</a>' % (book.remote_id, book.title)
@ -232,7 +232,7 @@ class GeneratedNote(Status):
class Comment(Status):
""" like a review but without a rating and transient """
"""like a review but without a rating and transient"""
book = fields.ForeignKey(
"Edition", on_delete=models.PROTECT, activitypub_field="inReplyToBook"
@ -253,7 +253,7 @@ class Comment(Status):
@property
def pure_content(self):
""" indicate the book in question for mastodon (or w/e) users """
"""indicate the book in question for mastodon (or w/e) users"""
return '%s<p>(comment on <a href="%s">"%s"</a>)</p>' % (
self.content,
self.book.remote_id,
@ -265,7 +265,7 @@ class Comment(Status):
class Quotation(Status):
""" like a review but without a rating and transient """
"""like a review but without a rating and transient"""
quote = fields.HtmlField()
book = fields.ForeignKey(
@ -274,7 +274,7 @@ class Quotation(Status):
@property
def pure_content(self):
""" indicate the book in question for mastodon (or w/e) users """
"""indicate the book in question for mastodon (or w/e) users"""
quote = re.sub(r"^<p>", '<p>"', self.quote)
quote = re.sub(r"</p>$", '"</p>', quote)
return '%s <p>-- <a href="%s">"%s"</a></p>%s' % (
@ -289,7 +289,7 @@ class Quotation(Status):
class Review(Status):
""" a book review """
"""a book review"""
name = fields.CharField(max_length=255, null=True)
book = fields.ForeignKey(
@ -306,7 +306,7 @@ class Review(Status):
@property
def pure_name(self):
""" clarify review names for mastodon serialization """
"""clarify review names for mastodon serialization"""
template = get_template("snippets/generated_status/review_pure_name.html")
return template.render(
{"book": self.book, "rating": self.rating, "name": self.name}
@ -314,7 +314,7 @@ class Review(Status):
@property
def pure_content(self):
""" indicate the book in question for mastodon (or w/e) users """
"""indicate the book in question for mastodon (or w/e) users"""
return self.content
activity_serializer = activitypub.Review
@ -322,7 +322,7 @@ class Review(Status):
class ReviewRating(Review):
""" a subtype of review that only contains a rating """
"""a subtype of review that only contains a rating"""
def save(self, *args, **kwargs):
if not self.rating:
@ -339,7 +339,7 @@ class ReviewRating(Review):
class Boost(ActivityMixin, Status):
""" boost'ing a post """
"""boost'ing a post"""
boosted_status = fields.ForeignKey(
"Status",
@ -350,7 +350,7 @@ class Boost(ActivityMixin, Status):
activity_serializer = activitypub.Announce
def save(self, *args, **kwargs):
""" save and notify """
"""save and notify"""
# This constraint can't work as it would cross tables.
# class Meta:
# unique_together = ('user', 'boosted_status')
@ -374,7 +374,7 @@ class Boost(ActivityMixin, Status):
)
def delete(self, *args, **kwargs):
""" delete and un-notify """
"""delete and un-notify"""
notification_model = apps.get_model("bookwyrm.Notification", require_ready=True)
notification_model.objects.filter(
user=self.boosted_status.user,
@ -385,7 +385,7 @@ class Boost(ActivityMixin, Status):
super().delete(*args, **kwargs)
def __init__(self, *args, **kwargs):
""" the user field is "actor" here instead of "attributedTo" """
"""the user field is "actor" here instead of "attributedTo" """
super().__init__(*args, **kwargs)
reserve_fields = ["user", "boosted_status", "published_date", "privacy"]

View file

@ -35,7 +35,7 @@ DeactivationReason = models.TextChoices(
class User(OrderedCollectionPageMixin, AbstractUser):
""" a user who wants to read books """
"""a user who wants to read books"""
username = fields.UsernameField()
email = models.EmailField(unique=True, null=True)
@ -130,38 +130,38 @@ class User(OrderedCollectionPageMixin, AbstractUser):
@property
def following_link(self):
""" just how to find out the following info """
"""just how to find out the following info"""
return "{:s}/following".format(self.remote_id)
@property
def alt_text(self):
""" alt text with username """
"""alt text with username"""
return "avatar for %s" % (self.localname or self.username)
@property
def display_name(self):
""" show the cleanest version of the user's name possible """
"""show the cleanest version of the user's name possible"""
if self.name and self.name != "":
return self.name
return self.localname or self.username
@property
def deleted(self):
""" for consistent naming """
"""for consistent naming"""
return not self.is_active
activity_serializer = activitypub.Person
@classmethod
def viewer_aware_objects(cls, viewer):
""" the user queryset filtered for the context of the logged in user """
"""the user queryset filtered for the context of the logged in user"""
queryset = cls.objects.filter(is_active=True)
if viewer and viewer.is_authenticated:
queryset = queryset.exclude(blocks=viewer)
return queryset
def to_outbox(self, filter_type=None, **kwargs):
""" an ordered collection of statuses """
"""an ordered collection of statuses"""
if filter_type:
filter_class = apps.get_model(
"bookwyrm.%s" % filter_type, require_ready=True
@ -188,7 +188,7 @@ class User(OrderedCollectionPageMixin, AbstractUser):
).serialize()
def to_following_activity(self, **kwargs):
""" activitypub following list """
"""activitypub following list"""
remote_id = "%s/following" % self.remote_id
return self.to_ordered_collection(
self.following.order_by("-updated_date").all(),
@ -198,7 +198,7 @@ class User(OrderedCollectionPageMixin, AbstractUser):
)
def to_followers_activity(self, **kwargs):
""" activitypub followers list """
"""activitypub followers list"""
remote_id = "%s/followers" % self.remote_id
return self.to_ordered_collection(
self.followers.order_by("-updated_date").all(),
@ -227,7 +227,7 @@ class User(OrderedCollectionPageMixin, AbstractUser):
return activity_object
def save(self, *args, **kwargs):
""" populate fields for new local users """
"""populate fields for new local users"""
created = not bool(self.id)
if not self.local and not re.match(regex.full_username, self.username):
# generate a username that uses the domain (webfinger format)
@ -292,19 +292,19 @@ class User(OrderedCollectionPageMixin, AbstractUser):
).save(broadcast=False)
def delete(self, *args, **kwargs):
""" deactivate rather than delete a user """
"""deactivate rather than delete a user"""
self.is_active = False
# skip the logic in this class's save()
super().save(*args, **kwargs)
@property
def local_path(self):
""" this model doesn't inherit bookwyrm model, so here we are """
"""this model doesn't inherit bookwyrm model, so here we are"""
return "/user/%s" % (self.localname or self.username)
class KeyPair(ActivitypubMixin, BookWyrmModel):
""" public and private keys for a user """
"""public and private keys for a user"""
private_key = models.TextField(blank=True, null=True)
public_key = fields.TextField(
@ -319,7 +319,7 @@ class KeyPair(ActivitypubMixin, BookWyrmModel):
return "%s/#main-key" % self.owner.remote_id
def save(self, *args, **kwargs):
""" create a key pair """
"""create a key pair"""
# no broadcasting happening here
if "broadcast" in kwargs:
del kwargs["broadcast"]
@ -337,7 +337,7 @@ class KeyPair(ActivitypubMixin, BookWyrmModel):
class AnnualGoal(BookWyrmModel):
""" set a goal for how many books you read in a year """
"""set a goal for how many books you read in a year"""
user = models.ForeignKey("User", on_delete=models.PROTECT)
goal = models.IntegerField(validators=[MinValueValidator(1)])
@ -347,17 +347,17 @@ class AnnualGoal(BookWyrmModel):
)
class Meta:
""" unqiueness constraint """
"""unqiueness constraint"""
unique_together = ("user", "year")
def get_remote_id(self):
""" put the year in the path """
"""put the year in the path"""
return "%s/goal/%d" % (self.user.remote_id, self.year)
@property
def books(self):
""" the books you've read this year """
"""the books you've read this year"""
return (
self.user.readthrough_set.filter(finish_date__year__gte=self.year)
.order_by("-finish_date")
@ -366,7 +366,7 @@ class AnnualGoal(BookWyrmModel):
@property
def ratings(self):
""" ratings for books read this year """
"""ratings for books read this year"""
book_ids = [r.book.id for r in self.books]
reviews = Review.objects.filter(
user=self.user,
@ -376,12 +376,12 @@ class AnnualGoal(BookWyrmModel):
@property
def progress_percent(self):
""" how close to your goal, in percent form """
"""how close to your goal, in percent form"""
return int(float(self.book_count / self.goal) * 100)
@property
def book_count(self):
""" how many books you've read this year """
"""how many books you've read this year"""
return self.user.readthrough_set.filter(
finish_date__year__gte=self.year
).count()
@ -389,7 +389,7 @@ class AnnualGoal(BookWyrmModel):
@app.task
def set_remote_server(user_id):
""" figure out the user's remote server in the background """
"""figure out the user's remote server in the background"""
user = User.objects.get(id=user_id)
actor_parts = urlparse(user.remote_id)
user.federated_server = get_or_create_remote_server(actor_parts.netloc)
@ -399,7 +399,7 @@ def set_remote_server(user_id):
def get_or_create_remote_server(domain):
""" get info on a remote server """
"""get info on a remote server"""
try:
return FederatedServer.objects.get(server_name=domain)
except FederatedServer.DoesNotExist:
@ -428,7 +428,7 @@ def get_or_create_remote_server(domain):
@app.task
def get_remote_reviews(outbox):
""" ingest reviews by a new remote bookwyrm user """
"""ingest reviews by a new remote bookwyrm user"""
outbox_page = outbox + "?page=true&type=Review"
data = get_data(outbox_page)

View file

@ -10,16 +10,16 @@ r = redis.Redis(
class RedisStore(ABC):
""" sets of ranked, related objects, like statuses for a user's feed """
"""sets of ranked, related objects, like statuses for a user's feed"""
max_length = settings.MAX_STREAM_LENGTH
def get_value(self, obj):
""" the object and rank """
"""the object and rank"""
return {obj.id: self.get_rank(obj)}
def add_object_to_related_stores(self, obj, execute=True):
""" add an object to all suitable stores """
"""add an object to all suitable stores"""
value = self.get_value(obj)
# we want to do this as a bulk operation, hence "pipeline"
pipeline = r.pipeline()
@ -34,14 +34,14 @@ class RedisStore(ABC):
return pipeline.execute()
def remove_object_from_related_stores(self, obj):
""" remove an object from all stores """
"""remove an object from all stores"""
pipeline = r.pipeline()
for store in self.get_stores_for_object(obj):
pipeline.zrem(store, -1, obj.id)
pipeline.execute()
def bulk_add_objects_to_store(self, objs, store):
""" add a list of objects to a given store """
"""add a list of objects to a given store"""
pipeline = r.pipeline()
for obj in objs[: self.max_length]:
pipeline.zadd(store, self.get_value(obj))
@ -50,18 +50,18 @@ class RedisStore(ABC):
pipeline.execute()
def bulk_remove_objects_from_store(self, objs, store):
""" remoev a list of objects from a given store """
"""remoev a list of objects from a given store"""
pipeline = r.pipeline()
for obj in objs[: self.max_length]:
pipeline.zrem(store, -1, obj.id)
pipeline.execute()
def get_store(self, store): # pylint: disable=no-self-use
""" load the values in a store """
"""load the values in a store"""
return r.zrevrange(store, 0, -1)
def populate_store(self, store):
""" go from zero to a store """
"""go from zero to a store"""
pipeline = r.pipeline()
queryset = self.get_objects_for_store(store)
@ -75,12 +75,12 @@ class RedisStore(ABC):
@abstractmethod
def get_objects_for_store(self, store):
""" a queryset of what should go in a store, used for populating it """
"""a queryset of what should go in a store, used for populating it"""
@abstractmethod
def get_stores_for_object(self, obj):
""" the stores that an object belongs in """
"""the stores that an object belongs in"""
@abstractmethod
def get_rank(self, obj):
""" how to rank an object """
"""how to rank an object"""

View file

@ -3,7 +3,7 @@ from html.parser import HTMLParser
class InputHtmlParser(HTMLParser): # pylint: disable=abstract-method
""" Removes any html that isn't allowed_tagsed from a block """
"""Removes any html that isn't allowed_tagsed from a block"""
def __init__(self):
HTMLParser.__init__(self)
@ -28,7 +28,7 @@ class InputHtmlParser(HTMLParser): # pylint: disable=abstract-method
self.allow_html = True
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.allowed_tags:
self.output.append(("tag", self.get_starttag_text()))
self.tag_stack.append(tag)
@ -36,7 +36,7 @@ class InputHtmlParser(HTMLParser): # pylint: disable=abstract-method
self.output.append(("data", ""))
def handle_endtag(self, tag):
""" keep the close tag """
"""keep the close tag"""
if not self.allow_html or tag not in self.allowed_tags:
self.output.append(("data", ""))
return
@ -51,11 +51,11 @@ class InputHtmlParser(HTMLParser): # pylint: disable=abstract-method
self.output.append(("tag", "</%s>" % tag))
def handle_data(self, data):
""" extract the answer, if we're in an answer tag """
"""extract the answer, if we're in an answer tag"""
self.output.append(("data", data))
def get_output(self):
""" convert the output from a list of tuples to a string """
"""convert the output from a list of tuples to a string"""
if self.tag_stack:
self.allow_html = False
if not self.allow_html:

View file

@ -34,6 +34,8 @@ LOCALE_PATHS = [
os.path.join(BASE_DIR, "locale"),
]
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/

View file

@ -13,7 +13,7 @@ MAX_SIGNATURE_AGE = 300
def create_key_pair():
""" a new public/private key pair, used for creating new users """
"""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")
@ -23,7 +23,7 @@ def create_key_pair():
def make_signature(sender, destination, date, digest):
""" uses a private key to sign an outgoing message """
"""uses a private key to sign an outgoing message"""
inbox_parts = urlparse(destination)
signature_headers = [
"(request-target): post %s" % inbox_parts.path,
@ -44,14 +44,14 @@ def make_signature(sender, destination, date, digest):
def make_digest(data):
""" creates a message digest for signing """
"""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 """
"""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
@ -66,7 +66,7 @@ def verify_digest(request):
class Signature:
""" read and validate incoming signatures """
"""read and validate incoming signatures"""
def __init__(self, key_id, headers, signature):
self.key_id = key_id
@ -75,7 +75,7 @@ class Signature:
@classmethod
def parse(cls, request):
""" extract and parse a signature from an http request """
"""extract and parse a signature from an http request"""
signature_dict = {}
for pair in request.headers["Signature"].split(","):
k, v = pair.split("=", 1)
@ -92,7 +92,7 @@ class Signature:
return cls(key_id, headers, signature)
def verify(self, public_key, request):
""" verify rsa signature """
"""verify rsa signature"""
if http_date_age(request.headers["date"]) > MAX_SIGNATURE_AGE:
raise ValueError("Request too old: %s" % (request.headers["date"],))
public_key = RSA.import_key(public_key)
@ -118,7 +118,7 @@ class Signature:
def http_date_age(datestr):
""" age of a signature in seconds """
"""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()

View file

@ -6,7 +6,7 @@ from bookwyrm.sanitize_html import InputHtmlParser
def create_generated_note(user, content, mention_books=None, privacy="public"):
""" a note created by the app about user activity """
"""a note created by the app about user activity"""
# sanitize input html
parser = InputHtmlParser()
parser.feed(content)

View file

@ -13,6 +13,16 @@
<div class="columns mt-3">
<section class="column is-three-quarters">
{% if request.GET.updated %}
<div class="notification is-primary">
{% if list.curation != "open" and request.user != list.user %}
{% trans "You successfully suggested a book for this list!" %}
{% else %}
{% trans "You successfully added a book to this list!" %}
{% endif %}
</div>
{% endif %}
{% if not items.object_list.exists %}
<p>{% trans "This list is currently empty" %}</p>
{% else %}

View file

@ -6,7 +6,7 @@
<figure class="media-left" aria-hidden="true">
<a class="image is-48x48" href="{{ status.user.local_path }}">
{% include 'snippets/avatar.html' with user=status.user ariaHide="true" medium="true" %}
</a>
</a>
</figure>
<div class="media-content">
@ -47,7 +47,7 @@
{% if status.book %}
{% if status.status_type == 'GeneratedNote' or status.status_type == 'Rating' %}
<a href="/book/{{ status.book.id }}">{{ status.book|title }}</a>{% if status.status_type == 'Rating' %}:
<a href="/book/{{ status.book.id }}">{{ status.book|title }}</a>{% if status.status_type == 'Rating' %}: {% include 'snippets/stars.html' with rating=status.rating %}
<span
itemprop="reviewRating"
itemscope
@ -71,7 +71,6 @@
<meta itemprop="bestRating" content="5">
</span>
{% include 'snippets/stars.html' with rating=status.rating %}
{% endif %}
{% else %}
{% include 'snippets/book_titleby.html' with book=status.book %}

View file

@ -29,4 +29,6 @@
<div>{% blocktrans with username=user.display_name %}{{ username }} has no followers{% endblocktrans %}</div>
{% endif %}
</div>
{% include 'snippets/pagination.html' with page=followers path=request.path %}
{% endblock %}

View file

@ -29,4 +29,6 @@
<div>{% blocktrans with username=user|username %}{{ username }} isn't following any users{% endblocktrans %}</div>
{% endif %}
</div>
{% include 'snippets/pagination.html' with page=following path=request.path %}
{% endblock %}

View file

@ -13,13 +13,13 @@ register = template.Library()
@register.filter(name="dict_key")
def dict_key(d, k):
""" Returns the given key from a dictionary. """
"""Returns the given key from a dictionary."""
return d.get(k) or 0
@register.filter(name="rating")
def get_rating(book, user):
""" get the overall rating of a book """
"""get the overall rating of a book"""
queryset = views.helpers.privacy_filter(
user, models.Review.objects.filter(book=book)
)
@ -28,7 +28,7 @@ def get_rating(book, user):
@register.filter(name="user_rating")
def get_user_rating(book, user):
""" get a user's rating of a book """
"""get a user's rating of a book"""
rating = (
models.Review.objects.filter(
user=user,
@ -45,19 +45,19 @@ def get_user_rating(book, user):
@register.filter(name="username")
def get_user_identifier(user):
""" use localname for local users, username for remote """
"""use localname for local users, username for remote"""
return user.localname if user.localname else user.username
@register.filter(name="notification_count")
def get_notification_count(user):
""" how many UNREAD notifications are there """
"""how many UNREAD notifications are there"""
return user.notification_set.filter(read=False).count()
@register.filter(name="replies")
def get_replies(status):
""" get all direct replies to a status """
"""get all direct replies to a status"""
# TODO: this limit could cause problems
return models.Status.objects.filter(
reply_parent=status,
@ -67,7 +67,7 @@ def get_replies(status):
@register.filter(name="parent")
def get_parent(status):
""" get the reply parent for a status """
"""get the reply parent for a status"""
return (
models.Status.objects.filter(id=status.reply_parent_id)
.select_subclasses()
@ -77,7 +77,7 @@ def get_parent(status):
@register.filter(name="liked")
def get_user_liked(user, status):
""" did the given user fav a status? """
"""did the given user fav a status?"""
try:
models.Favorite.objects.get(user=user, status=status)
return True
@ -87,13 +87,13 @@ def get_user_liked(user, status):
@register.filter(name="boosted")
def get_user_boosted(user, status):
""" did the given user fav a status? """
"""did the given user fav a status?"""
return user.id in status.boosters.all().values_list("user", flat=True)
@register.filter(name="follow_request_exists")
def follow_request_exists(user, requester):
""" see if there is a pending follow request for a user """
"""see if there is a pending follow request for a user"""
try:
models.UserFollowRequest.objects.filter(
user_subject=requester,
@ -106,7 +106,7 @@ def follow_request_exists(user, requester):
@register.filter(name="boosted_status")
def get_boosted(boost):
""" load a boosted status. have to do this or it wont get foregin keys """
"""load a boosted status. have to do this or it wont get foregin keys"""
return (
models.Status.objects.select_subclasses()
.filter(id=boost.boosted_status.id)
@ -116,19 +116,19 @@ def get_boosted(boost):
@register.filter(name="book_description")
def get_book_description(book):
""" use the work's text if the book doesn't have it """
"""use the work's text if the book doesn't have it"""
return book.description or book.parent_work.description
@register.filter(name="uuid")
def get_uuid(identifier):
""" for avoiding clashing ids when there are many forms """
"""for avoiding clashing ids when there are many forms"""
return "%s%s" % (identifier, uuid4())
@register.filter(name="to_markdown")
def get_markdown(content):
""" convert markdown to html """
"""convert markdown to html"""
if content:
return to_markdown(content)
return None
@ -136,7 +136,7 @@ def get_markdown(content):
@register.filter(name="mentions")
def get_mentions(status, user):
""" people to @ in a reply: the parent and all mentions """
"""people to @ in a reply: the parent and all mentions"""
mentions = set([status.user] + list(status.mention_users.all()))
return (
" ".join("@" + get_user_identifier(m) for m in mentions if not m == user) + " "
@ -145,7 +145,7 @@ def get_mentions(status, user):
@register.filter(name="status_preview_name")
def get_status_preview_name(obj):
""" text snippet with book context for a status """
"""text snippet with book context for a status"""
name = obj.__class__.__name__.lower()
if name == "review":
return "%s of <em>%s</em>" % (name, obj.book.title)
@ -158,7 +158,7 @@ def get_status_preview_name(obj):
@register.filter(name="next_shelf")
def get_next_shelf(current_shelf):
""" shelf you'd use to update reading progress """
"""shelf you'd use to update reading progress"""
if current_shelf == "to-read":
return "reading"
if current_shelf == "reading":
@ -170,7 +170,7 @@ def get_next_shelf(current_shelf):
@register.filter(name="title")
def get_title(book):
""" display the subtitle if the title is short """
"""display the subtitle if the title is short"""
if not book:
return ""
title = book.title
@ -181,7 +181,7 @@ def get_title(book):
@register.simple_tag(takes_context=False)
def related_status(notification):
""" for notifications """
"""for notifications"""
if not notification.related_status:
return None
if hasattr(notification.related_status, "quotation"):
@ -195,7 +195,7 @@ def related_status(notification):
@register.simple_tag(takes_context=True)
def active_shelf(context, book):
""" check what shelf a user has a book on, if any """
"""check what shelf a user has a book on, if any"""
shelf = models.ShelfBook.objects.filter(
shelf__user=context["request"].user, book__in=book.parent_work.editions.all()
).first()
@ -204,7 +204,7 @@ def active_shelf(context, book):
@register.simple_tag(takes_context=False)
def latest_read_through(book, user):
""" the most recent read activity """
"""the most recent read activity"""
return (
models.ReadThrough.objects.filter(user=user, book=book)
.order_by("-start_date")
@ -214,7 +214,7 @@ def latest_read_through(book, user):
@register.simple_tag(takes_context=False)
def active_read_through(book, user):
""" the most recent read activity """
"""the most recent read activity"""
return (
models.ReadThrough.objects.filter(
user=user, book=book, finish_date__isnull=True
@ -226,12 +226,12 @@ def active_read_through(book, user):
@register.simple_tag(takes_context=False)
def comparison_bool(str1, str2):
""" idk why I need to write a tag for this, it reutrns a bool """
"""idk why I need to write a tag for this, it reutrns a bool"""
return str1 == str2
@register.simple_tag(takes_context=False)
def get_lang():
""" get current language, strip to the first two letters """
"""get current language, strip to the first two letters"""
language = utils.translation.get_language()
return language[0 : language.find("-")]

View file

@ -21,10 +21,10 @@ from bookwyrm import models
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
class BaseActivity(TestCase):
""" the super class for model-linked activitypub dataclasses """
"""the super class for model-linked activitypub dataclasses"""
def setUp(self):
""" we're probably going to re-use this so why copy/paste """
"""we're probably going to re-use this so why copy/paste"""
self.user = models.User.objects.create_user(
"mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse"
)
@ -45,28 +45,28 @@ class BaseActivity(TestCase):
self.image_data = output.getvalue()
def test_init(self, _):
""" simple successfuly init """
"""simple successfuly init"""
instance = ActivityObject(id="a", type="b")
self.assertTrue(hasattr(instance, "id"))
self.assertTrue(hasattr(instance, "type"))
def test_init_missing(self, _):
""" init with missing required params """
"""init with missing required params"""
with self.assertRaises(ActivitySerializerError):
ActivityObject()
def test_init_extra_fields(self, _):
""" init ignoring additional fields """
"""init ignoring additional fields"""
instance = ActivityObject(id="a", type="b", fish="c")
self.assertTrue(hasattr(instance, "id"))
self.assertTrue(hasattr(instance, "type"))
def test_init_default_field(self, _):
""" replace an existing required field with a default field """
"""replace an existing required field with a default field"""
@dataclass(init=False)
class TestClass(ActivityObject):
""" test class with default field """
"""test class with default field"""
type: str = "TestObject"
@ -75,7 +75,7 @@ class BaseActivity(TestCase):
self.assertEqual(instance.type, "TestObject")
def test_serialize(self, _):
""" simple function for converting dataclass to dict """
"""simple function for converting dataclass to dict"""
instance = ActivityObject(id="a", type="b")
serialized = instance.serialize()
self.assertIsInstance(serialized, dict)
@ -84,7 +84,7 @@ class BaseActivity(TestCase):
@responses.activate
def test_resolve_remote_id(self, _):
""" look up or load remote data """
"""look up or load remote data"""
# existing item
result = resolve_remote_id("http://example.com/a/b", model=models.User)
self.assertEqual(result, self.user)
@ -106,14 +106,14 @@ class BaseActivity(TestCase):
self.assertEqual(result.name, "MOUSE?? MOUSE!!")
def test_to_model_invalid_model(self, _):
""" catch mismatch between activity type and model type """
"""catch mismatch between activity type and model type"""
instance = ActivityObject(id="a", type="b")
with self.assertRaises(ActivitySerializerError):
instance.to_model(model=models.User)
@responses.activate
def test_to_model_image(self, _):
""" update an image field """
"""update an image field"""
activity = activitypub.Person(
id=self.user.remote_id,
name="New Name",
@ -146,7 +146,7 @@ class BaseActivity(TestCase):
self.assertEqual(self.user.key_pair.public_key, "hi")
def test_to_model_many_to_many(self, _):
""" annoying that these all need special handling """
"""annoying that these all need special handling"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
status = models.Status.objects.create(
content="test status",
@ -216,7 +216,7 @@ class BaseActivity(TestCase):
@responses.activate
def test_set_related_field(self, _):
""" celery task to add back-references to created objects """
"""celery task to add back-references to created objects"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
status = models.Status.objects.create(
content="test status",

View file

@ -8,10 +8,10 @@ from bookwyrm import activitypub, models
class Quotation(TestCase):
""" we have hecka ways to create statuses """
"""we have hecka ways to create statuses"""
def setUp(self):
""" model objects we'll need """
"""model objects we'll need"""
with patch("bookwyrm.models.user.set_remote_server.delay"):
self.user = models.User.objects.create_user(
"mouse",
@ -30,7 +30,7 @@ class Quotation(TestCase):
self.status_data = json.loads(datafile.read_bytes())
def test_quotation_activity(self):
""" create a Quoteation ap object from json """
"""create a Quoteation ap object from json"""
quotation = activitypub.Quotation(**self.status_data)
self.assertEqual(quotation.type, "Quotation")
@ -41,7 +41,7 @@ class Quotation(TestCase):
self.assertEqual(quotation.published, "2020-05-10T02:38:31.150343+00:00")
def test_activity_to_model(self):
""" create a model instance from an activity object """
"""create a model instance from an activity object"""
activity = activitypub.Quotation(**self.status_data)
quotation = activity.to_model(model=models.Quotation)

View file

@ -10,10 +10,10 @@ from bookwyrm.settings import DOMAIN
class AbstractConnector(TestCase):
""" generic code for connecting to outside data sources """
"""generic code for connecting to outside data sources"""
def setUp(self):
""" we need an example connector """
"""we need an example connector"""
self.connector_info = models.Connector.objects.create(
identifier="example.com",
connector_file="openlibrary",
@ -38,7 +38,7 @@ class AbstractConnector(TestCase):
self.edition_data = edition_data
class TestConnector(abstract_connector.AbstractConnector):
""" nothing added here """
"""nothing added here"""
def format_search_result(self, search_result):
return search_result
@ -81,18 +81,18 @@ class AbstractConnector(TestCase):
)
def test_abstract_connector_init(self):
""" barebones connector for search with defaults """
"""barebones connector for search with defaults"""
self.assertIsInstance(self.connector.book_mappings, list)
def test_is_available(self):
""" this isn't used.... """
"""this isn't used...."""
self.assertTrue(self.connector.is_available())
self.connector.max_query_count = 1
self.connector.connector.query_count = 2
self.assertFalse(self.connector.is_available())
def test_get_or_create_book_existing(self):
""" find an existing book by remote/origin id """
"""find an existing book by remote/origin id"""
self.assertEqual(models.Book.objects.count(), 1)
self.assertEqual(
self.book.remote_id, "https://%s/book/%d" % (DOMAIN, self.book.id)
@ -113,7 +113,7 @@ class AbstractConnector(TestCase):
@responses.activate
def test_get_or_create_book_deduped(self):
""" load remote data and deduplicate """
"""load remote data and deduplicate"""
responses.add(
responses.GET, "https://example.com/book/abcd", json=self.edition_data
)
@ -125,7 +125,7 @@ class AbstractConnector(TestCase):
@responses.activate
def test_get_or_create_author(self):
""" load an author """
"""load an author"""
self.connector.author_mappings = [
Mapping("id"),
Mapping("name"),
@ -142,7 +142,7 @@ class AbstractConnector(TestCase):
self.assertEqual(result.origin_id, "https://www.example.com/author")
def test_get_or_create_author_existing(self):
""" get an existing author """
"""get an existing author"""
author = models.Author.objects.create(name="Test Author")
result = self.connector.get_or_create_author(author.remote_id)
self.assertEqual(author, result)

View file

@ -8,10 +8,10 @@ from bookwyrm.connectors.abstract_connector import Mapping, SearchResult
class AbstractConnector(TestCase):
""" generic code for connecting to outside data sources """
"""generic code for connecting to outside data sources"""
def setUp(self):
""" we need an example connector """
"""we need an example connector"""
self.connector_info = models.Connector.objects.create(
identifier="example.com",
connector_file="openlibrary",
@ -23,7 +23,7 @@ class AbstractConnector(TestCase):
)
class TestConnector(abstract_connector.AbstractMinimalConnector):
""" nothing added here """
"""nothing added here"""
def format_search_result(self, search_result):
return search_result
@ -43,7 +43,7 @@ class AbstractConnector(TestCase):
self.test_connector = TestConnector("example.com")
def test_abstract_minimal_connector_init(self):
""" barebones connector for search with defaults """
"""barebones connector for search with defaults"""
connector = self.test_connector
self.assertEqual(connector.connector, self.connector_info)
self.assertEqual(connector.base_url, "https://example.com")
@ -58,7 +58,7 @@ class AbstractConnector(TestCase):
@responses.activate
def test_search(self):
""" makes an http request to the outside service """
"""makes an http request to the outside service"""
responses.add(
responses.GET,
"https://example.com/search?q=a%20book%20title",
@ -73,7 +73,7 @@ class AbstractConnector(TestCase):
@responses.activate
def test_search_min_confidence(self):
""" makes an http request to the outside service """
"""makes an http request to the outside service"""
responses.add(
responses.GET,
"https://example.com/search?q=a%20book%20title&min_confidence=1",
@ -85,7 +85,7 @@ class AbstractConnector(TestCase):
@responses.activate
def test_isbn_search(self):
""" makes an http request to the outside service """
"""makes an http request to the outside service"""
responses.add(
responses.GET,
"https://example.com/isbn?q=123456",
@ -96,7 +96,7 @@ class AbstractConnector(TestCase):
self.assertEqual(len(results), 10)
def test_search_result(self):
""" a class that stores info about a search result """
"""a class that stores info about a search result"""
result = SearchResult(
title="Title",
key="https://example.com/book/1",
@ -109,21 +109,21 @@ class AbstractConnector(TestCase):
self.assertEqual(result.title, "Title")
def test_create_mapping(self):
""" maps remote fields for book data to bookwyrm activitypub fields """
"""maps remote fields for book data to bookwyrm activitypub fields"""
mapping = Mapping("isbn")
self.assertEqual(mapping.local_field, "isbn")
self.assertEqual(mapping.remote_field, "isbn")
self.assertEqual(mapping.formatter("bb"), "bb")
def test_create_mapping_with_remote(self):
""" the remote field is different than the local field """
"""the remote field is different than the local field"""
mapping = Mapping("isbn", remote_field="isbn13")
self.assertEqual(mapping.local_field, "isbn")
self.assertEqual(mapping.remote_field, "isbn13")
self.assertEqual(mapping.formatter("bb"), "bb")
def test_create_mapping_with_formatter(self):
""" a function is provided to modify the data """
"""a function is provided to modify the data"""
formatter = lambda x: "aa" + x
mapping = Mapping("isbn", formatter=formatter)
self.assertEqual(mapping.local_field, "isbn")

View file

@ -9,10 +9,10 @@ from bookwyrm.connectors.abstract_connector import SearchResult
class BookWyrmConnector(TestCase):
""" this connector doesn't do much, just search """
"""this connector doesn't do much, just search"""
def setUp(self):
""" create the connector """
"""create the connector"""
models.Connector.objects.create(
identifier="example.com",
connector_file="bookwyrm_connector",
@ -24,14 +24,14 @@ class BookWyrmConnector(TestCase):
self.connector = Connector("example.com")
def test_get_or_create_book_existing(self):
""" load book activity """
"""load book activity"""
work = models.Work.objects.create(title="Test Work")
book = models.Edition.objects.create(title="Test Edition", parent_work=work)
result = self.connector.get_or_create_book(book.remote_id)
self.assertEqual(book, result)
def test_format_search_result(self):
""" create a SearchResult object from search response json """
"""create a SearchResult object from search response json"""
datafile = pathlib.Path(__file__).parent.joinpath("../data/bw_search.json")
search_data = json.loads(datafile.read_bytes())
results = self.connector.parse_search_data(search_data)
@ -46,7 +46,7 @@ class BookWyrmConnector(TestCase):
self.assertEqual(result.connector, self.connector)
def test_format_isbn_search_result(self):
""" just gotta attach the connector """
"""just gotta attach the connector"""
datafile = pathlib.Path(__file__).parent.joinpath("../data/bw_search.json")
search_data = json.loads(datafile.read_bytes())
results = self.connector.parse_isbn_search_data(search_data)

View file

@ -8,10 +8,10 @@ from bookwyrm.connectors.self_connector import Connector as SelfConnector
class ConnectorManager(TestCase):
""" interface between the app and various connectors """
"""interface between the app and various connectors"""
def setUp(self):
""" we'll need some books and a connector info entry """
"""we'll need some books and a connector info entry"""
self.work = models.Work.objects.create(title="Example Work")
self.edition = models.Edition.objects.create(
@ -32,7 +32,7 @@ class ConnectorManager(TestCase):
)
def test_get_or_create_connector(self):
""" loads a connector if the data source is known or creates one """
"""loads a connector if the data source is known or creates one"""
remote_id = "https://example.com/object/1"
connector = connector_manager.get_or_create_connector(remote_id)
self.assertIsInstance(connector, BookWyrmConnector)
@ -43,7 +43,7 @@ class ConnectorManager(TestCase):
self.assertEqual(connector.identifier, same_connector.identifier)
def test_get_connectors(self):
""" load all connectors """
"""load all connectors"""
remote_id = "https://example.com/object/1"
connector_manager.get_or_create_connector(remote_id)
connectors = list(connector_manager.get_connectors())
@ -52,7 +52,7 @@ class ConnectorManager(TestCase):
self.assertIsInstance(connectors[1], BookWyrmConnector)
def test_search(self):
""" search all connectors """
"""search all connectors"""
results = connector_manager.search("Example")
self.assertEqual(len(results), 1)
self.assertIsInstance(results[0]["connector"], SelfConnector)
@ -60,7 +60,7 @@ class ConnectorManager(TestCase):
self.assertEqual(results[0]["results"][0].title, "Example Edition")
def test_search_isbn(self):
""" special handling if a query resembles an isbn """
"""special handling if a query resembles an isbn"""
results = connector_manager.search("0000000000")
self.assertEqual(len(results), 1)
self.assertIsInstance(results[0]["connector"], SelfConnector)
@ -68,20 +68,20 @@ class ConnectorManager(TestCase):
self.assertEqual(results[0]["results"][0].title, "Example Edition")
def test_local_search(self):
""" search only the local database """
"""search only the local database"""
results = connector_manager.local_search("Example")
self.assertEqual(len(results), 1)
self.assertEqual(results[0].title, "Example Edition")
def test_first_search_result(self):
""" only get one search result """
"""only get one search result"""
result = connector_manager.first_search_result("Example")
self.assertEqual(result.title, "Example Edition")
no_result = connector_manager.first_search_result("dkjfhg")
self.assertIsNone(no_result)
def test_load_connector(self):
""" load a connector object from the database entry """
"""load a connector object from the database entry"""
connector = connector_manager.load_connector(self.connector)
self.assertIsInstance(connector, SelfConnector)
self.assertEqual(connector.identifier, "test_connector")

View file

@ -16,10 +16,10 @@ from bookwyrm.connectors.connector_manager import ConnectorException
class Openlibrary(TestCase):
""" test loading data from openlibrary.org """
"""test loading data from openlibrary.org"""
def setUp(self):
""" creates the connector we'll use """
"""creates the connector we'll use"""
models.Connector.objects.create(
identifier="openlibrary.org",
name="OpenLibrary",
@ -42,7 +42,7 @@ class Openlibrary(TestCase):
self.edition_list_data = json.loads(edition_list_file.read_bytes())
def test_get_remote_id_from_data(self):
""" format the remote id from the data """
"""format the remote id from the data"""
data = {"key": "/work/OL1234W"}
result = self.connector.get_remote_id_from_data(data)
self.assertEqual(result, "https://openlibrary.org/work/OL1234W")
@ -51,13 +51,13 @@ class Openlibrary(TestCase):
self.connector.get_remote_id_from_data({})
def test_is_work_data(self):
""" detect if the loaded json is a work """
"""detect if the loaded json is a work"""
self.assertEqual(self.connector.is_work_data(self.work_data), True)
self.assertEqual(self.connector.is_work_data(self.edition_data), False)
@responses.activate
def test_get_edition_from_work_data(self):
""" loads a list of editions """
"""loads a list of editions"""
data = {"key": "/work/OL1234W"}
responses.add(
responses.GET,
@ -74,7 +74,7 @@ class Openlibrary(TestCase):
@responses.activate
def test_get_work_from_edition_data(self):
""" loads a list of editions """
"""loads a list of editions"""
data = {"works": [{"key": "/work/OL1234W"}]}
responses.add(
responses.GET,
@ -87,7 +87,7 @@ class Openlibrary(TestCase):
@responses.activate
def test_get_authors_from_data(self):
""" find authors in data """
"""find authors in data"""
responses.add(
responses.GET,
"https://openlibrary.org/authors/OL382982A",
@ -112,13 +112,13 @@ class Openlibrary(TestCase):
self.assertEqual(result.openlibrary_key, "OL453734A")
def test_get_cover_url(self):
""" formats a url that should contain the cover image """
"""formats a url that should contain the cover image"""
blob = ["image"]
result = self.connector.get_cover_url(blob)
self.assertEqual(result, "https://covers.openlibrary.org/b/id/image-L.jpg")
def test_parse_search_result(self):
""" extract the results from the search json response """
"""extract the results from the search json response"""
datafile = pathlib.Path(__file__).parent.joinpath("../data/ol_search.json")
search_data = json.loads(datafile.read_bytes())
result = self.connector.parse_search_data(search_data)
@ -126,7 +126,7 @@ class Openlibrary(TestCase):
self.assertEqual(len(result), 2)
def test_format_search_result(self):
""" translate json from openlibrary into SearchResult """
"""translate json from openlibrary into SearchResult"""
datafile = pathlib.Path(__file__).parent.joinpath("../data/ol_search.json")
search_data = json.loads(datafile.read_bytes())
results = self.connector.parse_search_data(search_data)
@ -141,7 +141,7 @@ class Openlibrary(TestCase):
self.assertEqual(result.connector, self.connector)
def test_parse_isbn_search_result(self):
""" extract the results from the search json response """
"""extract the results from the search json response"""
datafile = pathlib.Path(__file__).parent.joinpath("../data/ol_isbn_search.json")
search_data = json.loads(datafile.read_bytes())
result = self.connector.parse_isbn_search_data(search_data)
@ -149,7 +149,7 @@ class Openlibrary(TestCase):
self.assertEqual(len(result), 1)
def test_format_isbn_search_result(self):
""" translate json from openlibrary into SearchResult """
"""translate json from openlibrary into SearchResult"""
datafile = pathlib.Path(__file__).parent.joinpath("../data/ol_isbn_search.json")
search_data = json.loads(datafile.read_bytes())
results = self.connector.parse_isbn_search_data(search_data)
@ -165,7 +165,7 @@ class Openlibrary(TestCase):
@responses.activate
def test_load_edition_data(self):
""" format url from key and make request """
"""format url from key and make request"""
key = "OL1234W"
responses.add(
responses.GET,
@ -177,7 +177,7 @@ class Openlibrary(TestCase):
@responses.activate
def test_expand_book_data(self):
""" given a book, get more editions """
"""given a book, get more editions"""
work = models.Work.objects.create(title="Test Work", openlibrary_key="OL1234W")
edition = models.Edition.objects.create(title="Test Edition", parent_work=work)
@ -194,29 +194,29 @@ class Openlibrary(TestCase):
self.connector.expand_book_data(work)
def test_get_description(self):
""" should do some cleanup on the description data """
"""should do some cleanup on the description data"""
description = get_description(self.work_data["description"])
expected = "First in the Old Kingdom/Abhorsen series."
self.assertEqual(description, expected)
def test_get_openlibrary_key(self):
""" extracts the uuid """
"""extracts the uuid"""
key = get_openlibrary_key("/books/OL27320736M")
self.assertEqual(key, "OL27320736M")
def test_get_languages(self):
""" looks up languages from a list """
"""looks up languages from a list"""
languages = get_languages(self.edition_data["languages"])
self.assertEqual(languages, ["English"])
def test_pick_default_edition(self):
""" detect if the loaded json is an edition """
"""detect if the loaded json is an edition"""
edition = pick_default_edition(self.edition_list_data["entries"])
self.assertEqual(edition["key"], "/books/OL9788823M")
@responses.activate
def test_create_edition_from_data(self):
""" okay but can it actually create an edition with proper metadata """
"""okay but can it actually create an edition with proper metadata"""
work = models.Work.objects.create(title="Hello")
responses.add(
responses.GET,
@ -240,7 +240,7 @@ class Openlibrary(TestCase):
self.assertEqual(result.physical_format, "Hardcover")
def test_ignore_edition(self):
""" skip editions with poor metadata """
"""skip editions with poor metadata"""
self.assertFalse(ignore_edition({"isbn_13": "hi"}))
self.assertFalse(ignore_edition({"oclc_numbers": "hi"}))
self.assertFalse(ignore_edition({"covers": "hi"}))

View file

@ -9,10 +9,10 @@ from bookwyrm.settings import DOMAIN
class SelfConnector(TestCase):
""" just uses local data """
"""just uses local data"""
def setUp(self):
""" creating the connector """
"""creating the connector"""
models.Connector.objects.create(
identifier=DOMAIN,
name="Local",
@ -27,7 +27,7 @@ class SelfConnector(TestCase):
self.connector = Connector(DOMAIN)
def test_format_search_result(self):
""" create a SearchResult """
"""create a SearchResult"""
author = models.Author.objects.create(name="Anonymous")
edition = models.Edition.objects.create(
title="Edition of Example Work",
@ -42,7 +42,7 @@ class SelfConnector(TestCase):
self.assertEqual(result.connector, self.connector)
def test_search_rank(self):
""" prioritize certain results """
"""prioritize certain results"""
author = models.Author.objects.create(name="Anonymous")
edition = models.Edition.objects.create(
title="Edition of Example Work",
@ -78,7 +78,7 @@ class SelfConnector(TestCase):
self.assertEqual(results[2].title, "Edition of Example Work")
def test_search_multiple_editions(self):
""" it should get rid of duplicate editions for the same work """
"""it should get rid of duplicate editions for the same work"""
work = models.Work.objects.create(title="Work Title")
edition_1 = models.Edition.objects.create(
title="Edition 1 Title", parent_work=work

View file

@ -0,0 +1,5 @@
Book Id,Title,Author,Author l-f,Additional Authors,ISBN,ISBN13,My Rating,Average Rating,Publisher,Binding,Number of Pages,Year Published,Original Publication Year,Date Read,Date Added,Bookshelves,Bookshelves with positions,Exclusive Shelf,My Review,Spoiler,Private Notes,Read Count,Recommended For,Recommended By,Owned Copies,Original Purchase Date,Original Purchase Location,Condition,Condition Description,BCID
42036538,Gideon the Ninth (The Locked Tomb #1),Tamsyn Muir,"Muir, Tamsyn",,"=""1250313198""","=""9781250313195""",0,4.20,Tor,Hardcover,448,2019,2019,2020/10/25,2020/10/21,,,read,,,,1,,,0,,,,,
52691223,Subcutanean,Aaron A. Reed,"Reed, Aaron A.",,"=""""","=""""",0,4.45,,Paperback,232,2020,,2020/03/06,2020/03/05,,,read,,,,1,,,0,,,,,
28694510,Patisserie at Home,Mélanie Dupuis,"Dupuis, Mélanie",Anne Cazor,"=""0062445316""","=""9780062445315""",2,4.60,Harper Design,Hardcover,288,2016,,,2019/07/08,,,read,,,,2,,,0,,,,,
1 Book Id Title Author Author l-f Additional Authors ISBN ISBN13 My Rating Average Rating Publisher Binding Number of Pages Year Published Original Publication Year Date Read Date Added Bookshelves Bookshelves with positions Exclusive Shelf My Review Spoiler Private Notes Read Count Recommended For Recommended By Owned Copies Original Purchase Date Original Purchase Location Condition Condition Description BCID
2 42036538 Gideon the Ninth (The Locked Tomb #1) Tamsyn Muir Muir, Tamsyn ="1250313198" ="9781250313195" 0 4.20 Tor Hardcover 448 2019 2019 2020/10/25 2020/10/21 read 1 0
3 52691223 Subcutanean Aaron A. Reed Reed, Aaron A. ="" ="" 0 4.45 Paperback 232 2020 2020/03/06 2020/03/05 read 1 0
4 28694510 Patisserie at Home Mélanie Dupuis Dupuis, Mélanie Anne Cazor ="0062445316" ="9780062445315" 2 4.60 Harper Design Hardcover 288 2016 2019/07/08 read 2 0

View file

@ -14,10 +14,10 @@ from bookwyrm.settings import DOMAIN
class GoodreadsImport(TestCase):
""" importing from goodreads csv """
"""importing from goodreads csv"""
def setUp(self):
""" use a test csv """
"""use a test csv"""
self.importer = GoodreadsImporter()
datafile = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv")
self.csv = open(datafile, "r", encoding=self.importer.encoding)
@ -44,7 +44,7 @@ class GoodreadsImport(TestCase):
)
def test_create_job(self):
""" creates the import job entry and checks csv """
"""creates the import job entry and checks csv"""
import_job = self.importer.create_job(self.user, self.csv, False, "public")
self.assertEqual(import_job.user, self.user)
self.assertEqual(import_job.include_reviews, False)
@ -60,7 +60,7 @@ class GoodreadsImport(TestCase):
self.assertEqual(import_items[2].data["Book Id"], "28694510")
def test_create_retry_job(self):
""" trying again with items that didn't import """
"""trying again with items that didn't import"""
import_job = self.importer.create_job(self.user, self.csv, False, "unlisted")
import_items = models.ImportItem.objects.filter(job=import_job).all()[:2]
@ -78,7 +78,7 @@ class GoodreadsImport(TestCase):
self.assertEqual(retry_items[1].data["Book Id"], "52691223")
def test_start_import(self):
""" begin loading books """
"""begin loading books"""
import_job = self.importer.create_job(self.user, self.csv, False, "unlisted")
MockTask = namedtuple("Task", ("id"))
mock_task = MockTask(7)
@ -90,7 +90,7 @@ class GoodreadsImport(TestCase):
@responses.activate
def test_import_data(self):
""" resolve entry """
"""resolve entry"""
import_job = self.importer.create_job(self.user, self.csv, False, "unlisted")
book = models.Edition.objects.create(title="Test Book")
@ -105,7 +105,7 @@ class GoodreadsImport(TestCase):
self.assertEqual(import_item.book.id, book.id)
def test_handle_imported_book(self):
""" goodreads import added a book, this adds related connections """
"""goodreads import added a book, this adds related connections"""
shelf = self.user.shelf_set.filter(identifier="read").first()
self.assertIsNone(shelf.books.first())
@ -138,7 +138,7 @@ class GoodreadsImport(TestCase):
self.assertEqual(readthrough.finish_date.day, 25)
def test_handle_imported_book_already_shelved(self):
""" goodreads import added a book, this adds related connections """
"""goodreads import added a book, this adds related connections"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
shelf = self.user.shelf_set.filter(identifier="to-read").first()
models.ShelfBook.objects.create(shelf=shelf, user=self.user, book=self.book)
@ -171,7 +171,7 @@ class GoodreadsImport(TestCase):
self.assertEqual(readthrough.finish_date.day, 25)
def test_handle_import_twice(self):
""" re-importing books """
"""re-importing books"""
shelf = self.user.shelf_set.filter(identifier="read").first()
import_job = models.ImportJob.objects.create(user=self.user)
datafile = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv")
@ -206,7 +206,7 @@ class GoodreadsImport(TestCase):
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
def test_handle_imported_book_review(self, _):
""" goodreads review import """
"""goodreads review import"""
import_job = models.ImportJob.objects.create(user=self.user)
datafile = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv")
csv_file = open(datafile, "r")
@ -228,8 +228,34 @@ class GoodreadsImport(TestCase):
self.assertEqual(review.published_date.day, 8)
self.assertEqual(review.privacy, "unlisted")
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
def test_handle_imported_book_rating(self, _):
"""goodreads rating import"""
import_job = models.ImportJob.objects.create(user=self.user)
datafile = pathlib.Path(__file__).parent.joinpath(
"../data/goodreads-rating.csv"
)
csv_file = open(datafile, "r")
entry = list(csv.DictReader(csv_file))[2]
entry = self.importer.parse_fields(entry)
import_item = models.ImportItem.objects.create(
job_id=import_job.id, index=0, data=entry, book=self.book
)
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
handle_imported_book(
self.importer.service, self.user, import_item, True, "unlisted"
)
review = models.ReviewRating.objects.get(book=self.book, user=self.user)
self.assertIsInstance(review, models.ReviewRating)
self.assertEqual(review.rating, 2)
self.assertEqual(review.published_date.year, 2019)
self.assertEqual(review.published_date.month, 7)
self.assertEqual(review.published_date.day, 8)
self.assertEqual(review.privacy, "unlisted")
def test_handle_imported_book_reviews_disabled(self):
""" goodreads review import """
"""goodreads review import"""
import_job = models.ImportJob.objects.create(user=self.user)
datafile = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv")
csv_file = open(datafile, "r")

View file

@ -13,10 +13,10 @@ from bookwyrm.settings import DOMAIN
class LibrarythingImport(TestCase):
""" importing from librarything tsv """
"""importing from librarything tsv"""
def setUp(self):
""" use a test tsv """
"""use a test tsv"""
self.importer = LibrarythingImporter()
datafile = pathlib.Path(__file__).parent.joinpath("../data/librarything.tsv")
@ -45,7 +45,7 @@ class LibrarythingImport(TestCase):
)
def test_create_job(self):
""" creates the import job entry and checks csv """
"""creates the import job entry and checks csv"""
import_job = self.importer.create_job(self.user, self.csv, False, "public")
self.assertEqual(import_job.user, self.user)
self.assertEqual(import_job.include_reviews, False)
@ -61,7 +61,7 @@ class LibrarythingImport(TestCase):
self.assertEqual(import_items[2].data["Book Id"], "5015399")
def test_create_retry_job(self):
""" trying again with items that didn't import """
"""trying again with items that didn't import"""
import_job = self.importer.create_job(self.user, self.csv, False, "unlisted")
import_items = models.ImportItem.objects.filter(job=import_job).all()[:2]
@ -80,7 +80,7 @@ class LibrarythingImport(TestCase):
@responses.activate
def test_import_data(self):
""" resolve entry """
"""resolve entry"""
import_job = self.importer.create_job(self.user, self.csv, False, "unlisted")
book = models.Edition.objects.create(title="Test Book")
@ -95,7 +95,7 @@ class LibrarythingImport(TestCase):
self.assertEqual(import_item.book.id, book.id)
def test_handle_imported_book(self):
""" librarything import added a book, this adds related connections """
"""librarything import added a book, this adds related connections"""
shelf = self.user.shelf_set.filter(identifier="read").first()
self.assertIsNone(shelf.books.first())
@ -130,7 +130,7 @@ class LibrarythingImport(TestCase):
self.assertEqual(readthrough.finish_date.day, 8)
def test_handle_imported_book_already_shelved(self):
""" librarything import added a book, this adds related connections """
"""librarything import added a book, this adds related connections"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
shelf = self.user.shelf_set.filter(identifier="to-read").first()
models.ShelfBook.objects.create(shelf=shelf, user=self.user, book=self.book)
@ -165,7 +165,7 @@ class LibrarythingImport(TestCase):
self.assertEqual(readthrough.finish_date.day, 8)
def test_handle_import_twice(self):
""" re-importing books """
"""re-importing books"""
shelf = self.user.shelf_set.filter(identifier="read").first()
import_job = models.ImportJob.objects.create(user=self.user)
datafile = pathlib.Path(__file__).parent.joinpath("../data/librarything.tsv")
@ -202,7 +202,7 @@ class LibrarythingImport(TestCase):
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
def test_handle_imported_book_review(self, _):
""" librarything review import """
"""librarything review import"""
import_job = models.ImportJob.objects.create(user=self.user)
datafile = pathlib.Path(__file__).parent.joinpath("../data/librarything.tsv")
csv_file = open(datafile, "r", encoding=self.importer.encoding)
@ -225,7 +225,7 @@ class LibrarythingImport(TestCase):
self.assertEqual(review.privacy, "unlisted")
def test_handle_imported_book_reviews_disabled(self):
""" librarything review import """
"""librarything review import"""
import_job = models.ImportJob.objects.create(user=self.user)
datafile = pathlib.Path(__file__).parent.joinpath("../data/librarything.tsv")
csv_file = open(datafile, "r", encoding=self.importer.encoding)

View file

@ -8,10 +8,10 @@ from bookwyrm.management.commands.populate_streams import populate_streams
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay")
class Activitystreams(TestCase):
""" using redis to build activity streams """
"""using redis to build activity streams"""
def setUp(self):
""" we need some stuff """
"""we need some stuff"""
self.local_user = models.User.objects.create_user(
"mouse", "mouse@mouse.mouse", "password", local=True, localname="mouse"
)
@ -31,7 +31,7 @@ class Activitystreams(TestCase):
self.book = models.Edition.objects.create(title="test book")
def test_populate_streams(self, _):
""" make sure the function on the redis manager gets called """
"""make sure the function on the redis manager gets called"""
with patch("bookwyrm.activitystreams.ActivityStream.add_status"):
models.Comment.objects.create(
user=self.local_user, content="hi", book=self.book

View file

@ -15,10 +15,10 @@ from bookwyrm.models.activitypub_mixin import ActivityMixin, ObjectMixin
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
class ActivitypubMixins(TestCase):
""" functionality shared across models """
"""functionality shared across models"""
def setUp(self):
""" shared data """
"""shared data"""
self.local_user = models.User.objects.create_user(
"mouse", "mouse@mouse.com", "mouseword", local=True, localname="mouse"
)
@ -46,16 +46,16 @@ class ActivitypubMixins(TestCase):
# ActivitypubMixin
def test_to_activity(self, _):
""" model to ActivityPub json """
"""model to ActivityPub json"""
@dataclass(init=False)
class TestActivity(ActivityObject):
""" real simple mock """
"""real simple mock"""
type: str = "Test"
class TestModel(ActivitypubMixin, base_model.BookWyrmModel):
""" real simple mock model because BookWyrmModel is abstract """
"""real simple mock model because BookWyrmModel is abstract"""
instance = TestModel()
instance.remote_id = "https://www.example.com/test"
@ -67,7 +67,7 @@ class ActivitypubMixins(TestCase):
self.assertEqual(activity["type"], "Test")
def test_find_existing_by_remote_id(self, _):
""" attempt to match a remote id to an object in the db """
"""attempt to match a remote id to an object in the db"""
# uses a different remote id scheme
# this isn't really part of this test directly but it's helpful to state
book = models.Edition.objects.create(
@ -100,7 +100,7 @@ class ActivitypubMixins(TestCase):
result = models.Status.find_existing_by_remote_id("https://comment.net")
def test_find_existing(self, _):
""" match a blob of data to a model """
"""match a blob of data to a model"""
book = models.Edition.objects.create(
title="Test edition",
openlibrary_key="OL1234",
@ -110,7 +110,7 @@ class ActivitypubMixins(TestCase):
self.assertEqual(result, book)
def test_get_recipients_public_object(self, _):
""" determines the recipients for an object's broadcast """
"""determines the recipients for an object's broadcast"""
MockSelf = namedtuple("Self", ("privacy"))
mock_self = MockSelf("public")
recipients = ActivitypubMixin.get_recipients(mock_self)
@ -118,7 +118,7 @@ class ActivitypubMixins(TestCase):
self.assertEqual(recipients[0], self.remote_user.inbox)
def test_get_recipients_public_user_object_no_followers(self, _):
""" determines the recipients for a user's object broadcast """
"""determines the recipients for a user's object broadcast"""
MockSelf = namedtuple("Self", ("privacy", "user"))
mock_self = MockSelf("public", self.local_user)
@ -126,7 +126,7 @@ class ActivitypubMixins(TestCase):
self.assertEqual(len(recipients), 0)
def test_get_recipients_public_user_object(self, _):
""" determines the recipients for a user's object broadcast """
"""determines the recipients for a user's object broadcast"""
MockSelf = namedtuple("Self", ("privacy", "user"))
mock_self = MockSelf("public", self.local_user)
self.local_user.followers.add(self.remote_user)
@ -136,7 +136,7 @@ class ActivitypubMixins(TestCase):
self.assertEqual(recipients[0], self.remote_user.inbox)
def test_get_recipients_public_user_object_with_mention(self, _):
""" determines the recipients for a user's object broadcast """
"""determines the recipients for a user's object broadcast"""
MockSelf = namedtuple("Self", ("privacy", "user"))
mock_self = MockSelf("public", self.local_user)
self.local_user.followers.add(self.remote_user)
@ -159,7 +159,7 @@ class ActivitypubMixins(TestCase):
self.assertTrue(self.remote_user.inbox in recipients)
def test_get_recipients_direct(self, _):
""" determines the recipients for a user's object broadcast """
"""determines the recipients for a user's object broadcast"""
MockSelf = namedtuple("Self", ("privacy", "user"))
mock_self = MockSelf("public", self.local_user)
self.local_user.followers.add(self.remote_user)
@ -181,7 +181,7 @@ class ActivitypubMixins(TestCase):
self.assertEqual(recipients[0], another_remote_user.inbox)
def test_get_recipients_combine_inboxes(self, _):
""" should combine users with the same shared_inbox """
"""should combine users with the same shared_inbox"""
self.remote_user.shared_inbox = "http://example.com/inbox"
self.remote_user.save(broadcast=False)
with patch("bookwyrm.models.user.set_remote_server.delay"):
@ -205,7 +205,7 @@ class ActivitypubMixins(TestCase):
self.assertEqual(recipients[0], "http://example.com/inbox")
def test_get_recipients_software(self, _):
""" should differentiate between bookwyrm and other remote users """
"""should differentiate between bookwyrm and other remote users"""
with patch("bookwyrm.models.user.set_remote_server.delay"):
another_remote_user = models.User.objects.create_user(
"nutria",
@ -235,13 +235,13 @@ class ActivitypubMixins(TestCase):
# ObjectMixin
def test_object_save_create(self, _):
""" should save uneventufully when broadcast is disabled """
"""should save uneventufully when broadcast is disabled"""
class Success(Exception):
""" this means we got to the right method """
"""this means we got to the right method"""
class ObjectModel(ObjectMixin, base_model.BookWyrmModel):
""" real simple mock model because BookWyrmModel is abstract """
"""real simple mock model because BookWyrmModel is abstract"""
user = models.fields.ForeignKey("User", on_delete=db.models.CASCADE)
@ -252,7 +252,7 @@ class ActivitypubMixins(TestCase):
def broadcast(
self, activity, sender, **kwargs
): # pylint: disable=arguments-differ
""" do something """
"""do something"""
raise Success()
def to_create_activity(self, user): # pylint: disable=arguments-differ
@ -266,13 +266,13 @@ class ActivitypubMixins(TestCase):
ObjectModel(user=None).save()
def test_object_save_update(self, _):
""" should save uneventufully when broadcast is disabled """
"""should save uneventufully when broadcast is disabled"""
class Success(Exception):
""" this means we got to the right method """
"""this means we got to the right method"""
class UpdateObjectModel(ObjectMixin, base_model.BookWyrmModel):
""" real simple mock model because BookWyrmModel is abstract """
"""real simple mock model because BookWyrmModel is abstract"""
user = models.fields.ForeignKey("User", on_delete=db.models.CASCADE)
last_edited_by = models.fields.ForeignKey(
@ -292,13 +292,13 @@ class ActivitypubMixins(TestCase):
UpdateObjectModel(id=1, last_edited_by=self.local_user).save()
def test_object_save_delete(self, _):
""" should create delete activities when objects are deleted by flag """
"""should create delete activities when objects are deleted by flag"""
class ActivitySuccess(Exception):
""" this means we got to the right method """
"""this means we got to the right method"""
class DeletableObjectModel(ObjectMixin, base_model.BookWyrmModel):
""" real simple mock model because BookWyrmModel is abstract """
"""real simple mock model because BookWyrmModel is abstract"""
user = models.fields.ForeignKey("User", on_delete=db.models.CASCADE)
deleted = models.fields.BooleanField()
@ -314,7 +314,7 @@ class ActivitypubMixins(TestCase):
DeletableObjectModel(id=1, user=self.local_user, deleted=True).save()
def test_to_delete_activity(self, _):
""" wrapper for Delete activity """
"""wrapper for Delete activity"""
MockSelf = namedtuple("Self", ("remote_id", "to_activity"))
mock_self = MockSelf(
"https://example.com/status/1", lambda *args: self.object_mock
@ -329,7 +329,7 @@ class ActivitypubMixins(TestCase):
)
def test_to_update_activity(self, _):
""" ditto above but for Update """
"""ditto above but for Update"""
MockSelf = namedtuple("Self", ("remote_id", "to_activity"))
mock_self = MockSelf(
"https://example.com/status/1", lambda *args: self.object_mock
@ -347,7 +347,7 @@ class ActivitypubMixins(TestCase):
# Activity mixin
def test_to_undo_activity(self, _):
""" and again, for Undo """
"""and again, for Undo"""
MockSelf = namedtuple("Self", ("remote_id", "to_activity", "user"))
mock_self = MockSelf(
"https://example.com/status/1",

View file

@ -8,10 +8,10 @@ from bookwyrm.settings import DOMAIN
class BaseModel(TestCase):
""" functionality shared across models """
"""functionality shared across models"""
def setUp(self):
""" shared data """
"""shared data"""
self.local_user = models.User.objects.create_user(
"mouse", "mouse@mouse.com", "mouseword", local=True, localname="mouse"
)
@ -26,23 +26,26 @@ class BaseModel(TestCase):
outbox="https://example.com/users/rat/outbox",
)
class BookWyrmTestModel(base_model.BookWyrmModel):
"""just making it not abstract"""
self.test_model = BookWyrmTestModel()
def test_remote_id(self):
""" these should be generated """
instance = base_model.BookWyrmModel()
instance.id = 1
expected = instance.get_remote_id()
self.assertEqual(expected, "https://%s/bookwyrmmodel/1" % DOMAIN)
"""these should be generated"""
self.test_model.id = 1
expected = self.test_model.get_remote_id()
self.assertEqual(expected, "https://%s/bookwyrmtestmodel/1" % DOMAIN)
def test_remote_id_with_user(self):
""" format of remote id when there's a user object """
instance = base_model.BookWyrmModel()
instance.user = self.local_user
instance.id = 1
expected = instance.get_remote_id()
self.assertEqual(expected, "https://%s/user/mouse/bookwyrmmodel/1" % DOMAIN)
"""format of remote id when there's a user object"""
self.test_model.user = self.local_user
self.test_model.id = 1
expected = self.test_model.get_remote_id()
self.assertEqual(expected, "https://%s/user/mouse/bookwyrmtestmodel/1" % DOMAIN)
def test_set_remote_id(self):
""" this function sets remote ids after creation """
"""this function sets remote ids after creation"""
# using Work because it BookWrymModel is abstract and this requires save
# Work is a relatively not-fancy model.
instance = models.Work.objects.create(title="work title")
@ -59,7 +62,7 @@ class BaseModel(TestCase):
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
def test_object_visible_to_user(self, _):
""" does a user have permission to view an object """
"""does a user have permission to view an object"""
obj = models.Status.objects.create(
content="hi", user=self.remote_user, privacy="public"
)
@ -88,7 +91,7 @@ class BaseModel(TestCase):
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
def test_object_visible_to_user_follower(self, _):
""" what you can see if you follow a user """
"""what you can see if you follow a user"""
self.remote_user.followers.add(self.local_user)
obj = models.Status.objects.create(
content="hi", user=self.remote_user, privacy="followers"
@ -108,7 +111,7 @@ class BaseModel(TestCase):
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
def test_object_visible_to_user_blocked(self, _):
""" you can't see it if they block you """
"""you can't see it if they block you"""
self.remote_user.blocks.add(self.local_user)
obj = models.Status.objects.create(
content="hi", user=self.remote_user, privacy="public"

View file

@ -8,10 +8,10 @@ from bookwyrm.models.book import isbn_10_to_13, isbn_13_to_10
class Book(TestCase):
""" not too much going on in the books model but here we are """
"""not too much going on in the books model but here we are"""
def setUp(self):
""" we'll need some books """
"""we'll need some books"""
self.work = models.Work.objects.create(
title="Example Work", remote_id="https://example.com/book/1"
)
@ -25,17 +25,17 @@ class Book(TestCase):
)
def test_remote_id(self):
""" fanciness with remote/origin ids """
"""fanciness with remote/origin ids"""
remote_id = "https://%s/book/%d" % (settings.DOMAIN, self.work.id)
self.assertEqual(self.work.get_remote_id(), remote_id)
self.assertEqual(self.work.remote_id, remote_id)
def test_create_book(self):
""" you shouldn't be able to create Books (only editions and works) """
"""you shouldn't be able to create Books (only editions and works)"""
self.assertRaises(ValueError, models.Book.objects.create, title="Invalid Book")
def test_isbn_10_to_13(self):
""" checksums and so on """
"""checksums and so on"""
isbn_10 = "178816167X"
isbn_13 = isbn_10_to_13(isbn_10)
self.assertEqual(isbn_13, "9781788161671")
@ -45,7 +45,7 @@ class Book(TestCase):
self.assertEqual(isbn_13, "9781788161671")
def test_isbn_13_to_10(self):
""" checksums and so on """
"""checksums and so on"""
isbn_13 = "9781788161671"
isbn_10 = isbn_13_to_10(isbn_13)
self.assertEqual(isbn_10, "178816167X")
@ -55,7 +55,7 @@ class Book(TestCase):
self.assertEqual(isbn_10, "178816167X")
def test_get_edition_info(self):
""" text slug about an edition """
"""text slug about an edition"""
book = models.Edition.objects.create(title="Test Edition")
self.assertEqual(book.edition_info, "")
@ -77,7 +77,7 @@ class Book(TestCase):
self.assertEqual(book.alt_text, "Test Edition (worm, Glorbish language, 2020)")
def test_get_rank(self):
""" sets the data quality index for the book """
"""sets the data quality index for the book"""
# basic rank
self.assertEqual(self.first_edition.edition_rank, 0)

View file

@ -6,10 +6,10 @@ from bookwyrm import models
class FederatedServer(TestCase):
""" federate server management """
"""federate server management"""
def setUp(self):
""" we'll need a user """
"""we'll need a user"""
self.server = models.FederatedServer.objects.create(server_name="test.server")
with patch("bookwyrm.models.user.set_remote_server.delay"):
self.remote_user = models.User.objects.create_user(
@ -36,7 +36,7 @@ class FederatedServer(TestCase):
)
def test_block_unblock(self):
""" block a server and all users on it """
"""block a server and all users on it"""
self.assertEqual(self.server.status, "federated")
self.assertTrue(self.remote_user.is_active)
self.assertFalse(self.inactive_remote_user.is_active)

View file

@ -25,10 +25,10 @@ from bookwyrm.models.activitypub_mixin import ActivitypubMixin
# pylint: disable=too-many-public-methods
class ActivitypubFields(TestCase):
""" overwrites standard model feilds to work with activitypub """
"""overwrites standard model feilds to work with activitypub"""
def test_validate_remote_id(self):
""" should look like a url """
"""should look like a url"""
self.assertIsNone(fields.validate_remote_id("http://www.example.com"))
self.assertIsNone(fields.validate_remote_id("https://www.example.com"))
self.assertIsNone(fields.validate_remote_id("http://exle.com/dlg-23/x"))
@ -45,7 +45,7 @@ class ActivitypubFields(TestCase):
)
def test_activitypub_field_mixin(self):
""" generic mixin with super basic to and from functionality """
"""generic mixin with super basic to and from functionality"""
instance = fields.ActivitypubFieldMixin()
self.assertEqual(instance.field_to_activity("fish"), "fish")
self.assertEqual(instance.field_from_activity("fish"), "fish")
@ -63,11 +63,11 @@ class ActivitypubFields(TestCase):
self.assertEqual(instance.get_activitypub_field(), "snakeCaseName")
def test_set_field_from_activity(self):
""" setter from entire json blob """
"""setter from entire json blob"""
@dataclass
class TestModel:
""" real simple mock """
"""real simple mock"""
field_name: str
@ -82,11 +82,11 @@ class ActivitypubFields(TestCase):
self.assertEqual(mock_model.field_name, "hi")
def test_set_activity_from_field(self):
""" set json field given entire model """
"""set json field given entire model"""
@dataclass
class TestModel:
""" real simple mock """
"""real simple mock"""
field_name: str
unrelated: str
@ -100,7 +100,7 @@ class ActivitypubFields(TestCase):
self.assertEqual(data["fieldName"], "bip")
def test_remote_id_field(self):
""" just sets some defaults on charfield """
"""just sets some defaults on charfield"""
instance = fields.RemoteIdField()
self.assertEqual(instance.max_length, 255)
self.assertTrue(instance.deduplication_field)
@ -109,7 +109,7 @@ class ActivitypubFields(TestCase):
instance.run_validators("http://www.example.com/dlfjg 23/x")
def test_username_field(self):
""" again, just setting defaults on username field """
"""again, just setting defaults on username field"""
instance = fields.UsernameField()
self.assertEqual(instance.activitypub_field, "preferredUsername")
self.assertEqual(instance.max_length, 150)
@ -130,7 +130,7 @@ class ActivitypubFields(TestCase):
self.assertEqual(instance.field_to_activity("test@example.com"), "test")
def test_privacy_field_defaults(self):
""" post privacy field's many default values """
"""post privacy field's many default values"""
instance = fields.PrivacyField()
self.assertEqual(instance.max_length, 255)
self.assertEqual(
@ -143,11 +143,11 @@ class ActivitypubFields(TestCase):
)
def test_privacy_field_set_field_from_activity(self):
""" translate between to/cc fields and privacy """
"""translate between to/cc fields and privacy"""
@dataclass(init=False)
class TestActivity(ActivityObject):
""" real simple mock """
"""real simple mock"""
to: List[str]
cc: List[str]
@ -155,7 +155,7 @@ class ActivitypubFields(TestCase):
type: str = "Test"
class TestPrivacyModel(ActivitypubMixin, BookWyrmModel):
""" real simple mock model because BookWyrmModel is abstract """
"""real simple mock model because BookWyrmModel is abstract"""
privacy_field = fields.PrivacyField()
mention_users = fields.TagField(User)
@ -187,7 +187,7 @@ class ActivitypubFields(TestCase):
@patch("bookwyrm.models.activitypub_mixin.ObjectMixin.broadcast")
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
def test_privacy_field_set_activity_from_field(self, *_):
""" translate between to/cc fields and privacy """
"""translate between to/cc fields and privacy"""
user = User.objects.create_user(
"rat", "rat@rat.rat", "ratword", local=True, localname="rat"
)
@ -231,7 +231,7 @@ class ActivitypubFields(TestCase):
self.assertEqual(activity["cc"], [])
def test_foreign_key(self):
""" should be able to format a related model """
"""should be able to format a related model"""
instance = fields.ForeignKey("User", on_delete=models.CASCADE)
Serializable = namedtuple("Serializable", ("to_activity", "remote_id"))
item = Serializable(lambda: {"a": "b"}, "https://e.b/c")
@ -240,7 +240,7 @@ class ActivitypubFields(TestCase):
@responses.activate
def test_foreign_key_from_activity_str(self):
""" create a new object from a foreign key """
"""create a new object from a foreign key"""
instance = fields.ForeignKey(User, on_delete=models.CASCADE)
datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json")
userdata = json.loads(datafile.read_bytes())
@ -264,7 +264,7 @@ class ActivitypubFields(TestCase):
self.assertEqual(value.name, "MOUSE?? MOUSE!!")
def test_foreign_key_from_activity_dict(self):
""" test recieving activity json """
"""test recieving activity json"""
instance = fields.ForeignKey(User, on_delete=models.CASCADE)
datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json")
userdata = json.loads(datafile.read_bytes())
@ -284,7 +284,7 @@ class ActivitypubFields(TestCase):
# et cetera but we're not testing serializing user json
def test_foreign_key_from_activity_dict_existing(self):
""" test receiving a dict of an existing object in the db """
"""test receiving a dict of an existing object in the db"""
instance = fields.ForeignKey(User, on_delete=models.CASCADE)
datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json")
userdata = json.loads(datafile.read_bytes())
@ -302,7 +302,7 @@ class ActivitypubFields(TestCase):
self.assertEqual(value, user)
def test_foreign_key_from_activity_str_existing(self):
""" test receiving a remote id of an existing object in the db """
"""test receiving a remote id of an existing object in the db"""
instance = fields.ForeignKey(User, on_delete=models.CASCADE)
user = User.objects.create_user(
"mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse"
@ -315,14 +315,14 @@ class ActivitypubFields(TestCase):
self.assertEqual(value, user)
def test_one_to_one_field(self):
""" a gussied up foreign key """
"""a gussied up foreign key"""
instance = fields.OneToOneField("User", on_delete=models.CASCADE)
Serializable = namedtuple("Serializable", ("to_activity", "remote_id"))
item = Serializable(lambda: {"a": "b"}, "https://e.b/c")
self.assertEqual(instance.field_to_activity(item), {"a": "b"})
def test_many_to_many_field(self):
""" lists! """
"""lists!"""
instance = fields.ManyToManyField("User")
Serializable = namedtuple("Serializable", ("to_activity", "remote_id"))
@ -340,7 +340,7 @@ class ActivitypubFields(TestCase):
@responses.activate
def test_many_to_many_field_from_activity(self):
""" resolve related fields for a list, takes a list of remote ids """
"""resolve related fields for a list, takes a list of remote ids"""
instance = fields.ManyToManyField(User)
datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json")
userdata = json.loads(datafile.read_bytes())
@ -360,7 +360,7 @@ class ActivitypubFields(TestCase):
self.assertIsInstance(value[0], User)
def test_tag_field(self):
""" a special type of many to many field """
"""a special type of many to many field"""
instance = fields.TagField("User")
Serializable = namedtuple(
@ -379,13 +379,13 @@ class ActivitypubFields(TestCase):
self.assertEqual(result[0].type, "Serializable")
def test_tag_field_from_activity(self):
""" loadin' a list of items from Links """
"""loadin' a list of items from Links"""
# TODO
@responses.activate
@patch("bookwyrm.models.activitypub_mixin.ObjectMixin.broadcast")
def test_image_field(self, _):
""" storing images """
"""storing images"""
user = User.objects.create_user(
"mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse"
)
@ -423,7 +423,7 @@ class ActivitypubFields(TestCase):
self.assertIsInstance(loaded_image[1], ContentFile)
def test_datetime_field(self):
""" this one is pretty simple, it just has to use isoformat """
"""this one is pretty simple, it just has to use isoformat"""
instance = fields.DateTimeField()
now = timezone.now()
self.assertEqual(instance.field_to_activity(now), now.isoformat())
@ -431,12 +431,12 @@ class ActivitypubFields(TestCase):
self.assertEqual(instance.field_from_activity("bip"), None)
def test_array_field(self):
""" idk why it makes them strings but probably for a good reason """
"""idk why it makes them strings but probably for a good reason"""
instance = fields.ArrayField(fields.IntegerField)
self.assertEqual(instance.field_to_activity([0, 1]), ["0", "1"])
def test_html_field(self):
""" sanitizes html, the sanitizer has its own tests """
"""sanitizes html, the sanitizer has its own tests"""
instance = fields.HtmlField()
self.assertEqual(
instance.field_from_activity("<marquee><p>hi</p></marquee>"), "<p>hi</p>"

View file

@ -14,10 +14,10 @@ from bookwyrm.connectors.abstract_connector import SearchResult
class ImportJob(TestCase):
""" this is a fancy one!!! """
"""this is a fancy one!!!"""
def setUp(self):
""" data is from a goodreads export of The Raven Tower """
"""data is from a goodreads export of The Raven Tower"""
read_data = {
"Book Id": 39395857,
"Title": "The Raven Tower",
@ -72,30 +72,30 @@ class ImportJob(TestCase):
)
def test_isbn(self):
""" it unquotes the isbn13 field from data """
"""it unquotes the isbn13 field from data"""
expected = "9780356506999"
item = models.ImportItem.objects.get(index=1)
self.assertEqual(item.isbn, expected)
def test_shelf(self):
""" converts to the local shelf typology """
"""converts to the local shelf typology"""
expected = "reading"
self.assertEqual(self.item_1.shelf, expected)
def test_date_added(self):
""" converts to the local shelf typology """
"""converts to the local shelf typology"""
expected = datetime.datetime(2019, 4, 9, 0, 0, tzinfo=timezone.utc)
item = models.ImportItem.objects.get(index=1)
self.assertEqual(item.date_added, expected)
def test_date_read(self):
""" converts to the local shelf typology """
"""converts to the local shelf typology"""
expected = datetime.datetime(2019, 4, 12, 0, 0, tzinfo=timezone.utc)
item = models.ImportItem.objects.get(index=2)
self.assertEqual(item.date_read, expected)
def test_currently_reading_reads(self):
""" infer currently reading dates where available """
"""infer currently reading dates where available"""
expected = [
models.ReadThrough(
start_date=datetime.datetime(2019, 4, 9, 0, 0, tzinfo=timezone.utc)
@ -106,7 +106,7 @@ class ImportJob(TestCase):
self.assertEqual(actual.reads[0].finish_date, expected[0].finish_date)
def test_read_reads(self):
""" infer read dates where available """
"""infer read dates where available"""
actual = self.item_2
self.assertEqual(
actual.reads[0].start_date,
@ -118,14 +118,14 @@ class ImportJob(TestCase):
)
def test_unread_reads(self):
""" handle books with no read dates """
"""handle books with no read dates"""
expected = []
actual = models.ImportItem.objects.get(index=3)
self.assertEqual(actual.reads, expected)
@responses.activate
def test_get_book_from_isbn(self):
""" search and load books by isbn (9780356506999) """
"""search and load books by isbn (9780356506999)"""
connector_info = models.Connector.objects.create(
identifier="openlibrary.org",
name="OpenLibrary",

View file

@ -7,10 +7,10 @@ from bookwyrm import models, settings
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay")
class List(TestCase):
""" some activitypub oddness ahead """
"""some activitypub oddness ahead"""
def setUp(self):
""" look, a list """
"""look, a list"""
self.local_user = models.User.objects.create_user(
"mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse"
)
@ -18,7 +18,7 @@ class List(TestCase):
self.book = models.Edition.objects.create(title="hi", parent_work=work)
def test_remote_id(self, _):
""" shelves use custom remote ids """
"""shelves use custom remote ids"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
book_list = models.List.objects.create(
name="Test List", user=self.local_user
@ -27,7 +27,7 @@ class List(TestCase):
self.assertEqual(book_list.get_remote_id(), expected_id)
def test_to_activity(self, _):
""" jsonify it """
"""jsonify it"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
book_list = models.List.objects.create(
name="Test List", user=self.local_user
@ -41,7 +41,7 @@ class List(TestCase):
self.assertEqual(activity_json["owner"], self.local_user.remote_id)
def test_list_item(self, _):
""" a list entry """
"""a list entry"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
book_list = models.List.objects.create(
name="Test List", user=self.local_user, privacy="unlisted"
@ -59,7 +59,7 @@ class List(TestCase):
self.assertEqual(item.recipients, [])
def test_list_item_pending(self, _):
""" a list entry """
"""a list entry"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
book_list = models.List.objects.create(
name="Test List", user=self.local_user

View file

@ -6,10 +6,10 @@ from bookwyrm import models, settings
class ReadThrough(TestCase):
""" some activitypub oddness ahead """
"""some activitypub oddness ahead"""
def setUp(self):
""" look, a shelf """
"""look, a shelf"""
self.user = models.User.objects.create_user(
"mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse"
)
@ -27,7 +27,7 @@ class ReadThrough(TestCase):
)
def test_progress_update(self):
""" Test progress updates """
"""Test progress updates"""
self.readthrough.create_update() # No-op, no progress yet
self.readthrough.progress = 10
self.readthrough.create_update()

View file

@ -6,10 +6,10 @@ from bookwyrm import models
class Relationship(TestCase):
""" following, blocking, stuff like that """
"""following, blocking, stuff like that"""
def setUp(self):
""" we need some users for this """
"""we need some users for this"""
with patch("bookwyrm.models.user.set_remote_server.delay"):
self.remote_user = models.User.objects.create_user(
"rat",
@ -27,11 +27,11 @@ class Relationship(TestCase):
self.local_user.save(broadcast=False)
def test_user_follows_from_request(self):
""" convert a follow request into a follow """
"""convert a follow request into a follow"""
real_broadcast = models.UserFollowRequest.broadcast
def mock_broadcast(_, activity, user):
""" introspect what's being sent out """
"""introspect what's being sent out"""
self.assertEqual(user.remote_id, self.local_user.remote_id)
self.assertEqual(activity["type"], "Follow")
@ -54,7 +54,7 @@ class Relationship(TestCase):
models.UserFollowRequest.broadcast = real_broadcast
def test_user_follows_from_request_custom_remote_id(self):
""" store a specific remote id for a relationship provided by remote """
"""store a specific remote id for a relationship provided by remote"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
request = models.UserFollowRequest.objects.create(
user_subject=self.local_user,
@ -71,7 +71,7 @@ class Relationship(TestCase):
self.assertEqual(rel.user_object, self.remote_user)
def test_follow_request_activity(self):
""" accept a request and make it a relationship """
"""accept a request and make it a relationship"""
real_broadcast = models.UserFollowRequest.broadcast
def mock_broadcast(_, activity, user):
@ -88,7 +88,7 @@ class Relationship(TestCase):
models.UserFollowRequest.broadcast = real_broadcast
def test_follow_request_accept(self):
""" accept a request and make it a relationship """
"""accept a request and make it a relationship"""
real_broadcast = models.UserFollowRequest.broadcast
def mock_broadcast(_, activity, user):
@ -115,7 +115,7 @@ class Relationship(TestCase):
models.UserFollowRequest.broadcast = real_broadcast
def test_follow_request_reject(self):
""" accept a request and make it a relationship """
"""accept a request and make it a relationship"""
real_broadcast = models.UserFollowRequest.broadcast
def mock_reject(_, activity, user):

View file

@ -8,10 +8,10 @@ from bookwyrm import models, settings
# pylint: disable=unused-argument
class Shelf(TestCase):
""" some activitypub oddness ahead """
"""some activitypub oddness ahead"""
def setUp(self):
""" look, a shelf """
"""look, a shelf"""
self.local_user = models.User.objects.create_user(
"mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse"
)
@ -19,7 +19,7 @@ class Shelf(TestCase):
self.book = models.Edition.objects.create(title="test book", parent_work=work)
def test_remote_id(self):
""" shelves use custom remote ids """
"""shelves use custom remote ids"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
shelf = models.Shelf.objects.create(
name="Test Shelf", identifier="test-shelf", user=self.local_user
@ -28,7 +28,7 @@ class Shelf(TestCase):
self.assertEqual(shelf.get_remote_id(), expected_id)
def test_to_activity(self):
""" jsonify it """
"""jsonify it"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
shelf = models.Shelf.objects.create(
name="Test Shelf", identifier="test-shelf", user=self.local_user
@ -42,7 +42,7 @@ class Shelf(TestCase):
self.assertEqual(activity_json["owner"], self.local_user.remote_id)
def test_create_update_shelf(self):
""" create and broadcast shelf creation """
"""create and broadcast shelf creation"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock:
shelf = models.Shelf.objects.create(
@ -63,7 +63,7 @@ class Shelf(TestCase):
self.assertEqual(shelf.name, "arthur russel")
def test_shelve(self):
""" create and broadcast shelf creation """
"""create and broadcast shelf creation"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
shelf = models.Shelf.objects.create(
name="Test Shelf", identifier="test-shelf", user=self.local_user

View file

@ -17,10 +17,10 @@ from bookwyrm import activitypub, models, settings
@patch("bookwyrm.models.Status.broadcast")
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
class Status(TestCase):
""" lotta types of statuses """
"""lotta types of statuses"""
def setUp(self):
""" useful things for creating a status """
"""useful things for creating a status"""
self.local_user = models.User.objects.create_user(
"mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse"
)
@ -46,14 +46,14 @@ class Status(TestCase):
self.book.cover.save("test.jpg", ContentFile(output.getvalue()))
def test_status_generated_fields(self, *_):
""" setting remote id """
"""setting remote id"""
status = models.Status.objects.create(content="bleh", user=self.local_user)
expected_id = "https://%s/user/mouse/status/%d" % (settings.DOMAIN, status.id)
self.assertEqual(status.remote_id, expected_id)
self.assertEqual(status.privacy, "public")
def test_replies(self, *_):
""" get a list of replies """
"""get a list of replies"""
parent = models.Status.objects.create(content="hi", user=self.local_user)
child = models.Status.objects.create(
content="hello", reply_parent=parent, user=self.local_user
@ -72,7 +72,7 @@ class Status(TestCase):
self.assertIsInstance(replies.last(), models.Review)
def test_status_type(self, *_):
""" class name """
"""class name"""
self.assertEqual(models.Status().status_type, "Note")
self.assertEqual(models.Review().status_type, "Review")
self.assertEqual(models.Quotation().status_type, "Quotation")
@ -80,14 +80,14 @@ class Status(TestCase):
self.assertEqual(models.Boost().status_type, "Announce")
def test_boostable(self, *_):
""" can a status be boosted, based on privacy """
"""can a status be boosted, based on privacy"""
self.assertTrue(models.Status(privacy="public").boostable)
self.assertTrue(models.Status(privacy="unlisted").boostable)
self.assertFalse(models.Status(privacy="followers").boostable)
self.assertFalse(models.Status(privacy="direct").boostable)
def test_to_replies(self, *_):
""" activitypub replies collection """
"""activitypub replies collection"""
parent = models.Status.objects.create(content="hi", user=self.local_user)
child = models.Status.objects.create(
content="hello", reply_parent=parent, user=self.local_user
@ -104,7 +104,7 @@ class Status(TestCase):
self.assertEqual(replies["totalItems"], 2)
def test_status_to_activity(self, *_):
""" subclass of the base model version with a "pure" serializer """
"""subclass of the base model version with a "pure" serializer"""
status = models.Status.objects.create(
content="test content", user=self.local_user
)
@ -115,7 +115,7 @@ class Status(TestCase):
self.assertEqual(activity["sensitive"], False)
def test_status_to_activity_tombstone(self, *_):
""" subclass of the base model version with a "pure" serializer """
"""subclass of the base model version with a "pure" serializer"""
with patch(
"bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores"
):
@ -131,7 +131,7 @@ class Status(TestCase):
self.assertFalse(hasattr(activity, "content"))
def test_status_to_pure_activity(self, *_):
""" subclass of the base model version with a "pure" serializer """
"""subclass of the base model version with a "pure" serializer"""
status = models.Status.objects.create(
content="test content", user=self.local_user
)
@ -143,7 +143,7 @@ class Status(TestCase):
self.assertEqual(activity["attachment"], [])
def test_generated_note_to_activity(self, *_):
""" subclass of the base model version with a "pure" serializer """
"""subclass of the base model version with a "pure" serializer"""
status = models.GeneratedNote.objects.create(
content="test content", user=self.local_user
)
@ -157,7 +157,7 @@ class Status(TestCase):
self.assertEqual(len(activity["tag"]), 2)
def test_generated_note_to_pure_activity(self, *_):
""" subclass of the base model version with a "pure" serializer """
"""subclass of the base model version with a "pure" serializer"""
status = models.GeneratedNote.objects.create(
content="test content", user=self.local_user
)
@ -181,7 +181,7 @@ class Status(TestCase):
self.assertEqual(activity["attachment"][0].name, "Test Edition")
def test_comment_to_activity(self, *_):
""" subclass of the base model version with a "pure" serializer """
"""subclass of the base model version with a "pure" serializer"""
status = models.Comment.objects.create(
content="test content", user=self.local_user, book=self.book
)
@ -192,7 +192,7 @@ class Status(TestCase):
self.assertEqual(activity["inReplyToBook"], self.book.remote_id)
def test_comment_to_pure_activity(self, *_):
""" subclass of the base model version with a "pure" serializer """
"""subclass of the base model version with a "pure" serializer"""
status = models.Comment.objects.create(
content="test content", user=self.local_user, book=self.book
)
@ -212,7 +212,7 @@ class Status(TestCase):
self.assertEqual(activity["attachment"][0].name, "Test Edition")
def test_quotation_to_activity(self, *_):
""" subclass of the base model version with a "pure" serializer """
"""subclass of the base model version with a "pure" serializer"""
status = models.Quotation.objects.create(
quote="a sickening sense",
content="test content",
@ -227,7 +227,7 @@ class Status(TestCase):
self.assertEqual(activity["inReplyToBook"], self.book.remote_id)
def test_quotation_to_pure_activity(self, *_):
""" subclass of the base model version with a "pure" serializer """
"""subclass of the base model version with a "pure" serializer"""
status = models.Quotation.objects.create(
quote="a sickening sense",
content="test content",
@ -250,7 +250,7 @@ class Status(TestCase):
self.assertEqual(activity["attachment"][0].name, "Test Edition")
def test_review_to_activity(self, *_):
""" subclass of the base model version with a "pure" serializer """
"""subclass of the base model version with a "pure" serializer"""
status = models.Review.objects.create(
name="Review name",
content="test content",
@ -267,7 +267,7 @@ class Status(TestCase):
self.assertEqual(activity["inReplyToBook"], self.book.remote_id)
def test_review_to_pure_activity(self, *_):
""" subclass of the base model version with a "pure" serializer """
"""subclass of the base model version with a "pure" serializer"""
status = models.Review.objects.create(
name="Review's name",
content="test content",
@ -291,7 +291,7 @@ class Status(TestCase):
self.assertEqual(activity["attachment"][0].name, "Test Edition")
def test_review_to_pure_activity_no_rating(self, *_):
""" subclass of the base model version with a "pure" serializer """
"""subclass of the base model version with a "pure" serializer"""
status = models.Review.objects.create(
name="Review name",
content="test content",
@ -313,7 +313,7 @@ class Status(TestCase):
self.assertEqual(activity["attachment"][0].name, "Test Edition")
def test_reviewrating_to_pure_activity(self, *_):
""" subclass of the base model version with a "pure" serializer """
"""subclass of the base model version with a "pure" serializer"""
status = models.ReviewRating.objects.create(
rating=3.0,
user=self.local_user,
@ -335,11 +335,11 @@ class Status(TestCase):
self.assertEqual(activity["attachment"][0].name, "Test Edition")
def test_favorite(self, *_):
""" fav a status """
"""fav a status"""
real_broadcast = models.Favorite.broadcast
def fav_broadcast_mock(_, activity, user):
""" ok """
"""ok"""
self.assertEqual(user.remote_id, self.local_user.remote_id)
self.assertEqual(activity["type"], "Like")
@ -361,7 +361,7 @@ class Status(TestCase):
models.Favorite.broadcast = real_broadcast
def test_boost(self, *_):
""" boosting, this one's a bit fussy """
"""boosting, this one's a bit fussy"""
status = models.Status.objects.create(
content="test content", user=self.local_user
)
@ -373,7 +373,7 @@ class Status(TestCase):
self.assertEqual(activity, boost.to_activity(pure=True))
def test_notification(self, *_):
""" a simple model """
"""a simple model"""
notification = models.Notification.objects.create(
user=self.local_user, notification_type="FAVORITE"
)
@ -385,7 +385,7 @@ class Status(TestCase):
)
def test_create_broadcast(self, _, broadcast_mock):
""" should send out two verions of a status on create """
"""should send out two verions of a status on create"""
models.Comment.objects.create(
content="hi", user=self.local_user, book=self.book
)
@ -405,7 +405,7 @@ class Status(TestCase):
self.assertEqual(args["object"]["type"], "Comment")
def test_recipients_with_mentions(self, *_):
""" get recipients to broadcast a status """
"""get recipients to broadcast a status"""
status = models.GeneratedNote.objects.create(
content="test content", user=self.local_user
)
@ -414,7 +414,7 @@ class Status(TestCase):
self.assertEqual(status.recipients, [self.remote_user])
def test_recipients_with_reply_parent(self, *_):
""" get recipients to broadcast a status """
"""get recipients to broadcast a status"""
parent_status = models.GeneratedNote.objects.create(
content="test content", user=self.remote_user
)
@ -425,7 +425,7 @@ class Status(TestCase):
self.assertEqual(status.recipients, [self.remote_user])
def test_recipients_with_reply_parent_and_mentions(self, *_):
""" get recipients to broadcast a status """
"""get recipients to broadcast a status"""
parent_status = models.GeneratedNote.objects.create(
content="test content", user=self.remote_user
)
@ -438,7 +438,7 @@ class Status(TestCase):
@responses.activate
def test_ignore_activity_boost(self, *_):
""" don't bother with most remote statuses """
"""don't bother with most remote statuses"""
activity = activitypub.Announce(
id="http://www.faraway.com/boost/12",
actor=self.remote_user.remote_id,

View file

@ -22,7 +22,7 @@ class User(TestCase):
)
def test_computed_fields(self):
""" username instead of id here """
"""username instead of id here"""
expected_id = "https://%s/user/mouse" % DOMAIN
self.assertEqual(self.user.remote_id, expected_id)
self.assertEqual(self.user.username, "mouse@%s" % DOMAIN)
@ -155,7 +155,7 @@ class User(TestCase):
self.assertIsNone(server.application_version)
def test_delete_user(self):
""" deactivate a user """
"""deactivate a user"""
self.assertTrue(self.user.is_active)
with patch(
"bookwyrm.models.activitypub_mixin.broadcast_task.delay"

View file

@ -7,10 +7,10 @@ from bookwyrm import activitystreams, models
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay")
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
class Activitystreams(TestCase):
""" using redis to build activity streams """
"""using redis to build activity streams"""
def setUp(self):
""" use a test csv """
"""use a test csv"""
self.local_user = models.User.objects.create_user(
"mouse", "mouse@mouse.mouse", "password", local=True, localname="mouse"
)
@ -30,14 +30,14 @@ class Activitystreams(TestCase):
self.book = models.Edition.objects.create(title="test book")
class TestStream(activitystreams.ActivityStream):
""" test stream, don't have to do anything here """
"""test stream, don't have to do anything here"""
key = "test"
self.test_stream = TestStream()
def test_activitystream_class_ids(self, *_):
""" the abstract base class for stream objects """
"""the abstract base class for stream objects"""
self.assertEqual(
self.test_stream.stream_id(self.local_user),
"{}-test".format(self.local_user.id),
@ -48,7 +48,7 @@ class Activitystreams(TestCase):
)
def test_abstractstream_get_audience(self, *_):
""" get a list of users that should see a status """
"""get a list of users that should see a status"""
status = models.Status.objects.create(
user=self.remote_user, content="hi", privacy="public"
)
@ -59,7 +59,7 @@ class Activitystreams(TestCase):
self.assertTrue(self.another_user in users)
def test_abstractstream_get_audience_direct(self, *_):
""" get a list of users that should see a status """
"""get a list of users that should see a status"""
status = models.Status.objects.create(
user=self.remote_user,
content="hi",
@ -82,7 +82,7 @@ class Activitystreams(TestCase):
self.assertFalse(self.remote_user in users)
def test_abstractstream_get_audience_followers_remote_user(self, *_):
""" get a list of users that should see a status """
"""get a list of users that should see a status"""
status = models.Status.objects.create(
user=self.remote_user,
content="hi",
@ -92,7 +92,7 @@ class Activitystreams(TestCase):
self.assertFalse(users.exists())
def test_abstractstream_get_audience_followers_self(self, *_):
""" get a list of users that should see a status """
"""get a list of users that should see a status"""
status = models.Comment.objects.create(
user=self.local_user,
content="hi",
@ -105,7 +105,7 @@ class Activitystreams(TestCase):
self.assertFalse(self.remote_user in users)
def test_abstractstream_get_audience_followers_with_mention(self, *_):
""" get a list of users that should see a status """
"""get a list of users that should see a status"""
status = models.Comment.objects.create(
user=self.remote_user,
content="hi",
@ -120,7 +120,7 @@ class Activitystreams(TestCase):
self.assertFalse(self.remote_user in users)
def test_abstractstream_get_audience_followers_with_relationship(self, *_):
""" get a list of users that should see a status """
"""get a list of users that should see a status"""
self.remote_user.followers.add(self.local_user)
status = models.Comment.objects.create(
user=self.remote_user,
@ -134,7 +134,7 @@ class Activitystreams(TestCase):
self.assertFalse(self.remote_user in users)
def test_homestream_get_audience(self, *_):
""" get a list of users that should see a status """
"""get a list of users that should see a status"""
status = models.Status.objects.create(
user=self.remote_user, content="hi", privacy="public"
)
@ -142,7 +142,7 @@ class Activitystreams(TestCase):
self.assertFalse(users.exists())
def test_homestream_get_audience_with_mentions(self, *_):
""" get a list of users that should see a status """
"""get a list of users that should see a status"""
status = models.Status.objects.create(
user=self.remote_user, content="hi", privacy="public"
)
@ -152,7 +152,7 @@ class Activitystreams(TestCase):
self.assertFalse(self.another_user in users)
def test_homestream_get_audience_with_relationship(self, *_):
""" get a list of users that should see a status """
"""get a list of users that should see a status"""
self.remote_user.followers.add(self.local_user)
status = models.Status.objects.create(
user=self.remote_user, content="hi", privacy="public"
@ -162,7 +162,7 @@ class Activitystreams(TestCase):
self.assertFalse(self.another_user in users)
def test_localstream_get_audience_remote_status(self, *_):
""" get a list of users that should see a status """
"""get a list of users that should see a status"""
status = models.Status.objects.create(
user=self.remote_user, content="hi", privacy="public"
)
@ -170,7 +170,7 @@ class Activitystreams(TestCase):
self.assertEqual(users, [])
def test_localstream_get_audience_local_status(self, *_):
""" get a list of users that should see a status """
"""get a list of users that should see a status"""
status = models.Status.objects.create(
user=self.local_user, content="hi", privacy="public"
)
@ -179,7 +179,7 @@ class Activitystreams(TestCase):
self.assertTrue(self.another_user in users)
def test_localstream_get_audience_unlisted(self, *_):
""" get a list of users that should see a status """
"""get a list of users that should see a status"""
status = models.Status.objects.create(
user=self.local_user, content="hi", privacy="unlisted"
)
@ -187,7 +187,7 @@ class Activitystreams(TestCase):
self.assertEqual(users, [])
def test_federatedstream_get_audience(self, *_):
""" get a list of users that should see a status """
"""get a list of users that should see a status"""
status = models.Status.objects.create(
user=self.remote_user, content="hi", privacy="public"
)
@ -196,7 +196,7 @@ class Activitystreams(TestCase):
self.assertTrue(self.another_user in users)
def test_federatedstream_get_audience_unlisted(self, *_):
""" get a list of users that should see a status """
"""get a list of users that should see a status"""
status = models.Status.objects.create(
user=self.remote_user, content="hi", privacy="unlisted"
)

View file

@ -10,10 +10,10 @@ from bookwyrm import emailing, models
@patch("bookwyrm.emailing.send_email.delay")
class Emailing(TestCase):
""" every response to a get request, html or json """
"""every response to a get request, html or json"""
def setUp(self):
""" we need basic test data and mocks """
"""we need basic test data and mocks"""
self.factory = RequestFactory()
self.local_user = models.User.objects.create_user(
"mouse@local.com",
@ -25,7 +25,7 @@ class Emailing(TestCase):
models.SiteSettings.objects.create()
def test_invite_email(self, email_mock):
""" load the invite email """
"""load the invite email"""
invite_request = models.InviteRequest.objects.create(
email="test@email.com",
invite=models.SiteInvite.objects.create(user=self.local_user),
@ -40,7 +40,7 @@ class Emailing(TestCase):
self.assertEqual(len(args), 4)
def test_password_reset_email(self, email_mock):
""" load the password reset email """
"""load the password reset email"""
reset = models.PasswordReset.objects.create(user=self.local_user)
emailing.password_reset_email(reset)

View file

@ -5,10 +5,10 @@ from bookwyrm.sanitize_html import InputHtmlParser
class Sanitizer(TestCase):
""" sanitizer tests """
"""sanitizer tests"""
def test_no_html(self):
""" just text """
"""just text"""
input_text = "no html "
parser = InputHtmlParser()
parser.feed(input_text)
@ -16,7 +16,7 @@ class Sanitizer(TestCase):
self.assertEqual(input_text, output)
def test_valid_html(self):
""" leave the html untouched """
"""leave the html untouched"""
input_text = "<b>yes </b> <i>html</i>"
parser = InputHtmlParser()
parser.feed(input_text)
@ -24,7 +24,7 @@ class Sanitizer(TestCase):
self.assertEqual(input_text, output)
def test_valid_html_attrs(self):
""" and don't remove attributes """
"""and don't remove attributes"""
input_text = '<a href="fish.com">yes </a> <i>html</i>'
parser = InputHtmlParser()
parser.feed(input_text)
@ -32,7 +32,7 @@ class Sanitizer(TestCase):
self.assertEqual(input_text, output)
def test_invalid_html(self):
""" remove all html when the html is malformed """
"""remove all html when the html is malformed"""
input_text = "<b>yes <i>html</i>"
parser = InputHtmlParser()
parser.feed(input_text)
@ -46,7 +46,7 @@ class Sanitizer(TestCase):
self.assertEqual("yes html ", output)
def test_disallowed_html(self):
""" remove disallowed html but keep allowed html """
"""remove disallowed html but keep allowed html"""
input_text = "<div> yes <i>html</i></div>"
parser = InputHtmlParser()
parser.feed(input_text)

View file

@ -20,7 +20,7 @@ from bookwyrm.signatures import create_key_pair, make_signature, make_digest
def get_follow_activity(follower, followee):
""" generates a test activity """
"""generates a test activity"""
return Follow(
id="https://test.com/user/follow/id",
actor=follower.remote_id,
@ -33,10 +33,10 @@ Sender = namedtuple("Sender", ("remote_id", "key_pair"))
class Signature(TestCase):
""" signature test """
"""signature test"""
def setUp(self):
""" create users and test data """
"""create users and test data"""
self.mouse = models.User.objects.create_user(
"mouse@%s" % DOMAIN, "mouse@example.com", "", local=True, localname="mouse"
)
@ -56,7 +56,7 @@ class Signature(TestCase):
models.SiteSettings.objects.create()
def send(self, signature, now, data, digest):
""" test request """
"""test request"""
c = Client()
return c.post(
urlsplit(self.rat.inbox).path,
@ -74,7 +74,7 @@ class Signature(TestCase):
def send_test_request( # pylint: disable=too-many-arguments
self, sender, signer=None, send_data=None, digest=None, date=None
):
""" sends a follow request to the "rat" user """
"""sends a follow request to the "rat" user"""
now = date or http_date()
data = json.dumps(get_follow_activity(sender, self.rat))
digest = digest or make_digest(data)
@ -84,7 +84,7 @@ class Signature(TestCase):
return self.send(signature, now, send_data or data, digest)
def test_correct_signature(self):
""" this one should just work """
"""this one should just work"""
response = self.send_test_request(sender=self.mouse)
self.assertEqual(response.status_code, 200)
@ -96,7 +96,7 @@ class Signature(TestCase):
@responses.activate
def test_remote_signer(self):
""" signtures for remote users """
"""signtures for remote users"""
datafile = pathlib.Path(__file__).parent.joinpath("data/ap_user.json")
data = json.loads(datafile.read_bytes())
data["id"] = self.fake_remote.remote_id
@ -119,7 +119,7 @@ class Signature(TestCase):
@responses.activate
def test_key_needs_refresh(self):
""" an out of date key should be updated and the new key work """
"""an out of date key should be updated and the new key work"""
datafile = pathlib.Path(__file__).parent.joinpath("data/ap_user.json")
data = json.loads(datafile.read_bytes())
data["id"] = self.fake_remote.remote_id
@ -155,7 +155,7 @@ class Signature(TestCase):
@responses.activate
def test_nonexistent_signer(self):
""" fail when unable to look up signer """
"""fail when unable to look up signer"""
responses.add(
responses.GET,
self.fake_remote.remote_id,
@ -177,7 +177,7 @@ class Signature(TestCase):
@pytest.mark.integration
def test_invalid_digest(self):
""" signature digest must be valid """
"""signature digest must be valid"""
with patch("bookwyrm.activitypub.resolve_remote_id"):
response = self.send_test_request(
self.mouse, digest="SHA-256=AAAAAAAAAAAAAAAAAA"

View file

@ -12,10 +12,10 @@ from bookwyrm.templatetags import bookwyrm_tags
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
class TemplateTags(TestCase):
""" lotta different things here """
"""lotta different things here"""
def setUp(self):
""" create some filler objects """
"""create some filler objects"""
self.user = models.User.objects.create_user(
"mouse@example.com",
"mouse@mouse.mouse",
@ -34,34 +34,34 @@ class TemplateTags(TestCase):
self.book = models.Edition.objects.create(title="Test Book")
def test_dict_key(self, _):
""" just getting a value out of a dict """
"""just getting a value out of a dict"""
test_dict = {"a": 1, "b": 3}
self.assertEqual(bookwyrm_tags.dict_key(test_dict, "a"), 1)
self.assertEqual(bookwyrm_tags.dict_key(test_dict, "c"), 0)
def test_get_user_rating(self, _):
""" get a user's most recent rating of a book """
"""get a user's most recent rating of a book"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
models.Review.objects.create(user=self.user, book=self.book, rating=3)
self.assertEqual(bookwyrm_tags.get_user_rating(self.book, self.user), 3)
def test_get_user_rating_doesnt_exist(self, _):
""" there is no rating available """
"""there is no rating available"""
self.assertEqual(bookwyrm_tags.get_user_rating(self.book, self.user), 0)
def test_get_user_identifer_local(self, _):
""" fall back to the simplest uid available """
"""fall back to the simplest uid available"""
self.assertNotEqual(self.user.username, self.user.localname)
self.assertEqual(bookwyrm_tags.get_user_identifier(self.user), "mouse")
def test_get_user_identifer_remote(self, _):
""" for a remote user, should be their full username """
"""for a remote user, should be their full username"""
self.assertEqual(
bookwyrm_tags.get_user_identifier(self.remote_user), "rat@example.com"
)
def test_get_notification_count(self, _):
""" just countin' """
"""just countin'"""
self.assertEqual(bookwyrm_tags.get_notification_count(self.user), 0)
models.Notification.objects.create(user=self.user, notification_type="FAVORITE")
@ -74,7 +74,7 @@ class TemplateTags(TestCase):
self.assertEqual(bookwyrm_tags.get_notification_count(self.user), 2)
def test_get_replies(self, _):
""" direct replies to a status """
"""direct replies to a status"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
parent = models.Review.objects.create(
user=self.user, book=self.book, content="hi"
@ -102,7 +102,7 @@ class TemplateTags(TestCase):
self.assertFalse(third_child in replies)
def test_get_parent(self, _):
""" get the reply parent of a status """
"""get the reply parent of a status"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
parent = models.Review.objects.create(
user=self.user, book=self.book, content="hi"
@ -116,7 +116,7 @@ class TemplateTags(TestCase):
self.assertIsInstance(result, models.Review)
def test_get_user_liked(self, _):
""" did a user like a status """
"""did a user like a status"""
status = models.Review.objects.create(user=self.remote_user, book=self.book)
self.assertFalse(bookwyrm_tags.get_user_liked(self.user, status))
@ -125,7 +125,7 @@ class TemplateTags(TestCase):
self.assertTrue(bookwyrm_tags.get_user_liked(self.user, status))
def test_get_user_boosted(self, _):
""" did a user boost a status """
"""did a user boost a status"""
status = models.Review.objects.create(user=self.remote_user, book=self.book)
self.assertFalse(bookwyrm_tags.get_user_boosted(self.user, status))
@ -134,7 +134,7 @@ class TemplateTags(TestCase):
self.assertTrue(bookwyrm_tags.get_user_boosted(self.user, status))
def test_follow_request_exists(self, _):
""" does a user want to follow """
"""does a user want to follow"""
self.assertFalse(
bookwyrm_tags.follow_request_exists(self.user, self.remote_user)
)
@ -152,7 +152,7 @@ class TemplateTags(TestCase):
)
def test_get_boosted(self, _):
""" load a boosted status """
"""load a boosted status"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
status = models.Review.objects.create(user=self.remote_user, book=self.book)
boost = models.Boost.objects.create(user=self.user, boosted_status=status)
@ -161,7 +161,7 @@ class TemplateTags(TestCase):
self.assertEqual(boosted, status)
def test_get_book_description(self, _):
""" grab it from the edition or the parent """
"""grab it from the edition or the parent"""
work = models.Work.objects.create(title="Test Work")
self.book.parent_work = work
self.book.save()
@ -177,12 +177,12 @@ class TemplateTags(TestCase):
self.assertEqual(bookwyrm_tags.get_book_description(self.book), "hello")
def test_get_uuid(self, _):
""" uuid functionality """
"""uuid functionality"""
uuid = bookwyrm_tags.get_uuid("hi")
self.assertTrue(re.match(r"hi[A-Za-z0-9\-]", uuid))
def test_get_markdown(self, _):
""" mardown format data """
"""mardown format data"""
result = bookwyrm_tags.get_markdown("_hi_")
self.assertEqual(result, "<p><em>hi</em></p>")
@ -190,13 +190,13 @@ class TemplateTags(TestCase):
self.assertEqual(result, "<p><em>hi</em></p>")
def test_get_mentions(self, _):
""" list of people mentioned """
"""list of people mentioned"""
status = models.Status.objects.create(content="hi", user=self.remote_user)
result = bookwyrm_tags.get_mentions(status, self.user)
self.assertEqual(result, "@rat@example.com ")
def test_get_status_preview_name(self, _):
""" status context string """
"""status context string"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
status = models.Status.objects.create(content="hi", user=self.user)
result = bookwyrm_tags.get_status_preview_name(status)
@ -221,7 +221,7 @@ class TemplateTags(TestCase):
self.assertEqual(result, "quotation from <em>Test Book</em>")
def test_related_status(self, _):
""" gets the subclass model for a notification status """
"""gets the subclass model for a notification status"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
status = models.Status.objects.create(content="hi", user=self.user)
notification = models.Notification.objects.create(

View file

@ -12,10 +12,10 @@ from bookwyrm import models, views
# pylint: disable=too-many-public-methods
class Inbox(TestCase):
""" readthrough tests """
"""readthrough tests"""
def setUp(self):
""" basic user and book data """
"""basic user and book data"""
self.client = Client()
self.factory = RequestFactory()
local_user = models.User.objects.create_user(
@ -48,12 +48,12 @@ class Inbox(TestCase):
models.SiteSettings.objects.create()
def test_inbox_invalid_get(self):
""" shouldn't try to handle if the user is not found """
"""shouldn't try to handle if the user is not found"""
result = self.client.get("/inbox", content_type="application/json")
self.assertIsInstance(result, HttpResponseNotAllowed)
def test_inbox_invalid_user(self):
""" shouldn't try to handle if the user is not found """
"""shouldn't try to handle if the user is not found"""
result = self.client.post(
"/user/bleh/inbox",
'{"type": "Test", "object": "exists"}',
@ -62,7 +62,7 @@ class Inbox(TestCase):
self.assertIsInstance(result, HttpResponseNotFound)
def test_inbox_invalid_bad_signature(self):
""" bad request for invalid signature """
"""bad request for invalid signature"""
with patch("bookwyrm.views.inbox.has_valid_signature") as mock_valid:
mock_valid.return_value = False
result = self.client.post(
@ -73,7 +73,7 @@ class Inbox(TestCase):
self.assertEqual(result.status_code, 401)
def test_inbox_invalid_bad_signature_delete(self):
""" invalid signature for Delete is okay though """
"""invalid signature for Delete is okay though"""
with patch("bookwyrm.views.inbox.has_valid_signature") as mock_valid:
mock_valid.return_value = False
result = self.client.post(
@ -84,7 +84,7 @@ class Inbox(TestCase):
self.assertEqual(result.status_code, 200)
def test_inbox_unknown_type(self):
""" never heard of that activity type, don't have a handler for it """
"""never heard of that activity type, don't have a handler for it"""
with patch("bookwyrm.views.inbox.has_valid_signature") as mock_valid:
result = self.client.post(
"/inbox",
@ -95,7 +95,7 @@ class Inbox(TestCase):
self.assertIsInstance(result, HttpResponseNotFound)
def test_inbox_success(self):
""" a known type, for which we start a task """
"""a known type, for which we start a task"""
activity = self.create_json
activity["object"] = {
"id": "https://example.com/list/22",
@ -121,7 +121,7 @@ class Inbox(TestCase):
self.assertEqual(result.status_code, 200)
def test_is_blocked_user_agent(self):
""" check for blocked servers """
"""check for blocked servers"""
request = self.factory.post(
"",
HTTP_USER_AGENT="http.rb/4.4.1 (Mastodon/3.3.0; +https://mastodon.social/)",
@ -134,7 +134,7 @@ class Inbox(TestCase):
self.assertTrue(views.inbox.is_blocked_user_agent(request))
def test_is_blocked_activity(self):
""" check for blocked servers """
"""check for blocked servers"""
activity = {"actor": "https://mastodon.social/user/whaatever/else"}
self.assertFalse(views.inbox.is_blocked_activity(activity))
@ -144,7 +144,7 @@ class Inbox(TestCase):
self.assertTrue(views.inbox.is_blocked_activity(activity))
def test_create_by_deactivated_user(self):
""" don't let deactivated users post """
"""don't let deactivated users post"""
self.remote_user.delete(broadcast=False)
self.assertTrue(self.remote_user.deleted)
datafile = pathlib.Path(__file__).parent.joinpath("../../data/ap_note.json")

View file

@ -9,10 +9,10 @@ from bookwyrm import models, views
# pylint: disable=too-many-public-methods
class InboxAdd(TestCase):
""" inbox tests """
"""inbox tests"""
def setUp(self):
""" basic user and book data """
"""basic user and book data"""
local_user = models.User.objects.create_user(
"mouse@example.com",
"mouse@mouse.com",
@ -42,7 +42,7 @@ class InboxAdd(TestCase):
models.SiteSettings.objects.create()
def test_handle_add_book_to_shelf(self):
""" shelving a book """
"""shelving a book"""
shelf = models.Shelf.objects.create(user=self.remote_user, name="Test Shelf")
shelf.remote_id = "https://bookwyrm.social/user/mouse/shelf/to-read"
shelf.save()
@ -65,7 +65,7 @@ class InboxAdd(TestCase):
@responses.activate
def test_handle_add_book_to_list(self):
""" listing a book """
"""listing a book"""
responses.add(
responses.GET,
"https://bookwyrm.social/user/mouse/list/to-read",

View file

@ -9,10 +9,10 @@ from bookwyrm import models, views
# pylint: disable=too-many-public-methods
class InboxActivities(TestCase):
""" inbox tests """
"""inbox tests"""
def setUp(self):
""" basic user and book data """
"""basic user and book data"""
self.local_user = models.User.objects.create_user(
"mouse@example.com",
"mouse@mouse.com",
@ -52,7 +52,7 @@ class InboxActivities(TestCase):
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
def test_boost(self, redis_mock):
""" boost a status """
"""boost a status"""
self.assertEqual(models.Notification.objects.count(), 0)
activity = {
"type": "Announce",
@ -82,7 +82,7 @@ class InboxActivities(TestCase):
@responses.activate
@patch("bookwyrm.activitystreams.ActivityStream.add_status")
def test_boost_remote_status(self, redis_mock):
""" boost a status from a remote server """
"""boost a status from a remote server"""
work = models.Work.objects.create(title="work title")
book = models.Edition.objects.create(
title="Test",
@ -131,7 +131,7 @@ class InboxActivities(TestCase):
@responses.activate
def test_discarded_boost(self):
""" test a boost of a mastodon status that will be discarded """
"""test a boost of a mastodon status that will be discarded"""
status = models.Status(
content="hi",
user=self.remote_user,
@ -154,7 +154,7 @@ class InboxActivities(TestCase):
self.assertEqual(models.Boost.objects.count(), 0)
def test_unboost(self):
""" undo a boost """
"""undo a boost"""
with patch("bookwyrm.activitystreams.ActivityStream.add_status"):
boost = models.Boost.objects.create(
boosted_status=self.status, user=self.remote_user
@ -183,7 +183,7 @@ class InboxActivities(TestCase):
self.assertFalse(models.Boost.objects.exists())
def test_unboost_unknown_boost(self):
""" undo a boost """
"""undo a boost"""
activity = {
"type": "Undo",
"actor": "hi",

View file

@ -8,10 +8,10 @@ from bookwyrm import models, views
# pylint: disable=too-many-public-methods
class InboxBlock(TestCase):
""" inbox tests """
"""inbox tests"""
def setUp(self):
""" basic user and book data """
"""basic user and book data"""
self.local_user = models.User.objects.create_user(
"mouse@example.com",
"mouse@mouse.com",
@ -35,7 +35,7 @@ class InboxBlock(TestCase):
models.SiteSettings.objects.create()
def test_handle_blocks(self):
""" create a "block" database entry from an activity """
"""create a "block" database entry from an activity"""
self.local_user.followers.add(self.remote_user)
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
models.UserFollowRequest.objects.create(
@ -67,7 +67,7 @@ class InboxBlock(TestCase):
self.assertFalse(models.UserFollowRequest.objects.exists())
def test_handle_unblock(self):
""" unblock a user """
"""unblock a user"""
self.remote_user.blocks.add(self.local_user)
block = models.UserBlocks.objects.get()

View file

@ -11,10 +11,10 @@ from bookwyrm.activitypub import ActivitySerializerError
# pylint: disable=too-many-public-methods
class InboxCreate(TestCase):
""" readthrough tests """
"""readthrough tests"""
def setUp(self):
""" basic user and book data """
"""basic user and book data"""
self.local_user = models.User.objects.create_user(
"mouse@example.com",
"mouse@mouse.com",
@ -53,7 +53,7 @@ class InboxCreate(TestCase):
models.SiteSettings.objects.create()
def test_create_status(self):
""" the "it justs works" mode """
"""the "it justs works" mode"""
self.assertEqual(models.Status.objects.count(), 1)
datafile = pathlib.Path(__file__).parent.joinpath(
@ -84,7 +84,7 @@ class InboxCreate(TestCase):
self.assertEqual(models.Status.objects.count(), 2)
def test_create_status_remote_note_with_mention(self):
""" should only create it under the right circumstances """
"""should only create it under the right circumstances"""
self.assertEqual(models.Status.objects.count(), 1)
self.assertFalse(
models.Notification.objects.filter(user=self.local_user).exists()
@ -107,7 +107,7 @@ class InboxCreate(TestCase):
self.assertEqual(models.Notification.objects.get().notification_type, "MENTION")
def test_create_status_remote_note_with_reply(self):
""" should only create it under the right circumstances """
"""should only create it under the right circumstances"""
self.assertEqual(models.Status.objects.count(), 1)
self.assertFalse(models.Notification.objects.filter(user=self.local_user))
@ -128,7 +128,7 @@ class InboxCreate(TestCase):
self.assertEqual(models.Notification.objects.get().notification_type, "REPLY")
def test_create_list(self):
""" a new list """
"""a new list"""
activity = self.create_json
activity["object"] = {
"id": "https://example.com/list/22",
@ -152,7 +152,7 @@ class InboxCreate(TestCase):
self.assertEqual(book_list.remote_id, "https://example.com/list/22")
def test_create_unsupported_type(self):
""" ignore activities we know we can't handle """
"""ignore activities we know we can't handle"""
activity = self.create_json
activity["object"] = {
"id": "https://example.com/status/887",
@ -162,7 +162,7 @@ class InboxCreate(TestCase):
views.inbox.activity_task(activity)
def test_create_unknown_type(self):
""" ignore activities we know we've never heard of """
"""ignore activities we know we've never heard of"""
activity = self.create_json
activity["object"] = {
"id": "https://example.com/status/887",

View file

@ -9,10 +9,10 @@ from bookwyrm import models, views
# pylint: disable=too-many-public-methods
class InboxActivities(TestCase):
""" inbox tests """
"""inbox tests"""
def setUp(self):
""" basic user and book data """
"""basic user and book data"""
self.local_user = models.User.objects.create_user(
"mouse@example.com",
"mouse@mouse.com",
@ -50,7 +50,7 @@ class InboxActivities(TestCase):
models.SiteSettings.objects.create()
def test_delete_status(self):
""" remove a status """
"""remove a status"""
self.assertFalse(self.status.deleted)
activity = {
"type": "Delete",
@ -71,7 +71,7 @@ class InboxActivities(TestCase):
self.assertIsInstance(status.deleted_date, datetime)
def test_delete_status_notifications(self):
""" remove a status with related notifications """
"""remove a status with related notifications"""
models.Notification.objects.create(
related_status=self.status,
user=self.local_user,
@ -106,7 +106,7 @@ class InboxActivities(TestCase):
self.assertEqual(models.Notification.objects.get(), notif)
def test_delete_user(self):
""" delete a user """
"""delete a user"""
self.assertTrue(models.User.objects.get(username="rat@example.com").is_active)
activity = {
"@context": "https://www.w3.org/ns/activitystreams",
@ -121,7 +121,7 @@ class InboxActivities(TestCase):
self.assertFalse(models.User.objects.get(username="rat@example.com").is_active)
def test_delete_user_unknown(self):
""" don't worry about it if we don't know the user """
"""don't worry about it if we don't know the user"""
self.assertEqual(models.User.objects.filter(is_active=True).count(), 2)
activity = {
"@context": "https://www.w3.org/ns/activitystreams",

View file

@ -9,10 +9,10 @@ from bookwyrm import models, views
# pylint: disable=too-many-public-methods
class InboxRelationships(TestCase):
""" inbox tests """
"""inbox tests"""
def setUp(self):
""" basic user and book data """
"""basic user and book data"""
self.local_user = models.User.objects.create_user(
"mouse@example.com",
"mouse@mouse.com",
@ -36,7 +36,7 @@ class InboxRelationships(TestCase):
models.SiteSettings.objects.create()
def test_follow(self):
""" remote user wants to follow local user """
"""remote user wants to follow local user"""
activity = {
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://example.com/users/rat/follows/123",
@ -65,7 +65,7 @@ class InboxRelationships(TestCase):
self.assertEqual(follow.user_subject, self.remote_user)
def test_follow_duplicate(self):
""" remote user wants to follow local user twice """
"""remote user wants to follow local user twice"""
activity = {
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://example.com/users/rat/follows/123",
@ -92,7 +92,7 @@ class InboxRelationships(TestCase):
self.assertEqual(follow.user_subject, self.remote_user)
def test_follow_manually_approved(self):
""" needs approval before following """
"""needs approval before following"""
activity = {
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://example.com/users/rat/follows/123",
@ -122,7 +122,7 @@ class InboxRelationships(TestCase):
self.assertEqual(list(follow), [])
def test_undo_follow_request(self):
""" the requester cancels a follow request """
"""the requester cancels a follow request"""
self.local_user.manually_approves_followers = True
self.local_user.save(broadcast=False)
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
@ -152,7 +152,7 @@ class InboxRelationships(TestCase):
self.assertFalse(self.local_user.follower_requests.exists())
def test_unfollow(self):
""" remove a relationship """
"""remove a relationship"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
rel = models.UserFollows.objects.create(
user_subject=self.remote_user, user_object=self.local_user
@ -177,7 +177,7 @@ class InboxRelationships(TestCase):
self.assertIsNone(self.local_user.followers.first())
def test_follow_accept(self):
""" a remote user approved a follow request from local """
"""a remote user approved a follow request from local"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
rel = models.UserFollowRequest.objects.create(
user_subject=self.local_user, user_object=self.remote_user
@ -208,7 +208,7 @@ class InboxRelationships(TestCase):
self.assertEqual(follows.first(), self.local_user)
def test_follow_reject(self):
""" turn down a follow request """
"""turn down a follow request"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
rel = models.UserFollowRequest.objects.create(
user_subject=self.local_user, user_object=self.remote_user

View file

@ -8,10 +8,10 @@ from bookwyrm import models, views
# pylint: disable=too-many-public-methods
class InboxActivities(TestCase):
""" inbox tests """
"""inbox tests"""
def setUp(self):
""" basic user and book data """
"""basic user and book data"""
self.local_user = models.User.objects.create_user(
"mouse@example.com",
"mouse@mouse.com",
@ -50,7 +50,7 @@ class InboxActivities(TestCase):
models.SiteSettings.objects.create()
def test_handle_favorite(self):
""" fav a status """
"""fav a status"""
activity = {
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://example.com/fav/1",
@ -68,7 +68,7 @@ class InboxActivities(TestCase):
self.assertEqual(fav.user, self.remote_user)
def test_ignore_favorite(self):
""" don't try to save an unknown status """
"""don't try to save an unknown status"""
activity = {
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://example.com/fav/1",
@ -83,7 +83,7 @@ class InboxActivities(TestCase):
self.assertFalse(models.Favorite.objects.exists())
def test_handle_unfavorite(self):
""" fav a status """
"""fav a status"""
activity = {
"id": "https://example.com/fav/1#undo",
"type": "Undo",

View file

@ -8,10 +8,10 @@ from bookwyrm import models, views
# pylint: disable=too-many-public-methods
class InboxRemove(TestCase):
""" inbox tests """
"""inbox tests"""
def setUp(self):
""" basic user and book data """
"""basic user and book data"""
self.local_user = models.User.objects.create_user(
"mouse@example.com",
"mouse@mouse.com",
@ -41,7 +41,7 @@ class InboxRemove(TestCase):
models.SiteSettings.objects.create()
def test_handle_unshelve_book(self):
""" remove a book from a shelf """
"""remove a book from a shelf"""
shelf = models.Shelf.objects.create(user=self.remote_user, name="Test Shelf")
shelf.remote_id = "https://bookwyrm.social/user/mouse/shelf/to-read"
shelf.save()
@ -70,7 +70,7 @@ class InboxRemove(TestCase):
self.assertFalse(shelf.books.exists())
def test_handle_remove_book_from_list(self):
""" listing a book """
"""listing a book"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
booklist = models.List.objects.create(
name="test list",

View file

@ -10,10 +10,10 @@ from bookwyrm import models, views
# pylint: disable=too-many-public-methods
class InboxUpdate(TestCase):
""" inbox tests """
"""inbox tests"""
def setUp(self):
""" basic user and book data """
"""basic user and book data"""
self.local_user = models.User.objects.create_user(
"mouse@example.com",
"mouse@mouse.com",
@ -45,7 +45,7 @@ class InboxUpdate(TestCase):
models.SiteSettings.objects.create()
def test_update_list(self):
""" a new list """
"""a new list"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
book_list = models.List.objects.create(
name="hi", remote_id="https://example.com/list/22", user=self.local_user
@ -79,7 +79,7 @@ class InboxUpdate(TestCase):
self.assertEqual(book_list.remote_id, "https://example.com/list/22")
def test_update_user(self):
""" update an existing user """
"""update an existing user"""
models.UserFollows.objects.create(
user_subject=self.local_user,
user_object=self.remote_user,
@ -116,7 +116,7 @@ class InboxUpdate(TestCase):
self.assertTrue(self.local_user in self.remote_user.followers.all())
def test_update_edition(self):
""" update an existing edition """
"""update an existing edition"""
datafile = pathlib.Path(__file__).parent.joinpath("../../data/bw_edition.json")
bookdata = json.loads(datafile.read_bytes())
@ -146,7 +146,7 @@ class InboxUpdate(TestCase):
self.assertEqual(book.last_edited_by, self.remote_user)
def test_update_work(self):
""" update an existing edition """
"""update an existing edition"""
datafile = pathlib.Path(__file__).parent.joinpath("../../data/bw_work.json")
bookdata = json.loads(datafile.read_bytes())

View file

@ -14,10 +14,10 @@ from bookwyrm.settings import DOMAIN
# pylint: disable=too-many-public-methods
class AuthenticationViews(TestCase):
""" login and password management """
"""login and password management"""
def setUp(self):
""" we need basic test data and mocks """
"""we need basic test data and mocks"""
self.factory = RequestFactory()
self.local_user = models.User.objects.create_user(
"mouse@local.com",
@ -31,7 +31,7 @@ class AuthenticationViews(TestCase):
self.settings = models.SiteSettings.objects.create(id=1)
def test_login_get(self):
""" there are so many views, this just makes sure it LOADS """
"""there are so many views, this just makes sure it LOADS"""
login = views.Login.as_view()
request = self.factory.get("")
request.user = self.anonymous_user
@ -47,7 +47,7 @@ class AuthenticationViews(TestCase):
self.assertEqual(result.status_code, 302)
def test_register(self):
""" create a user """
"""create a user"""
view = views.Register.as_view()
self.assertEqual(models.User.objects.count(), 1)
request = self.factory.post(
@ -68,7 +68,7 @@ class AuthenticationViews(TestCase):
self.assertEqual(nutria.local, True)
def test_register_trailing_space(self):
""" django handles this so weirdly """
"""django handles this so weirdly"""
view = views.Register.as_view()
request = self.factory.post(
"register/",
@ -84,7 +84,7 @@ class AuthenticationViews(TestCase):
self.assertEqual(nutria.local, True)
def test_register_invalid_email(self):
""" gotta have an email """
"""gotta have an email"""
view = views.Register.as_view()
self.assertEqual(models.User.objects.count(), 1)
request = self.factory.post(
@ -95,7 +95,7 @@ class AuthenticationViews(TestCase):
response.render()
def test_register_invalid_username(self):
""" gotta have an email """
"""gotta have an email"""
view = views.Register.as_view()
self.assertEqual(models.User.objects.count(), 1)
request = self.factory.post(
@ -123,7 +123,7 @@ class AuthenticationViews(TestCase):
response.render()
def test_register_closed_instance(self):
""" you can't just register """
"""you can't just register"""
view = views.Register.as_view()
self.settings.allow_registration = False
self.settings.save()
@ -135,7 +135,7 @@ class AuthenticationViews(TestCase):
view(request)
def test_register_invite(self):
""" you can't just register """
"""you can't just register"""
view = views.Register.as_view()
self.settings.allow_registration = False
self.settings.save()

View file

@ -12,10 +12,10 @@ from bookwyrm.activitypub import ActivitypubResponse
class AuthorViews(TestCase):
""" author views"""
"""author views"""
def setUp(self):
""" we need basic test data and mocks """
"""we need basic test data and mocks"""
self.factory = RequestFactory()
self.local_user = models.User.objects.create_user(
"mouse@local.com",
@ -42,7 +42,7 @@ class AuthorViews(TestCase):
models.SiteSettings.objects.create()
def test_author_page(self):
""" there are so many views, this just makes sure it LOADS """
"""there are so many views, this just makes sure it LOADS"""
view = views.Author.as_view()
author = models.Author.objects.create(name="Jessica")
request = self.factory.get("")
@ -62,7 +62,7 @@ class AuthorViews(TestCase):
self.assertEqual(result.status_code, 200)
def test_edit_author_page(self):
""" there are so many views, this just makes sure it LOADS """
"""there are so many views, this just makes sure it LOADS"""
view = views.EditAuthor.as_view()
author = models.Author.objects.create(name="Test Author")
request = self.factory.get("")
@ -76,7 +76,7 @@ class AuthorViews(TestCase):
self.assertEqual(result.status_code, 200)
def test_edit_author(self):
""" edit an author """
"""edit an author"""
view = views.EditAuthor.as_view()
author = models.Author.objects.create(name="Test Author")
self.local_user.groups.add(self.group)
@ -93,7 +93,7 @@ class AuthorViews(TestCase):
self.assertEqual(author.last_edited_by, self.local_user)
def test_edit_author_non_editor(self):
""" edit an author with invalid post data"""
"""edit an author with invalid post data"""
view = views.EditAuthor.as_view()
author = models.Author.objects.create(name="Test Author")
form = forms.AuthorForm(instance=author)
@ -108,7 +108,7 @@ class AuthorViews(TestCase):
self.assertEqual(author.name, "Test Author")
def test_edit_author_invalid_form(self):
""" edit an author with invalid post data"""
"""edit an author with invalid post data"""
view = views.EditAuthor.as_view()
author = models.Author.objects.create(name="Test Author")
self.local_user.groups.add(self.group)

View file

@ -9,10 +9,10 @@ from bookwyrm import models, views
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay")
class BlockViews(TestCase):
""" view user and edit profile """
"""view user and edit profile"""
def setUp(self):
""" we need basic test data and mocks """
"""we need basic test data and mocks"""
self.factory = RequestFactory()
self.local_user = models.User.objects.create_user(
"mouse@local.com",
@ -34,7 +34,7 @@ class BlockViews(TestCase):
models.SiteSettings.objects.create()
def test_block_get(self, _):
""" there are so many views, this just makes sure it LOADS """
"""there are so many views, this just makes sure it LOADS"""
view = views.Block.as_view()
request = self.factory.get("")
request.user = self.local_user
@ -44,7 +44,7 @@ class BlockViews(TestCase):
self.assertEqual(result.status_code, 200)
def test_block_post(self, _):
""" create a "block" database entry from an activity """
"""create a "block" database entry from an activity"""
view = views.Block.as_view()
self.local_user.followers.add(self.remote_user)
models.UserFollowRequest.objects.create(
@ -65,7 +65,7 @@ class BlockViews(TestCase):
self.assertFalse(models.UserFollowRequest.objects.exists())
def test_unblock(self, _):
""" undo a block """
"""undo a block"""
self.local_user.blocks.add(self.remote_user)
request = self.factory.post("")
request.user = self.local_user

View file

@ -18,10 +18,10 @@ from bookwyrm.activitypub import ActivitypubResponse
class BookViews(TestCase):
""" books books books """
"""books books books"""
def setUp(self):
""" we need basic test data and mocks """
"""we need basic test data and mocks"""
self.factory = RequestFactory()
self.local_user = models.User.objects.create_user(
"mouse@local.com",
@ -81,7 +81,7 @@ class BookViews(TestCase):
)
def test_book_page(self):
""" there are so many views, this just makes sure it LOADS """
"""there are so many views, this just makes sure it LOADS"""
view = views.Book.as_view()
request = self.factory.get("")
request.user = self.local_user
@ -100,7 +100,7 @@ class BookViews(TestCase):
self.assertEqual(result.status_code, 200)
def test_edit_book_page(self):
""" there are so many views, this just makes sure it LOADS """
"""there are so many views, this just makes sure it LOADS"""
view = views.EditBook.as_view()
request = self.factory.get("")
request.user = self.local_user
@ -111,7 +111,7 @@ class BookViews(TestCase):
self.assertEqual(result.status_code, 200)
def test_edit_book(self):
""" lets a user edit a book """
"""lets a user edit a book"""
view = views.EditBook.as_view()
self.local_user.groups.add(self.group)
form = forms.EditionForm(instance=self.book)
@ -125,7 +125,7 @@ class BookViews(TestCase):
self.assertEqual(self.book.title, "New Title")
def test_edit_book_add_author(self):
""" lets a user edit a book with new authors """
"""lets a user edit a book with new authors"""
view = views.EditBook.as_view()
self.local_user.groups.add(self.group)
form = forms.EditionForm(instance=self.book)
@ -143,7 +143,7 @@ class BookViews(TestCase):
self.assertEqual(self.book.title, "Example Edition")
def test_edit_book_add_new_author_confirm(self):
""" lets a user edit a book confirmed with new authors """
"""lets a user edit a book confirmed with new authors"""
view = views.ConfirmEditBook.as_view()
self.local_user.groups.add(self.group)
form = forms.EditionForm(instance=self.book)
@ -162,7 +162,7 @@ class BookViews(TestCase):
self.assertEqual(self.book.authors.first().name, "Sappho")
def test_edit_book_remove_author(self):
""" remove an author from a book """
"""remove an author from a book"""
author = models.Author.objects.create(name="Sappho")
self.book.authors.add(author)
form = forms.EditionForm(instance=self.book)
@ -182,7 +182,7 @@ class BookViews(TestCase):
self.assertFalse(self.book.authors.exists())
def test_create_book(self):
""" create an entirely new book and work """
"""create an entirely new book and work"""
view = views.ConfirmEditBook.as_view()
self.local_user.groups.add(self.group)
form = forms.EditionForm()
@ -196,7 +196,7 @@ class BookViews(TestCase):
self.assertEqual(book.parent_work.title, "New Title")
def test_create_book_existing_work(self):
""" create an entirely new book and work """
"""create an entirely new book and work"""
view = views.ConfirmEditBook.as_view()
self.local_user.groups.add(self.group)
form = forms.EditionForm()
@ -211,7 +211,7 @@ class BookViews(TestCase):
self.assertEqual(book.parent_work, self.work)
def test_create_book_with_author(self):
""" create an entirely new book and work """
"""create an entirely new book and work"""
view = views.ConfirmEditBook.as_view()
self.local_user.groups.add(self.group)
form = forms.EditionForm()
@ -229,7 +229,7 @@ class BookViews(TestCase):
self.assertEqual(book.authors.first(), book.parent_work.authors.first())
def test_switch_edition(self):
""" updates user's relationships to a book """
"""updates user's relationships to a book"""
work = models.Work.objects.create(title="test work")
edition1 = models.Edition.objects.create(title="first ed", parent_work=work)
edition2 = models.Edition.objects.create(title="second ed", parent_work=work)
@ -253,7 +253,7 @@ class BookViews(TestCase):
self.assertEqual(models.ReadThrough.objects.get().book, edition2)
def test_editions_page(self):
""" there are so many views, this just makes sure it LOADS """
"""there are so many views, this just makes sure it LOADS"""
view = views.Editions.as_view()
request = self.factory.get("")
with patch("bookwyrm.views.books.is_api_request") as is_api:
@ -271,7 +271,7 @@ class BookViews(TestCase):
self.assertEqual(result.status_code, 200)
def test_upload_cover_file(self):
""" add a cover via file upload """
"""add a cover via file upload"""
self.assertFalse(self.book.cover)
image_file = pathlib.Path(__file__).parent.joinpath(
"../../static/images/default_avi.jpg"
@ -296,7 +296,7 @@ class BookViews(TestCase):
@responses.activate
def test_upload_cover_url(self):
""" add a cover via url """
"""add a cover via url"""
self.assertFalse(self.book.cover)
image_file = pathlib.Path(__file__).parent.joinpath(
"../../static/images/default_avi.jpg"

View file

@ -7,10 +7,10 @@ from bookwyrm import models, views
# pylint: disable=unused-argument
class DirectoryViews(TestCase):
""" tag views"""
"""tag views"""
def setUp(self):
""" we need basic test data and mocks """
"""we need basic test data and mocks"""
self.factory = RequestFactory()
self.local_user = models.User.objects.create_user(
"mouse@local.com",
@ -32,7 +32,7 @@ class DirectoryViews(TestCase):
models.SiteSettings.objects.create()
def test_directory_page(self):
""" there are so many views, this just makes sure it LOADS """
"""there are so many views, this just makes sure it LOADS"""
view = views.Directory.as_view()
request = self.factory.get("")
request.user = self.local_user

View file

@ -10,10 +10,10 @@ from bookwyrm import forms, models, views
class FederationViews(TestCase):
""" every response to a get request, html or json """
"""every response to a get request, html or json"""
def setUp(self):
""" we need basic test data and mocks """
"""we need basic test data and mocks"""
self.factory = RequestFactory()
self.local_user = models.User.objects.create_user(
"mouse@local.com",
@ -35,7 +35,7 @@ class FederationViews(TestCase):
models.SiteSettings.objects.create()
def test_federation_page(self):
""" there are so many views, this just makes sure it LOADS """
"""there are so many views, this just makes sure it LOADS"""
view = views.Federation.as_view()
request = self.factory.get("")
request.user = self.local_user
@ -46,7 +46,7 @@ class FederationViews(TestCase):
self.assertEqual(result.status_code, 200)
def test_server_page(self):
""" there are so many views, this just makes sure it LOADS """
"""there are so many views, this just makes sure it LOADS"""
server = models.FederatedServer.objects.create(server_name="hi.there.com")
view = views.FederatedServer.as_view()
request = self.factory.get("")
@ -59,7 +59,7 @@ class FederationViews(TestCase):
self.assertEqual(result.status_code, 200)
def test_server_page_block(self):
""" block a server """
"""block a server"""
server = models.FederatedServer.objects.create(server_name="hi.there.com")
self.remote_user.federated_server = server
self.remote_user.save()
@ -79,7 +79,7 @@ class FederationViews(TestCase):
self.assertFalse(self.remote_user.is_active)
def test_server_page_unblock(self):
""" unblock a server """
"""unblock a server"""
server = models.FederatedServer.objects.create(
server_name="hi.there.com", status="blocked"
)
@ -100,7 +100,7 @@ class FederationViews(TestCase):
self.assertTrue(self.remote_user.is_active)
def test_add_view_get(self):
""" there are so many views, this just makes sure it LOADS """
"""there are so many views, this just makes sure it LOADS"""
# create mode
view = views.AddFederatedServer.as_view()
request = self.factory.get("")
@ -113,7 +113,7 @@ class FederationViews(TestCase):
self.assertEqual(result.status_code, 200)
def test_add_view_post_create(self):
""" create a server entry """
"""create a server entry"""
form = forms.ServerForm()
form.data["server_name"] = "remote.server"
form.data["application_type"] = "coolsoft"
@ -131,7 +131,7 @@ class FederationViews(TestCase):
self.assertEqual(server.status, "blocked")
def test_import_blocklist(self):
""" load a json file with a list of servers to block """
"""load a json file with a list of servers to block"""
server = models.FederatedServer.objects.create(server_name="hi.there.com")
self.remote_user.federated_server = server
self.remote_user.save()

Some files were not shown because too many files have changed in this diff Show more