forked from mirrors/bookwyrm
Merge branch 'main' into frontend-book-cover
This commit is contained in:
commit
56d821970a
163 changed files with 11795 additions and 4501 deletions
|
@ -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)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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()}
|
||||||
|
|
|
@ -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]
|
||||||
)
|
)
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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})
|
||||||
|
|
|
@ -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,
|
||||||
|
)
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -10,15 +10,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()
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -10,7 +10,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,
|
||||||
|
@ -21,10 +21,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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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/",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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",)
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -10,16 +10,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()
|
||||||
|
@ -34,14 +34,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))
|
||||||
|
@ -50,18 +50,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)
|
||||||
|
|
||||||
|
@ -75,12 +75,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"""
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -34,6 +34,8 @@ LOCALE_PATHS = [
|
||||||
os.path.join(BASE_DIR, "locale"),
|
os.path.join(BASE_DIR, "locale"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
|
||||||
|
|
||||||
# Quick-start development settings - unsuitable for production
|
# Quick-start development settings - unsuitable for production
|
||||||
# See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/
|
# See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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("-")]
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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"}))
|
||||||
|
|
|
@ -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
|
||||||
|
|
5
bookwyrm/tests/data/goodreads-rating.csv
Normal file
5
bookwyrm/tests/data/goodreads-rating.csv
Normal 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,,,,,
|
||||||
|
|
|
|
@ -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")
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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"
|
||||||
)
|
)
|
||||||
|
@ -26,23 +26,26 @@ class BaseModel(TestCase):
|
||||||
outbox="https://example.com/users/rat/outbox",
|
outbox="https://example.com/users/rat/outbox",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class BookWyrmTestModel(base_model.BookWyrmModel):
|
||||||
|
"""just making it not abstract"""
|
||||||
|
|
||||||
|
self.test_model = BookWyrmTestModel()
|
||||||
|
|
||||||
def test_remote_id(self):
|
def test_remote_id(self):
|
||||||
""" these should be generated """
|
"""these should be generated"""
|
||||||
instance = base_model.BookWyrmModel()
|
self.test_model.id = 1
|
||||||
instance.id = 1
|
expected = self.test_model.get_remote_id()
|
||||||
expected = instance.get_remote_id()
|
self.assertEqual(expected, "https://%s/bookwyrmtestmodel/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()
|
self.test_model.user = self.local_user
|
||||||
instance.user = self.local_user
|
self.test_model.id = 1
|
||||||
instance.id = 1
|
expected = self.test_model.get_remote_id()
|
||||||
expected = instance.get_remote_id()
|
self.assertEqual(expected, "https://%s/user/mouse/bookwyrmtestmodel/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 +62,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 +91,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 +111,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"
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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>"
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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())
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue