Merge branch 'main' into production

This commit is contained in:
Mouse Reeve 2021-04-26 14:07:57 -07:00
commit 3d2a56090c
161 changed files with 11780 additions and 4491 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): 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) return naive_parse(activity_objects, activity_json)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -16,11 +16,11 @@ logger = logging.getLogger(__name__)
class ConnectorException(HTTPError): 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): def search(query, min_confidence=0.1):
""" find books based on arbitary keywords """ """find books based on arbitary keywords"""
if not query: if not query:
return [] return []
results = [] results = []
@ -67,20 +67,22 @@ def search(query, min_confidence=0.1):
return results return results
def local_search(query, min_confidence=0.1, raw=False): def local_search(query, min_confidence=0.1, raw=False, filters=None):
""" only look at local search results """ """only look at local search results"""
connector = load_connector(models.Connector.objects.get(local=True)) 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): 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)) connector = load_connector(models.Connector.objects.get(local=True))
return connector.isbn_search(query, raw=raw) return connector.isbn_search(query, raw=raw)
def first_search_result(query, min_confidence=0.1): 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(): for connector in get_connectors():
result = connector.search(query, min_confidence=min_confidence) result = connector.search(query, min_confidence=min_confidence)
if result: if result:
@ -89,13 +91,13 @@ def first_search_result(query, min_confidence=0.1):
def get_connectors(): def get_connectors():
""" load all connectors """ """load all connectors"""
for info in models.Connector.objects.order_by("priority").all(): for info in models.Connector.objects.order_by("priority").all():
yield load_connector(info) yield load_connector(info)
def get_or_create_connector(remote_id): 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) url = urlparse(remote_id)
identifier = url.netloc identifier = url.netloc
if not identifier: if not identifier:
@ -119,7 +121,7 @@ def get_or_create_connector(remote_id):
@app.task @app.task
def load_more_data(connector_id, book_id): 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_info = models.Connector.objects.get(id=connector_id)
connector = load_connector(connector_info) connector = load_connector(connector_info)
book = models.Book.objects.select_subclasses().get(id=book_id) 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): def load_connector(connector_info):
""" instantiate the connector class """ """instantiate the connector class"""
connector = importlib.import_module( connector = importlib.import_module(
"bookwyrm.connectors.%s" % connector_info.connector_file "bookwyrm.connectors.%s" % connector_info.connector_file
) )
@ -137,6 +139,6 @@ def load_connector(connector_info):
@receiver(signals.post_save, sender="bookwyrm.FederatedServer") @receiver(signals.post_save, sender="bookwyrm.FederatedServer")
# pylint: disable=unused-argument # pylint: disable=unused-argument
def create_connector(sender, instance, created, *args, **kwargs): 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": if instance.application_type == "bookwyrm":
get_or_create_connector("https://{:s}".format(instance.server_name)) get_or_create_connector("https://{:s}".format(instance.server_name))

View file

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

View file

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

View file

@ -3,5 +3,5 @@ from bookwyrm import models
def site_settings(request): # pylint: disable=unused-argument 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()} return {"site": models.SiteSettings.objects.get()}

View file

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

View file

@ -12,7 +12,7 @@ from bookwyrm import models
class CustomForm(ModelForm): class CustomForm(ModelForm):
""" add css classes to the forms """ """add css classes to the forms"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
css_classes = defaultdict(lambda: "") css_classes = defaultdict(lambda: "")
@ -198,7 +198,7 @@ class ImportForm(forms.Form):
class ExpiryWidget(widgets.Select): class ExpiryWidget(widgets.Select):
def value_from_datadict(self, data, files, name): 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) selected_string = super().value_from_datadict(data, files, name)
if selected_string == "day": if selected_string == "day":
@ -217,7 +217,7 @@ class ExpiryWidget(widgets.Select):
class InviteRequestForm(CustomForm): class InviteRequestForm(CustomForm):
def clean(self): 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() cleaned_data = super().clean()
email = cleaned_data.get("email") email = cleaned_data.get("email")
if email and models.User.objects.filter(email=email).exists(): if email and models.User.objects.filter(email=email).exists():

View file

@ -9,7 +9,7 @@ class GoodreadsImporter(Importer):
service = "GoodReads" service = "GoodReads"
def parse_fields(self, entry): 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}) entry.update({"import_source": self.service})
# add missing 'Date Started' field # add missing 'Date Started' field
entry.update({"Date Started": None}) entry.update({"Date Started": None})

View file

@ -10,7 +10,7 @@ logger = logging.getLogger(__name__)
class Importer: class Importer:
""" Generic class for csv data import from an outside service """ """Generic class for csv data import from an outside service"""
service = "Unknown" service = "Unknown"
delimiter = "," delimiter = ","
@ -18,7 +18,7 @@ class Importer:
mandatory_fields = ["Title", "Author"] mandatory_fields = ["Title", "Author"]
def create_job(self, user, csv_file, include_reviews, privacy): 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( job = ImportJob.objects.create(
user=user, include_reviews=include_reviews, privacy=privacy user=user, include_reviews=include_reviews, privacy=privacy
) )
@ -32,16 +32,16 @@ class Importer:
return job return job
def save_item(self, job, index, data): # pylint: disable=no-self-use 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() ImportItem(job=job, index=index, data=data).save()
def parse_fields(self, entry): def parse_fields(self, entry):
""" updates csv data with additional info """ """updates csv data with additional info"""
entry.update({"import_source": self.service}) entry.update({"import_source": self.service})
return entry return entry
def create_retry_job(self, user, original_job, items): 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( job = ImportJob.objects.create(
user=user, user=user,
include_reviews=original_job.include_reviews, include_reviews=original_job.include_reviews,
@ -53,7 +53,7 @@ class Importer:
return job return job
def start_import(self, job): def start_import(self, job):
""" initalizes a csv import job """ """initalizes a csv import job"""
result = import_data.delay(self.service, job.id) result = import_data.delay(self.service, job.id)
job.task_id = result.id job.task_id = result.id
job.save() job.save()
@ -61,7 +61,7 @@ class Importer:
@app.task @app.task
def import_data(source, job_id): 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) job = ImportJob.objects.get(id=job_id)
try: try:
for item in job.items.all(): 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): 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): if isinstance(item.book, models.Work):
item.book = item.book.default_edition item.book = item.book.default_edition
if not item.book: if not item.book:
@ -116,24 +116,33 @@ def handle_imported_book(source, user, item, include_reviews, privacy):
read.save() read.save()
if include_reviews and (item.rating or item.review): 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, # we don't know the publication date of the review,
# but "now" is a bad guess # but "now" is a bad guess
published_date_guess = item.date_read or item.date_added published_date_guess = item.date_read or item.date_added
models.Review.objects.create( if item.review:
user=user, review_title = (
book=item.book, "Review of {!r} on {!r}".format(
name=review_title, item.book.title,
content=item.review, source,
rating=item.rating, )
published_date=published_date_guess, if item.review
privacy=privacy, 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): class LibrarythingImporter(Importer):
""" csv downloads from librarything """ """csv downloads from librarything"""
service = "LibraryThing" service = "LibraryThing"
delimiter = "\t" delimiter = "\t"
@ -15,7 +15,7 @@ class LibrarythingImporter(Importer):
mandatory_fields = ["Title", "Primary Author"] mandatory_fields = ["Title", "Primary Author"]
def parse_fields(self, entry): def parse_fields(self, entry):
""" custom parsing for librarything """ """custom parsing for librarything"""
data = {} data = {}
data["import_source"] = self.service data["import_source"] = self.service
data["Book Id"] = entry["Book Id"] data["Book Id"] = entry["Book Id"]

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -8,7 +8,7 @@ from psycopg2.extras import execute_values
def convert_review_rating(app_registry, schema_editor): 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 db_alias = schema_editor.connection.alias
reviews = ( reviews = (
@ -29,7 +29,7 @@ VALUES %s""",
def unconvert_review_rating(app_registry, schema_editor): 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 # 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 # 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 # 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): 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]) activity[field[1]] = getattr(obj, field[0])
class ActivitypubMixin: class ActivitypubMixin:
""" add this mixin for models that are AP serializable """ """add this mixin for models that are AP serializable"""
activity_serializer = lambda: {} activity_serializer = lambda: {}
reverse_unfurl = False reverse_unfurl = False
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
""" collect some info on model fields """ """collect some info on model fields"""
self.image_fields = [] self.image_fields = []
self.many_to_many_fields = [] self.many_to_many_fields = []
self.simple_fields = [] # "simple" self.simple_fields = [] # "simple"
@ -85,7 +85,7 @@ class ActivitypubMixin:
@classmethod @classmethod
def find_existing_by_remote_id(cls, remote_id): 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}) return cls.find_existing({"id": remote_id})
@classmethod @classmethod
@ -126,7 +126,7 @@ class ActivitypubMixin:
return match.first() return match.first()
def broadcast(self, activity, sender, software=None): def broadcast(self, activity, sender, software=None):
""" send out an activity """ """send out an activity"""
broadcast_task.delay( broadcast_task.delay(
sender.id, sender.id,
json.dumps(activity, cls=activitypub.ActivityEncoder), json.dumps(activity, cls=activitypub.ActivityEncoder),
@ -134,7 +134,7 @@ class ActivitypubMixin:
) )
def get_recipients(self, software=None): 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 # first we have to figure out who should receive this activity
privacy = self.privacy if hasattr(self, "privacy") else "public" privacy = self.privacy if hasattr(self, "privacy") else "public"
# is this activity owned by a user (statuses, lists, shelves), or is it # is this activity owned by a user (statuses, lists, shelves), or is it
@ -182,20 +182,20 @@ class ActivitypubMixin:
return list(set(recipients)) return list(set(recipients))
def to_activity_dataclass(self): def to_activity_dataclass(self):
""" convert from a model to an activity """ """convert from a model to an activity"""
activity = generate_activity(self) activity = generate_activity(self)
return self.activity_serializer(**activity) return self.activity_serializer(**activity)
def to_activity(self, **kwargs): # pylint: disable=unused-argument 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() return self.to_activity_dataclass().serialize()
class ObjectMixin(ActivitypubMixin): 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): 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) broadcast = kwargs.get("broadcast", True)
# this bonus kwarg would cause an error in the base save method # this bonus kwarg would cause an error in the base save method
if "broadcast" in kwargs: if "broadcast" in kwargs:
@ -254,7 +254,7 @@ class ObjectMixin(ActivitypubMixin):
self.broadcast(activity, user) self.broadcast(activity, user)
def to_create_activity(self, user, **kwargs): 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) activity_object = self.to_activity_dataclass(**kwargs)
signature = None signature = None
@ -280,7 +280,7 @@ class ObjectMixin(ActivitypubMixin):
).serialize() ).serialize()
def to_delete_activity(self, user): def to_delete_activity(self, user):
""" notice of deletion """ """notice of deletion"""
return activitypub.Delete( return activitypub.Delete(
id=self.remote_id + "/activity", id=self.remote_id + "/activity",
actor=user.remote_id, actor=user.remote_id,
@ -290,7 +290,7 @@ class ObjectMixin(ActivitypubMixin):
).serialize() ).serialize()
def to_update_activity(self, user): 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()) activity_id = "%s#update/%s" % (self.remote_id, uuid4())
return activitypub.Update( return activitypub.Update(
id=activity_id, id=activity_id,
@ -306,13 +306,13 @@ class OrderedCollectionPageMixin(ObjectMixin):
@property @property
def collection_remote_id(self): 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 return self.remote_id
def to_ordered_collection( def to_ordered_collection(
self, queryset, remote_id=None, page=False, collection_only=False, **kwargs 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: if not queryset.ordered:
raise RuntimeError("queryset must be ordered") raise RuntimeError("queryset must be ordered")
@ -341,11 +341,11 @@ class OrderedCollectionPageMixin(ObjectMixin):
class OrderedCollectionMixin(OrderedCollectionPageMixin): class OrderedCollectionMixin(OrderedCollectionPageMixin):
""" extends activitypub models to work as ordered collections """ """extends activitypub models to work as ordered collections"""
@property @property
def collection_queryset(self): 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") raise NotImplementedError("Model must define collection_queryset")
activity_serializer = activitypub.OrderedCollection activity_serializer = activitypub.OrderedCollection
@ -354,24 +354,24 @@ class OrderedCollectionMixin(OrderedCollectionPageMixin):
return self.to_ordered_collection(self.collection_queryset, **kwargs) return self.to_ordered_collection(self.collection_queryset, **kwargs)
def to_activity(self, **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( return self.to_ordered_collection(
self.collection_queryset, **kwargs self.collection_queryset, **kwargs
).serialize() ).serialize()
class CollectionItemMixin(ActivitypubMixin): 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 activity_serializer = activitypub.CollectionItem
def broadcast(self, activity, sender, software="bookwyrm"): 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) super().broadcast(activity, sender, software=software)
@property @property
def privacy(self): 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) collection_field = getattr(self, self.collection_field)
if self.approved: if self.approved:
return collection_field.privacy return collection_field.privacy
@ -379,7 +379,7 @@ class CollectionItemMixin(ActivitypubMixin):
@property @property
def recipients(self): 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) collection_field = getattr(self, self.collection_field)
if collection_field.user.local: if collection_field.user.local:
# don't broadcast to yourself # don't broadcast to yourself
@ -387,7 +387,7 @@ class CollectionItemMixin(ActivitypubMixin):
return [collection_field.user] return [collection_field.user]
def save(self, *args, broadcast=True, **kwargs): def save(self, *args, broadcast=True, **kwargs):
""" broadcast updated """ """broadcast updated"""
# first off, we want to save normally no matter what # first off, we want to save normally no matter what
super().save(*args, **kwargs) super().save(*args, **kwargs)
@ -400,14 +400,14 @@ class CollectionItemMixin(ActivitypubMixin):
self.broadcast(activity, self.user) self.broadcast(activity, self.user)
def delete(self, *args, broadcast=True, **kwargs): def delete(self, *args, broadcast=True, **kwargs):
""" broadcast a remove activity """ """broadcast a remove activity"""
activity = self.to_remove_activity(self.user) activity = self.to_remove_activity(self.user)
super().delete(*args, **kwargs) super().delete(*args, **kwargs)
if self.user.local and broadcast: if self.user.local and broadcast:
self.broadcast(activity, self.user) self.broadcast(activity, self.user)
def to_add_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) collection_field = getattr(self, self.collection_field)
return activitypub.Add( return activitypub.Add(
id="{:s}#add".format(collection_field.remote_id), id="{:s}#add".format(collection_field.remote_id),
@ -417,7 +417,7 @@ class CollectionItemMixin(ActivitypubMixin):
).serialize() ).serialize()
def to_remove_activity(self, user): 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) collection_field = getattr(self, self.collection_field)
return activitypub.Remove( return activitypub.Remove(
id="{:s}#remove".format(collection_field.remote_id), id="{:s}#remove".format(collection_field.remote_id),
@ -428,24 +428,24 @@ class CollectionItemMixin(ActivitypubMixin):
class ActivityMixin(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): def save(self, *args, broadcast=True, **kwargs):
""" broadcast activity """ """broadcast activity"""
super().save(*args, **kwargs) super().save(*args, **kwargs)
user = self.user if hasattr(self, "user") else self.user_subject user = self.user if hasattr(self, "user") else self.user_subject
if broadcast and user.local: if broadcast and user.local:
self.broadcast(self.to_activity(), user) self.broadcast(self.to_activity(), user)
def delete(self, *args, broadcast=True, **kwargs): 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 user = self.user if hasattr(self, "user") else self.user_subject
if broadcast and user.local: if broadcast and user.local:
self.broadcast(self.to_undo_activity(), user) self.broadcast(self.to_undo_activity(), user)
super().delete(*args, **kwargs) super().delete(*args, **kwargs)
def to_undo_activity(self): def to_undo_activity(self):
""" undo an action """ """undo an action"""
user = self.user if hasattr(self, "user") else self.user_subject user = self.user if hasattr(self, "user") else self.user_subject
return activitypub.Undo( return activitypub.Undo(
id="%s#undo" % self.remote_id, id="%s#undo" % self.remote_id,
@ -455,7 +455,7 @@ class ActivityMixin(ActivitypubMixin):
def generate_activity(obj): def generate_activity(obj):
""" go through the fields on an object """ """go through the fields on an object"""
activity = {} activity = {}
for field in obj.activity_fields: for field in obj.activity_fields:
field.set_activity_from_field(activity, obj) field.set_activity_from_field(activity, obj)
@ -478,7 +478,7 @@ def generate_activity(obj):
def unfurl_related_field(related_field, sort_field=None): 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"): if sort_field and hasattr(related_field, "all"):
return [ return [
unfurl_related_field(i) for i in related_field.order_by(sort_field).all() 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 @app.task
def broadcast_task(sender_id, activity, recipients): 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) user_model = apps.get_model("bookwyrm.User", require_ready=True)
sender = user_model.objects.get(id=sender_id) sender = user_model.objects.get(id=sender_id)
for recipient in recipients: for recipient in recipients:
@ -505,7 +505,7 @@ def broadcast_task(sender_id, activity, recipients):
def sign_and_send(sender, data, destination): def sign_and_send(sender, data, destination):
""" crpyto whatever and http junk """ """crpyto whatever and http junk"""
now = http_date() now = http_date()
if not sender.key_pair.private_key: if not sender.key_pair.private_key:
@ -534,7 +534,7 @@ def sign_and_send(sender, data, destination):
def to_ordered_collection_page( def to_ordered_collection_page(
queryset, remote_id, id_only=False, page=1, pure=False, **kwargs 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) paginated = Paginator(queryset, PAGE_LENGTH)
activity_page = paginated.get_page(page) activity_page = paginated.get_page(page)

View file

@ -8,7 +8,7 @@ from . import fields
class Attachment(ActivitypubMixin, BookWyrmModel): 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 = models.ForeignKey(
"Status", on_delete=models.CASCADE, related_name="attachments", null=True "Status", on_delete=models.CASCADE, related_name="attachments", null=True
@ -16,13 +16,13 @@ class Attachment(ActivitypubMixin, BookWyrmModel):
reverse_unfurl = True reverse_unfurl = True
class Meta: 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 abstract = True
class Image(Attachment): class Image(Attachment):
""" an image attachment """ """an image attachment"""
image = fields.ImageField( image = fields.ImageField(
upload_to="status/", upload_to="status/",

View file

@ -9,7 +9,7 @@ from . import fields
class Author(BookDataModel): class Author(BookDataModel):
""" basic biographic info """ """basic biographic info"""
wikipedia_link = fields.CharField( wikipedia_link = fields.CharField(
max_length=255, blank=True, null=True, deduplication_field=True max_length=255, blank=True, null=True, deduplication_field=True
@ -24,7 +24,7 @@ class Author(BookDataModel):
bio = fields.HtmlField(null=True, blank=True) bio = fields.HtmlField(null=True, blank=True)
def get_remote_id(self): 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) return "https://%s/author/%s" % (DOMAIN, self.id)
activity_serializer = activitypub.Author activity_serializer = activitypub.Author

View file

@ -7,14 +7,14 @@ from .fields import RemoteIdField
class BookWyrmModel(models.Model): class BookWyrmModel(models.Model):
""" shared fields """ """shared fields"""
created_date = models.DateTimeField(auto_now_add=True) created_date = models.DateTimeField(auto_now_add=True)
updated_date = models.DateTimeField(auto_now=True) updated_date = models.DateTimeField(auto_now=True)
remote_id = RemoteIdField(null=True, activitypub_field="id") remote_id = RemoteIdField(null=True, activitypub_field="id")
def get_remote_id(self): 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 base_path = "https://%s" % DOMAIN
if hasattr(self, "user"): if hasattr(self, "user"):
base_path = "%s%s" % (base_path, self.user.local_path) 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) return "%s/%s/%d" % (base_path, model_name, self.id)
class Meta: 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 abstract = True
@property @property
def local_path(self): 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, "") return self.get_remote_id().replace("https://%s" % DOMAIN, "")
def visible_to_user(self, viewer): 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 # make sure this is an object with privacy owned by a user
if not hasattr(self, "user") or not hasattr(self, "privacy"): if not hasattr(self, "user") or not hasattr(self, "privacy"):
return None return None
@ -65,7 +65,7 @@ class BookWyrmModel(models.Model):
@receiver(models.signals.post_save) @receiver(models.signals.post_save)
# pylint: disable=unused-argument # pylint: disable=unused-argument
def set_remote_id(sender, instance, created, *args, **kwargs): 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"): if not created or not hasattr(instance, "get_remote_id"):
return return
if not instance.remote_id: if not instance.remote_id:

View file

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

View file

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

View file

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

View file

@ -13,7 +13,7 @@ FederationStatus = models.TextChoices(
class FederatedServer(BookWyrmModel): class FederatedServer(BookWyrmModel):
""" store which servers we federate with """ """store which servers we federate with"""
server_name = models.CharField(max_length=255, unique=True) server_name = models.CharField(max_length=255, unique=True)
status = models.CharField( status = models.CharField(
@ -25,7 +25,7 @@ class FederatedServer(BookWyrmModel):
notes = models.TextField(null=True, blank=True) notes = models.TextField(null=True, blank=True)
def block(self): def block(self):
""" block a server """ """block a server"""
self.status = "blocked" self.status = "blocked"
self.save() self.save()
@ -35,7 +35,7 @@ class FederatedServer(BookWyrmModel):
) )
def unblock(self): def unblock(self):
""" unblock a server """ """unblock a server"""
self.status = "federated" self.status = "federated"
self.save() self.save()
@ -45,7 +45,7 @@ class FederatedServer(BookWyrmModel):
@classmethod @classmethod
def is_blocked(cls, url): def is_blocked(cls, url):
""" look up if a domain is blocked """ """look up if a domain is blocked"""
url = urlparse(url) url = urlparse(url)
domain = url.netloc domain = url.netloc
return cls.objects.filter(server_name=domain, status="blocked").exists() 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): 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): if not value or not re.match(r"^http.?:\/\/[^\s]+$", value):
raise ValidationError( raise ValidationError(
_("%(value)s is not a valid remote_id"), _("%(value)s is not a valid remote_id"),
@ -27,7 +27,7 @@ def validate_remote_id(value):
def validate_localname(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): if not re.match(r"^[A-Za-z\-_\.0-9]+$", value):
raise ValidationError( raise ValidationError(
_("%(value)s is not a valid username"), _("%(value)s is not a valid username"),
@ -36,7 +36,7 @@ def validate_localname(value):
def validate_username(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): if not re.match(r"^[A-Za-z\-_\.0-9]+@[A-Za-z\-_\.0-9]+\.[a-z]{2,}$", value):
raise ValidationError( raise ValidationError(
_("%(value)s is not a valid username"), _("%(value)s is not a valid username"),
@ -45,7 +45,7 @@ def validate_username(value):
class ActivitypubFieldMixin: class ActivitypubFieldMixin:
""" make a database field serializable """ """make a database field serializable"""
def __init__( def __init__(
self, self,
@ -64,7 +64,7 @@ class ActivitypubFieldMixin:
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def set_field_from_activity(self, instance, data): 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: try:
value = getattr(data, self.get_activitypub_field()) value = getattr(data, self.get_activitypub_field())
except AttributeError: except AttributeError:
@ -78,7 +78,7 @@ class ActivitypubFieldMixin:
setattr(instance, self.name, formatted) setattr(instance, self.name, formatted)
def set_activity_from_field(self, activity, instance): def set_activity_from_field(self, activity, instance):
""" update the json object """ """update the json object"""
value = getattr(instance, self.name) value = getattr(instance, self.name)
formatted = self.field_to_activity(value) formatted = self.field_to_activity(value)
if formatted is None: if formatted is None:
@ -94,19 +94,19 @@ class ActivitypubFieldMixin:
activity[key] = formatted activity[key] = formatted
def field_to_activity(self, value): 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"): if hasattr(self, "activitypub_wrapper"):
return {self.activitypub_wrapper: value} return {self.activitypub_wrapper: value}
return value return value
def field_from_activity(self, 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"): if value and hasattr(self, "activitypub_wrapper"):
value = value.get(self.activitypub_wrapper) value = value.get(self.activitypub_wrapper)
return value return value
def get_activitypub_field(self): def get_activitypub_field(self):
""" model_field_name to activitypubFieldName """ """model_field_name to activitypubFieldName"""
if self.activitypub_field: if self.activitypub_field:
return self.activitypub_field return self.activitypub_field
name = self.name.split(".")[-1] name = self.name.split(".")[-1]
@ -115,7 +115,7 @@ class ActivitypubFieldMixin:
class ActivitypubRelatedFieldMixin(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): def __init__(self, *args, load_remote=True, **kwargs):
self.load_remote = load_remote self.load_remote = load_remote
@ -146,7 +146,7 @@ class ActivitypubRelatedFieldMixin(ActivitypubFieldMixin):
class RemoteIdField(ActivitypubFieldMixin, models.CharField): 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): def __init__(self, *args, max_length=255, validators=None, **kwargs):
validators = validators or [validate_remote_id] validators = validators or [validate_remote_id]
@ -156,7 +156,7 @@ class RemoteIdField(ActivitypubFieldMixin, models.CharField):
class UsernameField(ActivitypubFieldMixin, models.CharField): class UsernameField(ActivitypubFieldMixin, models.CharField):
""" activitypub-aware username field """ """activitypub-aware username field"""
def __init__(self, activitypub_field="preferredUsername", **kwargs): def __init__(self, activitypub_field="preferredUsername", **kwargs):
self.activitypub_field = activitypub_field self.activitypub_field = activitypub_field
@ -172,7 +172,7 @@ class UsernameField(ActivitypubFieldMixin, models.CharField):
) )
def deconstruct(self): def deconstruct(self):
""" implementation of models.Field deconstruct """ """implementation of models.Field deconstruct"""
name, path, args, kwargs = super().deconstruct() name, path, args, kwargs = super().deconstruct()
del kwargs["verbose_name"] del kwargs["verbose_name"]
del kwargs["max_length"] del kwargs["max_length"]
@ -191,7 +191,7 @@ PrivacyLevels = models.TextChoices(
class PrivacyField(ActivitypubFieldMixin, models.CharField): 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" public = "https://www.w3.org/ns/activitystreams#Public"
@ -236,7 +236,7 @@ class PrivacyField(ActivitypubFieldMixin, models.CharField):
class ForeignKey(ActivitypubRelatedFieldMixin, models.ForeignKey): class ForeignKey(ActivitypubRelatedFieldMixin, models.ForeignKey):
""" activitypub-aware foreign key field """ """activitypub-aware foreign key field"""
def field_to_activity(self, value): def field_to_activity(self, value):
if not value: if not value:
@ -245,7 +245,7 @@ class ForeignKey(ActivitypubRelatedFieldMixin, models.ForeignKey):
class OneToOneField(ActivitypubRelatedFieldMixin, models.OneToOneField): class OneToOneField(ActivitypubRelatedFieldMixin, models.OneToOneField):
""" activitypub-aware foreign key field """ """activitypub-aware foreign key field"""
def field_to_activity(self, value): def field_to_activity(self, value):
if not value: if not value:
@ -254,14 +254,14 @@ class OneToOneField(ActivitypubRelatedFieldMixin, models.OneToOneField):
class ManyToManyField(ActivitypubFieldMixin, models.ManyToManyField): 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): def __init__(self, *args, link_only=False, **kwargs):
self.link_only = link_only self.link_only = link_only
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def set_field_from_activity(self, instance, data): 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()) value = getattr(data, self.get_activitypub_field())
formatted = self.field_from_activity(value) formatted = self.field_from_activity(value)
if formatted is None or formatted is MISSING: if formatted is None or formatted is MISSING:
@ -293,7 +293,7 @@ class ManyToManyField(ActivitypubFieldMixin, models.ManyToManyField):
class TagField(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): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -333,7 +333,7 @@ class TagField(ManyToManyField):
def image_serializer(value, alt): def image_serializer(value, alt):
""" helper for serializing images """ """helper for serializing images"""
if value and hasattr(value, "url"): if value and hasattr(value, "url"):
url = value.url url = value.url
else: else:
@ -343,7 +343,7 @@ def image_serializer(value, alt):
class ImageField(ActivitypubFieldMixin, models.ImageField): class ImageField(ActivitypubFieldMixin, models.ImageField):
""" activitypub-aware image field """ """activitypub-aware image field"""
def __init__(self, *args, alt_field=None, **kwargs): def __init__(self, *args, alt_field=None, **kwargs):
self.alt_field = alt_field self.alt_field = alt_field
@ -351,7 +351,7 @@ class ImageField(ActivitypubFieldMixin, models.ImageField):
# pylint: disable=arguments-differ # pylint: disable=arguments-differ
def set_field_from_activity(self, instance, data, save=True): 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()) value = getattr(data, self.get_activitypub_field())
formatted = self.field_from_activity(value) formatted = self.field_from_activity(value)
if formatted is None or formatted is MISSING: if formatted is None or formatted is MISSING:
@ -397,7 +397,7 @@ class ImageField(ActivitypubFieldMixin, models.ImageField):
class DateTimeField(ActivitypubFieldMixin, models.DateTimeField): class DateTimeField(ActivitypubFieldMixin, models.DateTimeField):
""" activitypub-aware datetime field """ """activitypub-aware datetime field"""
def field_to_activity(self, value): def field_to_activity(self, value):
if not value: if not value:
@ -416,7 +416,7 @@ class DateTimeField(ActivitypubFieldMixin, models.DateTimeField):
class HtmlField(ActivitypubFieldMixin, models.TextField): class HtmlField(ActivitypubFieldMixin, models.TextField):
""" a text field for storing html """ """a text field for storing html"""
def field_from_activity(self, value): def field_from_activity(self, value):
if not value or value == MISSING: if not value or value == MISSING:
@ -427,30 +427,30 @@ class HtmlField(ActivitypubFieldMixin, models.TextField):
class ArrayField(ActivitypubFieldMixin, DjangoArrayField): class ArrayField(ActivitypubFieldMixin, DjangoArrayField):
""" activitypub-aware array field """ """activitypub-aware array field"""
def field_to_activity(self, value): def field_to_activity(self, value):
return [str(i) for i in value] return [str(i) for i in value]
class CharField(ActivitypubFieldMixin, models.CharField): class CharField(ActivitypubFieldMixin, models.CharField):
""" activitypub-aware char field """ """activitypub-aware char field"""
class TextField(ActivitypubFieldMixin, models.TextField): class TextField(ActivitypubFieldMixin, models.TextField):
""" activitypub-aware text field """ """activitypub-aware text field"""
class BooleanField(ActivitypubFieldMixin, models.BooleanField): class BooleanField(ActivitypubFieldMixin, models.BooleanField):
""" activitypub-aware boolean field """ """activitypub-aware boolean field"""
class IntegerField(ActivitypubFieldMixin, models.IntegerField): class IntegerField(ActivitypubFieldMixin, models.IntegerField):
""" activitypub-aware boolean field """ """activitypub-aware boolean field"""
class DecimalField(ActivitypubFieldMixin, models.DecimalField): class DecimalField(ActivitypubFieldMixin, models.DecimalField):
""" activitypub-aware boolean field """ """activitypub-aware boolean field"""
def field_to_activity(self, value): def field_to_activity(self, value):
if not value: if not value:

View file

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

View file

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

View file

@ -10,7 +10,7 @@ NotificationType = models.TextChoices(
class Notification(BookWyrmModel): 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) user = models.ForeignKey("User", on_delete=models.CASCADE)
related_book = models.ForeignKey("Edition", on_delete=models.CASCADE, null=True) related_book = models.ForeignKey("Edition", on_delete=models.CASCADE, null=True)
@ -29,7 +29,7 @@ class Notification(BookWyrmModel):
) )
def save(self, *args, **kwargs): 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 # there's probably a better way to do this
if self.__class__.objects.filter( if self.__class__.objects.filter(
user=self.user, user=self.user,
@ -45,7 +45,7 @@ class Notification(BookWyrmModel):
super().save(*args, **kwargs) super().save(*args, **kwargs)
class Meta: class Meta:
""" checks if notifcation is in enum list for valid types """ """checks if notifcation is in enum list for valid types"""
constraints = [ constraints = [
models.CheckConstraint( models.CheckConstraint(

View file

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

View file

@ -11,7 +11,7 @@ from . import fields
class UserRelationship(BookWyrmModel): class UserRelationship(BookWyrmModel):
""" many-to-many through table for followers """ """many-to-many through table for followers"""
user_subject = fields.ForeignKey( user_subject = fields.ForeignKey(
"User", "User",
@ -28,16 +28,16 @@ class UserRelationship(BookWyrmModel):
@property @property
def privacy(self): def privacy(self):
""" all relationships are handled directly with the participants """ """all relationships are handled directly with the participants"""
return "direct" return "direct"
@property @property
def recipients(self): 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] return [u for u in [self.user_subject, self.user_object] if not u.local]
class Meta: class Meta:
""" relationships should be unique """ """relationships should be unique"""
abstract = True abstract = True
constraints = [ constraints = [
@ -51,22 +51,22 @@ class UserRelationship(BookWyrmModel):
] ]
def get_remote_id(self): def get_remote_id(self):
""" use shelf identifier in remote_id """ """use shelf identifier in remote_id"""
base_path = self.user_subject.remote_id base_path = self.user_subject.remote_id
return "%s#follows/%d" % (base_path, self.id) return "%s#follows/%d" % (base_path, self.id)
class UserFollows(ActivityMixin, UserRelationship): class UserFollows(ActivityMixin, UserRelationship):
""" Following a user """ """Following a user"""
status = "follows" status = "follows"
def to_activity(self): # pylint: disable=arguments-differ 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)) return activitypub.Follow(**generate_activity(self))
def save(self, *args, **kwargs): 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 # blocking in either direction is a no-go
if UserBlocks.objects.filter( if UserBlocks.objects.filter(
Q( Q(
@ -85,7 +85,7 @@ class UserFollows(ActivityMixin, UserRelationship):
@classmethod @classmethod
def from_request(cls, follow_request): 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( return cls.objects.create(
user_subject=follow_request.user_subject, user_subject=follow_request.user_subject,
user_object=follow_request.user_object, user_object=follow_request.user_object,
@ -94,13 +94,13 @@ class UserFollows(ActivityMixin, UserRelationship):
class UserFollowRequest(ActivitypubMixin, UserRelationship): class UserFollowRequest(ActivitypubMixin, UserRelationship):
""" following a user requires manual or automatic confirmation """ """following a user requires manual or automatic confirmation"""
status = "follow_request" status = "follow_request"
activity_serializer = activitypub.Follow activity_serializer = activitypub.Follow
def save(self, *args, broadcast=True, **kwargs): 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 # if there's a request for a follow that already exists, accept it
# without changing the local database state # without changing the local database state
if UserFollows.objects.filter( if UserFollows.objects.filter(
@ -141,13 +141,13 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship):
) )
def get_accept_reject_id(self, status): 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 base_path = self.user_object.remote_id
return "%s#%s/%d" % (base_path, status, self.id or 0) return "%s#%s/%d" % (base_path, status, self.id or 0)
def accept(self, broadcast_only=False): def accept(self, broadcast_only=False):
""" turn this request into the real deal""" """turn this request into the real deal"""
user = self.user_object user = self.user_object
if not self.user_subject.local: if not self.user_subject.local:
activity = activitypub.Accept( activity = activitypub.Accept(
@ -164,7 +164,7 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship):
self.delete() self.delete()
def reject(self): def reject(self):
""" generate a Reject for this follow request """ """generate a Reject for this follow request"""
if self.user_object.local: if self.user_object.local:
activity = activitypub.Reject( activity = activitypub.Reject(
id=self.get_accept_reject_id(status="rejects"), id=self.get_accept_reject_id(status="rejects"),
@ -177,13 +177,13 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship):
class UserBlocks(ActivityMixin, 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" status = "blocks"
activity_serializer = activitypub.Block activity_serializer = activitypub.Block
def save(self, *args, **kwargs): 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) super().save(*args, **kwargs)
UserFollows.objects.filter( UserFollows.objects.filter(

View file

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

View file

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

View file

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

View file

@ -19,7 +19,7 @@ from . import fields
class Status(OrderedCollectionPageMixin, BookWyrmModel): 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 = fields.ForeignKey(
"User", on_delete=models.PROTECT, activitypub_field="attributedTo" "User", on_delete=models.PROTECT, activitypub_field="attributedTo"
@ -59,12 +59,12 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
deserialize_reverse_fields = [("attachments", "attachment")] deserialize_reverse_fields = [("attachments", "attachment")]
class Meta: class Meta:
""" default sorting """ """default sorting"""
ordering = ("-published_date",) ordering = ("-published_date",)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
""" save and notify """ """save and notify"""
super().save(*args, **kwargs) super().save(*args, **kwargs)
notification_model = apps.get_model("bookwyrm.Notification", require_ready=True) 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 def delete(self, *args, **kwargs): # pylint: disable=unused-argument
""" "delete" a status """ """ "delete" a status"""
if hasattr(self, "boosted_status"): if hasattr(self, "boosted_status"):
# okay but if it's a boost really delete it # okay but if it's a boost really delete it
super().delete(*args, **kwargs) super().delete(*args, **kwargs)
@ -109,7 +109,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
@property @property
def recipients(self): 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] mentions = [u for u in self.mention_users.all() if not u.local]
if ( if (
hasattr(self, "reply_parent") hasattr(self, "reply_parent")
@ -121,7 +121,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
@classmethod @classmethod
def ignore_activity(cls, activity): # pylint: disable=too-many-return-statements 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": if activity.type == "Announce":
try: try:
boosted = activitypub.resolve_remote_id( boosted = activitypub.resolve_remote_id(
@ -163,16 +163,16 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
@property @property
def status_type(self): 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__ return self.activity_serializer.__name__
@property @property
def boostable(self): def boostable(self):
""" you can't boost dms """ """you can't boost dms"""
return self.privacy in ["unlisted", "public"] return self.privacy in ["unlisted", "public"]
def to_replies(self, **kwargs): 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( return self.to_ordered_collection(
self.replies(self), self.replies(self),
remote_id="%s/replies" % self.remote_id, remote_id="%s/replies" % self.remote_id,
@ -181,7 +181,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
).serialize() ).serialize()
def to_activity_dataclass(self, pure=False): # pylint: disable=arguments-differ 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: if self.deleted:
return activitypub.Tombstone( return activitypub.Tombstone(
id=self.remote_id, id=self.remote_id,
@ -210,16 +210,16 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
return activity return activity
def to_activity(self, pure=False): # pylint: disable=arguments-differ 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() return self.to_activity_dataclass(pure=pure).serialize()
class GeneratedNote(Status): class GeneratedNote(Status):
""" these are app-generated messages about user activity """ """these are app-generated messages about user activity"""
@property @property
def pure_content(self): 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 message = self.content
books = ", ".join( books = ", ".join(
'<a href="%s">"%s"</a>' % (book.remote_id, book.title) '<a href="%s">"%s"</a>' % (book.remote_id, book.title)
@ -232,7 +232,7 @@ class GeneratedNote(Status):
class Comment(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( book = fields.ForeignKey(
"Edition", on_delete=models.PROTECT, activitypub_field="inReplyToBook" "Edition", on_delete=models.PROTECT, activitypub_field="inReplyToBook"
@ -253,7 +253,7 @@ class Comment(Status):
@property @property
def pure_content(self): 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>' % ( return '%s<p>(comment on <a href="%s">"%s"</a>)</p>' % (
self.content, self.content,
self.book.remote_id, self.book.remote_id,
@ -265,7 +265,7 @@ class Comment(Status):
class Quotation(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() quote = fields.HtmlField()
book = fields.ForeignKey( book = fields.ForeignKey(
@ -274,7 +274,7 @@ class Quotation(Status):
@property @property
def pure_content(self): 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>"', self.quote)
quote = re.sub(r"</p>$", '"</p>', quote) quote = re.sub(r"</p>$", '"</p>', quote)
return '%s <p>-- <a href="%s">"%s"</a></p>%s' % ( return '%s <p>-- <a href="%s">"%s"</a></p>%s' % (
@ -289,7 +289,7 @@ class Quotation(Status):
class Review(Status): class Review(Status):
""" a book review """ """a book review"""
name = fields.CharField(max_length=255, null=True) name = fields.CharField(max_length=255, null=True)
book = fields.ForeignKey( book = fields.ForeignKey(
@ -306,7 +306,7 @@ class Review(Status):
@property @property
def pure_name(self): 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") template = get_template("snippets/generated_status/review_pure_name.html")
return template.render( return template.render(
{"book": self.book, "rating": self.rating, "name": self.name} {"book": self.book, "rating": self.rating, "name": self.name}
@ -314,7 +314,7 @@ class Review(Status):
@property @property
def pure_content(self): 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 return self.content
activity_serializer = activitypub.Review activity_serializer = activitypub.Review
@ -322,7 +322,7 @@ class Review(Status):
class ReviewRating(Review): 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): def save(self, *args, **kwargs):
if not self.rating: if not self.rating:
@ -339,7 +339,7 @@ class ReviewRating(Review):
class Boost(ActivityMixin, Status): class Boost(ActivityMixin, Status):
""" boost'ing a post """ """boost'ing a post"""
boosted_status = fields.ForeignKey( boosted_status = fields.ForeignKey(
"Status", "Status",
@ -350,7 +350,7 @@ class Boost(ActivityMixin, Status):
activity_serializer = activitypub.Announce activity_serializer = activitypub.Announce
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
""" save and notify """ """save and notify"""
# This constraint can't work as it would cross tables. # This constraint can't work as it would cross tables.
# class Meta: # class Meta:
# unique_together = ('user', 'boosted_status') # unique_together = ('user', 'boosted_status')
@ -374,7 +374,7 @@ class Boost(ActivityMixin, Status):
) )
def delete(self, *args, **kwargs): 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 = apps.get_model("bookwyrm.Notification", require_ready=True)
notification_model.objects.filter( notification_model.objects.filter(
user=self.boosted_status.user, user=self.boosted_status.user,
@ -385,7 +385,7 @@ class Boost(ActivityMixin, Status):
super().delete(*args, **kwargs) super().delete(*args, **kwargs)
def __init__(self, *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) super().__init__(*args, **kwargs)
reserve_fields = ["user", "boosted_status", "published_date", "privacy"] reserve_fields = ["user", "boosted_status", "published_date", "privacy"]

View file

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

View file

@ -13,16 +13,16 @@ r = redis.Redis(
class RedisStore(ABC): 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 max_length = settings.MAX_STREAM_LENGTH
def get_value(self, obj): def get_value(self, obj):
""" the object and rank """ """the object and rank"""
return {obj.id: self.get_rank(obj)} return {obj.id: self.get_rank(obj)}
def add_object_to_related_stores(self, obj, execute=True): 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) value = self.get_value(obj)
# we want to do this as a bulk operation, hence "pipeline" # we want to do this as a bulk operation, hence "pipeline"
pipeline = r.pipeline() pipeline = r.pipeline()
@ -37,14 +37,14 @@ class RedisStore(ABC):
return pipeline.execute() return pipeline.execute()
def remove_object_from_related_stores(self, obj): def remove_object_from_related_stores(self, obj):
""" remove an object from all stores """ """remove an object from all stores"""
pipeline = r.pipeline() pipeline = r.pipeline()
for store in self.get_stores_for_object(obj): for store in self.get_stores_for_object(obj):
pipeline.zrem(store, -1, obj.id) pipeline.zrem(store, -1, obj.id)
pipeline.execute() pipeline.execute()
def bulk_add_objects_to_store(self, objs, store): 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() pipeline = r.pipeline()
for obj in objs[: self.max_length]: for obj in objs[: self.max_length]:
pipeline.zadd(store, self.get_value(obj)) pipeline.zadd(store, self.get_value(obj))
@ -53,18 +53,18 @@ class RedisStore(ABC):
pipeline.execute() pipeline.execute()
def bulk_remove_objects_from_store(self, objs, store): 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() pipeline = r.pipeline()
for obj in objs[: self.max_length]: for obj in objs[: self.max_length]:
pipeline.zrem(store, -1, obj.id) pipeline.zrem(store, -1, obj.id)
pipeline.execute() pipeline.execute()
def get_store(self, store): # pylint: disable=no-self-use 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) return r.zrevrange(store, 0, -1)
def populate_store(self, store): def populate_store(self, store):
""" go from zero to a store """ """go from zero to a store"""
pipeline = r.pipeline() pipeline = r.pipeline()
queryset = self.get_objects_for_store(store) queryset = self.get_objects_for_store(store)
@ -78,12 +78,12 @@ class RedisStore(ABC):
@abstractmethod @abstractmethod
def get_objects_for_store(self, store): 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 @abstractmethod
def get_stores_for_object(self, obj): def get_stores_for_object(self, obj):
""" the stores that an object belongs in """ """the stores that an object belongs in"""
@abstractmethod @abstractmethod
def get_rank(self, obj): 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 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): def __init__(self):
HTMLParser.__init__(self) HTMLParser.__init__(self)
@ -28,7 +28,7 @@ class InputHtmlParser(HTMLParser): # pylint: disable=abstract-method
self.allow_html = True self.allow_html = True
def handle_starttag(self, tag, attrs): def handle_starttag(self, tag, attrs):
""" check if the tag is valid """ """check if the tag is valid"""
if self.allow_html and tag in self.allowed_tags: if self.allow_html and tag in self.allowed_tags:
self.output.append(("tag", self.get_starttag_text())) self.output.append(("tag", self.get_starttag_text()))
self.tag_stack.append(tag) self.tag_stack.append(tag)
@ -36,7 +36,7 @@ class InputHtmlParser(HTMLParser): # pylint: disable=abstract-method
self.output.append(("data", "")) self.output.append(("data", ""))
def handle_endtag(self, tag): def handle_endtag(self, tag):
""" keep the close tag """ """keep the close tag"""
if not self.allow_html or tag not in self.allowed_tags: if not self.allow_html or tag not in self.allowed_tags:
self.output.append(("data", "")) self.output.append(("data", ""))
return return
@ -51,11 +51,11 @@ class InputHtmlParser(HTMLParser): # pylint: disable=abstract-method
self.output.append(("tag", "</%s>" % tag)) self.output.append(("tag", "</%s>" % tag))
def handle_data(self, data): 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)) self.output.append(("data", data))
def get_output(self): 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: if self.tag_stack:
self.allow_html = False self.allow_html = False
if not self.allow_html: if not self.allow_html:

View file

@ -13,7 +13,7 @@ MAX_SIGNATURE_AGE = 300
def create_key_pair(): 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 random_generator = Random.new().read
key = RSA.generate(1024, random_generator) key = RSA.generate(1024, random_generator)
private_key = key.export_key().decode("utf8") private_key = key.export_key().decode("utf8")
@ -23,7 +23,7 @@ def create_key_pair():
def make_signature(sender, destination, date, digest): def make_signature(sender, destination, date, digest):
""" uses a private key to sign an outgoing message """ """uses a private key to sign an outgoing message"""
inbox_parts = urlparse(destination) inbox_parts = urlparse(destination)
signature_headers = [ signature_headers = [
"(request-target): post %s" % inbox_parts.path, "(request-target): post %s" % inbox_parts.path,
@ -44,14 +44,14 @@ def make_signature(sender, destination, date, digest):
def make_digest(data): 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( return "SHA-256=" + b64encode(hashlib.sha256(data.encode("utf-8")).digest()).decode(
"utf-8" "utf-8"
) )
def verify_digest(request): 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) algorithm, digest = request.headers["digest"].split("=", 1)
if algorithm == "SHA-256": if algorithm == "SHA-256":
hash_function = hashlib.sha256 hash_function = hashlib.sha256
@ -66,7 +66,7 @@ def verify_digest(request):
class Signature: class Signature:
""" read and validate incoming signatures """ """read and validate incoming signatures"""
def __init__(self, key_id, headers, signature): def __init__(self, key_id, headers, signature):
self.key_id = key_id self.key_id = key_id
@ -75,7 +75,7 @@ class Signature:
@classmethod @classmethod
def parse(cls, request): def parse(cls, request):
""" extract and parse a signature from an http request """ """extract and parse a signature from an http request"""
signature_dict = {} signature_dict = {}
for pair in request.headers["Signature"].split(","): for pair in request.headers["Signature"].split(","):
k, v = pair.split("=", 1) k, v = pair.split("=", 1)
@ -92,7 +92,7 @@ class Signature:
return cls(key_id, headers, signature) return cls(key_id, headers, signature)
def verify(self, public_key, request): def verify(self, public_key, request):
""" verify rsa signature """ """verify rsa signature"""
if http_date_age(request.headers["date"]) > MAX_SIGNATURE_AGE: if http_date_age(request.headers["date"]) > MAX_SIGNATURE_AGE:
raise ValueError("Request too old: %s" % (request.headers["date"],)) raise ValueError("Request too old: %s" % (request.headers["date"],))
public_key = RSA.import_key(public_key) public_key = RSA.import_key(public_key)
@ -118,7 +118,7 @@ class Signature:
def http_date_age(datestr): 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") parsed = datetime.datetime.strptime(datestr, "%a, %d %b %Y %H:%M:%S GMT")
delta = datetime.datetime.utcnow() - parsed delta = datetime.datetime.utcnow() - parsed
return delta.total_seconds() return delta.total_seconds()

View file

@ -6,7 +6,7 @@ from bookwyrm.sanitize_html import InputHtmlParser
def create_generated_note(user, content, mention_books=None, privacy="public"): 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 # sanitize input html
parser = InputHtmlParser() parser = InputHtmlParser()
parser.feed(content) parser.feed(content)

View file

@ -13,6 +13,16 @@
<div class="columns mt-3"> <div class="columns mt-3">
<section class="column is-three-quarters"> <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 %} {% if not items.object_list.exists %}
<p>{% trans "This list is currently empty" %}</p> <p>{% trans "This list is currently empty" %}</p>
{% else %} {% else %}
@ -116,7 +126,7 @@
</div> </div>
<div class="column"> <div class="column">
<p>{% include 'snippets/book_titleby.html' with book=book %}</p> <p>{% include 'snippets/book_titleby.html' with book=book %}</p>
<form name="add-book" method="post" action="{% url 'list-add-book' %}"> <form name="add-book" method="post" action="{% url 'list-add-book' %}{% if query %}?q={{ query }}{% endif %}">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}"> <input type="hidden" name="book" value="{{ book.id }}">
<input type="hidden" name="list" value="{{ list.id }}"> <input type="hidden" name="list" value="{{ list.id }}">

View file

@ -6,7 +6,7 @@
<figure class="media-left" aria-hidden="true"> <figure class="media-left" aria-hidden="true">
<a class="image is-48x48" href="{{ status.user.local_path }}"> <a class="image is-48x48" href="{{ status.user.local_path }}">
{% include 'snippets/avatar.html' with user=status.user ariaHide="true" medium="true" %} {% include 'snippets/avatar.html' with user=status.user ariaHide="true" medium="true" %}
</a> </a>
</figure> </figure>
<div class="media-content"> <div class="media-content">
@ -47,7 +47,7 @@
{% if status.book %} {% if status.book %}
{% if status.status_type == 'GeneratedNote' or status.status_type == 'Rating' %} {% 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 <span
itemprop="reviewRating" itemprop="reviewRating"
itemscope itemscope
@ -71,7 +71,6 @@
<meta itemprop="bestRating" content="5"> <meta itemprop="bestRating" content="5">
</span> </span>
{% include 'snippets/stars.html' with rating=status.rating %}
{% endif %} {% endif %}
{% else %} {% else %}
{% include 'snippets/book_titleby.html' with book=status.book %} {% 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> <div>{% blocktrans with username=user.display_name %}{{ username }} has no followers{% endblocktrans %}</div>
{% endif %} {% endif %}
</div> </div>
{% include 'snippets/pagination.html' with page=followers path=request.path %}
{% endblock %} {% endblock %}

View file

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

View file

@ -13,13 +13,13 @@ register = template.Library()
@register.filter(name="dict_key") @register.filter(name="dict_key")
def dict_key(d, k): 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 return d.get(k) or 0
@register.filter(name="rating") @register.filter(name="rating")
def get_rating(book, user): def get_rating(book, user):
""" get the overall rating of a book """ """get the overall rating of a book"""
queryset = views.helpers.privacy_filter( queryset = views.helpers.privacy_filter(
user, models.Review.objects.filter(book=book) user, models.Review.objects.filter(book=book)
) )
@ -28,7 +28,7 @@ def get_rating(book, user):
@register.filter(name="user_rating") @register.filter(name="user_rating")
def get_user_rating(book, user): def get_user_rating(book, user):
""" get a user's rating of a book """ """get a user's rating of a book"""
rating = ( rating = (
models.Review.objects.filter( models.Review.objects.filter(
user=user, user=user,
@ -45,19 +45,19 @@ def get_user_rating(book, user):
@register.filter(name="username") @register.filter(name="username")
def get_user_identifier(user): 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 return user.localname if user.localname else user.username
@register.filter(name="notification_count") @register.filter(name="notification_count")
def get_notification_count(user): 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() return user.notification_set.filter(read=False).count()
@register.filter(name="replies") @register.filter(name="replies")
def get_replies(status): def get_replies(status):
""" get all direct replies to a status """ """get all direct replies to a status"""
# TODO: this limit could cause problems # TODO: this limit could cause problems
return models.Status.objects.filter( return models.Status.objects.filter(
reply_parent=status, reply_parent=status,
@ -67,7 +67,7 @@ def get_replies(status):
@register.filter(name="parent") @register.filter(name="parent")
def get_parent(status): def get_parent(status):
""" get the reply parent for a status """ """get the reply parent for a status"""
return ( return (
models.Status.objects.filter(id=status.reply_parent_id) models.Status.objects.filter(id=status.reply_parent_id)
.select_subclasses() .select_subclasses()
@ -77,7 +77,7 @@ def get_parent(status):
@register.filter(name="liked") @register.filter(name="liked")
def get_user_liked(user, status): def get_user_liked(user, status):
""" did the given user fav a status? """ """did the given user fav a status?"""
try: try:
models.Favorite.objects.get(user=user, status=status) models.Favorite.objects.get(user=user, status=status)
return True return True
@ -87,13 +87,13 @@ def get_user_liked(user, status):
@register.filter(name="boosted") @register.filter(name="boosted")
def get_user_boosted(user, status): 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) return user.id in status.boosters.all().values_list("user", flat=True)
@register.filter(name="follow_request_exists") @register.filter(name="follow_request_exists")
def follow_request_exists(user, requester): 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: try:
models.UserFollowRequest.objects.filter( models.UserFollowRequest.objects.filter(
user_subject=requester, user_subject=requester,
@ -106,7 +106,7 @@ def follow_request_exists(user, requester):
@register.filter(name="boosted_status") @register.filter(name="boosted_status")
def get_boosted(boost): 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 ( return (
models.Status.objects.select_subclasses() models.Status.objects.select_subclasses()
.filter(id=boost.boosted_status.id) .filter(id=boost.boosted_status.id)
@ -116,19 +116,19 @@ def get_boosted(boost):
@register.filter(name="book_description") @register.filter(name="book_description")
def get_book_description(book): 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 return book.description or book.parent_work.description
@register.filter(name="uuid") @register.filter(name="uuid")
def get_uuid(identifier): 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()) return "%s%s" % (identifier, uuid4())
@register.filter(name="to_markdown") @register.filter(name="to_markdown")
def get_markdown(content): def get_markdown(content):
""" convert markdown to html """ """convert markdown to html"""
if content: if content:
return to_markdown(content) return to_markdown(content)
return None return None
@ -136,7 +136,7 @@ def get_markdown(content):
@register.filter(name="mentions") @register.filter(name="mentions")
def get_mentions(status, user): 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())) mentions = set([status.user] + list(status.mention_users.all()))
return ( return (
" ".join("@" + get_user_identifier(m) for m in mentions if not m == user) + " " " ".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") @register.filter(name="status_preview_name")
def get_status_preview_name(obj): 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() name = obj.__class__.__name__.lower()
if name == "review": if name == "review":
return "%s of <em>%s</em>" % (name, obj.book.title) 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") @register.filter(name="next_shelf")
def get_next_shelf(current_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": if current_shelf == "to-read":
return "reading" return "reading"
if current_shelf == "reading": if current_shelf == "reading":
@ -170,7 +170,7 @@ def get_next_shelf(current_shelf):
@register.filter(name="title") @register.filter(name="title")
def get_title(book): def get_title(book):
""" display the subtitle if the title is short """ """display the subtitle if the title is short"""
if not book: if not book:
return "" return ""
title = book.title title = book.title
@ -181,7 +181,7 @@ def get_title(book):
@register.simple_tag(takes_context=False) @register.simple_tag(takes_context=False)
def related_status(notification): def related_status(notification):
""" for notifications """ """for notifications"""
if not notification.related_status: if not notification.related_status:
return None return None
if hasattr(notification.related_status, "quotation"): if hasattr(notification.related_status, "quotation"):
@ -195,7 +195,7 @@ def related_status(notification):
@register.simple_tag(takes_context=True) @register.simple_tag(takes_context=True)
def active_shelf(context, book): 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 = models.ShelfBook.objects.filter(
shelf__user=context["request"].user, book__in=book.parent_work.editions.all() shelf__user=context["request"].user, book__in=book.parent_work.editions.all()
).first() ).first()
@ -204,7 +204,7 @@ def active_shelf(context, book):
@register.simple_tag(takes_context=False) @register.simple_tag(takes_context=False)
def latest_read_through(book, user): def latest_read_through(book, user):
""" the most recent read activity """ """the most recent read activity"""
return ( return (
models.ReadThrough.objects.filter(user=user, book=book) models.ReadThrough.objects.filter(user=user, book=book)
.order_by("-start_date") .order_by("-start_date")
@ -214,7 +214,7 @@ def latest_read_through(book, user):
@register.simple_tag(takes_context=False) @register.simple_tag(takes_context=False)
def active_read_through(book, user): def active_read_through(book, user):
""" the most recent read activity """ """the most recent read activity"""
return ( return (
models.ReadThrough.objects.filter( models.ReadThrough.objects.filter(
user=user, book=book, finish_date__isnull=True user=user, book=book, finish_date__isnull=True
@ -226,12 +226,12 @@ def active_read_through(book, user):
@register.simple_tag(takes_context=False) @register.simple_tag(takes_context=False)
def comparison_bool(str1, str2): 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 return str1 == str2
@register.simple_tag(takes_context=False) @register.simple_tag(takes_context=False)
def get_lang(): 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() language = utils.translation.get_language()
return language[0 : language.find("-")] return language[0 : language.find("-")]

View file

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

View file

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

View file

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

View file

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

View file

@ -9,10 +9,10 @@ from bookwyrm.connectors.abstract_connector import SearchResult
class BookWyrmConnector(TestCase): class BookWyrmConnector(TestCase):
""" this connector doesn't do much, just search """ """this connector doesn't do much, just search"""
def setUp(self): def setUp(self):
""" create the connector """ """create the connector"""
models.Connector.objects.create( models.Connector.objects.create(
identifier="example.com", identifier="example.com",
connector_file="bookwyrm_connector", connector_file="bookwyrm_connector",
@ -24,14 +24,14 @@ class BookWyrmConnector(TestCase):
self.connector = Connector("example.com") self.connector = Connector("example.com")
def test_get_or_create_book_existing(self): def test_get_or_create_book_existing(self):
""" load book activity """ """load book activity"""
work = models.Work.objects.create(title="Test Work") work = models.Work.objects.create(title="Test Work")
book = models.Edition.objects.create(title="Test Edition", parent_work=work) book = models.Edition.objects.create(title="Test Edition", parent_work=work)
result = self.connector.get_or_create_book(book.remote_id) result = self.connector.get_or_create_book(book.remote_id)
self.assertEqual(book, result) self.assertEqual(book, result)
def test_format_search_result(self): 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") datafile = pathlib.Path(__file__).parent.joinpath("../data/bw_search.json")
search_data = json.loads(datafile.read_bytes()) search_data = json.loads(datafile.read_bytes())
results = self.connector.parse_search_data(search_data) results = self.connector.parse_search_data(search_data)
@ -46,7 +46,7 @@ class BookWyrmConnector(TestCase):
self.assertEqual(result.connector, self.connector) self.assertEqual(result.connector, self.connector)
def test_format_isbn_search_result(self): 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") datafile = pathlib.Path(__file__).parent.joinpath("../data/bw_search.json")
search_data = json.loads(datafile.read_bytes()) search_data = json.loads(datafile.read_bytes())
results = self.connector.parse_isbn_search_data(search_data) 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): class ConnectorManager(TestCase):
""" interface between the app and various connectors """ """interface between the app and various connectors"""
def setUp(self): 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.work = models.Work.objects.create(title="Example Work")
self.edition = models.Edition.objects.create( self.edition = models.Edition.objects.create(
@ -32,7 +32,7 @@ class ConnectorManager(TestCase):
) )
def test_get_or_create_connector(self): 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" remote_id = "https://example.com/object/1"
connector = connector_manager.get_or_create_connector(remote_id) connector = connector_manager.get_or_create_connector(remote_id)
self.assertIsInstance(connector, BookWyrmConnector) self.assertIsInstance(connector, BookWyrmConnector)
@ -43,7 +43,7 @@ class ConnectorManager(TestCase):
self.assertEqual(connector.identifier, same_connector.identifier) self.assertEqual(connector.identifier, same_connector.identifier)
def test_get_connectors(self): def test_get_connectors(self):
""" load all connectors """ """load all connectors"""
remote_id = "https://example.com/object/1" remote_id = "https://example.com/object/1"
connector_manager.get_or_create_connector(remote_id) connector_manager.get_or_create_connector(remote_id)
connectors = list(connector_manager.get_connectors()) connectors = list(connector_manager.get_connectors())
@ -52,7 +52,7 @@ class ConnectorManager(TestCase):
self.assertIsInstance(connectors[1], BookWyrmConnector) self.assertIsInstance(connectors[1], BookWyrmConnector)
def test_search(self): def test_search(self):
""" search all connectors """ """search all connectors"""
results = connector_manager.search("Example") results = connector_manager.search("Example")
self.assertEqual(len(results), 1) self.assertEqual(len(results), 1)
self.assertIsInstance(results[0]["connector"], SelfConnector) self.assertIsInstance(results[0]["connector"], SelfConnector)
@ -60,7 +60,7 @@ class ConnectorManager(TestCase):
self.assertEqual(results[0]["results"][0].title, "Example Edition") self.assertEqual(results[0]["results"][0].title, "Example Edition")
def test_search_isbn(self): 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") results = connector_manager.search("0000000000")
self.assertEqual(len(results), 1) self.assertEqual(len(results), 1)
self.assertIsInstance(results[0]["connector"], SelfConnector) self.assertIsInstance(results[0]["connector"], SelfConnector)
@ -68,20 +68,20 @@ class ConnectorManager(TestCase):
self.assertEqual(results[0]["results"][0].title, "Example Edition") self.assertEqual(results[0]["results"][0].title, "Example Edition")
def test_local_search(self): def test_local_search(self):
""" search only the local database """ """search only the local database"""
results = connector_manager.local_search("Example") results = connector_manager.local_search("Example")
self.assertEqual(len(results), 1) self.assertEqual(len(results), 1)
self.assertEqual(results[0].title, "Example Edition") self.assertEqual(results[0].title, "Example Edition")
def test_first_search_result(self): def test_first_search_result(self):
""" only get one search result """ """only get one search result"""
result = connector_manager.first_search_result("Example") result = connector_manager.first_search_result("Example")
self.assertEqual(result.title, "Example Edition") self.assertEqual(result.title, "Example Edition")
no_result = connector_manager.first_search_result("dkjfhg") no_result = connector_manager.first_search_result("dkjfhg")
self.assertIsNone(no_result) self.assertIsNone(no_result)
def test_load_connector(self): 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) connector = connector_manager.load_connector(self.connector)
self.assertIsInstance(connector, SelfConnector) self.assertIsInstance(connector, SelfConnector)
self.assertEqual(connector.identifier, "test_connector") self.assertEqual(connector.identifier, "test_connector")

View file

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

View file

@ -9,10 +9,10 @@ from bookwyrm.settings import DOMAIN
class SelfConnector(TestCase): class SelfConnector(TestCase):
""" just uses local data """ """just uses local data"""
def setUp(self): def setUp(self):
""" creating the connector """ """creating the connector"""
models.Connector.objects.create( models.Connector.objects.create(
identifier=DOMAIN, identifier=DOMAIN,
name="Local", name="Local",
@ -27,7 +27,7 @@ class SelfConnector(TestCase):
self.connector = Connector(DOMAIN) self.connector = Connector(DOMAIN)
def test_format_search_result(self): def test_format_search_result(self):
""" create a SearchResult """ """create a SearchResult"""
author = models.Author.objects.create(name="Anonymous") author = models.Author.objects.create(name="Anonymous")
edition = models.Edition.objects.create( edition = models.Edition.objects.create(
title="Edition of Example Work", title="Edition of Example Work",
@ -42,7 +42,7 @@ class SelfConnector(TestCase):
self.assertEqual(result.connector, self.connector) self.assertEqual(result.connector, self.connector)
def test_search_rank(self): def test_search_rank(self):
""" prioritize certain results """ """prioritize certain results"""
author = models.Author.objects.create(name="Anonymous") author = models.Author.objects.create(name="Anonymous")
edition = models.Edition.objects.create( edition = models.Edition.objects.create(
title="Edition of Example Work", title="Edition of Example Work",
@ -78,7 +78,7 @@ class SelfConnector(TestCase):
self.assertEqual(results[2].title, "Edition of Example Work") self.assertEqual(results[2].title, "Edition of Example Work")
def test_search_multiple_editions(self): 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") work = models.Work.objects.create(title="Work Title")
edition_1 = models.Edition.objects.create( edition_1 = models.Edition.objects.create(
title="Edition 1 Title", parent_work=work 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): class GoodreadsImport(TestCase):
""" importing from goodreads csv """ """importing from goodreads csv"""
def setUp(self): def setUp(self):
""" use a test csv """ """use a test csv"""
self.importer = GoodreadsImporter() self.importer = GoodreadsImporter()
datafile = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv") datafile = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv")
self.csv = open(datafile, "r", encoding=self.importer.encoding) self.csv = open(datafile, "r", encoding=self.importer.encoding)
@ -44,7 +44,7 @@ class GoodreadsImport(TestCase):
) )
def test_create_job(self): 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") import_job = self.importer.create_job(self.user, self.csv, False, "public")
self.assertEqual(import_job.user, self.user) self.assertEqual(import_job.user, self.user)
self.assertEqual(import_job.include_reviews, False) self.assertEqual(import_job.include_reviews, False)
@ -60,7 +60,7 @@ class GoodreadsImport(TestCase):
self.assertEqual(import_items[2].data["Book Id"], "28694510") self.assertEqual(import_items[2].data["Book Id"], "28694510")
def test_create_retry_job(self): 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_job = self.importer.create_job(self.user, self.csv, False, "unlisted")
import_items = models.ImportItem.objects.filter(job=import_job).all()[:2] 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") self.assertEqual(retry_items[1].data["Book Id"], "52691223")
def test_start_import(self): def test_start_import(self):
""" begin loading books """ """begin loading books"""
import_job = self.importer.create_job(self.user, self.csv, False, "unlisted") import_job = self.importer.create_job(self.user, self.csv, False, "unlisted")
MockTask = namedtuple("Task", ("id")) MockTask = namedtuple("Task", ("id"))
mock_task = MockTask(7) mock_task = MockTask(7)
@ -90,7 +90,7 @@ class GoodreadsImport(TestCase):
@responses.activate @responses.activate
def test_import_data(self): def test_import_data(self):
""" resolve entry """ """resolve entry"""
import_job = self.importer.create_job(self.user, self.csv, False, "unlisted") import_job = self.importer.create_job(self.user, self.csv, False, "unlisted")
book = models.Edition.objects.create(title="Test Book") book = models.Edition.objects.create(title="Test Book")
@ -105,7 +105,7 @@ class GoodreadsImport(TestCase):
self.assertEqual(import_item.book.id, book.id) self.assertEqual(import_item.book.id, book.id)
def test_handle_imported_book(self): 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() shelf = self.user.shelf_set.filter(identifier="read").first()
self.assertIsNone(shelf.books.first()) self.assertIsNone(shelf.books.first())
@ -138,7 +138,7 @@ class GoodreadsImport(TestCase):
self.assertEqual(readthrough.finish_date.day, 25) self.assertEqual(readthrough.finish_date.day, 25)
def test_handle_imported_book_already_shelved(self): 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"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
shelf = self.user.shelf_set.filter(identifier="to-read").first() shelf = self.user.shelf_set.filter(identifier="to-read").first()
models.ShelfBook.objects.create(shelf=shelf, user=self.user, book=self.book) 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) self.assertEqual(readthrough.finish_date.day, 25)
def test_handle_import_twice(self): def test_handle_import_twice(self):
""" re-importing books """ """re-importing books"""
shelf = self.user.shelf_set.filter(identifier="read").first() shelf = self.user.shelf_set.filter(identifier="read").first()
import_job = models.ImportJob.objects.create(user=self.user) import_job = models.ImportJob.objects.create(user=self.user)
datafile = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv") datafile = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv")
@ -206,7 +206,7 @@ class GoodreadsImport(TestCase):
@patch("bookwyrm.activitystreams.ActivityStream.add_status") @patch("bookwyrm.activitystreams.ActivityStream.add_status")
def test_handle_imported_book_review(self, _): def test_handle_imported_book_review(self, _):
""" goodreads review import """ """goodreads review import"""
import_job = models.ImportJob.objects.create(user=self.user) import_job = models.ImportJob.objects.create(user=self.user)
datafile = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv") datafile = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv")
csv_file = open(datafile, "r") csv_file = open(datafile, "r")
@ -228,8 +228,34 @@ class GoodreadsImport(TestCase):
self.assertEqual(review.published_date.day, 8) self.assertEqual(review.published_date.day, 8)
self.assertEqual(review.privacy, "unlisted") 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): def test_handle_imported_book_reviews_disabled(self):
""" goodreads review import """ """goodreads review import"""
import_job = models.ImportJob.objects.create(user=self.user) import_job = models.ImportJob.objects.create(user=self.user)
datafile = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv") datafile = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv")
csv_file = open(datafile, "r") csv_file = open(datafile, "r")

View file

@ -13,10 +13,10 @@ from bookwyrm.settings import DOMAIN
class LibrarythingImport(TestCase): class LibrarythingImport(TestCase):
""" importing from librarything tsv """ """importing from librarything tsv"""
def setUp(self): def setUp(self):
""" use a test tsv """ """use a test tsv"""
self.importer = LibrarythingImporter() self.importer = LibrarythingImporter()
datafile = pathlib.Path(__file__).parent.joinpath("../data/librarything.tsv") datafile = pathlib.Path(__file__).parent.joinpath("../data/librarything.tsv")
@ -45,7 +45,7 @@ class LibrarythingImport(TestCase):
) )
def test_create_job(self): 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") import_job = self.importer.create_job(self.user, self.csv, False, "public")
self.assertEqual(import_job.user, self.user) self.assertEqual(import_job.user, self.user)
self.assertEqual(import_job.include_reviews, False) self.assertEqual(import_job.include_reviews, False)
@ -61,7 +61,7 @@ class LibrarythingImport(TestCase):
self.assertEqual(import_items[2].data["Book Id"], "5015399") self.assertEqual(import_items[2].data["Book Id"], "5015399")
def test_create_retry_job(self): 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_job = self.importer.create_job(self.user, self.csv, False, "unlisted")
import_items = models.ImportItem.objects.filter(job=import_job).all()[:2] import_items = models.ImportItem.objects.filter(job=import_job).all()[:2]
@ -80,7 +80,7 @@ class LibrarythingImport(TestCase):
@responses.activate @responses.activate
def test_import_data(self): def test_import_data(self):
""" resolve entry """ """resolve entry"""
import_job = self.importer.create_job(self.user, self.csv, False, "unlisted") import_job = self.importer.create_job(self.user, self.csv, False, "unlisted")
book = models.Edition.objects.create(title="Test Book") book = models.Edition.objects.create(title="Test Book")
@ -95,7 +95,7 @@ class LibrarythingImport(TestCase):
self.assertEqual(import_item.book.id, book.id) self.assertEqual(import_item.book.id, book.id)
def test_handle_imported_book(self): 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() shelf = self.user.shelf_set.filter(identifier="read").first()
self.assertIsNone(shelf.books.first()) self.assertIsNone(shelf.books.first())
@ -130,7 +130,7 @@ class LibrarythingImport(TestCase):
self.assertEqual(readthrough.finish_date.day, 8) self.assertEqual(readthrough.finish_date.day, 8)
def test_handle_imported_book_already_shelved(self): 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"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
shelf = self.user.shelf_set.filter(identifier="to-read").first() shelf = self.user.shelf_set.filter(identifier="to-read").first()
models.ShelfBook.objects.create(shelf=shelf, user=self.user, book=self.book) 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) self.assertEqual(readthrough.finish_date.day, 8)
def test_handle_import_twice(self): def test_handle_import_twice(self):
""" re-importing books """ """re-importing books"""
shelf = self.user.shelf_set.filter(identifier="read").first() shelf = self.user.shelf_set.filter(identifier="read").first()
import_job = models.ImportJob.objects.create(user=self.user) import_job = models.ImportJob.objects.create(user=self.user)
datafile = pathlib.Path(__file__).parent.joinpath("../data/librarything.tsv") datafile = pathlib.Path(__file__).parent.joinpath("../data/librarything.tsv")
@ -202,7 +202,7 @@ class LibrarythingImport(TestCase):
@patch("bookwyrm.activitystreams.ActivityStream.add_status") @patch("bookwyrm.activitystreams.ActivityStream.add_status")
def test_handle_imported_book_review(self, _): def test_handle_imported_book_review(self, _):
""" librarything review import """ """librarything review import"""
import_job = models.ImportJob.objects.create(user=self.user) import_job = models.ImportJob.objects.create(user=self.user)
datafile = pathlib.Path(__file__).parent.joinpath("../data/librarything.tsv") datafile = pathlib.Path(__file__).parent.joinpath("../data/librarything.tsv")
csv_file = open(datafile, "r", encoding=self.importer.encoding) csv_file = open(datafile, "r", encoding=self.importer.encoding)
@ -225,7 +225,7 @@ class LibrarythingImport(TestCase):
self.assertEqual(review.privacy, "unlisted") self.assertEqual(review.privacy, "unlisted")
def test_handle_imported_book_reviews_disabled(self): def test_handle_imported_book_reviews_disabled(self):
""" librarything review import """ """librarything review import"""
import_job = models.ImportJob.objects.create(user=self.user) import_job = models.ImportJob.objects.create(user=self.user)
datafile = pathlib.Path(__file__).parent.joinpath("../data/librarything.tsv") datafile = pathlib.Path(__file__).parent.joinpath("../data/librarything.tsv")
csv_file = open(datafile, "r", encoding=self.importer.encoding) 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") @patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay")
class Activitystreams(TestCase): class Activitystreams(TestCase):
""" using redis to build activity streams """ """using redis to build activity streams"""
def setUp(self): def setUp(self):
""" we need some stuff """ """we need some stuff"""
self.local_user = models.User.objects.create_user( self.local_user = models.User.objects.create_user(
"mouse", "mouse@mouse.mouse", "password", local=True, localname="mouse" "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") self.book = models.Edition.objects.create(title="test book")
def test_populate_streams(self, _): 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"): with patch("bookwyrm.activitystreams.ActivityStream.add_status"):
models.Comment.objects.create( models.Comment.objects.create(
user=self.local_user, content="hi", book=self.book 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") @patch("bookwyrm.activitystreams.ActivityStream.add_status")
class ActivitypubMixins(TestCase): class ActivitypubMixins(TestCase):
""" functionality shared across models """ """functionality shared across models"""
def setUp(self): def setUp(self):
""" shared data """ """shared data"""
self.local_user = models.User.objects.create_user( self.local_user = models.User.objects.create_user(
"mouse", "mouse@mouse.com", "mouseword", local=True, localname="mouse" "mouse", "mouse@mouse.com", "mouseword", local=True, localname="mouse"
) )
@ -46,16 +46,16 @@ class ActivitypubMixins(TestCase):
# ActivitypubMixin # ActivitypubMixin
def test_to_activity(self, _): def test_to_activity(self, _):
""" model to ActivityPub json """ """model to ActivityPub json"""
@dataclass(init=False) @dataclass(init=False)
class TestActivity(ActivityObject): class TestActivity(ActivityObject):
""" real simple mock """ """real simple mock"""
type: str = "Test" type: str = "Test"
class TestModel(ActivitypubMixin, base_model.BookWyrmModel): 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 = TestModel()
instance.remote_id = "https://www.example.com/test" instance.remote_id = "https://www.example.com/test"
@ -67,7 +67,7 @@ class ActivitypubMixins(TestCase):
self.assertEqual(activity["type"], "Test") self.assertEqual(activity["type"], "Test")
def test_find_existing_by_remote_id(self, _): 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 # uses a different remote id scheme
# this isn't really part of this test directly but it's helpful to state # this isn't really part of this test directly but it's helpful to state
book = models.Edition.objects.create( book = models.Edition.objects.create(
@ -100,7 +100,7 @@ class ActivitypubMixins(TestCase):
result = models.Status.find_existing_by_remote_id("https://comment.net") result = models.Status.find_existing_by_remote_id("https://comment.net")
def test_find_existing(self, _): 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( book = models.Edition.objects.create(
title="Test edition", title="Test edition",
openlibrary_key="OL1234", openlibrary_key="OL1234",
@ -110,7 +110,7 @@ class ActivitypubMixins(TestCase):
self.assertEqual(result, book) self.assertEqual(result, book)
def test_get_recipients_public_object(self, _): 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")) MockSelf = namedtuple("Self", ("privacy"))
mock_self = MockSelf("public") mock_self = MockSelf("public")
recipients = ActivitypubMixin.get_recipients(mock_self) recipients = ActivitypubMixin.get_recipients(mock_self)
@ -118,7 +118,7 @@ class ActivitypubMixins(TestCase):
self.assertEqual(recipients[0], self.remote_user.inbox) self.assertEqual(recipients[0], self.remote_user.inbox)
def test_get_recipients_public_user_object_no_followers(self, _): 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")) MockSelf = namedtuple("Self", ("privacy", "user"))
mock_self = MockSelf("public", self.local_user) mock_self = MockSelf("public", self.local_user)
@ -126,7 +126,7 @@ class ActivitypubMixins(TestCase):
self.assertEqual(len(recipients), 0) self.assertEqual(len(recipients), 0)
def test_get_recipients_public_user_object(self, _): 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")) MockSelf = namedtuple("Self", ("privacy", "user"))
mock_self = MockSelf("public", self.local_user) mock_self = MockSelf("public", self.local_user)
self.local_user.followers.add(self.remote_user) self.local_user.followers.add(self.remote_user)
@ -136,7 +136,7 @@ class ActivitypubMixins(TestCase):
self.assertEqual(recipients[0], self.remote_user.inbox) self.assertEqual(recipients[0], self.remote_user.inbox)
def test_get_recipients_public_user_object_with_mention(self, _): 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")) MockSelf = namedtuple("Self", ("privacy", "user"))
mock_self = MockSelf("public", self.local_user) mock_self = MockSelf("public", self.local_user)
self.local_user.followers.add(self.remote_user) self.local_user.followers.add(self.remote_user)
@ -159,7 +159,7 @@ class ActivitypubMixins(TestCase):
self.assertTrue(self.remote_user.inbox in recipients) self.assertTrue(self.remote_user.inbox in recipients)
def test_get_recipients_direct(self, _): 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")) MockSelf = namedtuple("Self", ("privacy", "user"))
mock_self = MockSelf("public", self.local_user) mock_self = MockSelf("public", self.local_user)
self.local_user.followers.add(self.remote_user) self.local_user.followers.add(self.remote_user)
@ -181,7 +181,7 @@ class ActivitypubMixins(TestCase):
self.assertEqual(recipients[0], another_remote_user.inbox) self.assertEqual(recipients[0], another_remote_user.inbox)
def test_get_recipients_combine_inboxes(self, _): 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.shared_inbox = "http://example.com/inbox"
self.remote_user.save(broadcast=False) self.remote_user.save(broadcast=False)
with patch("bookwyrm.models.user.set_remote_server.delay"): with patch("bookwyrm.models.user.set_remote_server.delay"):
@ -205,7 +205,7 @@ class ActivitypubMixins(TestCase):
self.assertEqual(recipients[0], "http://example.com/inbox") self.assertEqual(recipients[0], "http://example.com/inbox")
def test_get_recipients_software(self, _): 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"): with patch("bookwyrm.models.user.set_remote_server.delay"):
another_remote_user = models.User.objects.create_user( another_remote_user = models.User.objects.create_user(
"nutria", "nutria",
@ -235,13 +235,13 @@ class ActivitypubMixins(TestCase):
# ObjectMixin # ObjectMixin
def test_object_save_create(self, _): def test_object_save_create(self, _):
""" should save uneventufully when broadcast is disabled """ """should save uneventufully when broadcast is disabled"""
class Success(Exception): 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): 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) user = models.fields.ForeignKey("User", on_delete=db.models.CASCADE)
@ -252,7 +252,7 @@ class ActivitypubMixins(TestCase):
def broadcast( def broadcast(
self, activity, sender, **kwargs self, activity, sender, **kwargs
): # pylint: disable=arguments-differ ): # pylint: disable=arguments-differ
""" do something """ """do something"""
raise Success() raise Success()
def to_create_activity(self, user): # pylint: disable=arguments-differ def to_create_activity(self, user): # pylint: disable=arguments-differ
@ -266,13 +266,13 @@ class ActivitypubMixins(TestCase):
ObjectModel(user=None).save() ObjectModel(user=None).save()
def test_object_save_update(self, _): def test_object_save_update(self, _):
""" should save uneventufully when broadcast is disabled """ """should save uneventufully when broadcast is disabled"""
class Success(Exception): 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): 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) user = models.fields.ForeignKey("User", on_delete=db.models.CASCADE)
last_edited_by = models.fields.ForeignKey( last_edited_by = models.fields.ForeignKey(
@ -292,13 +292,13 @@ class ActivitypubMixins(TestCase):
UpdateObjectModel(id=1, last_edited_by=self.local_user).save() UpdateObjectModel(id=1, last_edited_by=self.local_user).save()
def test_object_save_delete(self, _): 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): 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): 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) user = models.fields.ForeignKey("User", on_delete=db.models.CASCADE)
deleted = models.fields.BooleanField() deleted = models.fields.BooleanField()
@ -314,7 +314,7 @@ class ActivitypubMixins(TestCase):
DeletableObjectModel(id=1, user=self.local_user, deleted=True).save() DeletableObjectModel(id=1, user=self.local_user, deleted=True).save()
def test_to_delete_activity(self, _): def test_to_delete_activity(self, _):
""" wrapper for Delete activity """ """wrapper for Delete activity"""
MockSelf = namedtuple("Self", ("remote_id", "to_activity")) MockSelf = namedtuple("Self", ("remote_id", "to_activity"))
mock_self = MockSelf( mock_self = MockSelf(
"https://example.com/status/1", lambda *args: self.object_mock "https://example.com/status/1", lambda *args: self.object_mock
@ -329,7 +329,7 @@ class ActivitypubMixins(TestCase):
) )
def test_to_update_activity(self, _): def test_to_update_activity(self, _):
""" ditto above but for Update """ """ditto above but for Update"""
MockSelf = namedtuple("Self", ("remote_id", "to_activity")) MockSelf = namedtuple("Self", ("remote_id", "to_activity"))
mock_self = MockSelf( mock_self = MockSelf(
"https://example.com/status/1", lambda *args: self.object_mock "https://example.com/status/1", lambda *args: self.object_mock
@ -347,7 +347,7 @@ class ActivitypubMixins(TestCase):
# Activity mixin # Activity mixin
def test_to_undo_activity(self, _): def test_to_undo_activity(self, _):
""" and again, for Undo """ """and again, for Undo"""
MockSelf = namedtuple("Self", ("remote_id", "to_activity", "user")) MockSelf = namedtuple("Self", ("remote_id", "to_activity", "user"))
mock_self = MockSelf( mock_self = MockSelf(
"https://example.com/status/1", "https://example.com/status/1",

View file

@ -8,10 +8,10 @@ from bookwyrm.settings import DOMAIN
class BaseModel(TestCase): class BaseModel(TestCase):
""" functionality shared across models """ """functionality shared across models"""
def setUp(self): def setUp(self):
""" shared data """ """shared data"""
self.local_user = models.User.objects.create_user( self.local_user = models.User.objects.create_user(
"mouse", "mouse@mouse.com", "mouseword", local=True, localname="mouse" "mouse", "mouse@mouse.com", "mouseword", local=True, localname="mouse"
) )
@ -27,14 +27,14 @@ class BaseModel(TestCase):
) )
def test_remote_id(self): def test_remote_id(self):
""" these should be generated """ """these should be generated"""
instance = base_model.BookWyrmModel() instance = base_model.BookWyrmModel()
instance.id = 1 instance.id = 1
expected = instance.get_remote_id() expected = instance.get_remote_id()
self.assertEqual(expected, "https://%s/bookwyrmmodel/1" % DOMAIN) self.assertEqual(expected, "https://%s/bookwyrmmodel/1" % DOMAIN)
def test_remote_id_with_user(self): def test_remote_id_with_user(self):
""" format of remote id when there's a user object """ """format of remote id when there's a user object"""
instance = base_model.BookWyrmModel() instance = base_model.BookWyrmModel()
instance.user = self.local_user instance.user = self.local_user
instance.id = 1 instance.id = 1
@ -42,7 +42,7 @@ class BaseModel(TestCase):
self.assertEqual(expected, "https://%s/user/mouse/bookwyrmmodel/1" % DOMAIN) self.assertEqual(expected, "https://%s/user/mouse/bookwyrmmodel/1" % DOMAIN)
def test_set_remote_id(self): 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 # using Work because it BookWrymModel is abstract and this requires save
# Work is a relatively not-fancy model. # Work is a relatively not-fancy model.
instance = models.Work.objects.create(title="work title") instance = models.Work.objects.create(title="work title")
@ -59,7 +59,7 @@ class BaseModel(TestCase):
@patch("bookwyrm.activitystreams.ActivityStream.add_status") @patch("bookwyrm.activitystreams.ActivityStream.add_status")
def test_object_visible_to_user(self, _): 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( obj = models.Status.objects.create(
content="hi", user=self.remote_user, privacy="public" content="hi", user=self.remote_user, privacy="public"
) )
@ -88,7 +88,7 @@ class BaseModel(TestCase):
@patch("bookwyrm.activitystreams.ActivityStream.add_status") @patch("bookwyrm.activitystreams.ActivityStream.add_status")
def test_object_visible_to_user_follower(self, _): 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) self.remote_user.followers.add(self.local_user)
obj = models.Status.objects.create( obj = models.Status.objects.create(
content="hi", user=self.remote_user, privacy="followers" content="hi", user=self.remote_user, privacy="followers"
@ -108,7 +108,7 @@ class BaseModel(TestCase):
@patch("bookwyrm.activitystreams.ActivityStream.add_status") @patch("bookwyrm.activitystreams.ActivityStream.add_status")
def test_object_visible_to_user_blocked(self, _): 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) self.remote_user.blocks.add(self.local_user)
obj = models.Status.objects.create( obj = models.Status.objects.create(
content="hi", user=self.remote_user, privacy="public" 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): 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): def setUp(self):
""" we'll need some books """ """we'll need some books"""
self.work = models.Work.objects.create( self.work = models.Work.objects.create(
title="Example Work", remote_id="https://example.com/book/1" title="Example Work", remote_id="https://example.com/book/1"
) )
@ -25,17 +25,17 @@ class Book(TestCase):
) )
def test_remote_id(self): 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) remote_id = "https://%s/book/%d" % (settings.DOMAIN, self.work.id)
self.assertEqual(self.work.get_remote_id(), remote_id) self.assertEqual(self.work.get_remote_id(), remote_id)
self.assertEqual(self.work.remote_id, remote_id) self.assertEqual(self.work.remote_id, remote_id)
def test_create_book(self): 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") self.assertRaises(ValueError, models.Book.objects.create, title="Invalid Book")
def test_isbn_10_to_13(self): def test_isbn_10_to_13(self):
""" checksums and so on """ """checksums and so on"""
isbn_10 = "178816167X" isbn_10 = "178816167X"
isbn_13 = isbn_10_to_13(isbn_10) isbn_13 = isbn_10_to_13(isbn_10)
self.assertEqual(isbn_13, "9781788161671") self.assertEqual(isbn_13, "9781788161671")
@ -45,7 +45,7 @@ class Book(TestCase):
self.assertEqual(isbn_13, "9781788161671") self.assertEqual(isbn_13, "9781788161671")
def test_isbn_13_to_10(self): def test_isbn_13_to_10(self):
""" checksums and so on """ """checksums and so on"""
isbn_13 = "9781788161671" isbn_13 = "9781788161671"
isbn_10 = isbn_13_to_10(isbn_13) isbn_10 = isbn_13_to_10(isbn_13)
self.assertEqual(isbn_10, "178816167X") self.assertEqual(isbn_10, "178816167X")
@ -55,7 +55,7 @@ class Book(TestCase):
self.assertEqual(isbn_10, "178816167X") self.assertEqual(isbn_10, "178816167X")
def test_get_edition_info(self): def test_get_edition_info(self):
""" text slug about an edition """ """text slug about an edition"""
book = models.Edition.objects.create(title="Test Edition") book = models.Edition.objects.create(title="Test Edition")
self.assertEqual(book.edition_info, "") self.assertEqual(book.edition_info, "")
@ -77,7 +77,7 @@ class Book(TestCase):
self.assertEqual(book.alt_text, "Test Edition (worm, Glorbish language, 2020)") self.assertEqual(book.alt_text, "Test Edition (worm, Glorbish language, 2020)")
def test_get_rank(self): def test_get_rank(self):
""" sets the data quality index for the book """ """sets the data quality index for the book"""
# basic rank # basic rank
self.assertEqual(self.first_edition.edition_rank, 0) self.assertEqual(self.first_edition.edition_rank, 0)

View file

@ -6,10 +6,10 @@ from bookwyrm import models
class FederatedServer(TestCase): class FederatedServer(TestCase):
""" federate server management """ """federate server management"""
def setUp(self): def setUp(self):
""" we'll need a user """ """we'll need a user"""
self.server = models.FederatedServer.objects.create(server_name="test.server") self.server = models.FederatedServer.objects.create(server_name="test.server")
with patch("bookwyrm.models.user.set_remote_server.delay"): with patch("bookwyrm.models.user.set_remote_server.delay"):
self.remote_user = models.User.objects.create_user( self.remote_user = models.User.objects.create_user(
@ -36,7 +36,7 @@ class FederatedServer(TestCase):
) )
def test_block_unblock(self): 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.assertEqual(self.server.status, "federated")
self.assertTrue(self.remote_user.is_active) self.assertTrue(self.remote_user.is_active)
self.assertFalse(self.inactive_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 # pylint: disable=too-many-public-methods
class ActivitypubFields(TestCase): 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): 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("http://www.example.com"))
self.assertIsNone(fields.validate_remote_id("https://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")) 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): 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() instance = fields.ActivitypubFieldMixin()
self.assertEqual(instance.field_to_activity("fish"), "fish") self.assertEqual(instance.field_to_activity("fish"), "fish")
self.assertEqual(instance.field_from_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") self.assertEqual(instance.get_activitypub_field(), "snakeCaseName")
def test_set_field_from_activity(self): def test_set_field_from_activity(self):
""" setter from entire json blob """ """setter from entire json blob"""
@dataclass @dataclass
class TestModel: class TestModel:
""" real simple mock """ """real simple mock"""
field_name: str field_name: str
@ -82,11 +82,11 @@ class ActivitypubFields(TestCase):
self.assertEqual(mock_model.field_name, "hi") self.assertEqual(mock_model.field_name, "hi")
def test_set_activity_from_field(self): def test_set_activity_from_field(self):
""" set json field given entire model """ """set json field given entire model"""
@dataclass @dataclass
class TestModel: class TestModel:
""" real simple mock """ """real simple mock"""
field_name: str field_name: str
unrelated: str unrelated: str
@ -100,7 +100,7 @@ class ActivitypubFields(TestCase):
self.assertEqual(data["fieldName"], "bip") self.assertEqual(data["fieldName"], "bip")
def test_remote_id_field(self): def test_remote_id_field(self):
""" just sets some defaults on charfield """ """just sets some defaults on charfield"""
instance = fields.RemoteIdField() instance = fields.RemoteIdField()
self.assertEqual(instance.max_length, 255) self.assertEqual(instance.max_length, 255)
self.assertTrue(instance.deduplication_field) self.assertTrue(instance.deduplication_field)
@ -109,7 +109,7 @@ class ActivitypubFields(TestCase):
instance.run_validators("http://www.example.com/dlfjg 23/x") instance.run_validators("http://www.example.com/dlfjg 23/x")
def test_username_field(self): def test_username_field(self):
""" again, just setting defaults on username field """ """again, just setting defaults on username field"""
instance = fields.UsernameField() instance = fields.UsernameField()
self.assertEqual(instance.activitypub_field, "preferredUsername") self.assertEqual(instance.activitypub_field, "preferredUsername")
self.assertEqual(instance.max_length, 150) self.assertEqual(instance.max_length, 150)
@ -130,7 +130,7 @@ class ActivitypubFields(TestCase):
self.assertEqual(instance.field_to_activity("test@example.com"), "test") self.assertEqual(instance.field_to_activity("test@example.com"), "test")
def test_privacy_field_defaults(self): def test_privacy_field_defaults(self):
""" post privacy field's many default values """ """post privacy field's many default values"""
instance = fields.PrivacyField() instance = fields.PrivacyField()
self.assertEqual(instance.max_length, 255) self.assertEqual(instance.max_length, 255)
self.assertEqual( self.assertEqual(
@ -143,11 +143,11 @@ class ActivitypubFields(TestCase):
) )
def test_privacy_field_set_field_from_activity(self): 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) @dataclass(init=False)
class TestActivity(ActivityObject): class TestActivity(ActivityObject):
""" real simple mock """ """real simple mock"""
to: List[str] to: List[str]
cc: List[str] cc: List[str]
@ -155,7 +155,7 @@ class ActivitypubFields(TestCase):
type: str = "Test" type: str = "Test"
class TestPrivacyModel(ActivitypubMixin, BookWyrmModel): class TestPrivacyModel(ActivitypubMixin, BookWyrmModel):
""" real simple mock model because BookWyrmModel is abstract """ """real simple mock model because BookWyrmModel is abstract"""
privacy_field = fields.PrivacyField() privacy_field = fields.PrivacyField()
mention_users = fields.TagField(User) mention_users = fields.TagField(User)
@ -187,7 +187,7 @@ class ActivitypubFields(TestCase):
@patch("bookwyrm.models.activitypub_mixin.ObjectMixin.broadcast") @patch("bookwyrm.models.activitypub_mixin.ObjectMixin.broadcast")
@patch("bookwyrm.activitystreams.ActivityStream.add_status") @patch("bookwyrm.activitystreams.ActivityStream.add_status")
def test_privacy_field_set_activity_from_field(self, *_): 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( user = User.objects.create_user(
"rat", "rat@rat.rat", "ratword", local=True, localname="rat" "rat", "rat@rat.rat", "ratword", local=True, localname="rat"
) )
@ -231,7 +231,7 @@ class ActivitypubFields(TestCase):
self.assertEqual(activity["cc"], []) self.assertEqual(activity["cc"], [])
def test_foreign_key(self): 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) instance = fields.ForeignKey("User", on_delete=models.CASCADE)
Serializable = namedtuple("Serializable", ("to_activity", "remote_id")) Serializable = namedtuple("Serializable", ("to_activity", "remote_id"))
item = Serializable(lambda: {"a": "b"}, "https://e.b/c") item = Serializable(lambda: {"a": "b"}, "https://e.b/c")
@ -240,7 +240,7 @@ class ActivitypubFields(TestCase):
@responses.activate @responses.activate
def test_foreign_key_from_activity_str(self): 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) instance = fields.ForeignKey(User, on_delete=models.CASCADE)
datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json") datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json")
userdata = json.loads(datafile.read_bytes()) userdata = json.loads(datafile.read_bytes())
@ -264,7 +264,7 @@ class ActivitypubFields(TestCase):
self.assertEqual(value.name, "MOUSE?? MOUSE!!") self.assertEqual(value.name, "MOUSE?? MOUSE!!")
def test_foreign_key_from_activity_dict(self): def test_foreign_key_from_activity_dict(self):
""" test recieving activity json """ """test recieving activity json"""
instance = fields.ForeignKey(User, on_delete=models.CASCADE) instance = fields.ForeignKey(User, on_delete=models.CASCADE)
datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json") datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json")
userdata = json.loads(datafile.read_bytes()) userdata = json.loads(datafile.read_bytes())
@ -284,7 +284,7 @@ class ActivitypubFields(TestCase):
# et cetera but we're not testing serializing user json # et cetera but we're not testing serializing user json
def test_foreign_key_from_activity_dict_existing(self): 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) instance = fields.ForeignKey(User, on_delete=models.CASCADE)
datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json") datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json")
userdata = json.loads(datafile.read_bytes()) userdata = json.loads(datafile.read_bytes())
@ -302,7 +302,7 @@ class ActivitypubFields(TestCase):
self.assertEqual(value, user) self.assertEqual(value, user)
def test_foreign_key_from_activity_str_existing(self): 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) instance = fields.ForeignKey(User, on_delete=models.CASCADE)
user = User.objects.create_user( user = User.objects.create_user(
"mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" "mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse"
@ -315,14 +315,14 @@ class ActivitypubFields(TestCase):
self.assertEqual(value, user) self.assertEqual(value, user)
def test_one_to_one_field(self): 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) instance = fields.OneToOneField("User", on_delete=models.CASCADE)
Serializable = namedtuple("Serializable", ("to_activity", "remote_id")) Serializable = namedtuple("Serializable", ("to_activity", "remote_id"))
item = Serializable(lambda: {"a": "b"}, "https://e.b/c") item = Serializable(lambda: {"a": "b"}, "https://e.b/c")
self.assertEqual(instance.field_to_activity(item), {"a": "b"}) self.assertEqual(instance.field_to_activity(item), {"a": "b"})
def test_many_to_many_field(self): def test_many_to_many_field(self):
""" lists! """ """lists!"""
instance = fields.ManyToManyField("User") instance = fields.ManyToManyField("User")
Serializable = namedtuple("Serializable", ("to_activity", "remote_id")) Serializable = namedtuple("Serializable", ("to_activity", "remote_id"))
@ -340,7 +340,7 @@ class ActivitypubFields(TestCase):
@responses.activate @responses.activate
def test_many_to_many_field_from_activity(self): 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) instance = fields.ManyToManyField(User)
datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json") datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json")
userdata = json.loads(datafile.read_bytes()) userdata = json.loads(datafile.read_bytes())
@ -360,7 +360,7 @@ class ActivitypubFields(TestCase):
self.assertIsInstance(value[0], User) self.assertIsInstance(value[0], User)
def test_tag_field(self): 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") instance = fields.TagField("User")
Serializable = namedtuple( Serializable = namedtuple(
@ -379,13 +379,13 @@ class ActivitypubFields(TestCase):
self.assertEqual(result[0].type, "Serializable") self.assertEqual(result[0].type, "Serializable")
def test_tag_field_from_activity(self): def test_tag_field_from_activity(self):
""" loadin' a list of items from Links """ """loadin' a list of items from Links"""
# TODO # TODO
@responses.activate @responses.activate
@patch("bookwyrm.models.activitypub_mixin.ObjectMixin.broadcast") @patch("bookwyrm.models.activitypub_mixin.ObjectMixin.broadcast")
def test_image_field(self, _): def test_image_field(self, _):
""" storing images """ """storing images"""
user = User.objects.create_user( user = User.objects.create_user(
"mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" "mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse"
) )
@ -423,7 +423,7 @@ class ActivitypubFields(TestCase):
self.assertIsInstance(loaded_image[1], ContentFile) self.assertIsInstance(loaded_image[1], ContentFile)
def test_datetime_field(self): 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() instance = fields.DateTimeField()
now = timezone.now() now = timezone.now()
self.assertEqual(instance.field_to_activity(now), now.isoformat()) self.assertEqual(instance.field_to_activity(now), now.isoformat())
@ -431,12 +431,12 @@ class ActivitypubFields(TestCase):
self.assertEqual(instance.field_from_activity("bip"), None) self.assertEqual(instance.field_from_activity("bip"), None)
def test_array_field(self): 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) instance = fields.ArrayField(fields.IntegerField)
self.assertEqual(instance.field_to_activity([0, 1]), ["0", "1"]) self.assertEqual(instance.field_to_activity([0, 1]), ["0", "1"])
def test_html_field(self): def test_html_field(self):
""" sanitizes html, the sanitizer has its own tests """ """sanitizes html, the sanitizer has its own tests"""
instance = fields.HtmlField() instance = fields.HtmlField()
self.assertEqual( self.assertEqual(
instance.field_from_activity("<marquee><p>hi</p></marquee>"), "<p>hi</p>" 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): class ImportJob(TestCase):
""" this is a fancy one!!! """ """this is a fancy one!!!"""
def setUp(self): 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 = { read_data = {
"Book Id": 39395857, "Book Id": 39395857,
"Title": "The Raven Tower", "Title": "The Raven Tower",
@ -72,30 +72,30 @@ class ImportJob(TestCase):
) )
def test_isbn(self): def test_isbn(self):
""" it unquotes the isbn13 field from data """ """it unquotes the isbn13 field from data"""
expected = "9780356506999" expected = "9780356506999"
item = models.ImportItem.objects.get(index=1) item = models.ImportItem.objects.get(index=1)
self.assertEqual(item.isbn, expected) self.assertEqual(item.isbn, expected)
def test_shelf(self): def test_shelf(self):
""" converts to the local shelf typology """ """converts to the local shelf typology"""
expected = "reading" expected = "reading"
self.assertEqual(self.item_1.shelf, expected) self.assertEqual(self.item_1.shelf, expected)
def test_date_added(self): 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) expected = datetime.datetime(2019, 4, 9, 0, 0, tzinfo=timezone.utc)
item = models.ImportItem.objects.get(index=1) item = models.ImportItem.objects.get(index=1)
self.assertEqual(item.date_added, expected) self.assertEqual(item.date_added, expected)
def test_date_read(self): 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) expected = datetime.datetime(2019, 4, 12, 0, 0, tzinfo=timezone.utc)
item = models.ImportItem.objects.get(index=2) item = models.ImportItem.objects.get(index=2)
self.assertEqual(item.date_read, expected) self.assertEqual(item.date_read, expected)
def test_currently_reading_reads(self): def test_currently_reading_reads(self):
""" infer currently reading dates where available """ """infer currently reading dates where available"""
expected = [ expected = [
models.ReadThrough( models.ReadThrough(
start_date=datetime.datetime(2019, 4, 9, 0, 0, tzinfo=timezone.utc) 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) self.assertEqual(actual.reads[0].finish_date, expected[0].finish_date)
def test_read_reads(self): def test_read_reads(self):
""" infer read dates where available """ """infer read dates where available"""
actual = self.item_2 actual = self.item_2
self.assertEqual( self.assertEqual(
actual.reads[0].start_date, actual.reads[0].start_date,
@ -118,14 +118,14 @@ class ImportJob(TestCase):
) )
def test_unread_reads(self): def test_unread_reads(self):
""" handle books with no read dates """ """handle books with no read dates"""
expected = [] expected = []
actual = models.ImportItem.objects.get(index=3) actual = models.ImportItem.objects.get(index=3)
self.assertEqual(actual.reads, expected) self.assertEqual(actual.reads, expected)
@responses.activate @responses.activate
def test_get_book_from_isbn(self): 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( connector_info = models.Connector.objects.create(
identifier="openlibrary.org", identifier="openlibrary.org",
name="OpenLibrary", name="OpenLibrary",

View file

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

View file

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

View file

@ -6,10 +6,10 @@ from bookwyrm import models
class Relationship(TestCase): class Relationship(TestCase):
""" following, blocking, stuff like that """ """following, blocking, stuff like that"""
def setUp(self): def setUp(self):
""" we need some users for this """ """we need some users for this"""
with patch("bookwyrm.models.user.set_remote_server.delay"): with patch("bookwyrm.models.user.set_remote_server.delay"):
self.remote_user = models.User.objects.create_user( self.remote_user = models.User.objects.create_user(
"rat", "rat",
@ -27,11 +27,11 @@ class Relationship(TestCase):
self.local_user.save(broadcast=False) self.local_user.save(broadcast=False)
def test_user_follows_from_request(self): 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 real_broadcast = models.UserFollowRequest.broadcast
def mock_broadcast(_, activity, user): 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(user.remote_id, self.local_user.remote_id)
self.assertEqual(activity["type"], "Follow") self.assertEqual(activity["type"], "Follow")
@ -54,7 +54,7 @@ class Relationship(TestCase):
models.UserFollowRequest.broadcast = real_broadcast models.UserFollowRequest.broadcast = real_broadcast
def test_user_follows_from_request_custom_remote_id(self): 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"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
request = models.UserFollowRequest.objects.create( request = models.UserFollowRequest.objects.create(
user_subject=self.local_user, user_subject=self.local_user,
@ -71,7 +71,7 @@ class Relationship(TestCase):
self.assertEqual(rel.user_object, self.remote_user) self.assertEqual(rel.user_object, self.remote_user)
def test_follow_request_activity(self): 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 real_broadcast = models.UserFollowRequest.broadcast
def mock_broadcast(_, activity, user): def mock_broadcast(_, activity, user):
@ -88,7 +88,7 @@ class Relationship(TestCase):
models.UserFollowRequest.broadcast = real_broadcast models.UserFollowRequest.broadcast = real_broadcast
def test_follow_request_accept(self): 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 real_broadcast = models.UserFollowRequest.broadcast
def mock_broadcast(_, activity, user): def mock_broadcast(_, activity, user):
@ -115,7 +115,7 @@ class Relationship(TestCase):
models.UserFollowRequest.broadcast = real_broadcast models.UserFollowRequest.broadcast = real_broadcast
def test_follow_request_reject(self): 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 real_broadcast = models.UserFollowRequest.broadcast
def mock_reject(_, activity, user): def mock_reject(_, activity, user):

View file

@ -8,10 +8,10 @@ from bookwyrm import models, settings
# pylint: disable=unused-argument # pylint: disable=unused-argument
class Shelf(TestCase): class Shelf(TestCase):
""" some activitypub oddness ahead """ """some activitypub oddness ahead"""
def setUp(self): def setUp(self):
""" look, a shelf """ """look, a shelf"""
self.local_user = models.User.objects.create_user( self.local_user = models.User.objects.create_user(
"mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" "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) self.book = models.Edition.objects.create(title="test book", parent_work=work)
def test_remote_id(self): def test_remote_id(self):
""" shelves use custom remote ids """ """shelves use custom remote ids"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
shelf = models.Shelf.objects.create( shelf = models.Shelf.objects.create(
name="Test Shelf", identifier="test-shelf", user=self.local_user 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) self.assertEqual(shelf.get_remote_id(), expected_id)
def test_to_activity(self): def test_to_activity(self):
""" jsonify it """ """jsonify it"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
shelf = models.Shelf.objects.create( shelf = models.Shelf.objects.create(
name="Test Shelf", identifier="test-shelf", user=self.local_user 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) self.assertEqual(activity_json["owner"], self.local_user.remote_id)
def test_create_update_shelf(self): 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: with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock:
shelf = models.Shelf.objects.create( shelf = models.Shelf.objects.create(
@ -63,7 +63,7 @@ class Shelf(TestCase):
self.assertEqual(shelf.name, "arthur russel") self.assertEqual(shelf.name, "arthur russel")
def test_shelve(self): def test_shelve(self):
""" create and broadcast shelf creation """ """create and broadcast shelf creation"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
shelf = models.Shelf.objects.create( shelf = models.Shelf.objects.create(
name="Test Shelf", identifier="test-shelf", user=self.local_user 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.models.Status.broadcast")
@patch("bookwyrm.activitystreams.ActivityStream.add_status") @patch("bookwyrm.activitystreams.ActivityStream.add_status")
class Status(TestCase): class Status(TestCase):
""" lotta types of statuses """ """lotta types of statuses"""
def setUp(self): def setUp(self):
""" useful things for creating a status """ """useful things for creating a status"""
self.local_user = models.User.objects.create_user( self.local_user = models.User.objects.create_user(
"mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" "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())) self.book.cover.save("test.jpg", ContentFile(output.getvalue()))
def test_status_generated_fields(self, *_): def test_status_generated_fields(self, *_):
""" setting remote id """ """setting remote id"""
status = models.Status.objects.create(content="bleh", user=self.local_user) status = models.Status.objects.create(content="bleh", user=self.local_user)
expected_id = "https://%s/user/mouse/status/%d" % (settings.DOMAIN, status.id) expected_id = "https://%s/user/mouse/status/%d" % (settings.DOMAIN, status.id)
self.assertEqual(status.remote_id, expected_id) self.assertEqual(status.remote_id, expected_id)
self.assertEqual(status.privacy, "public") self.assertEqual(status.privacy, "public")
def test_replies(self, *_): 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) parent = models.Status.objects.create(content="hi", user=self.local_user)
child = models.Status.objects.create( child = models.Status.objects.create(
content="hello", reply_parent=parent, user=self.local_user content="hello", reply_parent=parent, user=self.local_user
@ -72,7 +72,7 @@ class Status(TestCase):
self.assertIsInstance(replies.last(), models.Review) self.assertIsInstance(replies.last(), models.Review)
def test_status_type(self, *_): def test_status_type(self, *_):
""" class name """ """class name"""
self.assertEqual(models.Status().status_type, "Note") self.assertEqual(models.Status().status_type, "Note")
self.assertEqual(models.Review().status_type, "Review") self.assertEqual(models.Review().status_type, "Review")
self.assertEqual(models.Quotation().status_type, "Quotation") self.assertEqual(models.Quotation().status_type, "Quotation")
@ -80,14 +80,14 @@ class Status(TestCase):
self.assertEqual(models.Boost().status_type, "Announce") self.assertEqual(models.Boost().status_type, "Announce")
def test_boostable(self, *_): 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="public").boostable)
self.assertTrue(models.Status(privacy="unlisted").boostable) self.assertTrue(models.Status(privacy="unlisted").boostable)
self.assertFalse(models.Status(privacy="followers").boostable) self.assertFalse(models.Status(privacy="followers").boostable)
self.assertFalse(models.Status(privacy="direct").boostable) self.assertFalse(models.Status(privacy="direct").boostable)
def test_to_replies(self, *_): def test_to_replies(self, *_):
""" activitypub replies collection """ """activitypub replies collection"""
parent = models.Status.objects.create(content="hi", user=self.local_user) parent = models.Status.objects.create(content="hi", user=self.local_user)
child = models.Status.objects.create( child = models.Status.objects.create(
content="hello", reply_parent=parent, user=self.local_user content="hello", reply_parent=parent, user=self.local_user
@ -104,7 +104,7 @@ class Status(TestCase):
self.assertEqual(replies["totalItems"], 2) self.assertEqual(replies["totalItems"], 2)
def test_status_to_activity(self, *_): 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( status = models.Status.objects.create(
content="test content", user=self.local_user content="test content", user=self.local_user
) )
@ -115,7 +115,7 @@ class Status(TestCase):
self.assertEqual(activity["sensitive"], False) self.assertEqual(activity["sensitive"], False)
def test_status_to_activity_tombstone(self, *_): 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( with patch(
"bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores" "bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores"
): ):
@ -131,7 +131,7 @@ class Status(TestCase):
self.assertFalse(hasattr(activity, "content")) self.assertFalse(hasattr(activity, "content"))
def test_status_to_pure_activity(self, *_): 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( status = models.Status.objects.create(
content="test content", user=self.local_user content="test content", user=self.local_user
) )
@ -143,7 +143,7 @@ class Status(TestCase):
self.assertEqual(activity["attachment"], []) self.assertEqual(activity["attachment"], [])
def test_generated_note_to_activity(self, *_): 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( status = models.GeneratedNote.objects.create(
content="test content", user=self.local_user content="test content", user=self.local_user
) )
@ -157,7 +157,7 @@ class Status(TestCase):
self.assertEqual(len(activity["tag"]), 2) self.assertEqual(len(activity["tag"]), 2)
def test_generated_note_to_pure_activity(self, *_): 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( status = models.GeneratedNote.objects.create(
content="test content", user=self.local_user content="test content", user=self.local_user
) )
@ -181,7 +181,7 @@ class Status(TestCase):
self.assertEqual(activity["attachment"][0].name, "Test Edition") self.assertEqual(activity["attachment"][0].name, "Test Edition")
def test_comment_to_activity(self, *_): 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( status = models.Comment.objects.create(
content="test content", user=self.local_user, book=self.book 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) self.assertEqual(activity["inReplyToBook"], self.book.remote_id)
def test_comment_to_pure_activity(self, *_): 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( status = models.Comment.objects.create(
content="test content", user=self.local_user, book=self.book 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") self.assertEqual(activity["attachment"][0].name, "Test Edition")
def test_quotation_to_activity(self, *_): 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( status = models.Quotation.objects.create(
quote="a sickening sense", quote="a sickening sense",
content="test content", content="test content",
@ -227,7 +227,7 @@ class Status(TestCase):
self.assertEqual(activity["inReplyToBook"], self.book.remote_id) self.assertEqual(activity["inReplyToBook"], self.book.remote_id)
def test_quotation_to_pure_activity(self, *_): 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( status = models.Quotation.objects.create(
quote="a sickening sense", quote="a sickening sense",
content="test content", content="test content",
@ -250,7 +250,7 @@ class Status(TestCase):
self.assertEqual(activity["attachment"][0].name, "Test Edition") self.assertEqual(activity["attachment"][0].name, "Test Edition")
def test_review_to_activity(self, *_): 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( status = models.Review.objects.create(
name="Review name", name="Review name",
content="test content", content="test content",
@ -267,7 +267,7 @@ class Status(TestCase):
self.assertEqual(activity["inReplyToBook"], self.book.remote_id) self.assertEqual(activity["inReplyToBook"], self.book.remote_id)
def test_review_to_pure_activity(self, *_): 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( status = models.Review.objects.create(
name="Review's name", name="Review's name",
content="test content", content="test content",
@ -291,7 +291,7 @@ class Status(TestCase):
self.assertEqual(activity["attachment"][0].name, "Test Edition") self.assertEqual(activity["attachment"][0].name, "Test Edition")
def test_review_to_pure_activity_no_rating(self, *_): 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( status = models.Review.objects.create(
name="Review name", name="Review name",
content="test content", content="test content",
@ -313,7 +313,7 @@ class Status(TestCase):
self.assertEqual(activity["attachment"][0].name, "Test Edition") self.assertEqual(activity["attachment"][0].name, "Test Edition")
def test_reviewrating_to_pure_activity(self, *_): 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( status = models.ReviewRating.objects.create(
rating=3.0, rating=3.0,
user=self.local_user, user=self.local_user,
@ -335,11 +335,11 @@ class Status(TestCase):
self.assertEqual(activity["attachment"][0].name, "Test Edition") self.assertEqual(activity["attachment"][0].name, "Test Edition")
def test_favorite(self, *_): def test_favorite(self, *_):
""" fav a status """ """fav a status"""
real_broadcast = models.Favorite.broadcast real_broadcast = models.Favorite.broadcast
def fav_broadcast_mock(_, activity, user): def fav_broadcast_mock(_, activity, user):
""" ok """ """ok"""
self.assertEqual(user.remote_id, self.local_user.remote_id) self.assertEqual(user.remote_id, self.local_user.remote_id)
self.assertEqual(activity["type"], "Like") self.assertEqual(activity["type"], "Like")
@ -361,7 +361,7 @@ class Status(TestCase):
models.Favorite.broadcast = real_broadcast models.Favorite.broadcast = real_broadcast
def test_boost(self, *_): def test_boost(self, *_):
""" boosting, this one's a bit fussy """ """boosting, this one's a bit fussy"""
status = models.Status.objects.create( status = models.Status.objects.create(
content="test content", user=self.local_user content="test content", user=self.local_user
) )
@ -373,7 +373,7 @@ class Status(TestCase):
self.assertEqual(activity, boost.to_activity(pure=True)) self.assertEqual(activity, boost.to_activity(pure=True))
def test_notification(self, *_): def test_notification(self, *_):
""" a simple model """ """a simple model"""
notification = models.Notification.objects.create( notification = models.Notification.objects.create(
user=self.local_user, notification_type="FAVORITE" user=self.local_user, notification_type="FAVORITE"
) )
@ -385,7 +385,7 @@ class Status(TestCase):
) )
def test_create_broadcast(self, _, broadcast_mock): 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( models.Comment.objects.create(
content="hi", user=self.local_user, book=self.book content="hi", user=self.local_user, book=self.book
) )
@ -405,7 +405,7 @@ class Status(TestCase):
self.assertEqual(args["object"]["type"], "Comment") self.assertEqual(args["object"]["type"], "Comment")
def test_recipients_with_mentions(self, *_): def test_recipients_with_mentions(self, *_):
""" get recipients to broadcast a status """ """get recipients to broadcast a status"""
status = models.GeneratedNote.objects.create( status = models.GeneratedNote.objects.create(
content="test content", user=self.local_user content="test content", user=self.local_user
) )
@ -414,7 +414,7 @@ class Status(TestCase):
self.assertEqual(status.recipients, [self.remote_user]) self.assertEqual(status.recipients, [self.remote_user])
def test_recipients_with_reply_parent(self, *_): 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( parent_status = models.GeneratedNote.objects.create(
content="test content", user=self.remote_user content="test content", user=self.remote_user
) )
@ -425,7 +425,7 @@ class Status(TestCase):
self.assertEqual(status.recipients, [self.remote_user]) self.assertEqual(status.recipients, [self.remote_user])
def test_recipients_with_reply_parent_and_mentions(self, *_): 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( parent_status = models.GeneratedNote.objects.create(
content="test content", user=self.remote_user content="test content", user=self.remote_user
) )
@ -438,7 +438,7 @@ class Status(TestCase):
@responses.activate @responses.activate
def test_ignore_activity_boost(self, *_): def test_ignore_activity_boost(self, *_):
""" don't bother with most remote statuses """ """don't bother with most remote statuses"""
activity = activitypub.Announce( activity = activitypub.Announce(
id="http://www.faraway.com/boost/12", id="http://www.faraway.com/boost/12",
actor=self.remote_user.remote_id, actor=self.remote_user.remote_id,

View file

@ -22,7 +22,7 @@ class User(TestCase):
) )
def test_computed_fields(self): def test_computed_fields(self):
""" username instead of id here """ """username instead of id here"""
expected_id = "https://%s/user/mouse" % DOMAIN expected_id = "https://%s/user/mouse" % DOMAIN
self.assertEqual(self.user.remote_id, expected_id) self.assertEqual(self.user.remote_id, expected_id)
self.assertEqual(self.user.username, "mouse@%s" % DOMAIN) self.assertEqual(self.user.username, "mouse@%s" % DOMAIN)
@ -155,7 +155,7 @@ class User(TestCase):
self.assertIsNone(server.application_version) self.assertIsNone(server.application_version)
def test_delete_user(self): def test_delete_user(self):
""" deactivate a user """ """deactivate a user"""
self.assertTrue(self.user.is_active) self.assertTrue(self.user.is_active)
with patch( with patch(
"bookwyrm.models.activitypub_mixin.broadcast_task.delay" "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.models.activitypub_mixin.broadcast_task.delay")
@patch("bookwyrm.activitystreams.ActivityStream.add_status") @patch("bookwyrm.activitystreams.ActivityStream.add_status")
class Activitystreams(TestCase): class Activitystreams(TestCase):
""" using redis to build activity streams """ """using redis to build activity streams"""
def setUp(self): def setUp(self):
""" use a test csv """ """use a test csv"""
self.local_user = models.User.objects.create_user( self.local_user = models.User.objects.create_user(
"mouse", "mouse@mouse.mouse", "password", local=True, localname="mouse" "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") self.book = models.Edition.objects.create(title="test book")
class TestStream(activitystreams.ActivityStream): class TestStream(activitystreams.ActivityStream):
""" test stream, don't have to do anything here """ """test stream, don't have to do anything here"""
key = "test" key = "test"
self.test_stream = TestStream() self.test_stream = TestStream()
def test_activitystream_class_ids(self, *_): def test_activitystream_class_ids(self, *_):
""" the abstract base class for stream objects """ """the abstract base class for stream objects"""
self.assertEqual( self.assertEqual(
self.test_stream.stream_id(self.local_user), self.test_stream.stream_id(self.local_user),
"{}-test".format(self.local_user.id), "{}-test".format(self.local_user.id),
@ -48,7 +48,7 @@ class Activitystreams(TestCase):
) )
def test_abstractstream_get_audience(self, *_): 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( status = models.Status.objects.create(
user=self.remote_user, content="hi", privacy="public" user=self.remote_user, content="hi", privacy="public"
) )
@ -59,7 +59,7 @@ class Activitystreams(TestCase):
self.assertTrue(self.another_user in users) self.assertTrue(self.another_user in users)
def test_abstractstream_get_audience_direct(self, *_): 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( status = models.Status.objects.create(
user=self.remote_user, user=self.remote_user,
content="hi", content="hi",
@ -82,7 +82,7 @@ class Activitystreams(TestCase):
self.assertFalse(self.remote_user in users) self.assertFalse(self.remote_user in users)
def test_abstractstream_get_audience_followers_remote_user(self, *_): 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( status = models.Status.objects.create(
user=self.remote_user, user=self.remote_user,
content="hi", content="hi",
@ -92,7 +92,7 @@ class Activitystreams(TestCase):
self.assertFalse(users.exists()) self.assertFalse(users.exists())
def test_abstractstream_get_audience_followers_self(self, *_): 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( status = models.Comment.objects.create(
user=self.local_user, user=self.local_user,
content="hi", content="hi",
@ -105,7 +105,7 @@ class Activitystreams(TestCase):
self.assertFalse(self.remote_user in users) self.assertFalse(self.remote_user in users)
def test_abstractstream_get_audience_followers_with_mention(self, *_): 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( status = models.Comment.objects.create(
user=self.remote_user, user=self.remote_user,
content="hi", content="hi",
@ -120,7 +120,7 @@ class Activitystreams(TestCase):
self.assertFalse(self.remote_user in users) self.assertFalse(self.remote_user in users)
def test_abstractstream_get_audience_followers_with_relationship(self, *_): 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) self.remote_user.followers.add(self.local_user)
status = models.Comment.objects.create( status = models.Comment.objects.create(
user=self.remote_user, user=self.remote_user,
@ -134,7 +134,7 @@ class Activitystreams(TestCase):
self.assertFalse(self.remote_user in users) self.assertFalse(self.remote_user in users)
def test_homestream_get_audience(self, *_): 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( status = models.Status.objects.create(
user=self.remote_user, content="hi", privacy="public" user=self.remote_user, content="hi", privacy="public"
) )
@ -142,7 +142,7 @@ class Activitystreams(TestCase):
self.assertFalse(users.exists()) self.assertFalse(users.exists())
def test_homestream_get_audience_with_mentions(self, *_): 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( status = models.Status.objects.create(
user=self.remote_user, content="hi", privacy="public" user=self.remote_user, content="hi", privacy="public"
) )
@ -152,7 +152,7 @@ class Activitystreams(TestCase):
self.assertFalse(self.another_user in users) self.assertFalse(self.another_user in users)
def test_homestream_get_audience_with_relationship(self, *_): 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) self.remote_user.followers.add(self.local_user)
status = models.Status.objects.create( status = models.Status.objects.create(
user=self.remote_user, content="hi", privacy="public" user=self.remote_user, content="hi", privacy="public"
@ -162,7 +162,7 @@ class Activitystreams(TestCase):
self.assertFalse(self.another_user in users) self.assertFalse(self.another_user in users)
def test_localstream_get_audience_remote_status(self, *_): 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( status = models.Status.objects.create(
user=self.remote_user, content="hi", privacy="public" user=self.remote_user, content="hi", privacy="public"
) )
@ -170,7 +170,7 @@ class Activitystreams(TestCase):
self.assertEqual(users, []) self.assertEqual(users, [])
def test_localstream_get_audience_local_status(self, *_): 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( status = models.Status.objects.create(
user=self.local_user, content="hi", privacy="public" user=self.local_user, content="hi", privacy="public"
) )
@ -179,7 +179,7 @@ class Activitystreams(TestCase):
self.assertTrue(self.another_user in users) self.assertTrue(self.another_user in users)
def test_localstream_get_audience_unlisted(self, *_): 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( status = models.Status.objects.create(
user=self.local_user, content="hi", privacy="unlisted" user=self.local_user, content="hi", privacy="unlisted"
) )
@ -187,7 +187,7 @@ class Activitystreams(TestCase):
self.assertEqual(users, []) self.assertEqual(users, [])
def test_federatedstream_get_audience(self, *_): 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( status = models.Status.objects.create(
user=self.remote_user, content="hi", privacy="public" user=self.remote_user, content="hi", privacy="public"
) )
@ -196,7 +196,7 @@ class Activitystreams(TestCase):
self.assertTrue(self.another_user in users) self.assertTrue(self.another_user in users)
def test_federatedstream_get_audience_unlisted(self, *_): 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( status = models.Status.objects.create(
user=self.remote_user, content="hi", privacy="unlisted" 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") @patch("bookwyrm.emailing.send_email.delay")
class Emailing(TestCase): class Emailing(TestCase):
""" every response to a get request, html or json """ """every response to a get request, html or json"""
def setUp(self): def setUp(self):
""" we need basic test data and mocks """ """we need basic test data and mocks"""
self.factory = RequestFactory() self.factory = RequestFactory()
self.local_user = models.User.objects.create_user( self.local_user = models.User.objects.create_user(
"mouse@local.com", "mouse@local.com",
@ -25,7 +25,7 @@ class Emailing(TestCase):
models.SiteSettings.objects.create() models.SiteSettings.objects.create()
def test_invite_email(self, email_mock): def test_invite_email(self, email_mock):
""" load the invite email """ """load the invite email"""
invite_request = models.InviteRequest.objects.create( invite_request = models.InviteRequest.objects.create(
email="test@email.com", email="test@email.com",
invite=models.SiteInvite.objects.create(user=self.local_user), invite=models.SiteInvite.objects.create(user=self.local_user),
@ -40,7 +40,7 @@ class Emailing(TestCase):
self.assertEqual(len(args), 4) self.assertEqual(len(args), 4)
def test_password_reset_email(self, email_mock): 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) reset = models.PasswordReset.objects.create(user=self.local_user)
emailing.password_reset_email(reset) emailing.password_reset_email(reset)

View file

@ -5,10 +5,10 @@ from bookwyrm.sanitize_html import InputHtmlParser
class Sanitizer(TestCase): class Sanitizer(TestCase):
""" sanitizer tests """ """sanitizer tests"""
def test_no_html(self): def test_no_html(self):
""" just text """ """just text"""
input_text = "no html " input_text = "no html "
parser = InputHtmlParser() parser = InputHtmlParser()
parser.feed(input_text) parser.feed(input_text)
@ -16,7 +16,7 @@ class Sanitizer(TestCase):
self.assertEqual(input_text, output) self.assertEqual(input_text, output)
def test_valid_html(self): def test_valid_html(self):
""" leave the html untouched """ """leave the html untouched"""
input_text = "<b>yes </b> <i>html</i>" input_text = "<b>yes </b> <i>html</i>"
parser = InputHtmlParser() parser = InputHtmlParser()
parser.feed(input_text) parser.feed(input_text)
@ -24,7 +24,7 @@ class Sanitizer(TestCase):
self.assertEqual(input_text, output) self.assertEqual(input_text, output)
def test_valid_html_attrs(self): 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>' input_text = '<a href="fish.com">yes </a> <i>html</i>'
parser = InputHtmlParser() parser = InputHtmlParser()
parser.feed(input_text) parser.feed(input_text)
@ -32,7 +32,7 @@ class Sanitizer(TestCase):
self.assertEqual(input_text, output) self.assertEqual(input_text, output)
def test_invalid_html(self): 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>" input_text = "<b>yes <i>html</i>"
parser = InputHtmlParser() parser = InputHtmlParser()
parser.feed(input_text) parser.feed(input_text)
@ -46,7 +46,7 @@ class Sanitizer(TestCase):
self.assertEqual("yes html ", output) self.assertEqual("yes html ", output)
def test_disallowed_html(self): 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>" input_text = "<div> yes <i>html</i></div>"
parser = InputHtmlParser() parser = InputHtmlParser()
parser.feed(input_text) 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): def get_follow_activity(follower, followee):
""" generates a test activity """ """generates a test activity"""
return Follow( return Follow(
id="https://test.com/user/follow/id", id="https://test.com/user/follow/id",
actor=follower.remote_id, actor=follower.remote_id,
@ -33,10 +33,10 @@ Sender = namedtuple("Sender", ("remote_id", "key_pair"))
class Signature(TestCase): class Signature(TestCase):
""" signature test """ """signature test"""
def setUp(self): def setUp(self):
""" create users and test data """ """create users and test data"""
self.mouse = models.User.objects.create_user( self.mouse = models.User.objects.create_user(
"mouse@%s" % DOMAIN, "mouse@example.com", "", local=True, localname="mouse" "mouse@%s" % DOMAIN, "mouse@example.com", "", local=True, localname="mouse"
) )
@ -56,7 +56,7 @@ class Signature(TestCase):
models.SiteSettings.objects.create() models.SiteSettings.objects.create()
def send(self, signature, now, data, digest): def send(self, signature, now, data, digest):
""" test request """ """test request"""
c = Client() c = Client()
return c.post( return c.post(
urlsplit(self.rat.inbox).path, urlsplit(self.rat.inbox).path,
@ -74,7 +74,7 @@ class Signature(TestCase):
def send_test_request( # pylint: disable=too-many-arguments def send_test_request( # pylint: disable=too-many-arguments
self, sender, signer=None, send_data=None, digest=None, date=None 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() now = date or http_date()
data = json.dumps(get_follow_activity(sender, self.rat)) data = json.dumps(get_follow_activity(sender, self.rat))
digest = digest or make_digest(data) digest = digest or make_digest(data)
@ -84,7 +84,7 @@ class Signature(TestCase):
return self.send(signature, now, send_data or data, digest) return self.send(signature, now, send_data or data, digest)
def test_correct_signature(self): def test_correct_signature(self):
""" this one should just work """ """this one should just work"""
response = self.send_test_request(sender=self.mouse) response = self.send_test_request(sender=self.mouse)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@ -96,7 +96,7 @@ class Signature(TestCase):
@responses.activate @responses.activate
def test_remote_signer(self): def test_remote_signer(self):
""" signtures for remote users """ """signtures for remote users"""
datafile = pathlib.Path(__file__).parent.joinpath("data/ap_user.json") datafile = pathlib.Path(__file__).parent.joinpath("data/ap_user.json")
data = json.loads(datafile.read_bytes()) data = json.loads(datafile.read_bytes())
data["id"] = self.fake_remote.remote_id data["id"] = self.fake_remote.remote_id
@ -119,7 +119,7 @@ class Signature(TestCase):
@responses.activate @responses.activate
def test_key_needs_refresh(self): 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") datafile = pathlib.Path(__file__).parent.joinpath("data/ap_user.json")
data = json.loads(datafile.read_bytes()) data = json.loads(datafile.read_bytes())
data["id"] = self.fake_remote.remote_id data["id"] = self.fake_remote.remote_id
@ -155,7 +155,7 @@ class Signature(TestCase):
@responses.activate @responses.activate
def test_nonexistent_signer(self): def test_nonexistent_signer(self):
""" fail when unable to look up signer """ """fail when unable to look up signer"""
responses.add( responses.add(
responses.GET, responses.GET,
self.fake_remote.remote_id, self.fake_remote.remote_id,
@ -177,7 +177,7 @@ class Signature(TestCase):
@pytest.mark.integration @pytest.mark.integration
def test_invalid_digest(self): def test_invalid_digest(self):
""" signature digest must be valid """ """signature digest must be valid"""
with patch("bookwyrm.activitypub.resolve_remote_id"): with patch("bookwyrm.activitypub.resolve_remote_id"):
response = self.send_test_request( response = self.send_test_request(
self.mouse, digest="SHA-256=AAAAAAAAAAAAAAAAAA" self.mouse, digest="SHA-256=AAAAAAAAAAAAAAAAAA"

View file

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

View file

@ -12,10 +12,10 @@ from bookwyrm import models, views
# pylint: disable=too-many-public-methods # pylint: disable=too-many-public-methods
class Inbox(TestCase): class Inbox(TestCase):
""" readthrough tests """ """readthrough tests"""
def setUp(self): def setUp(self):
""" basic user and book data """ """basic user and book data"""
self.client = Client() self.client = Client()
self.factory = RequestFactory() self.factory = RequestFactory()
local_user = models.User.objects.create_user( local_user = models.User.objects.create_user(
@ -48,12 +48,12 @@ class Inbox(TestCase):
models.SiteSettings.objects.create() models.SiteSettings.objects.create()
def test_inbox_invalid_get(self): 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") result = self.client.get("/inbox", content_type="application/json")
self.assertIsInstance(result, HttpResponseNotAllowed) self.assertIsInstance(result, HttpResponseNotAllowed)
def test_inbox_invalid_user(self): 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( result = self.client.post(
"/user/bleh/inbox", "/user/bleh/inbox",
'{"type": "Test", "object": "exists"}', '{"type": "Test", "object": "exists"}',
@ -62,7 +62,7 @@ class Inbox(TestCase):
self.assertIsInstance(result, HttpResponseNotFound) self.assertIsInstance(result, HttpResponseNotFound)
def test_inbox_invalid_bad_signature(self): 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: with patch("bookwyrm.views.inbox.has_valid_signature") as mock_valid:
mock_valid.return_value = False mock_valid.return_value = False
result = self.client.post( result = self.client.post(
@ -73,7 +73,7 @@ class Inbox(TestCase):
self.assertEqual(result.status_code, 401) self.assertEqual(result.status_code, 401)
def test_inbox_invalid_bad_signature_delete(self): 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: with patch("bookwyrm.views.inbox.has_valid_signature") as mock_valid:
mock_valid.return_value = False mock_valid.return_value = False
result = self.client.post( result = self.client.post(
@ -84,7 +84,7 @@ class Inbox(TestCase):
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_inbox_unknown_type(self): 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: with patch("bookwyrm.views.inbox.has_valid_signature") as mock_valid:
result = self.client.post( result = self.client.post(
"/inbox", "/inbox",
@ -95,7 +95,7 @@ class Inbox(TestCase):
self.assertIsInstance(result, HttpResponseNotFound) self.assertIsInstance(result, HttpResponseNotFound)
def test_inbox_success(self): 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 = self.create_json
activity["object"] = { activity["object"] = {
"id": "https://example.com/list/22", "id": "https://example.com/list/22",
@ -121,7 +121,7 @@ class Inbox(TestCase):
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_is_blocked_user_agent(self): def test_is_blocked_user_agent(self):
""" check for blocked servers """ """check for blocked servers"""
request = self.factory.post( request = self.factory.post(
"", "",
HTTP_USER_AGENT="http.rb/4.4.1 (Mastodon/3.3.0; +https://mastodon.social/)", 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)) self.assertTrue(views.inbox.is_blocked_user_agent(request))
def test_is_blocked_activity(self): def test_is_blocked_activity(self):
""" check for blocked servers """ """check for blocked servers"""
activity = {"actor": "https://mastodon.social/user/whaatever/else"} activity = {"actor": "https://mastodon.social/user/whaatever/else"}
self.assertFalse(views.inbox.is_blocked_activity(activity)) self.assertFalse(views.inbox.is_blocked_activity(activity))
@ -144,7 +144,7 @@ class Inbox(TestCase):
self.assertTrue(views.inbox.is_blocked_activity(activity)) self.assertTrue(views.inbox.is_blocked_activity(activity))
def test_create_by_deactivated_user(self): 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.remote_user.delete(broadcast=False)
self.assertTrue(self.remote_user.deleted) self.assertTrue(self.remote_user.deleted)
datafile = pathlib.Path(__file__).parent.joinpath("../../data/ap_note.json") 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 # pylint: disable=too-many-public-methods
class InboxAdd(TestCase): class InboxAdd(TestCase):
""" inbox tests """ """inbox tests"""
def setUp(self): def setUp(self):
""" basic user and book data """ """basic user and book data"""
local_user = models.User.objects.create_user( local_user = models.User.objects.create_user(
"mouse@example.com", "mouse@example.com",
"mouse@mouse.com", "mouse@mouse.com",
@ -42,7 +42,7 @@ class InboxAdd(TestCase):
models.SiteSettings.objects.create() models.SiteSettings.objects.create()
def test_handle_add_book_to_shelf(self): 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 = models.Shelf.objects.create(user=self.remote_user, name="Test Shelf")
shelf.remote_id = "https://bookwyrm.social/user/mouse/shelf/to-read" shelf.remote_id = "https://bookwyrm.social/user/mouse/shelf/to-read"
shelf.save() shelf.save()
@ -65,7 +65,7 @@ class InboxAdd(TestCase):
@responses.activate @responses.activate
def test_handle_add_book_to_list(self): def test_handle_add_book_to_list(self):
""" listing a book """ """listing a book"""
responses.add( responses.add(
responses.GET, responses.GET,
"https://bookwyrm.social/user/mouse/list/to-read", "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 # pylint: disable=too-many-public-methods
class InboxActivities(TestCase): class InboxActivities(TestCase):
""" inbox tests """ """inbox tests"""
def setUp(self): def setUp(self):
""" basic user and book data """ """basic user and book data"""
self.local_user = models.User.objects.create_user( self.local_user = models.User.objects.create_user(
"mouse@example.com", "mouse@example.com",
"mouse@mouse.com", "mouse@mouse.com",
@ -52,7 +52,7 @@ class InboxActivities(TestCase):
@patch("bookwyrm.activitystreams.ActivityStream.add_status") @patch("bookwyrm.activitystreams.ActivityStream.add_status")
def test_boost(self, redis_mock): def test_boost(self, redis_mock):
""" boost a status """ """boost a status"""
self.assertEqual(models.Notification.objects.count(), 0) self.assertEqual(models.Notification.objects.count(), 0)
activity = { activity = {
"type": "Announce", "type": "Announce",
@ -82,7 +82,7 @@ class InboxActivities(TestCase):
@responses.activate @responses.activate
@patch("bookwyrm.activitystreams.ActivityStream.add_status") @patch("bookwyrm.activitystreams.ActivityStream.add_status")
def test_boost_remote_status(self, redis_mock): 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") work = models.Work.objects.create(title="work title")
book = models.Edition.objects.create( book = models.Edition.objects.create(
title="Test", title="Test",
@ -131,7 +131,7 @@ class InboxActivities(TestCase):
@responses.activate @responses.activate
def test_discarded_boost(self): 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( status = models.Status(
content="hi", content="hi",
user=self.remote_user, user=self.remote_user,
@ -154,7 +154,7 @@ class InboxActivities(TestCase):
self.assertEqual(models.Boost.objects.count(), 0) self.assertEqual(models.Boost.objects.count(), 0)
def test_unboost(self): def test_unboost(self):
""" undo a boost """ """undo a boost"""
with patch("bookwyrm.activitystreams.ActivityStream.add_status"): with patch("bookwyrm.activitystreams.ActivityStream.add_status"):
boost = models.Boost.objects.create( boost = models.Boost.objects.create(
boosted_status=self.status, user=self.remote_user boosted_status=self.status, user=self.remote_user
@ -183,7 +183,7 @@ class InboxActivities(TestCase):
self.assertFalse(models.Boost.objects.exists()) self.assertFalse(models.Boost.objects.exists())
def test_unboost_unknown_boost(self): def test_unboost_unknown_boost(self):
""" undo a boost """ """undo a boost"""
activity = { activity = {
"type": "Undo", "type": "Undo",
"actor": "hi", "actor": "hi",

View file

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

View file

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

View file

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

View file

@ -9,10 +9,10 @@ from bookwyrm import models, views
# pylint: disable=too-many-public-methods # pylint: disable=too-many-public-methods
class InboxRelationships(TestCase): class InboxRelationships(TestCase):
""" inbox tests """ """inbox tests"""
def setUp(self): def setUp(self):
""" basic user and book data """ """basic user and book data"""
self.local_user = models.User.objects.create_user( self.local_user = models.User.objects.create_user(
"mouse@example.com", "mouse@example.com",
"mouse@mouse.com", "mouse@mouse.com",
@ -36,7 +36,7 @@ class InboxRelationships(TestCase):
models.SiteSettings.objects.create() models.SiteSettings.objects.create()
def test_follow(self): def test_follow(self):
""" remote user wants to follow local user """ """remote user wants to follow local user"""
activity = { activity = {
"@context": "https://www.w3.org/ns/activitystreams", "@context": "https://www.w3.org/ns/activitystreams",
"id": "https://example.com/users/rat/follows/123", "id": "https://example.com/users/rat/follows/123",
@ -65,7 +65,7 @@ class InboxRelationships(TestCase):
self.assertEqual(follow.user_subject, self.remote_user) self.assertEqual(follow.user_subject, self.remote_user)
def test_follow_duplicate(self): def test_follow_duplicate(self):
""" remote user wants to follow local user twice """ """remote user wants to follow local user twice"""
activity = { activity = {
"@context": "https://www.w3.org/ns/activitystreams", "@context": "https://www.w3.org/ns/activitystreams",
"id": "https://example.com/users/rat/follows/123", "id": "https://example.com/users/rat/follows/123",
@ -92,7 +92,7 @@ class InboxRelationships(TestCase):
self.assertEqual(follow.user_subject, self.remote_user) self.assertEqual(follow.user_subject, self.remote_user)
def test_follow_manually_approved(self): def test_follow_manually_approved(self):
""" needs approval before following """ """needs approval before following"""
activity = { activity = {
"@context": "https://www.w3.org/ns/activitystreams", "@context": "https://www.w3.org/ns/activitystreams",
"id": "https://example.com/users/rat/follows/123", "id": "https://example.com/users/rat/follows/123",
@ -122,7 +122,7 @@ class InboxRelationships(TestCase):
self.assertEqual(list(follow), []) self.assertEqual(list(follow), [])
def test_undo_follow_request(self): 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.manually_approves_followers = True
self.local_user.save(broadcast=False) self.local_user.save(broadcast=False)
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
@ -152,7 +152,7 @@ class InboxRelationships(TestCase):
self.assertFalse(self.local_user.follower_requests.exists()) self.assertFalse(self.local_user.follower_requests.exists())
def test_unfollow(self): def test_unfollow(self):
""" remove a relationship """ """remove a relationship"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
rel = models.UserFollows.objects.create( rel = models.UserFollows.objects.create(
user_subject=self.remote_user, user_object=self.local_user user_subject=self.remote_user, user_object=self.local_user
@ -177,7 +177,7 @@ class InboxRelationships(TestCase):
self.assertIsNone(self.local_user.followers.first()) self.assertIsNone(self.local_user.followers.first())
def test_follow_accept(self): 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"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
rel = models.UserFollowRequest.objects.create( rel = models.UserFollowRequest.objects.create(
user_subject=self.local_user, user_object=self.remote_user user_subject=self.local_user, user_object=self.remote_user
@ -208,7 +208,7 @@ class InboxRelationships(TestCase):
self.assertEqual(follows.first(), self.local_user) self.assertEqual(follows.first(), self.local_user)
def test_follow_reject(self): def test_follow_reject(self):
""" turn down a follow request """ """turn down a follow request"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
rel = models.UserFollowRequest.objects.create( rel = models.UserFollowRequest.objects.create(
user_subject=self.local_user, user_object=self.remote_user 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 # pylint: disable=too-many-public-methods
class InboxActivities(TestCase): class InboxActivities(TestCase):
""" inbox tests """ """inbox tests"""
def setUp(self): def setUp(self):
""" basic user and book data """ """basic user and book data"""
self.local_user = models.User.objects.create_user( self.local_user = models.User.objects.create_user(
"mouse@example.com", "mouse@example.com",
"mouse@mouse.com", "mouse@mouse.com",
@ -50,7 +50,7 @@ class InboxActivities(TestCase):
models.SiteSettings.objects.create() models.SiteSettings.objects.create()
def test_handle_favorite(self): def test_handle_favorite(self):
""" fav a status """ """fav a status"""
activity = { activity = {
"@context": "https://www.w3.org/ns/activitystreams", "@context": "https://www.w3.org/ns/activitystreams",
"id": "https://example.com/fav/1", "id": "https://example.com/fav/1",
@ -68,7 +68,7 @@ class InboxActivities(TestCase):
self.assertEqual(fav.user, self.remote_user) self.assertEqual(fav.user, self.remote_user)
def test_ignore_favorite(self): def test_ignore_favorite(self):
""" don't try to save an unknown status """ """don't try to save an unknown status"""
activity = { activity = {
"@context": "https://www.w3.org/ns/activitystreams", "@context": "https://www.w3.org/ns/activitystreams",
"id": "https://example.com/fav/1", "id": "https://example.com/fav/1",
@ -83,7 +83,7 @@ class InboxActivities(TestCase):
self.assertFalse(models.Favorite.objects.exists()) self.assertFalse(models.Favorite.objects.exists())
def test_handle_unfavorite(self): def test_handle_unfavorite(self):
""" fav a status """ """fav a status"""
activity = { activity = {
"id": "https://example.com/fav/1#undo", "id": "https://example.com/fav/1#undo",
"type": "Undo", "type": "Undo",

View file

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

View file

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

View file

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

View file

@ -12,10 +12,10 @@ from bookwyrm.activitypub import ActivitypubResponse
class AuthorViews(TestCase): class AuthorViews(TestCase):
""" author views""" """author views"""
def setUp(self): def setUp(self):
""" we need basic test data and mocks """ """we need basic test data and mocks"""
self.factory = RequestFactory() self.factory = RequestFactory()
self.local_user = models.User.objects.create_user( self.local_user = models.User.objects.create_user(
"mouse@local.com", "mouse@local.com",
@ -42,7 +42,7 @@ class AuthorViews(TestCase):
models.SiteSettings.objects.create() models.SiteSettings.objects.create()
def test_author_page(self): 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() view = views.Author.as_view()
author = models.Author.objects.create(name="Jessica") author = models.Author.objects.create(name="Jessica")
request = self.factory.get("") request = self.factory.get("")
@ -62,7 +62,7 @@ class AuthorViews(TestCase):
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_edit_author_page(self): 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() view = views.EditAuthor.as_view()
author = models.Author.objects.create(name="Test Author") author = models.Author.objects.create(name="Test Author")
request = self.factory.get("") request = self.factory.get("")
@ -76,7 +76,7 @@ class AuthorViews(TestCase):
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_edit_author(self): def test_edit_author(self):
""" edit an author """ """edit an author"""
view = views.EditAuthor.as_view() view = views.EditAuthor.as_view()
author = models.Author.objects.create(name="Test Author") author = models.Author.objects.create(name="Test Author")
self.local_user.groups.add(self.group) self.local_user.groups.add(self.group)
@ -93,7 +93,7 @@ class AuthorViews(TestCase):
self.assertEqual(author.last_edited_by, self.local_user) self.assertEqual(author.last_edited_by, self.local_user)
def test_edit_author_non_editor(self): 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() view = views.EditAuthor.as_view()
author = models.Author.objects.create(name="Test Author") author = models.Author.objects.create(name="Test Author")
form = forms.AuthorForm(instance=author) form = forms.AuthorForm(instance=author)
@ -108,7 +108,7 @@ class AuthorViews(TestCase):
self.assertEqual(author.name, "Test Author") self.assertEqual(author.name, "Test Author")
def test_edit_author_invalid_form(self): 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() view = views.EditAuthor.as_view()
author = models.Author.objects.create(name="Test Author") author = models.Author.objects.create(name="Test Author")
self.local_user.groups.add(self.group) 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") @patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay")
class BlockViews(TestCase): class BlockViews(TestCase):
""" view user and edit profile """ """view user and edit profile"""
def setUp(self): def setUp(self):
""" we need basic test data and mocks """ """we need basic test data and mocks"""
self.factory = RequestFactory() self.factory = RequestFactory()
self.local_user = models.User.objects.create_user( self.local_user = models.User.objects.create_user(
"mouse@local.com", "mouse@local.com",
@ -34,7 +34,7 @@ class BlockViews(TestCase):
models.SiteSettings.objects.create() models.SiteSettings.objects.create()
def test_block_get(self, _): 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() view = views.Block.as_view()
request = self.factory.get("") request = self.factory.get("")
request.user = self.local_user request.user = self.local_user
@ -44,7 +44,7 @@ class BlockViews(TestCase):
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_block_post(self, _): 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() view = views.Block.as_view()
self.local_user.followers.add(self.remote_user) self.local_user.followers.add(self.remote_user)
models.UserFollowRequest.objects.create( models.UserFollowRequest.objects.create(
@ -65,7 +65,7 @@ class BlockViews(TestCase):
self.assertFalse(models.UserFollowRequest.objects.exists()) self.assertFalse(models.UserFollowRequest.objects.exists())
def test_unblock(self, _): def test_unblock(self, _):
""" undo a block """ """undo a block"""
self.local_user.blocks.add(self.remote_user) self.local_user.blocks.add(self.remote_user)
request = self.factory.post("") request = self.factory.post("")
request.user = self.local_user request.user = self.local_user

View file

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

View file

@ -7,10 +7,10 @@ from bookwyrm import models, views
# pylint: disable=unused-argument # pylint: disable=unused-argument
class DirectoryViews(TestCase): class DirectoryViews(TestCase):
""" tag views""" """tag views"""
def setUp(self): def setUp(self):
""" we need basic test data and mocks """ """we need basic test data and mocks"""
self.factory = RequestFactory() self.factory = RequestFactory()
self.local_user = models.User.objects.create_user( self.local_user = models.User.objects.create_user(
"mouse@local.com", "mouse@local.com",
@ -32,7 +32,7 @@ class DirectoryViews(TestCase):
models.SiteSettings.objects.create() models.SiteSettings.objects.create()
def test_directory_page(self): 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() view = views.Directory.as_view()
request = self.factory.get("") request = self.factory.get("")
request.user = self.local_user request.user = self.local_user

View file

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

View file

@ -17,10 +17,10 @@ from bookwyrm.activitypub import ActivitypubResponse
@patch("bookwyrm.activitystreams.ActivityStream.get_activity_stream") @patch("bookwyrm.activitystreams.ActivityStream.get_activity_stream")
@patch("bookwyrm.activitystreams.ActivityStream.add_status") @patch("bookwyrm.activitystreams.ActivityStream.add_status")
class FeedViews(TestCase): class FeedViews(TestCase):
""" activity feed, statuses, dms """ """activity feed, statuses, dms"""
def setUp(self): def setUp(self):
""" we need basic test data and mocks """ """we need basic test data and mocks"""
self.factory = RequestFactory() self.factory = RequestFactory()
self.local_user = models.User.objects.create_user( self.local_user = models.User.objects.create_user(
"mouse@local.com", "mouse@local.com",
@ -37,7 +37,7 @@ class FeedViews(TestCase):
models.SiteSettings.objects.create() models.SiteSettings.objects.create()
def test_feed(self, *_): def test_feed(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.Feed.as_view() view = views.Feed.as_view()
request = self.factory.get("") request = self.factory.get("")
request.user = self.local_user request.user = self.local_user
@ -47,7 +47,7 @@ class FeedViews(TestCase):
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_status_page(self, *_): def test_status_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.Status.as_view() view = views.Status.as_view()
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
status = models.Status.objects.create(content="hi", user=self.local_user) status = models.Status.objects.create(content="hi", user=self.local_user)
@ -67,7 +67,7 @@ class FeedViews(TestCase):
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_status_page_not_found(self, *_): def test_status_page_not_found(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.Status.as_view() view = views.Status.as_view()
request = self.factory.get("") request = self.factory.get("")
@ -79,7 +79,7 @@ class FeedViews(TestCase):
self.assertEqual(result.status_code, 404) self.assertEqual(result.status_code, 404)
def test_status_page_with_image(self, *_): def test_status_page_with_image(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.Status.as_view() view = views.Status.as_view()
image_file = pathlib.Path(__file__).parent.joinpath( image_file = pathlib.Path(__file__).parent.joinpath(
@ -115,7 +115,7 @@ class FeedViews(TestCase):
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_replies_page(self, *_): def test_replies_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.Replies.as_view() view = views.Replies.as_view()
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
status = models.Status.objects.create(content="hi", user=self.local_user) status = models.Status.objects.create(content="hi", user=self.local_user)
@ -135,7 +135,7 @@ class FeedViews(TestCase):
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_direct_messages_page(self, *_): def test_direct_messages_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.DirectMessage.as_view() view = views.DirectMessage.as_view()
request = self.factory.get("") request = self.factory.get("")
request.user = self.local_user request.user = self.local_user
@ -145,7 +145,7 @@ class FeedViews(TestCase):
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_get_suggested_book(self, *_): def test_get_suggested_book(self, *_):
""" gets books the ~*~ algorithm ~*~ thinks you want to post about """ """gets books the ~*~ algorithm ~*~ thinks you want to post about"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
models.ShelfBook.objects.create( models.ShelfBook.objects.create(
book=self.book, book=self.book,

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