diff --git a/bookwyrm/activitypub/__init__.py b/bookwyrm/activitypub/__init__.py index d363fbd53..bfb22fa32 100644 --- a/bookwyrm/activitypub/__init__.py +++ b/bookwyrm/activitypub/__init__.py @@ -27,5 +27,5 @@ activity_objects = {c[0]: c[1] for c in cls_members if hasattr(c[1], "to_model") def parse(activity_json): - """ figure out what activity this is and parse it """ + """figure out what activity this is and parse it""" return naive_parse(activity_objects, activity_json) diff --git a/bookwyrm/activitypub/base_activity.py b/bookwyrm/activitypub/base_activity.py index dd2795bb1..5349e1dd0 100644 --- a/bookwyrm/activitypub/base_activity.py +++ b/bookwyrm/activitypub/base_activity.py @@ -10,11 +10,11 @@ from bookwyrm.tasks import app class ActivitySerializerError(ValueError): - """ routine problems serializing activitypub json """ + """routine problems serializing activitypub json""" class ActivityEncoder(JSONEncoder): - """ used to convert an Activity object into json """ + """used to convert an Activity object into json""" def default(self, o): return o.__dict__ @@ -22,7 +22,7 @@ class ActivityEncoder(JSONEncoder): @dataclass class Link: - """ for tagging a book in a status """ + """for tagging a book in a status""" href: str name: str @@ -31,14 +31,14 @@ class Link: @dataclass class Mention(Link): - """ a subtype of Link for mentioning an actor """ + """a subtype of Link for mentioning an actor""" type: str = "Mention" @dataclass class Signature: - """ public key block """ + """public key block""" creator: str created: str @@ -47,7 +47,7 @@ class Signature: def naive_parse(activity_objects, activity_json, serializer=None): - """ this navigates circular import issues """ + """this navigates circular import issues""" if not serializer: if activity_json.get("publicKeyPem"): # ugh @@ -67,7 +67,7 @@ def naive_parse(activity_objects, activity_json, serializer=None): @dataclass(init=False) class ActivityObject: - """ actor activitypub json """ + """actor activitypub json""" id: str type: str @@ -106,7 +106,7 @@ class ActivityObject: setattr(self, field.name, value) def to_model(self, model=None, instance=None, allow_create=True, save=True): - """ convert from an activity to a model instance """ + """convert from an activity to a model instance""" model = model or get_model_from_type(self.type) # only reject statuses if we're potentially creating them @@ -181,7 +181,7 @@ class ActivityObject: return instance def serialize(self): - """ convert to dictionary with context attr """ + """convert to dictionary with context attr""" data = self.__dict__.copy() # recursively serialize for (k, v) in data.items(): @@ -200,7 +200,7 @@ class ActivityObject: def set_related_field( model_name, origin_model_name, related_field_name, related_remote_id, data ): - """ load reverse related fields (editions, attachments) without blocking """ + """load reverse related fields (editions, attachments) without blocking""" model = apps.get_model("bookwyrm.%s" % model_name, require_ready=True) origin_model = apps.get_model("bookwyrm.%s" % origin_model_name, require_ready=True) @@ -236,7 +236,7 @@ def set_related_field( def get_model_from_type(activity_type): - """ given the activity, what type of model """ + """given the activity, what type of model""" models = apps.get_models() model = [ m @@ -255,7 +255,7 @@ def get_model_from_type(activity_type): def resolve_remote_id( remote_id, model=None, refresh=False, save=True, get_activity=False ): - """ take a remote_id and return an instance, creating if necessary """ + """take a remote_id and return an instance, creating if necessary""" if model: # a bonus check we can do if we already know the model result = model.find_existing_by_remote_id(remote_id) if result and not refresh: diff --git a/bookwyrm/activitypub/book.py b/bookwyrm/activitypub/book.py index 7615adcf7..f6ebf9131 100644 --- a/bookwyrm/activitypub/book.py +++ b/bookwyrm/activitypub/book.py @@ -8,9 +8,10 @@ from .image import Document @dataclass(init=False) class Book(ActivityObject): - """ serializes an edition or work, abstract """ + """serializes an edition or work, abstract""" title: str + lastEditedBy: str = None sortTitle: str = "" subtitle: str = "" description: str = "" @@ -34,7 +35,7 @@ class Book(ActivityObject): @dataclass(init=False) class Edition(Book): - """ Edition instance of a book object """ + """Edition instance of a book object""" work: str isbn10: str = "" @@ -51,7 +52,7 @@ class Edition(Book): @dataclass(init=False) class Work(Book): - """ work instance of a book object """ + """work instance of a book object""" lccn: str = "" defaultEdition: str = "" @@ -61,9 +62,10 @@ class Work(Book): @dataclass(init=False) class Author(ActivityObject): - """ author of a book """ + """author of a book""" name: str + lastEditedBy: str = None born: str = None died: str = None aliases: List[str] = field(default_factory=lambda: []) diff --git a/bookwyrm/activitypub/image.py b/bookwyrm/activitypub/image.py index a7120ce4b..7950faaf8 100644 --- a/bookwyrm/activitypub/image.py +++ b/bookwyrm/activitypub/image.py @@ -5,7 +5,7 @@ from .base_activity import ActivityObject @dataclass(init=False) class Document(ActivityObject): - """ a document """ + """a document""" url: str name: str = "" @@ -15,6 +15,6 @@ class Document(ActivityObject): @dataclass(init=False) class Image(Document): - """ an image """ + """an image""" type: str = "Image" diff --git a/bookwyrm/activitypub/note.py b/bookwyrm/activitypub/note.py index e1a42958c..b501c3d61 100644 --- a/bookwyrm/activitypub/note.py +++ b/bookwyrm/activitypub/note.py @@ -9,19 +9,19 @@ from .image import Document @dataclass(init=False) class Tombstone(ActivityObject): - """ the placeholder for a deleted status """ + """the placeholder for a deleted status""" type: str = "Tombstone" def to_model(self, *args, **kwargs): # pylint: disable=unused-argument - """ this should never really get serialized, just searched for """ + """this should never really get serialized, just searched for""" model = apps.get_model("bookwyrm.Status") return model.find_existing_by_remote_id(self.id) @dataclass(init=False) class Note(ActivityObject): - """ Note activity """ + """Note activity""" published: str attributedTo: str @@ -39,7 +39,7 @@ class Note(ActivityObject): @dataclass(init=False) class Article(Note): - """ what's an article except a note with more fields """ + """what's an article except a note with more fields""" name: str type: str = "Article" @@ -47,14 +47,14 @@ class Article(Note): @dataclass(init=False) class GeneratedNote(Note): - """ just a re-typed note """ + """just a re-typed note""" type: str = "GeneratedNote" @dataclass(init=False) class Comment(Note): - """ like a note but with a book """ + """like a note but with a book""" inReplyToBook: str type: str = "Comment" @@ -62,7 +62,7 @@ class Comment(Note): @dataclass(init=False) class Quotation(Comment): - """ a quote and commentary on a book """ + """a quote and commentary on a book""" quote: str type: str = "Quotation" @@ -70,7 +70,7 @@ class Quotation(Comment): @dataclass(init=False) class Review(Comment): - """ a full book review """ + """a full book review""" name: str = None rating: int = None @@ -79,7 +79,7 @@ class Review(Comment): @dataclass(init=False) class Rating(Comment): - """ just a star rating """ + """just a star rating""" rating: int content: str = None diff --git a/bookwyrm/activitypub/ordered_collection.py b/bookwyrm/activitypub/ordered_collection.py index 650f6a407..e3a83be8e 100644 --- a/bookwyrm/activitypub/ordered_collection.py +++ b/bookwyrm/activitypub/ordered_collection.py @@ -7,7 +7,7 @@ from .base_activity import ActivityObject @dataclass(init=False) class OrderedCollection(ActivityObject): - """ structure of an ordered collection activity """ + """structure of an ordered collection activity""" totalItems: int first: str @@ -19,7 +19,7 @@ class OrderedCollection(ActivityObject): @dataclass(init=False) class OrderedCollectionPrivate(OrderedCollection): - """ an ordered collection with privacy settings """ + """an ordered collection with privacy settings""" to: List[str] = field(default_factory=lambda: []) cc: List[str] = field(default_factory=lambda: []) @@ -27,14 +27,14 @@ class OrderedCollectionPrivate(OrderedCollection): @dataclass(init=False) class Shelf(OrderedCollectionPrivate): - """ structure of an ordered collection activity """ + """structure of an ordered collection activity""" type: str = "Shelf" @dataclass(init=False) class BookList(OrderedCollectionPrivate): - """ structure of an ordered collection activity """ + """structure of an ordered collection activity""" summary: str = None curation: str = "closed" @@ -43,7 +43,7 @@ class BookList(OrderedCollectionPrivate): @dataclass(init=False) class OrderedCollectionPage(ActivityObject): - """ structure of an ordered collection activity """ + """structure of an ordered collection activity""" partOf: str orderedItems: List @@ -54,7 +54,7 @@ class OrderedCollectionPage(ActivityObject): @dataclass(init=False) class CollectionItem(ActivityObject): - """ an item in a collection """ + """an item in a collection""" actor: str type: str = "CollectionItem" @@ -62,7 +62,7 @@ class CollectionItem(ActivityObject): @dataclass(init=False) class ListItem(CollectionItem): - """ a book on a list """ + """a book on a list""" book: str notes: str = None @@ -73,7 +73,7 @@ class ListItem(CollectionItem): @dataclass(init=False) class ShelfItem(CollectionItem): - """ a book on a list """ + """a book on a list""" book: str type: str = "ShelfItem" diff --git a/bookwyrm/activitypub/person.py b/bookwyrm/activitypub/person.py index 9231bd955..d5f379461 100644 --- a/bookwyrm/activitypub/person.py +++ b/bookwyrm/activitypub/person.py @@ -8,7 +8,7 @@ from .image import Image @dataclass(init=False) class PublicKey(ActivityObject): - """ public key block """ + """public key block""" owner: str publicKeyPem: str @@ -17,7 +17,7 @@ class PublicKey(ActivityObject): @dataclass(init=False) class Person(ActivityObject): - """ actor activitypub json """ + """actor activitypub json""" preferredUsername: str inbox: str diff --git a/bookwyrm/activitypub/verbs.py b/bookwyrm/activitypub/verbs.py index c2cbfea31..f26936d7b 100644 --- a/bookwyrm/activitypub/verbs.py +++ b/bookwyrm/activitypub/verbs.py @@ -9,13 +9,13 @@ from .ordered_collection import CollectionItem @dataclass(init=False) class Verb(ActivityObject): - """generic fields for activities """ + """generic fields for activities""" actor: str object: ActivityObject def action(self): - """ usually we just want to update and save """ + """usually we just want to update and save""" # self.object may return None if the object is invalid in an expected way # ie, Question type if self.object: @@ -24,7 +24,7 @@ class Verb(ActivityObject): @dataclass(init=False) class Create(Verb): - """ Create activity """ + """Create activity""" to: List[str] cc: List[str] = field(default_factory=lambda: []) @@ -34,14 +34,14 @@ class Create(Verb): @dataclass(init=False) class Delete(Verb): - """ Create activity """ + """Create activity""" to: List[str] cc: List[str] = field(default_factory=lambda: []) type: str = "Delete" def action(self): - """ find and delete the activity object """ + """find and delete the activity object""" if not self.object: return @@ -59,25 +59,25 @@ class Delete(Verb): @dataclass(init=False) class Update(Verb): - """ Update activity """ + """Update activity""" to: List[str] type: str = "Update" def action(self): - """ update a model instance from the dataclass """ + """update a model instance from the dataclass""" if self.object: self.object.to_model(allow_create=False) @dataclass(init=False) class Undo(Verb): - """ Undo an activity """ + """Undo an activity""" type: str = "Undo" def action(self): - """ find and remove the activity object """ + """find and remove the activity object""" if isinstance(self.object, str): # it may be that sometihng should be done with these, but idk what # this seems just to be coming from pleroma @@ -103,64 +103,64 @@ class Undo(Verb): @dataclass(init=False) class Follow(Verb): - """ Follow activity """ + """Follow activity""" object: str type: str = "Follow" def action(self): - """ relationship save """ + """relationship save""" self.to_model() @dataclass(init=False) class Block(Verb): - """ Block activity """ + """Block activity""" object: str type: str = "Block" def action(self): - """ relationship save """ + """relationship save""" self.to_model() @dataclass(init=False) class Accept(Verb): - """ Accept activity """ + """Accept activity""" object: Follow type: str = "Accept" def action(self): - """ find and remove the activity object """ + """find and remove the activity object""" obj = self.object.to_model(save=False, allow_create=False) obj.accept() @dataclass(init=False) class Reject(Verb): - """ Reject activity """ + """Reject activity""" object: Follow type: str = "Reject" def action(self): - """ find and remove the activity object """ + """find and remove the activity object""" obj = self.object.to_model(save=False, allow_create=False) obj.reject() @dataclass(init=False) class Add(Verb): - """Add activity """ + """Add activity""" target: ActivityObject object: CollectionItem type: str = "Add" def action(self): - """ figure out the target to assign the item to a collection """ + """figure out the target to assign the item to a collection""" target = resolve_remote_id(self.target) item = self.object.to_model(save=False) setattr(item, item.collection_field, target) @@ -169,31 +169,32 @@ class Add(Verb): @dataclass(init=False) class Remove(Add): - """Remove activity """ + """Remove activity""" type: str = "Remove" def action(self): - """ find and remove the activity object """ + """find and remove the activity object""" obj = self.object.to_model(save=False, allow_create=False) - obj.delete() + if obj: + obj.delete() @dataclass(init=False) class Like(Verb): - """ a user faving an object """ + """a user faving an object""" object: str type: str = "Like" def action(self): - """ like """ + """like""" self.to_model() @dataclass(init=False) class Announce(Verb): - """ boosting a status """ + """boosting a status""" published: str to: List[str] = field(default_factory=lambda: []) @@ -202,5 +203,5 @@ class Announce(Verb): type: str = "Announce" def action(self): - """ boost """ + """boost""" self.to_model() diff --git a/bookwyrm/activitystreams.py b/bookwyrm/activitystreams.py index 949ae9dad..86321cd83 100644 --- a/bookwyrm/activitystreams.py +++ b/bookwyrm/activitystreams.py @@ -8,22 +8,22 @@ from bookwyrm.views.helpers import privacy_filter class ActivityStream(RedisStore): - """ a category of activity stream (like home, local, federated) """ + """a category of activity stream (like home, local, federated)""" def stream_id(self, user): - """ the redis key for this user's instance of this stream """ + """the redis key for this user's instance of this stream""" return "{}-{}".format(user.id, self.key) def unread_id(self, user): - """ the redis key for this user's unread count for this stream """ + """the redis key for this user's unread count for this stream""" return "{}-unread".format(self.stream_id(user)) def get_rank(self, obj): # pylint: disable=no-self-use - """ statuses are sorted by date published """ + """statuses are sorted by date published""" return obj.published_date.timestamp() def add_status(self, status): - """ add a status to users' feeds """ + """add a status to users' feeds""" # the pipeline contains all the add-to-stream activities pipeline = self.add_object_to_related_stores(status, execute=False) @@ -35,19 +35,19 @@ class ActivityStream(RedisStore): pipeline.execute() def add_user_statuses(self, viewer, user): - """ add a user's statuses to another user's feed """ + """add a user's statuses to another user's feed""" # only add the statuses that the viewer should be able to see (ie, not dms) statuses = privacy_filter(viewer, user.status_set.all()) self.bulk_add_objects_to_store(statuses, self.stream_id(viewer)) def remove_user_statuses(self, viewer, user): - """ remove a user's status from another user's feed """ + """remove a user's status from another user's feed""" # remove all so that followers only statuses are removed statuses = user.status_set.all() self.bulk_remove_objects_from_store(statuses, self.stream_id(viewer)) def get_activity_stream(self, user): - """ load the statuses to be displayed """ + """load the statuses to be displayed""" # clear unreads for this feed r.set(self.unread_id(user), 0) @@ -59,15 +59,15 @@ class ActivityStream(RedisStore): ) def get_unread_count(self, user): - """ get the unread status count for this user's feed """ + """get the unread status count for this user's feed""" return int(r.get(self.unread_id(user)) or 0) def populate_streams(self, user): - """ go from zero to a timeline """ + """go from zero to a timeline""" self.populate_store(self.stream_id(user)) def get_audience(self, status): # pylint: disable=no-self-use - """ given a status, what users should see it """ + """given a status, what users should see it""" # direct messages don't appeard in feeds, direct comments/reviews/etc do if status.privacy == "direct" and status.status_type == "Note": return [] @@ -98,7 +98,7 @@ class ActivityStream(RedisStore): return [self.stream_id(u) for u in self.get_audience(obj)] def get_statuses_for_user(self, user): # pylint: disable=no-self-use - """ given a user, what statuses should they see on this stream """ + """given a user, what statuses should they see on this stream""" return privacy_filter( user, models.Status.objects.select_subclasses(), @@ -111,7 +111,7 @@ class ActivityStream(RedisStore): class HomeStream(ActivityStream): - """ users you follow """ + """users you follow""" key = "home" @@ -134,7 +134,7 @@ class HomeStream(ActivityStream): class LocalStream(ActivityStream): - """ users you follow """ + """users you follow""" key = "local" @@ -154,7 +154,7 @@ class LocalStream(ActivityStream): class FederatedStream(ActivityStream): - """ users you follow """ + """users you follow""" key = "federated" @@ -182,7 +182,7 @@ streams = { @receiver(signals.post_save) # pylint: disable=unused-argument def add_status_on_create(sender, instance, created, *args, **kwargs): - """ add newly created statuses to activity feeds """ + """add newly created statuses to activity feeds""" # we're only interested in new statuses if not issubclass(sender, models.Status): return @@ -203,7 +203,7 @@ def add_status_on_create(sender, instance, created, *args, **kwargs): @receiver(signals.post_delete, sender=models.Boost) # pylint: disable=unused-argument def remove_boost_on_delete(sender, instance, *args, **kwargs): - """ boosts are deleted """ + """boosts are deleted""" # we're only interested in new statuses for stream in streams.values(): stream.remove_object_from_related_stores(instance) @@ -212,7 +212,7 @@ def remove_boost_on_delete(sender, instance, *args, **kwargs): @receiver(signals.post_save, sender=models.UserFollows) # pylint: disable=unused-argument def add_statuses_on_follow(sender, instance, created, *args, **kwargs): - """ add a newly followed user's statuses to feeds """ + """add a newly followed user's statuses to feeds""" if not created or not instance.user_subject.local: return HomeStream().add_user_statuses(instance.user_subject, instance.user_object) @@ -221,7 +221,7 @@ def add_statuses_on_follow(sender, instance, created, *args, **kwargs): @receiver(signals.post_delete, sender=models.UserFollows) # pylint: disable=unused-argument def remove_statuses_on_unfollow(sender, instance, *args, **kwargs): - """ remove statuses from a feed on unfollow """ + """remove statuses from a feed on unfollow""" if not instance.user_subject.local: return HomeStream().remove_user_statuses(instance.user_subject, instance.user_object) @@ -230,7 +230,7 @@ def remove_statuses_on_unfollow(sender, instance, *args, **kwargs): @receiver(signals.post_save, sender=models.UserBlocks) # pylint: disable=unused-argument def remove_statuses_on_block(sender, instance, *args, **kwargs): - """ remove statuses from all feeds on block """ + """remove statuses from all feeds on block""" # blocks apply ot all feeds if instance.user_subject.local: for stream in streams.values(): @@ -245,7 +245,7 @@ def remove_statuses_on_block(sender, instance, *args, **kwargs): @receiver(signals.post_delete, sender=models.UserBlocks) # pylint: disable=unused-argument def add_statuses_on_unblock(sender, instance, *args, **kwargs): - """ remove statuses from all feeds on block """ + """remove statuses from all feeds on block""" public_streams = [LocalStream(), FederatedStream()] # add statuses back to streams with statuses from anyone if instance.user_subject.local: @@ -261,7 +261,7 @@ def add_statuses_on_unblock(sender, instance, *args, **kwargs): @receiver(signals.post_save, sender=models.User) # pylint: disable=unused-argument def populate_streams_on_account_create(sender, instance, created, *args, **kwargs): - """ build a user's feeds when they join """ + """build a user's feeds when they join""" if not created or not instance.local: return diff --git a/bookwyrm/connectors/abstract_connector.py b/bookwyrm/connectors/abstract_connector.py index 2fe5d825c..264b5a38e 100644 --- a/bookwyrm/connectors/abstract_connector.py +++ b/bookwyrm/connectors/abstract_connector.py @@ -16,7 +16,7 @@ logger = logging.getLogger(__name__) class AbstractMinimalConnector(ABC): - """ just the bare bones, for other bookwyrm instances """ + """just the bare bones, for other bookwyrm instances""" def __init__(self, identifier): # load connector settings @@ -39,7 +39,7 @@ class AbstractMinimalConnector(ABC): setattr(self, field, getattr(info, field)) def search(self, query, min_confidence=None): - """ free text search """ + """free text search""" params = {} if min_confidence: params["min_confidence"] = min_confidence @@ -55,7 +55,7 @@ class AbstractMinimalConnector(ABC): return results def isbn_search(self, query): - """ isbn search """ + """isbn search""" params = {} data = get_data( "%s%s" % (self.isbn_search_url, query), @@ -70,27 +70,27 @@ class AbstractMinimalConnector(ABC): @abstractmethod def get_or_create_book(self, remote_id): - """ pull up a book record by whatever means possible """ + """pull up a book record by whatever means possible""" @abstractmethod def parse_search_data(self, data): - """ turn the result json from a search into a list """ + """turn the result json from a search into a list""" @abstractmethod def format_search_result(self, search_result): - """ create a SearchResult obj from json """ + """create a SearchResult obj from json""" @abstractmethod def parse_isbn_search_data(self, data): - """ turn the result json from a search into a list """ + """turn the result json from a search into a list""" @abstractmethod def format_isbn_search_result(self, search_result): - """ create a SearchResult obj from json """ + """create a SearchResult obj from json""" class AbstractConnector(AbstractMinimalConnector): - """ generic book data connector """ + """generic book data connector""" def __init__(self, identifier): super().__init__(identifier) @@ -99,14 +99,14 @@ class AbstractConnector(AbstractMinimalConnector): self.book_mappings = [] def is_available(self): - """ check if you're allowed to use this connector """ + """check if you're allowed to use this connector""" if self.max_query_count is not None: if self.connector.query_count >= self.max_query_count: return False return True def get_or_create_book(self, remote_id): - """ translate arbitrary json into an Activitypub dataclass """ + """translate arbitrary json into an Activitypub dataclass""" # first, check if we have the origin_id saved existing = models.Edition.find_existing_by_remote_id( remote_id @@ -151,7 +151,7 @@ class AbstractConnector(AbstractMinimalConnector): return edition def create_edition_from_data(self, work, edition_data): - """ if we already have the work, we're ready """ + """if we already have the work, we're ready""" mapped_data = dict_from_mappings(edition_data, self.book_mappings) mapped_data["work"] = work.remote_id edition_activity = activitypub.Edition(**mapped_data) @@ -171,7 +171,7 @@ class AbstractConnector(AbstractMinimalConnector): return edition def get_or_create_author(self, remote_id): - """ load that author """ + """load that author""" existing = models.Author.find_existing_by_remote_id(remote_id) if existing: return existing @@ -189,23 +189,23 @@ class AbstractConnector(AbstractMinimalConnector): @abstractmethod def is_work_data(self, data): - """ differentiate works and editions """ + """differentiate works and editions""" @abstractmethod def get_edition_from_work_data(self, data): - """ every work needs at least one edition """ + """every work needs at least one edition""" @abstractmethod def get_work_from_edition_data(self, data): - """ every edition needs a work """ + """every edition needs a work""" @abstractmethod def get_authors_from_data(self, data): - """ load author data """ + """load author data""" @abstractmethod def expand_book_data(self, book): - """ get more info on a book """ + """get more info on a book""" def dict_from_mappings(data, mappings): @@ -218,7 +218,7 @@ def dict_from_mappings(data, mappings): def get_data(url, params=None): - """ wrapper for request.get """ + """wrapper for request.get""" # check if the url is blocked if models.FederatedServer.is_blocked(url): raise ConnectorException( @@ -250,7 +250,7 @@ def get_data(url, params=None): def get_image(url): - """ wrapper for requesting an image """ + """wrapper for requesting an image""" try: resp = requests.get( url, @@ -268,7 +268,7 @@ def get_image(url): @dataclass class SearchResult: - """ standardized search result object """ + """standardized search result object""" title: str key: str @@ -284,14 +284,14 @@ class SearchResult: ) def json(self): - """ serialize a connector for json response """ + """serialize a connector for json response""" serialized = asdict(self) del serialized["connector"] return serialized class Mapping: - """ associate a local database field with a field in an external dataset """ + """associate a local database field with a field in an external dataset""" def __init__(self, local_field, remote_field=None, formatter=None): noop = lambda x: x @@ -301,7 +301,7 @@ class Mapping: self.formatter = formatter or noop def get_value(self, data): - """ pull a field from incoming json and return the formatted version """ + """pull a field from incoming json and return the formatted version""" value = data.get(self.remote_field) if not value: return None diff --git a/bookwyrm/connectors/bookwyrm_connector.py b/bookwyrm/connectors/bookwyrm_connector.py index f7869d55c..640a0bca7 100644 --- a/bookwyrm/connectors/bookwyrm_connector.py +++ b/bookwyrm/connectors/bookwyrm_connector.py @@ -4,7 +4,7 @@ from .abstract_connector import AbstractMinimalConnector, SearchResult class Connector(AbstractMinimalConnector): - """ this is basically just for search """ + """this is basically just for search""" def get_or_create_book(self, remote_id): edition = activitypub.resolve_remote_id(remote_id, model=models.Edition) diff --git a/bookwyrm/connectors/connector_manager.py b/bookwyrm/connectors/connector_manager.py index 53198c0a9..20d273e0f 100644 --- a/bookwyrm/connectors/connector_manager.py +++ b/bookwyrm/connectors/connector_manager.py @@ -16,11 +16,11 @@ logger = logging.getLogger(__name__) class ConnectorException(HTTPError): - """ when the connector can't do what was asked """ + """when the connector can't do what was asked""" def search(query, min_confidence=0.1): - """ find books based on arbitary keywords """ + """find books based on arbitary keywords""" if not query: return [] results = [] @@ -68,19 +68,19 @@ def search(query, min_confidence=0.1): def local_search(query, min_confidence=0.1, raw=False): - """ only look at local search results """ + """only look at local search results""" connector = load_connector(models.Connector.objects.get(local=True)) return connector.search(query, min_confidence=min_confidence, raw=raw) def isbn_local_search(query, raw=False): - """ only look at local search results """ + """only look at local search results""" connector = load_connector(models.Connector.objects.get(local=True)) return connector.isbn_search(query, raw=raw) def first_search_result(query, min_confidence=0.1): - """ search until you find a result that fits """ + """search until you find a result that fits""" for connector in get_connectors(): result = connector.search(query, min_confidence=min_confidence) if result: @@ -89,13 +89,13 @@ def first_search_result(query, min_confidence=0.1): def get_connectors(): - """ load all connectors """ + """load all connectors""" for info in models.Connector.objects.order_by("priority").all(): yield load_connector(info) def get_or_create_connector(remote_id): - """ get the connector related to the object's server """ + """get the connector related to the object's server""" url = urlparse(remote_id) identifier = url.netloc if not identifier: @@ -119,7 +119,7 @@ def get_or_create_connector(remote_id): @app.task def load_more_data(connector_id, book_id): - """ background the work of getting all 10,000 editions of LoTR """ + """background the work of getting all 10,000 editions of LoTR""" connector_info = models.Connector.objects.get(id=connector_id) connector = load_connector(connector_info) book = models.Book.objects.select_subclasses().get(id=book_id) @@ -127,7 +127,7 @@ def load_more_data(connector_id, book_id): def load_connector(connector_info): - """ instantiate the connector class """ + """instantiate the connector class""" connector = importlib.import_module( "bookwyrm.connectors.%s" % connector_info.connector_file ) @@ -137,6 +137,6 @@ def load_connector(connector_info): @receiver(signals.post_save, sender="bookwyrm.FederatedServer") # pylint: disable=unused-argument def create_connector(sender, instance, created, *args, **kwargs): - """ create a connector to an external bookwyrm server """ + """create a connector to an external bookwyrm server""" if instance.application_type == "bookwyrm": get_or_create_connector("https://{:s}".format(instance.server_name)) diff --git a/bookwyrm/connectors/openlibrary.py b/bookwyrm/connectors/openlibrary.py index 8ee738eb8..a7c30b663 100644 --- a/bookwyrm/connectors/openlibrary.py +++ b/bookwyrm/connectors/openlibrary.py @@ -9,7 +9,7 @@ from .openlibrary_languages import languages class Connector(AbstractConnector): - """ instantiate a connector for OL """ + """instantiate a connector for OL""" def __init__(self, identifier): super().__init__(identifier) @@ -59,7 +59,7 @@ class Connector(AbstractConnector): ] def get_remote_id_from_data(self, data): - """ format a url from an openlibrary id field """ + """format a url from an openlibrary id field""" try: key = data["key"] except KeyError: @@ -87,7 +87,7 @@ class Connector(AbstractConnector): return get_data(url) def get_authors_from_data(self, data): - """ parse author json and load or create authors """ + """parse author json and load or create authors""" for author_blob in data.get("authors", []): author_blob = author_blob.get("author", author_blob) # this id is "/authors/OL1234567A" @@ -99,7 +99,7 @@ class Connector(AbstractConnector): yield author def get_cover_url(self, cover_blob, size="L"): - """ ask openlibrary for the cover """ + """ask openlibrary for the cover""" if not cover_blob: return None cover_id = cover_blob[0] @@ -141,7 +141,7 @@ class Connector(AbstractConnector): ) def load_edition_data(self, olkey): - """ query openlibrary for editions of a work """ + """query openlibrary for editions of a work""" url = "%s/works/%s/editions" % (self.books_url, olkey) return get_data(url) @@ -166,7 +166,7 @@ class Connector(AbstractConnector): def ignore_edition(edition_data): - """ don't load a million editions that have no metadata """ + """don't load a million editions that have no metadata""" # an isbn, we love to see it if edition_data.get("isbn_13") or edition_data.get("isbn_10"): return False @@ -185,19 +185,19 @@ def ignore_edition(edition_data): def get_description(description_blob): - """ descriptions can be a string or a dict """ + """descriptions can be a string or a dict""" if isinstance(description_blob, dict): return description_blob.get("value") return description_blob def get_openlibrary_key(key): - """ convert /books/OL27320736M into OL27320736M """ + """convert /books/OL27320736M into OL27320736M""" return key.split("/")[-1] def get_languages(language_blob): - """ /language/eng -> English """ + """/language/eng -> English""" langs = [] for lang in language_blob: langs.append(languages.get(lang.get("key", ""), None)) @@ -205,7 +205,7 @@ def get_languages(language_blob): def pick_default_edition(options): - """ favor physical copies with covers in english """ + """favor physical copies with covers in english""" if not options: return None if len(options) == 1: diff --git a/bookwyrm/connectors/self_connector.py b/bookwyrm/connectors/self_connector.py index 500ffd74f..22835941f 100644 --- a/bookwyrm/connectors/self_connector.py +++ b/bookwyrm/connectors/self_connector.py @@ -10,11 +10,11 @@ from .abstract_connector import AbstractConnector, SearchResult class Connector(AbstractConnector): - """ instantiate a connector """ + """instantiate a connector""" # pylint: disable=arguments-differ def search(self, query, min_confidence=0.1, raw=False): - """ search your local database """ + """search your local database""" if not query: return [] # first, try searching unqiue identifiers @@ -35,7 +35,7 @@ class Connector(AbstractConnector): return search_results def isbn_search(self, query, raw=False): - """ search your local database """ + """search your local database""" if not query: return [] @@ -87,11 +87,11 @@ class Connector(AbstractConnector): return None def parse_isbn_search_data(self, data): - """ it's already in the right format, don't even worry about it """ + """it's already in the right format, don't even worry about it""" return data def parse_search_data(self, data): - """ it's already in the right format, don't even worry about it """ + """it's already in the right format, don't even worry about it""" return data def expand_book_data(self, book): @@ -99,7 +99,7 @@ class Connector(AbstractConnector): def search_identifiers(query): - """ tries remote_id, isbn; defined as dedupe fields on the model """ + """tries remote_id, isbn; defined as dedupe fields on the model""" filters = [ {f.name: query} for f in models.Edition._meta.get_fields() @@ -115,7 +115,7 @@ def search_identifiers(query): def search_title_author(query, min_confidence): - """ searches for title and author """ + """searches for title and author""" vector = ( SearchVector("title", weight="A") + SearchVector("subtitle", weight="B") diff --git a/bookwyrm/context_processors.py b/bookwyrm/context_processors.py index 8f79a6529..f5f251866 100644 --- a/bookwyrm/context_processors.py +++ b/bookwyrm/context_processors.py @@ -3,5 +3,5 @@ from bookwyrm import models def site_settings(request): # pylint: disable=unused-argument - """ include the custom info about the site """ + """include the custom info about the site""" return {"site": models.SiteSettings.objects.get()} diff --git a/bookwyrm/emailing.py b/bookwyrm/emailing.py index 1804254b0..657310b05 100644 --- a/bookwyrm/emailing.py +++ b/bookwyrm/emailing.py @@ -8,7 +8,7 @@ from bookwyrm.settings import DOMAIN def email_data(): - """ fields every email needs """ + """fields every email needs""" site = models.SiteSettings.objects.get() if site.logo_small: logo_path = "/images/{}".format(site.logo_small.url) @@ -24,14 +24,14 @@ def email_data(): def invite_email(invite_request): - """ send out an invite code """ + """send out an invite code""" data = email_data() data["invite_link"] = invite_request.invite.link send_email.delay(invite_request.email, *format_email("invite", data)) def password_reset_email(reset_code): - """ generate a password reset email """ + """generate a password reset email""" data = email_data() data["reset_link"] = reset_code.link data["user"] = reset_code.user.display_name @@ -39,7 +39,7 @@ def password_reset_email(reset_code): def format_email(email_name, data): - """ render the email templates """ + """render the email templates""" subject = ( get_template("email/{}/subject.html".format(email_name)).render(data).strip() ) @@ -58,7 +58,7 @@ def format_email(email_name, data): @app.task def send_email(recipient, subject, html_content, text_content): - """ use a task to send the email """ + """use a task to send the email""" email = EmailMultiAlternatives( subject, text_content, settings.DEFAULT_FROM_EMAIL, [recipient] ) diff --git a/bookwyrm/forms.py b/bookwyrm/forms.py index 7c41323c0..b6197f33a 100644 --- a/bookwyrm/forms.py +++ b/bookwyrm/forms.py @@ -3,7 +3,7 @@ import datetime from collections import defaultdict from django import forms -from django.forms import ModelForm, PasswordInput, widgets +from django.forms import ModelForm, PasswordInput, widgets, ChoiceField from django.forms.widgets import Textarea from django.utils import timezone from django.utils.translation import gettext_lazy as _ @@ -12,7 +12,7 @@ from bookwyrm import models class CustomForm(ModelForm): - """ add css classes to the forms """ + """add css classes to the forms""" def __init__(self, *args, **kwargs): css_classes = defaultdict(lambda: "") @@ -150,12 +150,10 @@ class LimitedEditUserForm(CustomForm): help_texts = {f: None for f in fields} -class TagForm(CustomForm): +class UserGroupForm(CustomForm): class Meta: - model = models.Tag - fields = ["name"] - help_texts = {f: None for f in fields} - labels = {"name": "Add a tag"} + model = models.User + fields = ["groups"] class CoverForm(CustomForm): @@ -200,7 +198,7 @@ class ImportForm(forms.Form): class ExpiryWidget(widgets.Select): def value_from_datadict(self, data, files, name): - """ human-readable exiration time buckets """ + """human-readable exiration time buckets""" selected_string = super().value_from_datadict(data, files, name) if selected_string == "day": @@ -219,7 +217,7 @@ class ExpiryWidget(widgets.Select): class InviteRequestForm(CustomForm): def clean(self): - """ make sure the email isn't in use by a registered user """ + """make sure the email isn't in use by a registered user""" cleaned_data = super().clean() email = cleaned_data.get("email") if email and models.User.objects.filter(email=email).exists(): @@ -287,3 +285,20 @@ class ServerForm(CustomForm): class Meta: model = models.FederatedServer exclude = ["remote_id"] + + +class SortListForm(forms.Form): + sort_by = ChoiceField( + choices=( + ("order", _("List Order")), + ("title", _("Book Title")), + ("rating", _("Rating")), + ), + label=_("Sort By"), + ) + direction = ChoiceField( + choices=( + ("ascending", _("Ascending")), + ("descending", _("Descending")), + ), + ) diff --git a/bookwyrm/importers/goodreads_import.py b/bookwyrm/importers/goodreads_import.py index 0b126c14c..7b577ea85 100644 --- a/bookwyrm/importers/goodreads_import.py +++ b/bookwyrm/importers/goodreads_import.py @@ -9,7 +9,7 @@ class GoodreadsImporter(Importer): service = "GoodReads" def parse_fields(self, entry): - """ handle the specific fields in goodreads csvs """ + """handle the specific fields in goodreads csvs""" entry.update({"import_source": self.service}) # add missing 'Date Started' field entry.update({"Date Started": None}) diff --git a/bookwyrm/importers/importer.py b/bookwyrm/importers/importer.py index ddbfa3048..c1e418979 100644 --- a/bookwyrm/importers/importer.py +++ b/bookwyrm/importers/importer.py @@ -10,7 +10,7 @@ logger = logging.getLogger(__name__) class Importer: - """ Generic class for csv data import from an outside service """ + """Generic class for csv data import from an outside service""" service = "Unknown" delimiter = "," @@ -18,7 +18,7 @@ class Importer: mandatory_fields = ["Title", "Author"] def create_job(self, user, csv_file, include_reviews, privacy): - """ check over a csv and creates a database entry for the job""" + """check over a csv and creates a database entry for the job""" job = ImportJob.objects.create( user=user, include_reviews=include_reviews, privacy=privacy ) @@ -32,16 +32,16 @@ class Importer: return job def save_item(self, job, index, data): # pylint: disable=no-self-use - """ creates and saves an import item """ + """creates and saves an import item""" ImportItem(job=job, index=index, data=data).save() def parse_fields(self, entry): - """ updates csv data with additional info """ + """updates csv data with additional info""" entry.update({"import_source": self.service}) return entry def create_retry_job(self, user, original_job, items): - """ retry items that didn't import """ + """retry items that didn't import""" job = ImportJob.objects.create( user=user, include_reviews=original_job.include_reviews, @@ -53,7 +53,7 @@ class Importer: return job def start_import(self, job): - """ initalizes a csv import job """ + """initalizes a csv import job""" result = import_data.delay(self.service, job.id) job.task_id = result.id job.save() @@ -61,7 +61,7 @@ class Importer: @app.task def import_data(source, job_id): - """ does the actual lookup work in a celery task """ + """does the actual lookup work in a celery task""" job = ImportJob.objects.get(id=job_id) try: for item in job.items.all(): @@ -89,7 +89,7 @@ def import_data(source, job_id): def handle_imported_book(source, user, item, include_reviews, privacy): - """ process a csv and then post about it """ + """process a csv and then post about it""" if isinstance(item.book, models.Work): item.book = item.book.default_edition if not item.book: diff --git a/bookwyrm/importers/librarything_import.py b/bookwyrm/importers/librarything_import.py index 3755cb1ad..b3175a82d 100644 --- a/bookwyrm/importers/librarything_import.py +++ b/bookwyrm/importers/librarything_import.py @@ -6,7 +6,7 @@ from . import Importer class LibrarythingImporter(Importer): - """ csv downloads from librarything """ + """csv downloads from librarything""" service = "LibraryThing" delimiter = "\t" @@ -15,7 +15,7 @@ class LibrarythingImporter(Importer): mandatory_fields = ["Title", "Primary Author"] def parse_fields(self, entry): - """ custom parsing for librarything """ + """custom parsing for librarything""" data = {} data["import_source"] = self.service data["Book Id"] = entry["Book Id"] diff --git a/bookwyrm/management/commands/deduplicate_book_data.py b/bookwyrm/management/commands/deduplicate_book_data.py index edd91a717..ed01a7843 100644 --- a/bookwyrm/management/commands/deduplicate_book_data.py +++ b/bookwyrm/management/commands/deduplicate_book_data.py @@ -6,7 +6,7 @@ from bookwyrm import models def update_related(canonical, obj): - """ update all the models with fk to the object being removed """ + """update all the models with fk to the object being removed""" # move related models to canonical related_models = [ (r.remote_field.name, r.related_model) for r in canonical._meta.related_objects @@ -24,7 +24,7 @@ def update_related(canonical, obj): def copy_data(canonical, obj): - """ try to get the most data possible """ + """try to get the most data possible""" for data_field in obj._meta.get_fields(): if not hasattr(data_field, "activitypub_field"): continue @@ -38,7 +38,7 @@ def copy_data(canonical, obj): def dedupe_model(model): - """ combine duplicate editions and update related models """ + """combine duplicate editions and update related models""" fields = model._meta.get_fields() dedupe_fields = [ f for f in fields if hasattr(f, "deduplication_field") and f.deduplication_field @@ -68,12 +68,12 @@ def dedupe_model(model): class Command(BaseCommand): - """ dedplucate allllll the book data models """ + """dedplucate allllll the book data models""" help = "merges duplicate book data" # pylint: disable=no-self-use,unused-argument def handle(self, *args, **options): - """ run deudplications """ + """run deudplications""" dedupe_model(models.Edition) dedupe_model(models.Work) dedupe_model(models.Author) diff --git a/bookwyrm/management/commands/erase_streams.py b/bookwyrm/management/commands/erase_streams.py index 042e857fc..1d34b1bb6 100644 --- a/bookwyrm/management/commands/erase_streams.py +++ b/bookwyrm/management/commands/erase_streams.py @@ -10,15 +10,15 @@ r = redis.Redis( def erase_streams(): - """ throw the whole redis away """ + """throw the whole redis away""" r.flushall() class Command(BaseCommand): - """ delete activity streams for all users """ + """delete activity streams for all users""" help = "Delete all the user streams" # pylint: disable=no-self-use,unused-argument def handle(self, *args, **options): - """ flush all, baby """ + """flush all, baby""" erase_streams() diff --git a/bookwyrm/management/commands/initdb.py b/bookwyrm/management/commands/initdb.py index a86a1652e..0c0cc61ff 100644 --- a/bookwyrm/management/commands/initdb.py +++ b/bookwyrm/management/commands/initdb.py @@ -108,7 +108,7 @@ def init_connectors(): def init_federated_servers(): - """ big no to nazis """ + """big no to nazis""" built_in_blocks = ["gab.ai", "gab.com"] for server in built_in_blocks: FederatedServer.objects.create( diff --git a/bookwyrm/management/commands/populate_streams.py b/bookwyrm/management/commands/populate_streams.py index 4cd2036a0..04f6bf6e2 100644 --- a/bookwyrm/management/commands/populate_streams.py +++ b/bookwyrm/management/commands/populate_streams.py @@ -10,7 +10,7 @@ r = redis.Redis( def populate_streams(): - """ build all the streams for all the users """ + """build all the streams for all the users""" users = models.User.objects.filter( local=True, is_active=True, @@ -21,10 +21,10 @@ def populate_streams(): class Command(BaseCommand): - """ start all over with user streams """ + """start all over with user streams""" help = "Populate streams for all users" # pylint: disable=no-self-use,unused-argument def handle(self, *args, **options): - """ run feed builder """ + """run feed builder""" populate_streams() diff --git a/bookwyrm/management/commands/remove_editions.py b/bookwyrm/management/commands/remove_editions.py index 6829c6d10..9eb9b7da8 100644 --- a/bookwyrm/management/commands/remove_editions.py +++ b/bookwyrm/management/commands/remove_editions.py @@ -5,7 +5,7 @@ from bookwyrm import models def remove_editions(): - """ combine duplicate editions and update related models """ + """combine duplicate editions and update related models""" # not in use filters = { "%s__isnull" % r.name: True for r in models.Edition._meta.related_objects @@ -33,10 +33,10 @@ def remove_editions(): class Command(BaseCommand): - """ dedplucate allllll the book data models """ + """dedplucate allllll the book data models""" help = "merges duplicate book data" # pylint: disable=no-self-use,unused-argument def handle(self, *args, **options): - """ run deudplications """ + """run deudplications""" remove_editions() diff --git a/bookwyrm/migrations/0046_reviewrating.py b/bookwyrm/migrations/0046_reviewrating.py index 8d1490042..26f6f36a6 100644 --- a/bookwyrm/migrations/0046_reviewrating.py +++ b/bookwyrm/migrations/0046_reviewrating.py @@ -8,7 +8,7 @@ from psycopg2.extras import execute_values def convert_review_rating(app_registry, schema_editor): - """ take rating type Reviews and convert them to ReviewRatings """ + """take rating type Reviews and convert them to ReviewRatings""" db_alias = schema_editor.connection.alias reviews = ( @@ -29,7 +29,7 @@ VALUES %s""", def unconvert_review_rating(app_registry, schema_editor): - """ undo the conversion from ratings back to reviews""" + """undo the conversion from ratings back to reviews""" # All we need to do to revert this is drop the table, which Django will do # on its own, as long as we have a valid reverse function. So, this is a # no-op function so Django will do its thing diff --git a/bookwyrm/migrations/0067_denullify_list_item_order.py b/bookwyrm/migrations/0067_denullify_list_item_order.py new file mode 100644 index 000000000..51e28371b --- /dev/null +++ b/bookwyrm/migrations/0067_denullify_list_item_order.py @@ -0,0 +1,30 @@ +from django.db import migrations + + +def forwards_func(apps, schema_editor): + # Set all values for ListItem.order + BookList = apps.get_model("bookwyrm", "List") + db_alias = schema_editor.connection.alias + for book_list in BookList.objects.using(db_alias).all(): + for i, item in enumerate(book_list.listitem_set.order_by("id"), 1): + item.order = i + item.save() + + +def reverse_func(apps, schema_editor): + # null all values for ListItem.order + BookList = apps.get_model("bookwyrm", "List") + db_alias = schema_editor.connection.alias + for book_list in BookList.objects.using(db_alias).all(): + for item in book_list.listitem_set.order_by("id"): + item.order = None + item.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0066_user_deactivation_reason"), + ] + + operations = [migrations.RunPython(forwards_func, reverse_func)] diff --git a/bookwyrm/migrations/0068_ordering_for_list_items.py b/bookwyrm/migrations/0068_ordering_for_list_items.py new file mode 100644 index 000000000..fa64f13c0 --- /dev/null +++ b/bookwyrm/migrations/0068_ordering_for_list_items.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.6 on 2021-04-08 16:15 + +import bookwyrm.models.fields +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0067_denullify_list_item_order"), + ] + + operations = [ + migrations.AlterField( + model_name="listitem", + name="order", + field=bookwyrm.models.fields.IntegerField(), + ), + migrations.AlterUniqueTogether( + name="listitem", + unique_together={("order", "book_list"), ("book", "book_list")}, + ), + ] diff --git a/bookwyrm/migrations/0069_auto_20210422_1604.py b/bookwyrm/migrations/0069_auto_20210422_1604.py new file mode 100644 index 000000000..6591e7b92 --- /dev/null +++ b/bookwyrm/migrations/0069_auto_20210422_1604.py @@ -0,0 +1,34 @@ +# Generated by Django 3.1.8 on 2021-04-22 16:04 + +import bookwyrm.models.fields +from django.conf import settings +from django.db import migrations +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0068_ordering_for_list_items"), + ] + + operations = [ + migrations.AlterField( + model_name="author", + name="last_edited_by", + field=bookwyrm.models.fields.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AlterField( + model_name="book", + name="last_edited_by", + field=bookwyrm.models.fields.ForeignKey( + null=True, + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/bookwyrm/migrations/0070_auto_20210423_0121.py b/bookwyrm/migrations/0070_auto_20210423_0121.py new file mode 100644 index 000000000..0b04c3ca2 --- /dev/null +++ b/bookwyrm/migrations/0070_auto_20210423_0121.py @@ -0,0 +1,35 @@ +# Generated by Django 3.1.8 on 2021-04-23 01:21 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0069_auto_20210422_1604"), + ] + + operations = [ + migrations.AlterUniqueTogether( + name="usertag", + unique_together=None, + ), + migrations.RemoveField( + model_name="usertag", + name="book", + ), + migrations.RemoveField( + model_name="usertag", + name="tag", + ), + migrations.RemoveField( + model_name="usertag", + name="user", + ), + migrations.DeleteModel( + name="Tag", + ), + migrations.DeleteModel( + name="UserTag", + ), + ] diff --git a/bookwyrm/models/__init__.py b/bookwyrm/models/__init__.py index 35e32c2cf..2a25a5251 100644 --- a/bookwyrm/models/__init__.py +++ b/bookwyrm/models/__init__.py @@ -17,8 +17,6 @@ from .favorite import Favorite from .notification import Notification from .readthrough import ReadThrough, ProgressUpdate, ProgressMode -from .tag import Tag, UserTag - from .user import User, KeyPair, AnnualGoal from .relationship import UserFollows, UserFollowRequest, UserBlocks from .report import Report, ReportComment diff --git a/bookwyrm/models/activitypub_mixin.py b/bookwyrm/models/activitypub_mixin.py index f687e96cb..83b4c0abe 100644 --- a/bookwyrm/models/activitypub_mixin.py +++ b/bookwyrm/models/activitypub_mixin.py @@ -31,18 +31,18 @@ PropertyField = namedtuple("PropertyField", ("set_activity_from_field")) def set_activity_from_property_field(activity, obj, field): - """ assign a model property value to the activity json """ + """assign a model property value to the activity json""" activity[field[1]] = getattr(obj, field[0]) class ActivitypubMixin: - """ add this mixin for models that are AP serializable """ + """add this mixin for models that are AP serializable""" activity_serializer = lambda: {} reverse_unfurl = False def __init__(self, *args, **kwargs): - """ collect some info on model fields """ + """collect some info on model fields""" self.image_fields = [] self.many_to_many_fields = [] self.simple_fields = [] # "simple" @@ -85,7 +85,7 @@ class ActivitypubMixin: @classmethod def find_existing_by_remote_id(cls, remote_id): - """ look up a remote id in the db """ + """look up a remote id in the db""" return cls.find_existing({"id": remote_id}) @classmethod @@ -126,7 +126,7 @@ class ActivitypubMixin: return match.first() def broadcast(self, activity, sender, software=None): - """ send out an activity """ + """send out an activity""" broadcast_task.delay( sender.id, json.dumps(activity, cls=activitypub.ActivityEncoder), @@ -134,7 +134,7 @@ class ActivitypubMixin: ) def get_recipients(self, software=None): - """ figure out which inbox urls to post to """ + """figure out which inbox urls to post to""" # first we have to figure out who should receive this activity privacy = self.privacy if hasattr(self, "privacy") else "public" # is this activity owned by a user (statuses, lists, shelves), or is it @@ -148,13 +148,17 @@ class ActivitypubMixin: mentions = self.recipients if hasattr(self, "recipients") else [] # we always send activities to explicitly mentioned users' inboxes - recipients = [u.inbox for u in mentions or []] + recipients = [u.inbox for u in mentions or [] if not u.local] # unless it's a dm, all the followers should receive the activity if privacy != "direct": # we will send this out to a subset of all remote users - queryset = user_model.viewer_aware_objects(user).filter( - local=False, + queryset = ( + user_model.viewer_aware_objects(user) + .filter( + local=False, + ) + .distinct() ) # filter users first by whether they're using the desired software # this lets us send book updates only to other bw servers @@ -175,23 +179,23 @@ class ActivitypubMixin: "inbox", flat=True ) recipients += list(shared_inboxes) + list(inboxes) - return recipients + return list(set(recipients)) def to_activity_dataclass(self): - """ convert from a model to an activity """ + """convert from a model to an activity""" activity = generate_activity(self) return self.activity_serializer(**activity) def to_activity(self, **kwargs): # pylint: disable=unused-argument - """ convert from a model to a json activity """ + """convert from a model to a json activity""" return self.to_activity_dataclass().serialize() class ObjectMixin(ActivitypubMixin): - """ add this mixin for object models that are AP serializable """ + """add this mixin for object models that are AP serializable""" def save(self, *args, created=None, **kwargs): - """ broadcast created/updated/deleted objects as appropriate """ + """broadcast created/updated/deleted objects as appropriate""" broadcast = kwargs.get("broadcast", True) # this bonus kwarg would cause an error in the base save method if "broadcast" in kwargs: @@ -200,7 +204,9 @@ class ObjectMixin(ActivitypubMixin): created = created or not bool(self.id) # first off, we want to save normally no matter what super().save(*args, **kwargs) - if not broadcast: + if not broadcast or ( + hasattr(self, "status_type") and self.status_type == "Announce" + ): return # this will work for objects owned by a user (lists, shelves) @@ -248,7 +254,7 @@ class ObjectMixin(ActivitypubMixin): self.broadcast(activity, user) def to_create_activity(self, user, **kwargs): - """ returns the object wrapped in a Create activity """ + """returns the object wrapped in a Create activity""" activity_object = self.to_activity_dataclass(**kwargs) signature = None @@ -274,7 +280,7 @@ class ObjectMixin(ActivitypubMixin): ).serialize() def to_delete_activity(self, user): - """ notice of deletion """ + """notice of deletion""" return activitypub.Delete( id=self.remote_id + "/activity", actor=user.remote_id, @@ -284,7 +290,7 @@ class ObjectMixin(ActivitypubMixin): ).serialize() def to_update_activity(self, user): - """ wrapper for Updates to an activity """ + """wrapper for Updates to an activity""" activity_id = "%s#update/%s" % (self.remote_id, uuid4()) return activitypub.Update( id=activity_id, @@ -300,13 +306,13 @@ class OrderedCollectionPageMixin(ObjectMixin): @property def collection_remote_id(self): - """ this can be overriden if there's a special remote id, ie outbox """ + """this can be overriden if there's a special remote id, ie outbox""" return self.remote_id def to_ordered_collection( self, queryset, remote_id=None, page=False, collection_only=False, **kwargs ): - """ an ordered collection of whatevers """ + """an ordered collection of whatevers""" if not queryset.ordered: raise RuntimeError("queryset must be ordered") @@ -335,11 +341,11 @@ class OrderedCollectionPageMixin(ObjectMixin): class OrderedCollectionMixin(OrderedCollectionPageMixin): - """ extends activitypub models to work as ordered collections """ + """extends activitypub models to work as ordered collections""" @property def collection_queryset(self): - """ usually an ordered collection model aggregates a different model """ + """usually an ordered collection model aggregates a different model""" raise NotImplementedError("Model must define collection_queryset") activity_serializer = activitypub.OrderedCollection @@ -348,24 +354,24 @@ class OrderedCollectionMixin(OrderedCollectionPageMixin): return self.to_ordered_collection(self.collection_queryset, **kwargs) def to_activity(self, **kwargs): - """ an ordered collection of the specified model queryset """ + """an ordered collection of the specified model queryset""" return self.to_ordered_collection( self.collection_queryset, **kwargs ).serialize() class CollectionItemMixin(ActivitypubMixin): - """ for items that are part of an (Ordered)Collection """ + """for items that are part of an (Ordered)Collection""" activity_serializer = activitypub.CollectionItem def broadcast(self, activity, sender, software="bookwyrm"): - """ only send book collection updates to other bookwyrm instances """ + """only send book collection updates to other bookwyrm instances""" super().broadcast(activity, sender, software=software) @property def privacy(self): - """ inherit the privacy of the list, or direct if pending """ + """inherit the privacy of the list, or direct if pending""" collection_field = getattr(self, self.collection_field) if self.approved: return collection_field.privacy @@ -373,7 +379,7 @@ class CollectionItemMixin(ActivitypubMixin): @property def recipients(self): - """ the owner of the list is a direct recipient """ + """the owner of the list is a direct recipient""" collection_field = getattr(self, self.collection_field) if collection_field.user.local: # don't broadcast to yourself @@ -381,7 +387,7 @@ class CollectionItemMixin(ActivitypubMixin): return [collection_field.user] def save(self, *args, broadcast=True, **kwargs): - """ broadcast updated """ + """broadcast updated""" # first off, we want to save normally no matter what super().save(*args, **kwargs) @@ -394,14 +400,14 @@ class CollectionItemMixin(ActivitypubMixin): self.broadcast(activity, self.user) def delete(self, *args, broadcast=True, **kwargs): - """ broadcast a remove activity """ + """broadcast a remove activity""" activity = self.to_remove_activity(self.user) super().delete(*args, **kwargs) if self.user.local and broadcast: self.broadcast(activity, self.user) def to_add_activity(self, user): - """ AP for shelving a book""" + """AP for shelving a book""" collection_field = getattr(self, self.collection_field) return activitypub.Add( id="{:s}#add".format(collection_field.remote_id), @@ -411,7 +417,7 @@ class CollectionItemMixin(ActivitypubMixin): ).serialize() def to_remove_activity(self, user): - """ AP for un-shelving a book""" + """AP for un-shelving a book""" collection_field = getattr(self, self.collection_field) return activitypub.Remove( id="{:s}#remove".format(collection_field.remote_id), @@ -422,24 +428,24 @@ class CollectionItemMixin(ActivitypubMixin): class ActivityMixin(ActivitypubMixin): - """ add this mixin for models that are AP serializable """ + """add this mixin for models that are AP serializable""" def save(self, *args, broadcast=True, **kwargs): - """ broadcast activity """ + """broadcast activity""" super().save(*args, **kwargs) user = self.user if hasattr(self, "user") else self.user_subject if broadcast and user.local: self.broadcast(self.to_activity(), user) def delete(self, *args, broadcast=True, **kwargs): - """ nevermind, undo that activity """ + """nevermind, undo that activity""" user = self.user if hasattr(self, "user") else self.user_subject if broadcast and user.local: self.broadcast(self.to_undo_activity(), user) super().delete(*args, **kwargs) def to_undo_activity(self): - """ undo an action """ + """undo an action""" user = self.user if hasattr(self, "user") else self.user_subject return activitypub.Undo( id="%s#undo" % self.remote_id, @@ -449,7 +455,7 @@ class ActivityMixin(ActivitypubMixin): def generate_activity(obj): - """ go through the fields on an object """ + """go through the fields on an object""" activity = {} for field in obj.activity_fields: field.set_activity_from_field(activity, obj) @@ -472,7 +478,7 @@ def generate_activity(obj): def unfurl_related_field(related_field, sort_field=None): - """ load reverse lookups (like public key owner or Status attachment """ + """load reverse lookups (like public key owner or Status attachment""" if sort_field and hasattr(related_field, "all"): return [ unfurl_related_field(i) for i in related_field.order_by(sort_field).all() @@ -488,7 +494,7 @@ def unfurl_related_field(related_field, sort_field=None): @app.task def broadcast_task(sender_id, activity, recipients): - """ the celery task for broadcast """ + """the celery task for broadcast""" user_model = apps.get_model("bookwyrm.User", require_ready=True) sender = user_model.objects.get(id=sender_id) for recipient in recipients: @@ -499,7 +505,7 @@ def broadcast_task(sender_id, activity, recipients): def sign_and_send(sender, data, destination): - """ crpyto whatever and http junk """ + """crpyto whatever and http junk""" now = http_date() if not sender.key_pair.private_key: @@ -528,7 +534,7 @@ def sign_and_send(sender, data, destination): def to_ordered_collection_page( queryset, remote_id, id_only=False, page=1, pure=False, **kwargs ): - """ serialize and pagiante a queryset """ + """serialize and pagiante a queryset""" paginated = Paginator(queryset, PAGE_LENGTH) activity_page = paginated.get_page(page) diff --git a/bookwyrm/models/attachment.py b/bookwyrm/models/attachment.py index eaeca11e2..c8b2e51c2 100644 --- a/bookwyrm/models/attachment.py +++ b/bookwyrm/models/attachment.py @@ -8,7 +8,7 @@ from . import fields class Attachment(ActivitypubMixin, BookWyrmModel): - """ an image (or, in the future, video etc) associated with a status """ + """an image (or, in the future, video etc) associated with a status""" status = models.ForeignKey( "Status", on_delete=models.CASCADE, related_name="attachments", null=True @@ -16,13 +16,13 @@ class Attachment(ActivitypubMixin, BookWyrmModel): reverse_unfurl = True class Meta: - """ one day we'll have other types of attachments besides images """ + """one day we'll have other types of attachments besides images""" abstract = True class Image(Attachment): - """ an image attachment """ + """an image attachment""" image = fields.ImageField( upload_to="status/", diff --git a/bookwyrm/models/author.py b/bookwyrm/models/author.py index 4c5fe6c8f..b9a4b146b 100644 --- a/bookwyrm/models/author.py +++ b/bookwyrm/models/author.py @@ -9,7 +9,7 @@ from . import fields class Author(BookDataModel): - """ basic biographic info """ + """basic biographic info""" wikipedia_link = fields.CharField( max_length=255, blank=True, null=True, deduplication_field=True @@ -24,7 +24,7 @@ class Author(BookDataModel): bio = fields.HtmlField(null=True, blank=True) def get_remote_id(self): - """ editions and works both use "book" instead of model_name """ + """editions and works both use "book" instead of model_name""" return "https://%s/author/%s" % (DOMAIN, self.id) activity_serializer = activitypub.Author diff --git a/bookwyrm/models/base_model.py b/bookwyrm/models/base_model.py index 261c96868..e85ff7338 100644 --- a/bookwyrm/models/base_model.py +++ b/bookwyrm/models/base_model.py @@ -7,14 +7,14 @@ from .fields import RemoteIdField class BookWyrmModel(models.Model): - """ shared fields """ + """shared fields""" created_date = models.DateTimeField(auto_now_add=True) updated_date = models.DateTimeField(auto_now=True) remote_id = RemoteIdField(null=True, activitypub_field="id") def get_remote_id(self): - """ generate a url that resolves to the local object """ + """generate a url that resolves to the local object""" base_path = "https://%s" % DOMAIN if hasattr(self, "user"): base_path = "%s%s" % (base_path, self.user.local_path) @@ -22,17 +22,17 @@ class BookWyrmModel(models.Model): return "%s/%s/%d" % (base_path, model_name, self.id) class Meta: - """ this is just here to provide default fields for other models """ + """this is just here to provide default fields for other models""" abstract = True @property def local_path(self): - """ how to link to this object in the local app """ + """how to link to this object in the local app""" return self.get_remote_id().replace("https://%s" % DOMAIN, "") def visible_to_user(self, viewer): - """ is a user authorized to view an object? """ + """is a user authorized to view an object?""" # make sure this is an object with privacy owned by a user if not hasattr(self, "user") or not hasattr(self, "privacy"): return None @@ -65,7 +65,7 @@ class BookWyrmModel(models.Model): @receiver(models.signals.post_save) # pylint: disable=unused-argument def set_remote_id(sender, instance, created, *args, **kwargs): - """ set the remote_id after save (when the id is available) """ + """set the remote_id after save (when the id is available)""" if not created or not hasattr(instance, "get_remote_id"): return if not instance.remote_id: diff --git a/bookwyrm/models/book.py b/bookwyrm/models/book.py index a6824c0ad..dd098e560 100644 --- a/bookwyrm/models/book.py +++ b/bookwyrm/models/book.py @@ -13,7 +13,7 @@ from . import fields class BookDataModel(ObjectMixin, BookWyrmModel): - """ fields shared between editable book data (books, works, authors) """ + """fields shared between editable book data (books, works, authors)""" origin_id = models.CharField(max_length=255, null=True, blank=True) openlibrary_key = fields.CharField( @@ -26,15 +26,19 @@ class BookDataModel(ObjectMixin, BookWyrmModel): max_length=255, blank=True, null=True, deduplication_field=True ) - last_edited_by = models.ForeignKey("User", on_delete=models.PROTECT, null=True) + last_edited_by = fields.ForeignKey( + "User", + on_delete=models.PROTECT, + null=True, + ) class Meta: - """ can't initialize this model, that wouldn't make sense """ + """can't initialize this model, that wouldn't make sense""" abstract = True def save(self, *args, **kwargs): - """ ensure that the remote_id is within this instance """ + """ensure that the remote_id is within this instance""" if self.id: self.remote_id = self.get_remote_id() else: @@ -43,12 +47,12 @@ class BookDataModel(ObjectMixin, BookWyrmModel): return super().save(*args, **kwargs) def broadcast(self, activity, sender, software="bookwyrm"): - """ only send book data updates to other bookwyrm instances """ + """only send book data updates to other bookwyrm instances""" super().broadcast(activity, sender, software=software) class Book(BookDataModel): - """ a generic book, which can mean either an edition or a work """ + """a generic book, which can mean either an edition or a work""" connector = models.ForeignKey("Connector", on_delete=models.PROTECT, null=True) @@ -79,17 +83,17 @@ class Book(BookDataModel): @property def author_text(self): - """ format a list of authors """ + """format a list of authors""" return ", ".join(a.name for a in self.authors.all()) @property def latest_readthrough(self): - """ most recent readthrough activity """ + """most recent readthrough activity""" return self.readthrough_set.order_by("-updated_date").first() @property def edition_info(self): - """ properties of this edition, as a string """ + """properties of this edition, as a string""" items = [ self.physical_format if hasattr(self, "physical_format") else None, self.languages[0] + " language" @@ -102,20 +106,20 @@ class Book(BookDataModel): @property def alt_text(self): - """ image alt test """ + """image alt test""" text = "%s" % self.title if self.edition_info: text += " (%s)" % self.edition_info return text def save(self, *args, **kwargs): - """ can't be abstract for query reasons, but you shouldn't USE it """ + """can't be abstract for query reasons, but you shouldn't USE it""" if not isinstance(self, Edition) and not isinstance(self, Work): raise ValueError("Books should be added as Editions or Works") return super().save(*args, **kwargs) def get_remote_id(self): - """ editions and works both use "book" instead of model_name """ + """editions and works both use "book" instead of model_name""" return "https://%s/book/%d" % (DOMAIN, self.id) def __repr__(self): @@ -127,7 +131,7 @@ class Book(BookDataModel): class Work(OrderedCollectionPageMixin, Book): - """ a work (an abstract concept of a book that manifests in an edition) """ + """a work (an abstract concept of a book that manifests in an edition)""" # library of congress catalog control number lccn = fields.CharField( @@ -139,19 +143,19 @@ class Work(OrderedCollectionPageMixin, Book): ) def save(self, *args, **kwargs): - """ set some fields on the edition object """ + """set some fields on the edition object""" # set rank for edition in self.editions.all(): edition.save() return super().save(*args, **kwargs) def get_default_edition(self): - """ in case the default edition is not set """ + """in case the default edition is not set""" return self.default_edition or self.editions.order_by("-edition_rank").first() @transaction.atomic() def reset_default_edition(self): - """ sets a new default edition based on computed rank """ + """sets a new default edition based on computed rank""" self.default_edition = None # editions are re-ranked implicitly self.save() @@ -159,11 +163,11 @@ class Work(OrderedCollectionPageMixin, Book): self.save() def to_edition_list(self, **kwargs): - """ an ordered collection of editions """ + """an ordered collection of editions""" return self.to_ordered_collection( self.editions.order_by("-edition_rank").all(), remote_id="%s/editions" % self.remote_id, - **kwargs + **kwargs, ) activity_serializer = activitypub.Work @@ -172,7 +176,7 @@ class Work(OrderedCollectionPageMixin, Book): class Edition(Book): - """ an edition of a book """ + """an edition of a book""" # these identifiers only apply to editions, not works isbn_10 = fields.CharField( @@ -211,7 +215,7 @@ class Edition(Book): name_field = "title" def get_rank(self, ignore_default=False): - """ calculate how complete the data is on this edition """ + """calculate how complete the data is on this edition""" if ( not ignore_default and self.parent_work @@ -231,7 +235,7 @@ class Edition(Book): return rank def save(self, *args, **kwargs): - """ set some fields on the edition object """ + """set some fields on the edition object""" # calculate isbn 10/13 if self.isbn_13 and self.isbn_13[:3] == "978" and not self.isbn_10: self.isbn_10 = isbn_13_to_10(self.isbn_13) @@ -245,7 +249,7 @@ class Edition(Book): def isbn_10_to_13(isbn_10): - """ convert an isbn 10 into an isbn 13 """ + """convert an isbn 10 into an isbn 13""" isbn_10 = re.sub(r"[^0-9X]", "", isbn_10) # drop the last character of the isbn 10 number (the original checkdigit) converted = isbn_10[:9] @@ -267,7 +271,7 @@ def isbn_10_to_13(isbn_10): def isbn_13_to_10(isbn_13): - """ convert isbn 13 to 10, if possible """ + """convert isbn 13 to 10, if possible""" if isbn_13[:3] != "978": return None diff --git a/bookwyrm/models/connector.py b/bookwyrm/models/connector.py index 11bdbee20..6043fc026 100644 --- a/bookwyrm/models/connector.py +++ b/bookwyrm/models/connector.py @@ -9,7 +9,7 @@ ConnectorFiles = models.TextChoices("ConnectorFiles", CONNECTORS) class Connector(BookWyrmModel): - """ book data source connectors """ + """book data source connectors""" identifier = models.CharField(max_length=255, unique=True) priority = models.IntegerField(default=2) @@ -32,7 +32,7 @@ class Connector(BookWyrmModel): query_count_expiry = models.DateTimeField(auto_now_add=True, blank=True) class Meta: - """ check that there's code to actually use this connector """ + """check that there's code to actually use this connector""" constraints = [ models.CheckConstraint( diff --git a/bookwyrm/models/favorite.py b/bookwyrm/models/favorite.py index 7b72d175f..c45181196 100644 --- a/bookwyrm/models/favorite.py +++ b/bookwyrm/models/favorite.py @@ -11,7 +11,7 @@ from .status import Status class Favorite(ActivityMixin, BookWyrmModel): - """ fav'ing a post """ + """fav'ing a post""" user = fields.ForeignKey( "User", on_delete=models.PROTECT, activitypub_field="actor" @@ -24,11 +24,11 @@ class Favorite(ActivityMixin, BookWyrmModel): @classmethod def ignore_activity(cls, activity): - """ don't bother with incoming favs of unknown statuses """ + """don't bother with incoming favs of unknown statuses""" return not Status.objects.filter(remote_id=activity.object).exists() def save(self, *args, **kwargs): - """ update user active time """ + """update user active time""" self.user.last_active_date = timezone.now() self.user.save(broadcast=False) super().save(*args, **kwargs) @@ -45,7 +45,7 @@ class Favorite(ActivityMixin, BookWyrmModel): ) def delete(self, *args, **kwargs): - """ delete and delete notifications """ + """delete and delete notifications""" # check for notification if self.status.user.local: notification_model = apps.get_model( @@ -62,6 +62,6 @@ class Favorite(ActivityMixin, BookWyrmModel): super().delete(*args, **kwargs) class Meta: - """ can't fav things twice """ + """can't fav things twice""" unique_together = ("user", "status") diff --git a/bookwyrm/models/federated_server.py b/bookwyrm/models/federated_server.py index aa2b2f6af..7d446ca0d 100644 --- a/bookwyrm/models/federated_server.py +++ b/bookwyrm/models/federated_server.py @@ -13,7 +13,7 @@ FederationStatus = models.TextChoices( class FederatedServer(BookWyrmModel): - """ store which servers we federate with """ + """store which servers we federate with""" server_name = models.CharField(max_length=255, unique=True) status = models.CharField( @@ -25,7 +25,7 @@ class FederatedServer(BookWyrmModel): notes = models.TextField(null=True, blank=True) def block(self): - """ block a server """ + """block a server""" self.status = "blocked" self.save() @@ -35,7 +35,7 @@ class FederatedServer(BookWyrmModel): ) def unblock(self): - """ unblock a server """ + """unblock a server""" self.status = "federated" self.save() @@ -45,7 +45,7 @@ class FederatedServer(BookWyrmModel): @classmethod def is_blocked(cls, url): - """ look up if a domain is blocked """ + """look up if a domain is blocked""" url = urlparse(url) domain = url.netloc return cls.objects.filter(server_name=domain, status="blocked").exists() diff --git a/bookwyrm/models/fields.py b/bookwyrm/models/fields.py index 2aefae51f..123b3efa4 100644 --- a/bookwyrm/models/fields.py +++ b/bookwyrm/models/fields.py @@ -18,7 +18,7 @@ from bookwyrm.settings import DOMAIN def validate_remote_id(value): - """ make sure the remote_id looks like a url """ + """make sure the remote_id looks like a url""" if not value or not re.match(r"^http.?:\/\/[^\s]+$", value): raise ValidationError( _("%(value)s is not a valid remote_id"), @@ -27,7 +27,7 @@ def validate_remote_id(value): def validate_localname(value): - """ make sure localnames look okay """ + """make sure localnames look okay""" if not re.match(r"^[A-Za-z\-_\.0-9]+$", value): raise ValidationError( _("%(value)s is not a valid username"), @@ -36,7 +36,7 @@ def validate_localname(value): def validate_username(value): - """ make sure usernames look okay """ + """make sure usernames look okay""" if not re.match(r"^[A-Za-z\-_\.0-9]+@[A-Za-z\-_\.0-9]+\.[a-z]{2,}$", value): raise ValidationError( _("%(value)s is not a valid username"), @@ -45,7 +45,7 @@ def validate_username(value): class ActivitypubFieldMixin: - """ make a database field serializable """ + """make a database field serializable""" def __init__( self, @@ -64,7 +64,7 @@ class ActivitypubFieldMixin: super().__init__(*args, **kwargs) def set_field_from_activity(self, instance, data): - """ helper function for assinging a value to the field """ + """helper function for assinging a value to the field""" try: value = getattr(data, self.get_activitypub_field()) except AttributeError: @@ -78,7 +78,7 @@ class ActivitypubFieldMixin: setattr(instance, self.name, formatted) def set_activity_from_field(self, activity, instance): - """ update the json object """ + """update the json object""" value = getattr(instance, self.name) formatted = self.field_to_activity(value) if formatted is None: @@ -94,19 +94,19 @@ class ActivitypubFieldMixin: activity[key] = formatted def field_to_activity(self, value): - """ formatter to convert a model value into activitypub """ + """formatter to convert a model value into activitypub""" if hasattr(self, "activitypub_wrapper"): return {self.activitypub_wrapper: value} return value def field_from_activity(self, value): - """ formatter to convert activitypub into a model value """ + """formatter to convert activitypub into a model value""" if value and hasattr(self, "activitypub_wrapper"): value = value.get(self.activitypub_wrapper) return value def get_activitypub_field(self): - """ model_field_name to activitypubFieldName """ + """model_field_name to activitypubFieldName""" if self.activitypub_field: return self.activitypub_field name = self.name.split(".")[-1] @@ -115,7 +115,7 @@ class ActivitypubFieldMixin: class ActivitypubRelatedFieldMixin(ActivitypubFieldMixin): - """ default (de)serialization for foreign key and one to one """ + """default (de)serialization for foreign key and one to one""" def __init__(self, *args, load_remote=True, **kwargs): self.load_remote = load_remote @@ -146,7 +146,7 @@ class ActivitypubRelatedFieldMixin(ActivitypubFieldMixin): class RemoteIdField(ActivitypubFieldMixin, models.CharField): - """ a url that serves as a unique identifier """ + """a url that serves as a unique identifier""" def __init__(self, *args, max_length=255, validators=None, **kwargs): validators = validators or [validate_remote_id] @@ -156,7 +156,7 @@ class RemoteIdField(ActivitypubFieldMixin, models.CharField): class UsernameField(ActivitypubFieldMixin, models.CharField): - """ activitypub-aware username field """ + """activitypub-aware username field""" def __init__(self, activitypub_field="preferredUsername", **kwargs): self.activitypub_field = activitypub_field @@ -172,7 +172,7 @@ class UsernameField(ActivitypubFieldMixin, models.CharField): ) def deconstruct(self): - """ implementation of models.Field deconstruct """ + """implementation of models.Field deconstruct""" name, path, args, kwargs = super().deconstruct() del kwargs["verbose_name"] del kwargs["max_length"] @@ -191,7 +191,7 @@ PrivacyLevels = models.TextChoices( class PrivacyField(ActivitypubFieldMixin, models.CharField): - """ this maps to two differente activitypub fields """ + """this maps to two differente activitypub fields""" public = "https://www.w3.org/ns/activitystreams#Public" @@ -236,7 +236,7 @@ class PrivacyField(ActivitypubFieldMixin, models.CharField): class ForeignKey(ActivitypubRelatedFieldMixin, models.ForeignKey): - """ activitypub-aware foreign key field """ + """activitypub-aware foreign key field""" def field_to_activity(self, value): if not value: @@ -245,7 +245,7 @@ class ForeignKey(ActivitypubRelatedFieldMixin, models.ForeignKey): class OneToOneField(ActivitypubRelatedFieldMixin, models.OneToOneField): - """ activitypub-aware foreign key field """ + """activitypub-aware foreign key field""" def field_to_activity(self, value): if not value: @@ -254,14 +254,14 @@ class OneToOneField(ActivitypubRelatedFieldMixin, models.OneToOneField): class ManyToManyField(ActivitypubFieldMixin, models.ManyToManyField): - """ activitypub-aware many to many field """ + """activitypub-aware many to many field""" def __init__(self, *args, link_only=False, **kwargs): self.link_only = link_only super().__init__(*args, **kwargs) def set_field_from_activity(self, instance, data): - """ helper function for assinging a value to the field """ + """helper function for assinging a value to the field""" value = getattr(data, self.get_activitypub_field()) formatted = self.field_from_activity(value) if formatted is None or formatted is MISSING: @@ -275,9 +275,12 @@ class ManyToManyField(ActivitypubFieldMixin, models.ManyToManyField): return [i.remote_id for i in value.all()] def field_from_activity(self, value): - items = [] if value is None or value is MISSING: - return [] + return None + if not isinstance(value, list): + # If this is a link, we currently aren't doing anything with it + return None + items = [] for remote_id in value: try: validate_remote_id(remote_id) @@ -290,7 +293,7 @@ class ManyToManyField(ActivitypubFieldMixin, models.ManyToManyField): class TagField(ManyToManyField): - """ special case of many to many that uses Tags """ + """special case of many to many that uses Tags""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -330,7 +333,7 @@ class TagField(ManyToManyField): def image_serializer(value, alt): - """ helper for serializing images """ + """helper for serializing images""" if value and hasattr(value, "url"): url = value.url else: @@ -340,7 +343,7 @@ def image_serializer(value, alt): class ImageField(ActivitypubFieldMixin, models.ImageField): - """ activitypub-aware image field """ + """activitypub-aware image field""" def __init__(self, *args, alt_field=None, **kwargs): self.alt_field = alt_field @@ -348,7 +351,7 @@ class ImageField(ActivitypubFieldMixin, models.ImageField): # pylint: disable=arguments-differ def set_field_from_activity(self, instance, data, save=True): - """ helper function for assinging a value to the field """ + """helper function for assinging a value to the field""" value = getattr(data, self.get_activitypub_field()) formatted = self.field_from_activity(value) if formatted is None or formatted is MISSING: @@ -394,7 +397,7 @@ class ImageField(ActivitypubFieldMixin, models.ImageField): class DateTimeField(ActivitypubFieldMixin, models.DateTimeField): - """ activitypub-aware datetime field """ + """activitypub-aware datetime field""" def field_to_activity(self, value): if not value: @@ -413,7 +416,7 @@ class DateTimeField(ActivitypubFieldMixin, models.DateTimeField): class HtmlField(ActivitypubFieldMixin, models.TextField): - """ a text field for storing html """ + """a text field for storing html""" def field_from_activity(self, value): if not value or value == MISSING: @@ -424,30 +427,30 @@ class HtmlField(ActivitypubFieldMixin, models.TextField): class ArrayField(ActivitypubFieldMixin, DjangoArrayField): - """ activitypub-aware array field """ + """activitypub-aware array field""" def field_to_activity(self, value): return [str(i) for i in value] class CharField(ActivitypubFieldMixin, models.CharField): - """ activitypub-aware char field """ + """activitypub-aware char field""" class TextField(ActivitypubFieldMixin, models.TextField): - """ activitypub-aware text field """ + """activitypub-aware text field""" class BooleanField(ActivitypubFieldMixin, models.BooleanField): - """ activitypub-aware boolean field """ + """activitypub-aware boolean field""" class IntegerField(ActivitypubFieldMixin, models.IntegerField): - """ activitypub-aware boolean field """ + """activitypub-aware boolean field""" class DecimalField(ActivitypubFieldMixin, models.DecimalField): - """ activitypub-aware boolean field """ + """activitypub-aware boolean field""" def field_to_activity(self, value): if not value: diff --git a/bookwyrm/models/import_job.py b/bookwyrm/models/import_job.py index 026cf7cd5..1b1152abc 100644 --- a/bookwyrm/models/import_job.py +++ b/bookwyrm/models/import_job.py @@ -20,7 +20,7 @@ GOODREADS_SHELVES = { def unquote_string(text): - """ resolve csv quote weirdness """ + """resolve csv quote weirdness""" match = re.match(r'="([^"]*)"', text) if match: return match.group(1) @@ -28,7 +28,7 @@ def unquote_string(text): def construct_search_term(title, author): - """ formulate a query for the data connector """ + """formulate a query for the data connector""" # Strip brackets (usually series title from search term) title = re.sub(r"\s*\([^)]*\)\s*", "", title) # Open library doesn't like including author initials in search term. @@ -38,7 +38,7 @@ def construct_search_term(title, author): class ImportJob(models.Model): - """ entry for a specific request for book data import """ + """entry for a specific request for book data import""" user = models.ForeignKey(User, on_delete=models.CASCADE) created_date = models.DateTimeField(default=timezone.now) @@ -51,7 +51,7 @@ class ImportJob(models.Model): retry = models.BooleanField(default=False) def save(self, *args, **kwargs): - """ save and notify """ + """save and notify""" super().save(*args, **kwargs) if self.complete: notification_model = apps.get_model( @@ -65,7 +65,7 @@ class ImportJob(models.Model): class ImportItem(models.Model): - """ a single line of a csv being imported """ + """a single line of a csv being imported""" job = models.ForeignKey(ImportJob, on_delete=models.CASCADE, related_name="items") index = models.IntegerField() @@ -74,11 +74,11 @@ class ImportItem(models.Model): fail_reason = models.TextField(null=True) def resolve(self): - """ try various ways to lookup a book """ + """try various ways to lookup a book""" self.book = self.get_book_from_isbn() or self.get_book_from_title_author() def get_book_from_isbn(self): - """ search by isbn """ + """search by isbn""" search_result = connector_manager.first_search_result( self.isbn, min_confidence=0.999 ) @@ -88,7 +88,7 @@ class ImportItem(models.Model): return None def get_book_from_title_author(self): - """ search by title and author """ + """search by title and author""" search_term = construct_search_term(self.title, self.author) search_result = connector_manager.first_search_result( search_term, min_confidence=0.999 @@ -100,60 +100,60 @@ class ImportItem(models.Model): @property def title(self): - """ get the book title """ + """get the book title""" return self.data["Title"] @property def author(self): - """ get the book title """ + """get the book title""" return self.data["Author"] @property def isbn(self): - """ pulls out the isbn13 field from the csv line data """ + """pulls out the isbn13 field from the csv line data""" return unquote_string(self.data["ISBN13"]) @property def shelf(self): - """ the goodreads shelf field """ + """the goodreads shelf field""" if self.data["Exclusive Shelf"]: return GOODREADS_SHELVES.get(self.data["Exclusive Shelf"]) return None @property def review(self): - """ a user-written review, to be imported with the book data """ + """a user-written review, to be imported with the book data""" return self.data["My Review"] @property def rating(self): - """ x/5 star rating for a book """ + """x/5 star rating for a book""" return int(self.data["My Rating"]) @property def date_added(self): - """ when the book was added to this dataset """ + """when the book was added to this dataset""" if self.data["Date Added"]: return timezone.make_aware(dateutil.parser.parse(self.data["Date Added"])) return None @property def date_started(self): - """ when the book was started """ + """when the book was started""" if "Date Started" in self.data and self.data["Date Started"]: return timezone.make_aware(dateutil.parser.parse(self.data["Date Started"])) return None @property def date_read(self): - """ the date a book was completed """ + """the date a book was completed""" if self.data["Date Read"]: return timezone.make_aware(dateutil.parser.parse(self.data["Date Read"])) return None @property def reads(self): - """ formats a read through dataset for the book in this line """ + """formats a read through dataset for the book in this line""" start_date = self.date_started # Goodreads special case (no 'date started' field) diff --git a/bookwyrm/models/list.py b/bookwyrm/models/list.py index 4d6b53cde..2a5c3382a 100644 --- a/bookwyrm/models/list.py +++ b/bookwyrm/models/list.py @@ -21,7 +21,7 @@ CurationType = models.TextChoices( class List(OrderedCollectionMixin, BookWyrmModel): - """ a list of books """ + """a list of books""" name = fields.CharField(max_length=100) user = fields.ForeignKey( @@ -41,22 +41,22 @@ class List(OrderedCollectionMixin, BookWyrmModel): activity_serializer = activitypub.BookList def get_remote_id(self): - """ don't want the user to be in there in this case """ + """don't want the user to be in there in this case""" return "https://%s/list/%d" % (DOMAIN, self.id) @property def collection_queryset(self): - """ list of books for this shelf, overrides OrderedCollectionMixin """ - return self.books.filter(listitem__approved=True).all().order_by("listitem") + """list of books for this shelf, overrides OrderedCollectionMixin""" + return self.books.filter(listitem__approved=True).order_by("listitem") class Meta: - """ default sorting """ + """default sorting""" ordering = ("-updated_date",) class ListItem(CollectionItemMixin, BookWyrmModel): - """ ok """ + """ok""" book = fields.ForeignKey( "Edition", on_delete=models.PROTECT, activitypub_field="book" @@ -67,14 +67,14 @@ class ListItem(CollectionItemMixin, BookWyrmModel): ) notes = fields.TextField(blank=True, null=True) approved = models.BooleanField(default=True) - order = fields.IntegerField(blank=True, null=True) + order = fields.IntegerField() endorsement = models.ManyToManyField("User", related_name="endorsers") activity_serializer = activitypub.ListItem collection_field = "book_list" def save(self, *args, **kwargs): - """ create a notification too """ + """create a notification too""" created = not bool(self.id) super().save(*args, **kwargs) # tick the updated date on the parent list @@ -93,7 +93,7 @@ class ListItem(CollectionItemMixin, BookWyrmModel): ) class Meta: - """ an opinionated constraint! you can't put a book on a list twice """ - - unique_together = ("book", "book_list") + # A book may only be placed into a list once, and each order in the list may be used only + # once + unique_together = (("book", "book_list"), ("order", "book_list")) ordering = ("-created_date",) diff --git a/bookwyrm/models/notification.py b/bookwyrm/models/notification.py index 233d635b8..ff0b4e5a6 100644 --- a/bookwyrm/models/notification.py +++ b/bookwyrm/models/notification.py @@ -10,7 +10,7 @@ NotificationType = models.TextChoices( class Notification(BookWyrmModel): - """ you've been tagged, liked, followed, etc """ + """you've been tagged, liked, followed, etc""" user = models.ForeignKey("User", on_delete=models.CASCADE) related_book = models.ForeignKey("Edition", on_delete=models.CASCADE, null=True) @@ -29,7 +29,7 @@ class Notification(BookWyrmModel): ) def save(self, *args, **kwargs): - """ save, but don't make dupes """ + """save, but don't make dupes""" # there's probably a better way to do this if self.__class__.objects.filter( user=self.user, @@ -45,7 +45,7 @@ class Notification(BookWyrmModel): super().save(*args, **kwargs) class Meta: - """ checks if notifcation is in enum list for valid types """ + """checks if notifcation is in enum list for valid types""" constraints = [ models.CheckConstraint( diff --git a/bookwyrm/models/readthrough.py b/bookwyrm/models/readthrough.py index 1a5fcb0d5..664daa13d 100644 --- a/bookwyrm/models/readthrough.py +++ b/bookwyrm/models/readthrough.py @@ -7,14 +7,14 @@ from .base_model import BookWyrmModel class ProgressMode(models.TextChoices): - """ types of prgress available """ + """types of prgress available""" PAGE = "PG", "page" PERCENT = "PCT", "percent" class ReadThrough(BookWyrmModel): - """ Store a read through a book in the database. """ + """Store a read through a book in the database.""" user = models.ForeignKey("User", on_delete=models.PROTECT) book = models.ForeignKey("Edition", on_delete=models.PROTECT) @@ -28,13 +28,13 @@ class ReadThrough(BookWyrmModel): finish_date = models.DateTimeField(blank=True, null=True) def save(self, *args, **kwargs): - """ update user active time """ + """update user active time""" self.user.last_active_date = timezone.now() self.user.save(broadcast=False) super().save(*args, **kwargs) def create_update(self): - """ add update to the readthrough """ + """add update to the readthrough""" if self.progress: return self.progressupdate_set.create( user=self.user, progress=self.progress, mode=self.progress_mode @@ -43,7 +43,7 @@ class ReadThrough(BookWyrmModel): class ProgressUpdate(BookWyrmModel): - """ Store progress through a book in the database. """ + """Store progress through a book in the database.""" user = models.ForeignKey("User", on_delete=models.PROTECT) readthrough = models.ForeignKey("ReadThrough", on_delete=models.CASCADE) @@ -53,7 +53,7 @@ class ProgressUpdate(BookWyrmModel): ) def save(self, *args, **kwargs): - """ update user active time """ + """update user active time""" self.user.last_active_date = timezone.now() self.user.save(broadcast=False) super().save(*args, **kwargs) diff --git a/bookwyrm/models/relationship.py b/bookwyrm/models/relationship.py index 998d7bed5..12f4c51af 100644 --- a/bookwyrm/models/relationship.py +++ b/bookwyrm/models/relationship.py @@ -11,7 +11,7 @@ from . import fields class UserRelationship(BookWyrmModel): - """ many-to-many through table for followers """ + """many-to-many through table for followers""" user_subject = fields.ForeignKey( "User", @@ -28,16 +28,16 @@ class UserRelationship(BookWyrmModel): @property def privacy(self): - """ all relationships are handled directly with the participants """ + """all relationships are handled directly with the participants""" return "direct" @property def recipients(self): - """ the remote user needs to recieve direct broadcasts """ + """the remote user needs to recieve direct broadcasts""" return [u for u in [self.user_subject, self.user_object] if not u.local] class Meta: - """ relationships should be unique """ + """relationships should be unique""" abstract = True constraints = [ @@ -50,24 +50,23 @@ class UserRelationship(BookWyrmModel): ), ] - def get_remote_id(self, status=None): # pylint: disable=arguments-differ - """ use shelf identifier in remote_id """ - status = status or "follows" + def get_remote_id(self): + """use shelf identifier in remote_id""" base_path = self.user_subject.remote_id - return "%s#%s/%d" % (base_path, status, self.id) + return "%s#follows/%d" % (base_path, self.id) class UserFollows(ActivityMixin, UserRelationship): - """ Following a user """ + """Following a user""" status = "follows" def to_activity(self): # pylint: disable=arguments-differ - """ overrides default to manually set serializer """ + """overrides default to manually set serializer""" return activitypub.Follow(**generate_activity(self)) def save(self, *args, **kwargs): - """ really really don't let a user follow someone who blocked them """ + """really really don't let a user follow someone who blocked them""" # blocking in either direction is a no-go if UserBlocks.objects.filter( Q( @@ -86,7 +85,7 @@ class UserFollows(ActivityMixin, UserRelationship): @classmethod def from_request(cls, follow_request): - """ converts a follow request into a follow relationship """ + """converts a follow request into a follow relationship""" return cls.objects.create( user_subject=follow_request.user_subject, user_object=follow_request.user_object, @@ -95,19 +94,22 @@ class UserFollows(ActivityMixin, UserRelationship): class UserFollowRequest(ActivitypubMixin, UserRelationship): - """ following a user requires manual or automatic confirmation """ + """following a user requires manual or automatic confirmation""" status = "follow_request" activity_serializer = activitypub.Follow def save(self, *args, broadcast=True, **kwargs): - """ make sure the follow or block relationship doesn't already exist """ - # don't create a request if a follow already exists + """make sure the follow or block relationship doesn't already exist""" + # if there's a request for a follow that already exists, accept it + # without changing the local database state if UserFollows.objects.filter( user_subject=self.user_subject, user_object=self.user_object, ).exists(): - raise IntegrityError() + self.accept(broadcast_only=True) + return + # blocking in either direction is a no-go if UserBlocks.objects.filter( Q( @@ -138,25 +140,34 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship): notification_type=notification_type, ) - def accept(self): - """ turn this request into the real deal""" + def get_accept_reject_id(self, status): + """get id for sending an accept or reject of a local user""" + + base_path = self.user_object.remote_id + return "%s#%s/%d" % (base_path, status, self.id or 0) + + def accept(self, broadcast_only=False): + """turn this request into the real deal""" user = self.user_object if not self.user_subject.local: activity = activitypub.Accept( - id=self.get_remote_id(status="accepts"), + id=self.get_accept_reject_id(status="accepts"), actor=self.user_object.remote_id, object=self.to_activity(), ).serialize() self.broadcast(activity, user) + if broadcast_only: + return + with transaction.atomic(): UserFollows.from_request(self) self.delete() def reject(self): - """ generate a Reject for this follow request """ + """generate a Reject for this follow request""" if self.user_object.local: activity = activitypub.Reject( - id=self.get_remote_id(status="rejects"), + id=self.get_accept_reject_id(status="rejects"), actor=self.user_object.remote_id, object=self.to_activity(), ).serialize() @@ -166,13 +177,13 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship): class UserBlocks(ActivityMixin, UserRelationship): - """ prevent another user from following you and seeing your posts """ + """prevent another user from following you and seeing your posts""" status = "blocks" activity_serializer = activitypub.Block def save(self, *args, **kwargs): - """ remove follow or follow request rels after a block is created """ + """remove follow or follow request rels after a block is created""" super().save(*args, **kwargs) UserFollows.objects.filter( diff --git a/bookwyrm/models/report.py b/bookwyrm/models/report.py index f9e8905bf..7ff4c9091 100644 --- a/bookwyrm/models/report.py +++ b/bookwyrm/models/report.py @@ -6,7 +6,7 @@ from .base_model import BookWyrmModel class Report(BookWyrmModel): - """ reported status or user """ + """reported status or user""" reporter = models.ForeignKey( "User", related_name="reporter", on_delete=models.PROTECT @@ -17,7 +17,7 @@ class Report(BookWyrmModel): resolved = models.BooleanField(default=False) def save(self, *args, **kwargs): - """ notify admins when a report is created """ + """notify admins when a report is created""" super().save(*args, **kwargs) user_model = apps.get_model("bookwyrm.User", require_ready=True) # moderators and superusers should be notified @@ -34,7 +34,7 @@ class Report(BookWyrmModel): ) class Meta: - """ don't let users report themselves """ + """don't let users report themselves""" constraints = [ models.CheckConstraint(check=~Q(reporter=F("user")), name="self_report") @@ -43,13 +43,13 @@ class Report(BookWyrmModel): class ReportComment(BookWyrmModel): - """ updates on a report """ + """updates on a report""" user = models.ForeignKey("User", on_delete=models.PROTECT) note = models.TextField() report = models.ForeignKey(Report, on_delete=models.PROTECT) class Meta: - """ sort comments """ + """sort comments""" ordering = ("-created_date",) diff --git a/bookwyrm/models/shelf.py b/bookwyrm/models/shelf.py index 5bbb84b9b..4110ae8dc 100644 --- a/bookwyrm/models/shelf.py +++ b/bookwyrm/models/shelf.py @@ -9,7 +9,7 @@ from . import fields class Shelf(OrderedCollectionMixin, BookWyrmModel): - """ a list of books owned by a user """ + """a list of books owned by a user""" TO_READ = "to-read" READING = "reading" @@ -34,36 +34,36 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel): activity_serializer = activitypub.Shelf def save(self, *args, **kwargs): - """ set the identifier """ + """set the identifier""" super().save(*args, **kwargs) if not self.identifier: self.identifier = self.get_identifier() super().save(*args, **kwargs, broadcast=False) def get_identifier(self): - """ custom-shelf-123 for the url """ + """custom-shelf-123 for the url""" slug = re.sub(r"[^\w]", "", self.name).lower() return "{:s}-{:d}".format(slug, self.id) @property def collection_queryset(self): - """ list of books for this shelf, overrides OrderedCollectionMixin """ - return self.books.all().order_by("shelfbook") + """list of books for this shelf, overrides OrderedCollectionMixin""" + return self.books.order_by("shelfbook") def get_remote_id(self): - """ shelf identifier instead of id """ + """shelf identifier instead of id""" base_path = self.user.remote_id identifier = self.identifier or self.get_identifier() return "%s/books/%s" % (base_path, identifier) class Meta: - """ user/shelf unqiueness """ + """user/shelf unqiueness""" unique_together = ("user", "identifier") class ShelfBook(CollectionItemMixin, BookWyrmModel): - """ many to many join table for books and shelves """ + """many to many join table for books and shelves""" book = fields.ForeignKey( "Edition", on_delete=models.PROTECT, activitypub_field="book" diff --git a/bookwyrm/models/site.py b/bookwyrm/models/site.py index 1eb318694..193cffb7a 100644 --- a/bookwyrm/models/site.py +++ b/bookwyrm/models/site.py @@ -12,7 +12,7 @@ from .user import User class SiteSettings(models.Model): - """ customized settings for this instance """ + """customized settings for this instance""" name = models.CharField(default="BookWyrm", max_length=100) instance_tagline = models.CharField( @@ -35,7 +35,7 @@ class SiteSettings(models.Model): @classmethod def get(cls): - """ gets the site settings db entry or defaults """ + """gets the site settings db entry or defaults""" try: return cls.objects.get(id=1) except cls.DoesNotExist: @@ -45,12 +45,12 @@ class SiteSettings(models.Model): def new_access_code(): - """ the identifier for a user invite """ + """the identifier for a user invite""" return base64.b32encode(Random.get_random_bytes(5)).decode("ascii") class SiteInvite(models.Model): - """ gives someone access to create an account on the instance """ + """gives someone access to create an account on the instance""" created_date = models.DateTimeField(auto_now_add=True) code = models.CharField(max_length=32, default=new_access_code) @@ -61,19 +61,19 @@ class SiteInvite(models.Model): invitees = models.ManyToManyField(User, related_name="invitees") def valid(self): - """ make sure it hasn't expired or been used """ + """make sure it hasn't expired or been used""" return (self.expiry is None or self.expiry > timezone.now()) and ( self.use_limit is None or self.times_used < self.use_limit ) @property def link(self): - """ formats the invite link """ + """formats the invite link""" return "https://{}/invite/{}".format(DOMAIN, self.code) class InviteRequest(BookWyrmModel): - """ prospective users can request an invite """ + """prospective users can request an invite""" email = models.EmailField(max_length=255, unique=True) invite = models.ForeignKey( @@ -83,30 +83,30 @@ class InviteRequest(BookWyrmModel): ignored = models.BooleanField(default=False) def save(self, *args, **kwargs): - """ don't create a request for a registered email """ + """don't create a request for a registered email""" if not self.id and User.objects.filter(email=self.email).exists(): raise IntegrityError() super().save(*args, **kwargs) def get_passowrd_reset_expiry(): - """ give people a limited time to use the link """ + """give people a limited time to use the link""" now = timezone.now() return now + datetime.timedelta(days=1) class PasswordReset(models.Model): - """ gives someone access to create an account on the instance """ + """gives someone access to create an account on the instance""" code = models.CharField(max_length=32, default=new_access_code) expiry = models.DateTimeField(default=get_passowrd_reset_expiry) user = models.OneToOneField(User, on_delete=models.CASCADE) def valid(self): - """ make sure it hasn't expired or been used """ + """make sure it hasn't expired or been used""" return self.expiry > timezone.now() @property def link(self): - """ formats the invite link """ + """formats the invite link""" return "https://{}/password-reset/{}".format(DOMAIN, self.code) diff --git a/bookwyrm/models/status.py b/bookwyrm/models/status.py index 360288e93..bd21ec563 100644 --- a/bookwyrm/models/status.py +++ b/bookwyrm/models/status.py @@ -19,7 +19,7 @@ from . import fields class Status(OrderedCollectionPageMixin, BookWyrmModel): - """ any post, like a reply to a review, etc """ + """any post, like a reply to a review, etc""" user = fields.ForeignKey( "User", on_delete=models.PROTECT, activitypub_field="attributedTo" @@ -59,12 +59,12 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel): deserialize_reverse_fields = [("attachments", "attachment")] class Meta: - """ default sorting """ + """default sorting""" ordering = ("-published_date",) def save(self, *args, **kwargs): - """ save and notify """ + """save and notify""" super().save(*args, **kwargs) notification_model = apps.get_model("bookwyrm.Notification", require_ready=True) @@ -98,7 +98,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel): ) def delete(self, *args, **kwargs): # pylint: disable=unused-argument - """ "delete" a status """ + """ "delete" a status""" if hasattr(self, "boosted_status"): # okay but if it's a boost really delete it super().delete(*args, **kwargs) @@ -109,7 +109,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel): @property def recipients(self): - """ tagged users who definitely need to get this status in broadcast """ + """tagged users who definitely need to get this status in broadcast""" mentions = [u for u in self.mention_users.all() if not u.local] if ( hasattr(self, "reply_parent") @@ -121,7 +121,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel): @classmethod def ignore_activity(cls, activity): # pylint: disable=too-many-return-statements - """ keep notes if they are replies to existing statuses """ + """keep notes if they are replies to existing statuses""" if activity.type == "Announce": try: boosted = activitypub.resolve_remote_id( @@ -163,16 +163,16 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel): @property def status_type(self): - """ expose the type of status for the ui using activity type """ + """expose the type of status for the ui using activity type""" return self.activity_serializer.__name__ @property def boostable(self): - """ you can't boost dms """ + """you can't boost dms""" return self.privacy in ["unlisted", "public"] def to_replies(self, **kwargs): - """ helper function for loading AP serialized replies to a status """ + """helper function for loading AP serialized replies to a status""" return self.to_ordered_collection( self.replies(self), remote_id="%s/replies" % self.remote_id, @@ -181,7 +181,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel): ).serialize() def to_activity_dataclass(self, pure=False): # pylint: disable=arguments-differ - """ return tombstone if the status is deleted """ + """return tombstone if the status is deleted""" if self.deleted: return activitypub.Tombstone( id=self.remote_id, @@ -210,16 +210,16 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel): return activity def to_activity(self, pure=False): # pylint: disable=arguments-differ - """ json serialized activitypub class """ + """json serialized activitypub class""" return self.to_activity_dataclass(pure=pure).serialize() class GeneratedNote(Status): - """ these are app-generated messages about user activity """ + """these are app-generated messages about user activity""" @property def pure_content(self): - """ indicate the book in question for mastodon (or w/e) users """ + """indicate the book in question for mastodon (or w/e) users""" message = self.content books = ", ".join( '"%s"' % (book.remote_id, book.title) @@ -232,7 +232,7 @@ class GeneratedNote(Status): class Comment(Status): - """ like a review but without a rating and transient """ + """like a review but without a rating and transient""" book = fields.ForeignKey( "Edition", on_delete=models.PROTECT, activitypub_field="inReplyToBook" @@ -253,7 +253,7 @@ class Comment(Status): @property def pure_content(self): - """ indicate the book in question for mastodon (or w/e) users """ + """indicate the book in question for mastodon (or w/e) users""" return '%s

(comment on "%s")

' % ( self.content, self.book.remote_id, @@ -265,7 +265,7 @@ class Comment(Status): class Quotation(Status): - """ like a review but without a rating and transient """ + """like a review but without a rating and transient""" quote = fields.HtmlField() book = fields.ForeignKey( @@ -274,7 +274,7 @@ class Quotation(Status): @property def pure_content(self): - """ indicate the book in question for mastodon (or w/e) users """ + """indicate the book in question for mastodon (or w/e) users""" quote = re.sub(r"^

", '

"', self.quote) quote = re.sub(r"

$", '"

', quote) return '%s

-- "%s"

%s' % ( @@ -289,7 +289,7 @@ class Quotation(Status): class Review(Status): - """ a book review """ + """a book review""" name = fields.CharField(max_length=255, null=True) book = fields.ForeignKey( @@ -306,7 +306,7 @@ class Review(Status): @property def pure_name(self): - """ clarify review names for mastodon serialization """ + """clarify review names for mastodon serialization""" template = get_template("snippets/generated_status/review_pure_name.html") return template.render( {"book": self.book, "rating": self.rating, "name": self.name} @@ -314,7 +314,7 @@ class Review(Status): @property def pure_content(self): - """ indicate the book in question for mastodon (or w/e) users """ + """indicate the book in question for mastodon (or w/e) users""" return self.content activity_serializer = activitypub.Review @@ -322,7 +322,7 @@ class Review(Status): class ReviewRating(Review): - """ a subtype of review that only contains a rating """ + """a subtype of review that only contains a rating""" def save(self, *args, **kwargs): if not self.rating: @@ -339,7 +339,7 @@ class ReviewRating(Review): class Boost(ActivityMixin, Status): - """ boost'ing a post """ + """boost'ing a post""" boosted_status = fields.ForeignKey( "Status", @@ -350,7 +350,17 @@ class Boost(ActivityMixin, Status): activity_serializer = activitypub.Announce def save(self, *args, **kwargs): - """ save and notify """ + """save and notify""" + # This constraint can't work as it would cross tables. + # class Meta: + # unique_together = ('user', 'boosted_status') + if ( + Boost.objects.filter(boosted_status=self.boosted_status, user=self.user) + .exclude(id=self.id) + .exists() + ): + return + super().save(*args, **kwargs) if not self.boosted_status.user.local or self.boosted_status.user == self.user: return @@ -364,7 +374,7 @@ class Boost(ActivityMixin, Status): ) def delete(self, *args, **kwargs): - """ delete and un-notify """ + """delete and un-notify""" notification_model = apps.get_model("bookwyrm.Notification", require_ready=True) notification_model.objects.filter( user=self.boosted_status.user, @@ -375,7 +385,7 @@ class Boost(ActivityMixin, Status): super().delete(*args, **kwargs) def __init__(self, *args, **kwargs): - """ the user field is "actor" here instead of "attributedTo" """ + """the user field is "actor" here instead of "attributedTo" """ super().__init__(*args, **kwargs) reserve_fields = ["user", "boosted_status", "published_date", "privacy"] diff --git a/bookwyrm/models/tag.py b/bookwyrm/models/tag.py deleted file mode 100644 index 2c45b8f91..000000000 --- a/bookwyrm/models/tag.py +++ /dev/null @@ -1,63 +0,0 @@ -""" models for storing different kinds of Activities """ -import urllib.parse - -from django.apps import apps -from django.db import models - -from bookwyrm import activitypub -from bookwyrm.settings import DOMAIN -from .activitypub_mixin import CollectionItemMixin, OrderedCollectionMixin -from .base_model import BookWyrmModel -from . import fields - - -class Tag(OrderedCollectionMixin, BookWyrmModel): - """ freeform tags for books """ - - name = fields.CharField(max_length=100, unique=True) - identifier = models.CharField(max_length=100) - - @property - def books(self): - """ count of books associated with this tag """ - edition_model = apps.get_model("bookwyrm.Edition", require_ready=True) - return ( - edition_model.objects.filter(usertag__tag__identifier=self.identifier) - .order_by("-created_date") - .distinct() - ) - - collection_queryset = books - - def get_remote_id(self): - """ tag should use identifier not id in remote_id """ - base_path = "https://%s" % DOMAIN - return "%s/tag/%s" % (base_path, self.identifier) - - def save(self, *args, **kwargs): - """ create a url-safe lookup key for the tag """ - if not self.id: - # add identifiers to new tags - self.identifier = urllib.parse.quote_plus(self.name) - super().save(*args, **kwargs) - - -class UserTag(CollectionItemMixin, BookWyrmModel): - """ an instance of a tag on a book by a user """ - - user = fields.ForeignKey( - "User", on_delete=models.PROTECT, activitypub_field="actor" - ) - book = fields.ForeignKey( - "Edition", on_delete=models.PROTECT, activitypub_field="object" - ) - tag = fields.ForeignKey("Tag", on_delete=models.PROTECT, activitypub_field="target") - - activity_serializer = activitypub.Add - object_field = "book" - collection_field = "tag" - - class Meta: - """ unqiueness constraint """ - - unique_together = ("user", "book", "tag") diff --git a/bookwyrm/models/user.py b/bookwyrm/models/user.py index 0f98c82dd..3efbd6ac8 100644 --- a/bookwyrm/models/user.py +++ b/bookwyrm/models/user.py @@ -35,7 +35,7 @@ DeactivationReason = models.TextChoices( class User(OrderedCollectionPageMixin, AbstractUser): - """ a user who wants to read books """ + """a user who wants to read books""" username = fields.UsernameField() email = models.EmailField(unique=True, null=True) @@ -130,38 +130,38 @@ class User(OrderedCollectionPageMixin, AbstractUser): @property def following_link(self): - """ just how to find out the following info """ + """just how to find out the following info""" return "{:s}/following".format(self.remote_id) @property def alt_text(self): - """ alt text with username """ + """alt text with username""" return "avatar for %s" % (self.localname or self.username) @property def display_name(self): - """ show the cleanest version of the user's name possible """ + """show the cleanest version of the user's name possible""" if self.name and self.name != "": return self.name return self.localname or self.username @property def deleted(self): - """ for consistent naming """ + """for consistent naming""" return not self.is_active activity_serializer = activitypub.Person @classmethod def viewer_aware_objects(cls, viewer): - """ the user queryset filtered for the context of the logged in user """ + """the user queryset filtered for the context of the logged in user""" queryset = cls.objects.filter(is_active=True) if viewer and viewer.is_authenticated: queryset = queryset.exclude(blocks=viewer) return queryset def to_outbox(self, filter_type=None, **kwargs): - """ an ordered collection of statuses """ + """an ordered collection of statuses""" if filter_type: filter_class = apps.get_model( "bookwyrm.%s" % filter_type, require_ready=True @@ -188,7 +188,7 @@ class User(OrderedCollectionPageMixin, AbstractUser): ).serialize() def to_following_activity(self, **kwargs): - """ activitypub following list """ + """activitypub following list""" remote_id = "%s/following" % self.remote_id return self.to_ordered_collection( self.following.order_by("-updated_date").all(), @@ -198,7 +198,7 @@ class User(OrderedCollectionPageMixin, AbstractUser): ) def to_followers_activity(self, **kwargs): - """ activitypub followers list """ + """activitypub followers list""" remote_id = "%s/followers" % self.remote_id return self.to_ordered_collection( self.followers.order_by("-updated_date").all(), @@ -227,7 +227,7 @@ class User(OrderedCollectionPageMixin, AbstractUser): return activity_object def save(self, *args, **kwargs): - """ populate fields for new local users """ + """populate fields for new local users""" created = not bool(self.id) if not self.local and not re.match(regex.full_username, self.username): # generate a username that uses the domain (webfinger format) @@ -292,19 +292,19 @@ class User(OrderedCollectionPageMixin, AbstractUser): ).save(broadcast=False) def delete(self, *args, **kwargs): - """ deactivate rather than delete a user """ + """deactivate rather than delete a user""" self.is_active = False # skip the logic in this class's save() super().save(*args, **kwargs) @property def local_path(self): - """ this model doesn't inherit bookwyrm model, so here we are """ + """this model doesn't inherit bookwyrm model, so here we are""" return "/user/%s" % (self.localname or self.username) class KeyPair(ActivitypubMixin, BookWyrmModel): - """ public and private keys for a user """ + """public and private keys for a user""" private_key = models.TextField(blank=True, null=True) public_key = fields.TextField( @@ -319,7 +319,7 @@ class KeyPair(ActivitypubMixin, BookWyrmModel): return "%s/#main-key" % self.owner.remote_id def save(self, *args, **kwargs): - """ create a key pair """ + """create a key pair""" # no broadcasting happening here if "broadcast" in kwargs: del kwargs["broadcast"] @@ -337,7 +337,7 @@ class KeyPair(ActivitypubMixin, BookWyrmModel): class AnnualGoal(BookWyrmModel): - """ set a goal for how many books you read in a year """ + """set a goal for how many books you read in a year""" user = models.ForeignKey("User", on_delete=models.PROTECT) goal = models.IntegerField(validators=[MinValueValidator(1)]) @@ -347,17 +347,17 @@ class AnnualGoal(BookWyrmModel): ) class Meta: - """ unqiueness constraint """ + """unqiueness constraint""" unique_together = ("user", "year") def get_remote_id(self): - """ put the year in the path """ + """put the year in the path""" return "%s/goal/%d" % (self.user.remote_id, self.year) @property def books(self): - """ the books you've read this year """ + """the books you've read this year""" return ( self.user.readthrough_set.filter(finish_date__year__gte=self.year) .order_by("-finish_date") @@ -366,7 +366,7 @@ class AnnualGoal(BookWyrmModel): @property def ratings(self): - """ ratings for books read this year """ + """ratings for books read this year""" book_ids = [r.book.id for r in self.books] reviews = Review.objects.filter( user=self.user, @@ -376,12 +376,12 @@ class AnnualGoal(BookWyrmModel): @property def progress_percent(self): - """ how close to your goal, in percent form """ + """how close to your goal, in percent form""" return int(float(self.book_count / self.goal) * 100) @property def book_count(self): - """ how many books you've read this year """ + """how many books you've read this year""" return self.user.readthrough_set.filter( finish_date__year__gte=self.year ).count() @@ -389,7 +389,7 @@ class AnnualGoal(BookWyrmModel): @app.task def set_remote_server(user_id): - """ figure out the user's remote server in the background """ + """figure out the user's remote server in the background""" user = User.objects.get(id=user_id) actor_parts = urlparse(user.remote_id) user.federated_server = get_or_create_remote_server(actor_parts.netloc) @@ -399,7 +399,7 @@ def set_remote_server(user_id): def get_or_create_remote_server(domain): - """ get info on a remote server """ + """get info on a remote server""" try: return FederatedServer.objects.get(server_name=domain) except FederatedServer.DoesNotExist: @@ -428,7 +428,7 @@ def get_or_create_remote_server(domain): @app.task def get_remote_reviews(outbox): - """ ingest reviews by a new remote bookwyrm user """ + """ingest reviews by a new remote bookwyrm user""" outbox_page = outbox + "?page=true&type=Review" data = get_data(outbox_page) diff --git a/bookwyrm/redis_store.py b/bookwyrm/redis_store.py index 4236d6df2..259bc4fd6 100644 --- a/bookwyrm/redis_store.py +++ b/bookwyrm/redis_store.py @@ -10,16 +10,16 @@ r = redis.Redis( class RedisStore(ABC): - """ sets of ranked, related objects, like statuses for a user's feed """ + """sets of ranked, related objects, like statuses for a user's feed""" max_length = settings.MAX_STREAM_LENGTH def get_value(self, obj): - """ the object and rank """ + """the object and rank""" return {obj.id: self.get_rank(obj)} def add_object_to_related_stores(self, obj, execute=True): - """ add an object to all suitable stores """ + """add an object to all suitable stores""" value = self.get_value(obj) # we want to do this as a bulk operation, hence "pipeline" pipeline = r.pipeline() @@ -34,14 +34,14 @@ class RedisStore(ABC): return pipeline.execute() def remove_object_from_related_stores(self, obj): - """ remove an object from all stores """ + """remove an object from all stores""" pipeline = r.pipeline() for store in self.get_stores_for_object(obj): pipeline.zrem(store, -1, obj.id) pipeline.execute() def bulk_add_objects_to_store(self, objs, store): - """ add a list of objects to a given store """ + """add a list of objects to a given store""" pipeline = r.pipeline() for obj in objs[: self.max_length]: pipeline.zadd(store, self.get_value(obj)) @@ -50,18 +50,18 @@ class RedisStore(ABC): pipeline.execute() def bulk_remove_objects_from_store(self, objs, store): - """ remoev a list of objects from a given store """ + """remoev a list of objects from a given store""" pipeline = r.pipeline() for obj in objs[: self.max_length]: pipeline.zrem(store, -1, obj.id) pipeline.execute() def get_store(self, store): # pylint: disable=no-self-use - """ load the values in a store """ + """load the values in a store""" return r.zrevrange(store, 0, -1) def populate_store(self, store): - """ go from zero to a store """ + """go from zero to a store""" pipeline = r.pipeline() queryset = self.get_objects_for_store(store) @@ -75,12 +75,12 @@ class RedisStore(ABC): @abstractmethod def get_objects_for_store(self, store): - """ a queryset of what should go in a store, used for populating it """ + """a queryset of what should go in a store, used for populating it""" @abstractmethod def get_stores_for_object(self, obj): - """ the stores that an object belongs in """ + """the stores that an object belongs in""" @abstractmethod def get_rank(self, obj): - """ how to rank an object """ + """how to rank an object""" diff --git a/bookwyrm/sanitize_html.py b/bookwyrm/sanitize_html.py index 2a630f838..0be64c58c 100644 --- a/bookwyrm/sanitize_html.py +++ b/bookwyrm/sanitize_html.py @@ -3,7 +3,7 @@ from html.parser import HTMLParser class InputHtmlParser(HTMLParser): # pylint: disable=abstract-method - """ Removes any html that isn't allowed_tagsed from a block """ + """Removes any html that isn't allowed_tagsed from a block""" def __init__(self): HTMLParser.__init__(self) @@ -28,7 +28,7 @@ class InputHtmlParser(HTMLParser): # pylint: disable=abstract-method self.allow_html = True def handle_starttag(self, tag, attrs): - """ check if the tag is valid """ + """check if the tag is valid""" if self.allow_html and tag in self.allowed_tags: self.output.append(("tag", self.get_starttag_text())) self.tag_stack.append(tag) @@ -36,7 +36,7 @@ class InputHtmlParser(HTMLParser): # pylint: disable=abstract-method self.output.append(("data", "")) def handle_endtag(self, tag): - """ keep the close tag """ + """keep the close tag""" if not self.allow_html or tag not in self.allowed_tags: self.output.append(("data", "")) return @@ -51,11 +51,11 @@ class InputHtmlParser(HTMLParser): # pylint: disable=abstract-method self.output.append(("tag", "" % tag)) def handle_data(self, data): - """ extract the answer, if we're in an answer tag """ + """extract the answer, if we're in an answer tag""" self.output.append(("data", data)) def get_output(self): - """ convert the output from a list of tuples to a string """ + """convert the output from a list of tuples to a string""" if self.tag_stack: self.allow_html = False if not self.allow_html: diff --git a/bookwyrm/settings.py b/bookwyrm/settings.py index 7ea8c5950..fb5488e7a 100644 --- a/bookwyrm/settings.py +++ b/bookwyrm/settings.py @@ -153,7 +153,7 @@ LANGUAGES = [ ("de-de", _("German")), ("es", _("Spanish")), ("fr-fr", _("French")), - ("zh-cn", _("Simplified Chinese")), + ("zh-hans", _("Simplified Chinese")), ] diff --git a/bookwyrm/signatures.py b/bookwyrm/signatures.py index 80cbfdc79..5488cf9be 100644 --- a/bookwyrm/signatures.py +++ b/bookwyrm/signatures.py @@ -13,7 +13,7 @@ MAX_SIGNATURE_AGE = 300 def create_key_pair(): - """ a new public/private key pair, used for creating new users """ + """a new public/private key pair, used for creating new users""" random_generator = Random.new().read key = RSA.generate(1024, random_generator) private_key = key.export_key().decode("utf8") @@ -23,7 +23,7 @@ def create_key_pair(): def make_signature(sender, destination, date, digest): - """ uses a private key to sign an outgoing message """ + """uses a private key to sign an outgoing message""" inbox_parts = urlparse(destination) signature_headers = [ "(request-target): post %s" % inbox_parts.path, @@ -44,14 +44,14 @@ def make_signature(sender, destination, date, digest): def make_digest(data): - """ creates a message digest for signing """ + """creates a message digest for signing""" return "SHA-256=" + b64encode(hashlib.sha256(data.encode("utf-8")).digest()).decode( "utf-8" ) def verify_digest(request): - """ checks if a digest is syntactically valid and matches the message """ + """checks if a digest is syntactically valid and matches the message""" algorithm, digest = request.headers["digest"].split("=", 1) if algorithm == "SHA-256": hash_function = hashlib.sha256 @@ -66,7 +66,7 @@ def verify_digest(request): class Signature: - """ read and validate incoming signatures """ + """read and validate incoming signatures""" def __init__(self, key_id, headers, signature): self.key_id = key_id @@ -75,7 +75,7 @@ class Signature: @classmethod def parse(cls, request): - """ extract and parse a signature from an http request """ + """extract and parse a signature from an http request""" signature_dict = {} for pair in request.headers["Signature"].split(","): k, v = pair.split("=", 1) @@ -92,7 +92,7 @@ class Signature: return cls(key_id, headers, signature) def verify(self, public_key, request): - """ verify rsa signature """ + """verify rsa signature""" if http_date_age(request.headers["date"]) > MAX_SIGNATURE_AGE: raise ValueError("Request too old: %s" % (request.headers["date"],)) public_key = RSA.import_key(public_key) @@ -118,7 +118,7 @@ class Signature: def http_date_age(datestr): - """ age of a signature in seconds """ + """age of a signature in seconds""" parsed = datetime.datetime.strptime(datestr, "%a, %d %b %Y %H:%M:%S GMT") delta = datetime.datetime.utcnow() - parsed return delta.total_seconds() diff --git a/bookwyrm/static/css/bookwyrm.css b/bookwyrm/static/css/bookwyrm.css index 67eb1ebac..9e74d69f1 100644 --- a/bookwyrm/static/css/bookwyrm.css +++ b/bookwyrm/static/css/bookwyrm.css @@ -1,6 +1,5 @@ html { scroll-behavior: smooth; - scroll-padding-top: 20%; } body { @@ -30,6 +29,40 @@ body { min-width: 75% !important; } +/** Utilities not covered by Bulma + ******************************************************************************/ + +@media only screen and (max-width: 768px) { + .is-sr-only-mobile { + border: none !important; + clip: rect(0, 0, 0, 0) !important; + height: 0.01em !important; + overflow: hidden !important; + padding: 0 !important; + position: absolute !important; + white-space: nowrap !important; + width: 0.01em !important; + } + + .m-0-mobile { + margin: 0 !important; + } +} + +.button.is-transparent { + background-color: transparent; +} + +.card.is-stretchable { + display: flex; + flex-direction: column; + height: 100%; +} + +.card.is-stretchable .card-content { + flex-grow: 1; +} + /** Shelving ******************************************************************************/ @@ -86,6 +119,13 @@ body { } } +/** Stars + ******************************************************************************/ + +.stars { + white-space: nowrap; +} + /** Stars in a review form * * Specificity makes hovering taking over checked inputs. @@ -256,3 +296,53 @@ body { opacity: 0.5; cursor: not-allowed; } + +/* Book preview table + ******************************************************************************/ + +.book-preview td { + vertical-align: middle; +} + +@media only screen and (max-width: 768px) { + table.is-mobile, + table.is-mobile tbody { + display: block; + } + + table.is-mobile tr { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + border-top: 1px solid #dbdbdb; + } + + table.is-mobile td { + display: block; + box-sizing: border-box; + flex: 1 0 100%; + order: 2; + border-bottom: 0; + } + + table.is-mobile td.book-preview-top-row { + order: 1; + flex-basis: auto; + } + + table.is-mobile td[data-title]:not(:empty)::before { + content: attr(data-title); + display: block; + font-size: 0.75em; + font-weight: bold; + } + + table.is-mobile td:empty { + padding: 0; + } + + table.is-mobile th, + table.is-mobile thead { + display: none; + } +} diff --git a/bookwyrm/status.py b/bookwyrm/status.py index 793bd742d..09fbdc06e 100644 --- a/bookwyrm/status.py +++ b/bookwyrm/status.py @@ -6,7 +6,7 @@ from bookwyrm.sanitize_html import InputHtmlParser def create_generated_note(user, content, mention_books=None, privacy="public"): - """ a note created by the app about user activity """ + """a note created by the app about user activity""" # sanitize input html parser = InputHtmlParser() parser.feed(content) diff --git a/bookwyrm/templates/book/book.html b/bookwyrm/templates/book/book.html index d263e0e07..97f105bf7 100644 --- a/bookwyrm/templates/book/book.html +++ b/bookwyrm/templates/book/book.html @@ -67,31 +67,16 @@ {% endif %} -
-
- {% if book.isbn_13 %} -
-
{% trans "ISBN:" %}
-
{{ book.isbn_13 }}
+
+ {% with book=book %} +
+ {% include 'book/publisher_info.html' %}
- {% endif %} - {% if book.oclc_number %} -
-
{% trans "OCLC Number:" %}
-
{{ book.oclc_number }}
+
+ {% include 'book/book_identifiers.html' %}
- {% endif %} - - {% if book.asin %} -
-
{% trans "ASIN:" %}
-
{{ book.asin }}
-
- {% endif %} -
- - {% include 'book/publisher_info.html' with book=book %} + {% endwith %} {% if book.openlibrary_key %}

{% trans "View on OpenLibrary" %}

@@ -261,7 +246,36 @@
- {% for review in reviews %} + {% if request.user.is_authenticated %} + + {% endif %} + + {% for review in statuses %}
- {% include 'snippets/pagination.html' with page=reviews path=book.local_path anchor="#reviews" %} + {% include 'snippets/pagination.html' with page=statuses path=request.path anchor="#reviews" %}
diff --git a/bookwyrm/templates/book/book_identifiers.html b/bookwyrm/templates/book/book_identifiers.html new file mode 100644 index 000000000..d71ea4096 --- /dev/null +++ b/bookwyrm/templates/book/book_identifiers.html @@ -0,0 +1,27 @@ +{% spaceless %} + +{% load i18n %} + +
+ {% if book.isbn_13 %} +
+
{% trans "ISBN:" %}
+
{{ book.isbn_13 }}
+
+ {% endif %} + + {% if book.oclc_number %} +
+
{% trans "OCLC Number:" %}
+
{{ book.oclc_number }}
+
+ {% endif %} + + {% if book.asin %} +
+
{% trans "ASIN:" %}
+
{{ book.asin }}
+
+ {% endif %} +
+{% endspaceless %} diff --git a/bookwyrm/templates/book/edit_book.html b/bookwyrm/templates/book/edit_book.html index af5d4d695..1702cf5d8 100644 --- a/bookwyrm/templates/book/edit_book.html +++ b/bookwyrm/templates/book/edit_book.html @@ -109,7 +109,10 @@

{{ error | escape }}

{% endfor %} -

{{ form.series }}

+

+ + +

{% for error in form.series.errors %}

{{ error | escape }}

{% endfor %} diff --git a/bookwyrm/templates/book/editions.html b/bookwyrm/templates/book/editions.html index 91259465e..70f067f76 100644 --- a/bookwyrm/templates/book/editions.html +++ b/bookwyrm/templates/book/editions.html @@ -25,7 +25,18 @@ {{ book.title }} - {% include 'book/publisher_info.html' with book=book %} + + {% with book=book %} +
+
+ {% include 'book/publisher_info.html' %} +
+ +
+ {% include 'book/book_identifiers.html' %} +
+
+ {% endwith %}
{% include 'snippets/shelve_button/shelve_button.html' with book=book switch_mode=True %} diff --git a/bookwyrm/templates/book/publisher_info.html b/bookwyrm/templates/book/publisher_info.html index a16332c5d..b7975a623 100644 --- a/bookwyrm/templates/book/publisher_info.html +++ b/bookwyrm/templates/book/publisher_info.html @@ -1,6 +1,7 @@ {% spaceless %} {% load i18n %} +{% load humanize %}

{% with format=book.physical_format pages=book.pages %} @@ -39,7 +40,7 @@ {% endif %}

- {% with date=book.published_date|date:'M jS Y' publisher=book.publishers|join:', ' %} + {% with date=book.published_date|naturalday publisher=book.publishers|join:', ' %} {% if date or book.first_published_date %}

- {% if request.user.is_authenticated and not list.curation == 'closed' or request.user == list.user %}
+

{% trans "Sort List" %}

+ + +
+ {{ sort_form.sort_by }} +
+ +
+ {{ sort_form.direction }} +
+
+ +
+ + {% if request.user.is_authenticated and not list.curation == 'closed' or request.user == list.user %}

{% if list.curation == 'open' or request.user == list.user %}{% trans "Add Books" %}{% else %}{% trans "Suggest Books" %}{% endif %}

@@ -93,7 +126,7 @@
{% endif %} {% endfor %} -
{% endif %} + {% endblock %} diff --git a/bookwyrm/templates/moderation/report.html b/bookwyrm/templates/moderation/report.html index 1cadd28d4..934799e33 100644 --- a/bookwyrm/templates/moderation/report.html +++ b/bookwyrm/templates/moderation/report.html @@ -15,76 +15,9 @@ {% include 'moderation/report_preview.html' with report=report %} -
-
-

{% trans "User details" %}

-
-
- {% if not report.user.local %} - {% with server=report.user.federated_server %} -
-

{% trans "Instance details" %}

-
- {% if server %} -
{{ server.server_name }}
-
-
-
{% trans "Software:" %}
-
{{ server.application_type }}
-
-
-
{% trans "Version:" %}
-
{{ server.application_version }}
-
-
-
{% trans "Status:" %}
-
{{ server.status }}
-
-
- {% if server.notes %} -
{% trans "Notes" %}
-
- {{ server.notes }} -
- {% endif %} - -

- {% trans "View instance" %} -

- {% else %} - {% trans "Not set" %} - {% endif %} -
-
- {% endwith %} - {% endif %} -
- -
-

{% trans "Actions" %}

-
-

- {% trans "Send direct message" %} -

- - {% csrf_token %} - {% if report.user.is_active %} - - {% else %} - - {% endif %} - -
-
+{% include 'user_admin/user_moderation_actions.html' with user=report.user %}

{% trans "Moderator Comments" %}

@@ -118,7 +51,7 @@ {% for status in report.statuses.select_subclasses.all %}
  • {% if status.deleted %} - {% trans "Statuses has been deleted" %} + {% trans "Status has been deleted" %} {% else %} {% include 'snippets/status/status.html' with status=status moderation_mode=True %} {% endif %} diff --git a/bookwyrm/templates/moderation/reports.html b/bookwyrm/templates/moderation/reports.html index 72cadae5a..f9d9d99b6 100644 --- a/bookwyrm/templates/moderation/reports.html +++ b/bookwyrm/templates/moderation/reports.html @@ -30,7 +30,7 @@
  • -{% include 'settings/user_admin_filters.html' %} +{% include 'user_admin/user_admin_filters.html' %}
    {% if not reports %} diff --git a/bookwyrm/templates/notifications.html b/bookwyrm/templates/notifications.html index 7c694d78b..ba0a25cd0 100644 --- a/bookwyrm/templates/notifications.html +++ b/bookwyrm/templates/notifications.html @@ -123,7 +123,7 @@ {% include 'snippets/status_preview.html' with status=related_status %}
    - {{ related_status.published_date | post_date }} + {{ related_status.published_date|timesince }} {% include 'snippets/privacy-icons.html' with item=related_status %}
    diff --git a/bookwyrm/templates/snippets/book_titleby.html b/bookwyrm/templates/snippets/book_titleby.html index e561a8a33..80127fb72 100644 --- a/bookwyrm/templates/snippets/book_titleby.html +++ b/bookwyrm/templates/snippets/book_titleby.html @@ -1,7 +1,8 @@ {% load i18n %} +{% load bookwyrm_tags %} {% if book.authors %} -{% blocktrans with path=book.local_path title=book.title %}{{ title }} by {% endblocktrans %}{% include 'snippets/authors.html' with book=book %} +{% blocktrans with path=book.local_path title=book|title %}{{ title }} by {% endblocktrans %}{% include 'snippets/authors.html' with book=book %} {% else %} -{{ book.title }} +{{ book|title }} {% endif %} diff --git a/bookwyrm/templates/snippets/boost_button.html b/bookwyrm/templates/snippets/boost_button.html index e590c58d8..3a01fc82d 100644 --- a/bookwyrm/templates/snippets/boost_button.html +++ b/bookwyrm/templates/snippets/boost_button.html @@ -4,18 +4,16 @@ {% with status.id|uuid as uuid %}
    {% csrf_token %} -
    {% csrf_token %} -
    {% endwith %} diff --git a/bookwyrm/templates/snippets/create_status_form.html b/bookwyrm/templates/snippets/create_status_form.html index 57e36d963..a74230ad1 100644 --- a/bookwyrm/templates/snippets/create_status_form.html +++ b/bookwyrm/templates/snippets/create_status_form.html @@ -6,14 +6,16 @@ {% if type == 'review' %} -
    +
    - +
    + +
    {% endif %} -
    +
    {% if type != 'reply' and type != 'direct' %} -
    + + {# Supplemental fields #} {% if type == 'quotation' %} -
    +
    {% include 'snippets/content_warning_field.html' with parent_status=status %} - +
    + +
    {% elif type == 'comment' %} -
    +
    {% active_shelf book as active_shelf %} {% if active_shelf.shelf.identifier == 'reading' and book.latest_readthrough %} @@ -58,11 +69,13 @@
    -
    - +
    +
    + +
    {% if readthrough.progress_mode == 'PG' and book.pages %} @@ -73,9 +86,12 @@ {% endif %}
    {% endif %} - + + {# bottom bar #} -
    + + +
    {% trans "Include spoiler alert" as button_text %} diff --git a/bookwyrm/templates/snippets/fav_button.html b/bookwyrm/templates/snippets/fav_button.html index adb3d0333..cd22822a5 100644 --- a/bookwyrm/templates/snippets/fav_button.html +++ b/bookwyrm/templates/snippets/fav_button.html @@ -3,18 +3,17 @@ {% with status.id|uuid as uuid %}
    {% csrf_token %} -
    {% csrf_token %} -
    {% endwith %} diff --git a/bookwyrm/templates/snippets/generated_status/review_pure_name.html b/bookwyrm/templates/snippets/generated_status/review_pure_name.html index 90a6936ed..259601914 100644 --- a/bookwyrm/templates/snippets/generated_status/review_pure_name.html +++ b/bookwyrm/templates/snippets/generated_status/review_pure_name.html @@ -1,10 +1,10 @@ {% load i18n %} {% if rating %} -{% blocktrans with book_title=book.title display_rating=rating|floatformat:"0" review_title=name count counter=rating %}Review of "{{ book_title }}" ({{ display_rating }} star): {{ review_title }}{% plural %}Review of "{{ book_title }}" ({{ display_rating }} stars): {{ review_title }}{% endblocktrans %} +{% blocktrans with book_title=book.title|safe display_rating=rating|floatformat:"0" review_title=name|safe count counter=rating %}Review of "{{ book_title }}" ({{ display_rating }} star): {{ review_title }}{% plural %}Review of "{{ book_title }}" ({{ display_rating }} stars): {{ review_title }}{% endblocktrans %} {% else %} -{% blocktrans with book_title=book.title review_title=name %}Review of "{{ book_title }}": {{ review_title }}{% endblocktrans %} +{% blocktrans with book_title=book.title|safe review_title=name|safe %}Review of "{{ book_title }}": {{ review_title }}{% endblocktrans %} {% endif %} diff --git a/bookwyrm/templates/snippets/shelf_selector.html b/bookwyrm/templates/snippets/shelf_selector.html index 0be4465f1..0036a4e90 100644 --- a/bookwyrm/templates/snippets/shelf_selector.html +++ b/bookwyrm/templates/snippets/shelf_selector.html @@ -7,23 +7,23 @@ {% block dropdown-list %} {% for shelf in request.user.shelf_set.all %} -
  • -
  • {% endfor %} - -
  • -
  • {% endblock %} diff --git a/bookwyrm/templates/snippets/shelve_button/shelve_button_dropdown.html b/bookwyrm/templates/snippets/shelve_button/shelve_button_dropdown.html index c3f325bff..4439bfc28 100644 --- a/bookwyrm/templates/snippets/shelve_button/shelve_button_dropdown.html +++ b/bookwyrm/templates/snippets/shelve_button/shelve_button_dropdown.html @@ -7,5 +7,5 @@ {% endblock %} {% block dropdown-list %} -{% include 'snippets/shelve_button/shelve_button_options.html' with active_shelf=active_shelf shelves=request.user.shelf_set.all dropdown=True class="shelf-option is-fullwidth is-small" %} +{% include 'snippets/shelve_button/shelve_button_options.html' with active_shelf=active_shelf shelves=request.user.shelf_set.all dropdown=True class="shelf-option is-fullwidth is-small is-radiusless is-white" %} {% endblock %} diff --git a/bookwyrm/templates/snippets/shelve_button/shelve_button_options.html b/bookwyrm/templates/snippets/shelve_button/shelve_button_options.html index 2a1b99e1b..1eaa24635 100644 --- a/bookwyrm/templates/snippets/shelve_button/shelve_button_options.html +++ b/bookwyrm/templates/snippets/shelve_button/shelve_button_options.html @@ -2,8 +2,8 @@ {% load i18n %} {% for shelf in shelves %} {% comparison_bool shelf.identifier active_shelf.shelf.identifier as is_current %} -{% if dropdown %}
  • {% endif %} -
  • - +
  • {% endif %} {% if active_shelf.shelf %} -
  • - +
  • {% endif %} diff --git a/bookwyrm/templates/snippets/stars.html b/bookwyrm/templates/snippets/stars.html index 2b40a9e3c..ac049f254 100644 --- a/bookwyrm/templates/snippets/stars.html +++ b/bookwyrm/templates/snippets/stars.html @@ -1,7 +1,7 @@ {% spaceless %} {% load i18n %} -

    + {% if rating %} {% blocktranslate trimmed with rating=rating|floatformat count counter=rating|length %} @@ -23,5 +23,5 @@ aria-hidden="true" > {% endfor %} -

    + {% endspaceless %} diff --git a/bookwyrm/templates/snippets/status/book_preview.html b/bookwyrm/templates/snippets/status/book_preview.html deleted file mode 100644 index 920b9f538..000000000 --- a/bookwyrm/templates/snippets/status/book_preview.html +++ /dev/null @@ -1,14 +0,0 @@ -{% load bookwyrm_tags %} -
    -
    -
    - {% include 'snippets/book_cover.html' with book=book %} - {% include 'snippets/stars.html' with rating=book|rating:request.user %} - {% include 'snippets/shelve_button/shelve_button.html' with book=book %} -
    -
    -
    -

    {% include 'snippets/book_titleby.html' with book=book %}

    - {% include 'snippets/trimmed_text.html' with full=book|book_description %} -
    -
    diff --git a/bookwyrm/templates/snippets/status/content_status.html b/bookwyrm/templates/snippets/status/content_status.html new file mode 100644 index 000000000..b2946b83b --- /dev/null +++ b/bookwyrm/templates/snippets/status/content_status.html @@ -0,0 +1,135 @@ +{% load bookwyrm_tags %} +{% load i18n %} + +{% with status_type=status.status_type %} +
    + +
    + {% if not hide_book %} + {% with book=status.book|default:status.mention_books.first %} + {% if book %} +
    +
    +
    + {% include 'snippets/book_cover.html' with book=book %} + {% include 'snippets/stars.html' with rating=book|rating:request.user %} + {% include 'snippets/shelve_button/shelve_button.html' with book=book %} +
    +
    +

    {{ book|book_description|to_markdown|default:""|safe|truncatewords_html:15 }}

    +
    +
    +
    + {% endif %} + {% endwith %} + {% endif %} + +
    + {% if status_type == 'Review' %} +
    +

    + {{ status.name|escape }} +

    + +

    + + {% include 'snippets/stars.html' with rating=status.rating %} +

    +
    + {% endif %} + + {% if status.content_warning %} +
    +

    {{ status.content_warning }}

    + + {% trans "Show more" as button_text %} + + {% with text=button_text class="is-small" controls_text="show-status-cw" controls_uid=status.id %} + {% include 'snippets/toggle/open_button.html' %} + {% endwith %} +
    + {% endif %} + + +
    +
    +
    + +{% endwith %} + diff --git a/bookwyrm/templates/snippets/status/generated_status.html b/bookwyrm/templates/snippets/status/generated_status.html new file mode 100644 index 000000000..cb65a6f29 --- /dev/null +++ b/bookwyrm/templates/snippets/status/generated_status.html @@ -0,0 +1,23 @@ +{% spaceless %} + +{% load bookwyrm_tags %} +{% load i18n %} + +{% if not hide_book %} +{% with book=status.book|default:status.mention_books.first %} +
    + +
    +

    {% include 'snippets/book_titleby.html' with book=book %}

    +

    {{ book|book_description|to_markdown|default:""|safe|truncatewords_html:20 }}

    + {% include 'snippets/shelve_button/shelve_button.html' with book=book %} +
    +
    +{% endwith %} +{% endif %} + +{% endspaceless %} diff --git a/bookwyrm/templates/snippets/status/layout.html b/bookwyrm/templates/snippets/status/layout.html new file mode 100644 index 000000000..6014158ff --- /dev/null +++ b/bookwyrm/templates/snippets/status/layout.html @@ -0,0 +1,76 @@ +{% extends 'components/card.html' %} +{% load i18n %} +{% load bookwyrm_tags %} +{% load humanize %} + +{% block card-header %} +
    + {% include 'snippets/status/status_header.html' with status=status %} +
    +{% endblock %} + +{% block card-content %}{% endblock %} + +{% block card-footer %} +{% if moderation_mode and perms.bookwyrm.moderate_post %} + +{% elif no_interact %} +{# nothing here #} +{% elif request.user.is_authenticated %} + + + +{% if not moderation_mode %} + +{% endif %} + +{% else %} + +{% endif %} +{% endblock %} + +{% block card-bonus %} +{% if request.user.is_authenticated and not moderation_mode %} +{% with status.id|uuid as uuid %} + +{% endwith %} +{% endif %} +{% endblock %} diff --git a/bookwyrm/templates/snippets/status/status_body.html b/bookwyrm/templates/snippets/status/status_body.html index ffa71d5e1..2c8a0e045 100644 --- a/bookwyrm/templates/snippets/status/status_body.html +++ b/bookwyrm/templates/snippets/status/status_body.html @@ -1,90 +1,14 @@ -{% extends 'components/card.html' %} -{% load i18n %} - -{% load bookwyrm_tags %} -{% load humanize %} - -{% block card-header %} -

    - {% include 'snippets/status/status_header.html' with status=status %} -

    -{% endblock %} - +{% extends 'snippets/status/layout.html' %} {% block card-content %} - {% include 'snippets/status/status_content.html' with status=status %} -{% endblock %} +{% with status_type=status.status_type %} - -{% block card-footer %} - - - - - -{% if not moderation_mode %} - +{% if status_type == 'GeneratedNote' or status_type == 'Rating' %} + {% include 'snippets/status/generated_status.html' with status=status %} +{% else %} + {% include 'snippets/status/content_status.html' with status=status %} {% endif %} -{% endblock %} - -{% block card-bonus %} -{% if request.user.is_authenticated and not moderation_mode %} -{% with status.id|uuid as uuid %} - {% endwith %} -{% endif %} {% endblock %} + diff --git a/bookwyrm/templates/snippets/status/status_content.html b/bookwyrm/templates/snippets/status/status_content.html deleted file mode 100644 index 402c4aabd..000000000 --- a/bookwyrm/templates/snippets/status/status_content.html +++ /dev/null @@ -1,137 +0,0 @@ -{% spaceless %} - -{% load bookwyrm_tags %} -{% load i18n %} - -{% with status_type=status.status_type %} -
    - {% if status_type == 'Review' or status_type == 'Rating' %} -
    - {% if status.name %} -

    - {{ status.name|escape }} -

    - {% endif %} - - - - - {% if status_type == 'Rating' %} - {# @todo Is it possible to not hard-code the value? #} - - {% endif %} - - - {% include 'snippets/stars.html' with rating=status.rating %} -
    - {% endif %} - - {% if status.content_warning %} -
    -

    {{ status.content_warning }}

    - - {% trans "Show more" as button_text %} - - {% with text=button_text class="is-small" controls_text="show-status-cw" controls_uid=status.id %} - {% include 'snippets/toggle/open_button.html' %} - {% endwith %} -
    - {% endif %} - - -
    - -{% if not hide_book %} - {% if status.book or status.mention_books.count %} -
    - {% if status.book %} - {% with book=status.book %} - {% include 'snippets/status/book_preview.html' %} - {% endwith %} - {% elif status.mention_books.count %} - {% with book=status.mention_books.first %} - {% include 'snippets/status/book_preview.html' %} - {% endwith %} - {% endif %} -
    - {% endif %} -{% endif %} -{% endwith %} -{% endspaceless %} diff --git a/bookwyrm/templates/snippets/status/status_header.html b/bookwyrm/templates/snippets/status/status_header.html index 6493bd548..3b46b9ce1 100644 --- a/bookwyrm/templates/snippets/status/status_header.html +++ b/bookwyrm/templates/snippets/status/status_header.html @@ -1,53 +1,108 @@ {% load bookwyrm_tags %} {% load i18n %} - +
    + -{% if status.status_type == 'GeneratedNote' %} - {{ status.content | safe }} -{% elif status.status_type == 'Rating' %} - {% trans "rated" %} -{% elif status.status_type == 'Review' %} - {% trans "reviewed" %} -{% elif status.status_type == 'Comment' %} - {% trans "commented on" %} -{% elif status.status_type == 'Quotation' %} - {% trans "quoted" %} -{% elif status.reply_parent %} - {% with parent_status=status|parent %} +
    +

    + - {% endwith %} -{% endif %} -{% if status.book %} -{{ status.book.title }} -{% elif status.mention_books %} -{{ status.mention_books.first.title }} -{% endif %} + {% if status.status_type == 'GeneratedNote' %} + {{ status.content | safe }} + {% elif status.status_type == 'Rating' %} + {% trans "rated" %} + {% elif status.status_type == 'Review' %} + {% trans "reviewed" %} + {% elif status.status_type == 'Comment' %} + {% trans "commented on" %} + {% elif status.status_type == 'Quotation' %} + {% trans "quoted" %} + {% elif status.reply_parent %} + {% with parent_status=status|parent %} -{% if status.progress %} -

    -({% if status.progress_mode == 'PG' %}{% include 'snippets/page_text.html' with page=status.progress total_pages=status.book.pages %}{% else %}{{ status.progress }}%{% endif %}) -

    -{% endif %} + {% blocktrans with username=parent_status.user.display_name user_path=parent_status.user.local_path status_path=parent_status.local_path %}replied to {{ username}}'s status{% endblocktrans %} + {% endwith %} + {% endif %} + + {% if status.book %} + {% if status.status_type == 'GeneratedNote' or status.status_type == 'Rating' %} + {{ status.book|title }}{% if status.status_type == 'Rating' %}: + +

    +

    + {{ status.published_date|timesince }} + {% if status.progress %} + + {% if status.progress_mode == 'PG' %} + ({% include 'snippets/page_text.html' with page=status.progress total_pages=status.book.pages %}) + {% else %} + ({{ status.progress }}%) + {% endif %} + + {% endif %} + {% include 'snippets/privacy-icons.html' with item=status %} +

    +
    +
    diff --git a/bookwyrm/templates/snippets/status/status_options.html b/bookwyrm/templates/snippets/status/status_options.html index a76cbc393..549039b7c 100644 --- a/bookwyrm/templates/snippets/status/status_options.html +++ b/bookwyrm/templates/snippets/status/status_options.html @@ -3,27 +3,26 @@ {% load bookwyrm_tags %} {% block dropdown-trigger %} - - {% trans "More options" %} - + +{% trans "More options" %} {% endblock %} {% block dropdown-list %} {% if status.user == request.user %} {# things you can do to your own statuses #} -
  • -
  • {% if status.status_type != 'GeneratedNote' and status.status_type != 'Rating' %} -
  • - @@ -31,13 +30,15 @@ {% endif %} {% else %} {# things you can do to other people's statuses #} -
  • - {% trans "Send direct message" %} +
  • -
  • +
  • -
  • +
  • {% endif %} diff --git a/bookwyrm/templates/snippets/tag.html b/bookwyrm/templates/snippets/tag.html deleted file mode 100644 index 507def723..000000000 --- a/bookwyrm/templates/snippets/tag.html +++ /dev/null @@ -1,23 +0,0 @@ -{% load i18n %} -
    -
    - {% csrf_token %} - - - -
    - - {{ tag.tag.name }} - - {% if tag.tag.identifier in user_tags %} - - {% else %} - - {% endif %} -
    -
    -
    diff --git a/bookwyrm/templates/snippets/toggle/toggle_button.html b/bookwyrm/templates/snippets/toggle/toggle_button.html index fe1823f1d..410f823bd 100644 --- a/bookwyrm/templates/snippets/toggle/toggle_button.html +++ b/bookwyrm/templates/snippets/toggle/toggle_button.html @@ -10,9 +10,12 @@ > {% if icon %} - + {{ text }} + {% elif icon_with_text %} + + {{ text }} {% else %} {{ text }} {% endif %} diff --git a/bookwyrm/templates/snippets/trimmed_text.html b/bookwyrm/templates/snippets/trimmed_text.html index e1728b8f6..0dd19e6ee 100644 --- a/bookwyrm/templates/snippets/trimmed_text.html +++ b/bookwyrm/templates/snippets/trimmed_text.html @@ -1,11 +1,10 @@ -{% spaceless %} {% load bookwyrm_tags %} {% load i18n %} {% with 0|uuid as uuid %} {% if full %} {% with full|to_markdown|safe as full %} - {% with full|to_markdown|safe|truncatewords_html:60 as trimmed %} + {% with full|to_markdown|safe|truncatewords_html:150 as trimmed %} {% if not no_trim and trimmed != full %}
    @@ -46,4 +45,3 @@ {% endwith %} {% endif %} {% endwith %} -{% endspaceless %} diff --git a/bookwyrm/templates/tag.html b/bookwyrm/templates/tag.html deleted file mode 100644 index b6fa67783..000000000 --- a/bookwyrm/templates/tag.html +++ /dev/null @@ -1,14 +0,0 @@ -{% extends 'layout.html' %} -{% load i18n %} -{% load bookwyrm_tags %} - -{% block title %}{{ tag.name }}{% endblock %} - -{% block content %} -
    -

    {% blocktrans %}Books tagged "{{ tag.name }}"{% endblocktrans %}

    - {% include 'snippets/book_tiles.html' with books=books.all %} -
    -{% endblock %} - - diff --git a/bookwyrm/templates/user/shelf.html b/bookwyrm/templates/user/shelf.html index 0732327be..947e9f70b 100644 --- a/bookwyrm/templates/user/shelf.html +++ b/bookwyrm/templates/user/shelf.html @@ -68,63 +68,66 @@
    {% if books|length > 0 %} -
    - - - - - - - - - - {% if ratings %}{% endif %} - {% if shelf.user == request.user %} - - {% endif %} - - {% for book in books %} - - - - - - {% latest_read_through book user as read_through %} - - - {% if ratings %} - - {% endif %} - {% if shelf.user == request.user %} - - {% endif %} - - {% endfor %} +
    {% trans "Cover" %}{% trans "Title" %}{% trans "Author" %}{% trans "Shelved" %}{% trans "Started" %}{% trans "Finished" %}{% trans "Rating" %}
    - {% include 'snippets/book_cover.html' with book=book size="small" %} - - {{ book.title }} - - {% include 'snippets/authors.html' %} - - {{ book.created_date | naturalday }} - - {{ read_through.start_date | naturalday |default_if_none:""}} - - {{ read_through.finish_date | naturalday |default_if_none:""}} - - {% include 'snippets/stars.html' with rating=ratings|dict_key:book.id %} - - {% with right=True %} - {% if not shelf.id %} - {% active_shelf book as current %} - {% include 'snippets/shelf_selector.html' with current=current.shelf class="is-small" %} - {% else %} - {% include 'snippets/shelf_selector.html' with current=shelf class="is-small" %} - {% endif %} - {% endwith %} -
    + + + + + + + + + {% if ratings %}{% endif %} + {% if shelf.user == request.user %} + + {% endif %} + + + + {% for book in books %} + {% spaceless %} + + + + + + {% latest_read_through book user as read_through %} + + + {% if ratings %} + + {% endif %} + {% if shelf.user == request.user %} + + {% endif %} + + {% endspaceless %} + {% endfor %} +
    {% trans "Cover" %}{% trans "Title" %}{% trans "Author" %}{% trans "Shelved" %}{% trans "Started" %}{% trans "Finished" %}{% trans "Rating" %}
    + {% include 'snippets/book_cover.html' with book=book size="small" %} + + {{ book.title }} + + {% include 'snippets/authors.html' %} + + {{ book.created_date | naturalday }} + + {{ read_through.start_date | naturalday |default_if_none:""}} + + {{ read_through.finish_date | naturalday |default_if_none:""}} + + {% include 'snippets/stars.html' with rating=ratings|dict_key:book.id %} + + {% with right=True %} + {% if not shelf.id %} + {% active_shelf book as current %} + {% include 'snippets/shelf_selector.html' with current=current.shelf class="is-small" %} + {% else %} + {% include 'snippets/shelf_selector.html' with current=shelf class="is-small" %} + {% endif %} + {% endwith %} +
    -
    {% else %}

    {% trans "This shelf is empty." %}

    {% if shelf.id and shelf.editable %} diff --git a/bookwyrm/templates/settings/server_filter.html b/bookwyrm/templates/user_admin/server_filter.html similarity index 100% rename from bookwyrm/templates/settings/server_filter.html rename to bookwyrm/templates/user_admin/server_filter.html diff --git a/bookwyrm/templates/user_admin/user.html b/bookwyrm/templates/user_admin/user.html new file mode 100644 index 000000000..463906501 --- /dev/null +++ b/bookwyrm/templates/user_admin/user.html @@ -0,0 +1,19 @@ +{% extends 'settings/admin_layout.html' %} +{% load i18n %} +{% load bookwyrm_tags %} +{% load humanize %} + +{% block title %}{{ user.username }}{% endblock %} +{% block header %}{{ user.username }}{% endblock %} + +{% block panel %} + + +{% include 'user_admin/user_info.html' with user=user %} + +{% include 'user_admin/user_moderation_actions.html' with user=user %} + +{% endblock %} + diff --git a/bookwyrm/templates/settings/user_admin.html b/bookwyrm/templates/user_admin/user_admin.html similarity index 93% rename from bookwyrm/templates/settings/user_admin.html rename to bookwyrm/templates/user_admin/user_admin.html index a96d37f5b..2ab526a9f 100644 --- a/bookwyrm/templates/settings/user_admin.html +++ b/bookwyrm/templates/user_admin/user_admin.html @@ -13,7 +13,7 @@ {% block panel %} -{% include 'settings/user_admin_filters.html' %} +{% include 'user_admin/user_admin_filters.html' %} @@ -41,7 +41,7 @@ {% for user in users %} - + diff --git a/bookwyrm/templates/settings/user_admin_filters.html b/bookwyrm/templates/user_admin/user_admin_filters.html similarity index 51% rename from bookwyrm/templates/settings/user_admin_filters.html rename to bookwyrm/templates/user_admin/user_admin_filters.html index a7b5c8aa4..57e017e5f 100644 --- a/bookwyrm/templates/settings/user_admin_filters.html +++ b/bookwyrm/templates/user_admin/user_admin_filters.html @@ -1,6 +1,6 @@ {% extends 'snippets/filters_panel/filters_panel.html' %} {% block filter_fields %} -{% include 'settings/server_filter.html' %} -{% include 'settings/username_filter.html' %} +{% include 'user_admin/server_filter.html' %} +{% include 'user_admin/username_filter.html' %} {% endblock %} diff --git a/bookwyrm/templates/user_admin/user_info.html b/bookwyrm/templates/user_admin/user_info.html new file mode 100644 index 000000000..e5f5d5806 --- /dev/null +++ b/bookwyrm/templates/user_admin/user_info.html @@ -0,0 +1,56 @@ +{% load i18n %} +{% load bookwyrm_tags %} +
    +
    +

    {% trans "User details" %}

    +
    + {% include 'user/user_preview.html' with user=user %} + {% if user.summary %} +
    + {{ user.summary | to_markdown | safe }} +
    + {% endif %} + +

    {% trans "View user profile" %}

    +
    +
    + {% if not user.local %} + {% with server=user.federated_server %} +
    +

    {% trans "Instance details" %}

    +
    + {% if server %} +
    {{ server.server_name }}
    +
    +
    +
    {% trans "Software:" %}
    +
    {{ server.application_type }}
    +
    +
    +
    {% trans "Version:" %}
    +
    {{ server.application_version }}
    +
    +
    +
    {% trans "Status:" %}
    +
    {{ server.status }}
    +
    +
    + {% if server.notes %} +
    {% trans "Notes" %}
    +
    + {{ server.notes }} +
    + {% endif %} + +

    + {% trans "View instance" %} +

    + {% else %} + {% trans "Not set" %} + {% endif %} +
    +
    + {% endwith %} + {% endif %} +
    + diff --git a/bookwyrm/templates/user_admin/user_moderation_actions.html b/bookwyrm/templates/user_admin/user_moderation_actions.html new file mode 100644 index 000000000..816e787a2 --- /dev/null +++ b/bookwyrm/templates/user_admin/user_moderation_actions.html @@ -0,0 +1,42 @@ +{% load i18n %} +
    +

    {% trans "Actions" %}

    +
    +

    + {% trans "Send direct message" %} +

    +
    + {% csrf_token %} + {% if user.is_active %} + + {% else %} + + {% endif %} + +
    + {% if user.local %} +
    +
    + {% csrf_token %} + + {% if group_form.non_field_errors %} + {{ group_form.non_field_errors }} + {% endif %} + {% with group=user.groups.first %} +
    + +
    + {% for error in group_form.groups.errors %} +

    {{ error | escape }}

    + {% endfor %} + {% endwith %} + + +
    + {% endif %} +
    diff --git a/bookwyrm/templates/settings/username_filter.html b/bookwyrm/templates/user_admin/username_filter.html similarity index 100% rename from bookwyrm/templates/settings/username_filter.html rename to bookwyrm/templates/user_admin/username_filter.html diff --git a/bookwyrm/templatetags/bookwyrm_tags.py b/bookwyrm/templatetags/bookwyrm_tags.py index 775c61903..2ed0cbc2b 100644 --- a/bookwyrm/templatetags/bookwyrm_tags.py +++ b/bookwyrm/templatetags/bookwyrm_tags.py @@ -1,11 +1,8 @@ """ template filters """ from uuid import uuid4 -from datetime import datetime -from dateutil.relativedelta import relativedelta -from django import template +from django import template, utils from django.db.models import Avg -from django.utils import timezone from bookwyrm import models, views from bookwyrm.views.status import to_markdown @@ -16,13 +13,13 @@ register = template.Library() @register.filter(name="dict_key") def dict_key(d, k): - """ Returns the given key from a dictionary. """ + """Returns the given key from a dictionary.""" return d.get(k) or 0 @register.filter(name="rating") def get_rating(book, user): - """ get the overall rating of a book """ + """get the overall rating of a book""" queryset = views.helpers.privacy_filter( user, models.Review.objects.filter(book=book) ) @@ -31,7 +28,7 @@ def get_rating(book, user): @register.filter(name="user_rating") def get_user_rating(book, user): - """ get a user's rating of a book """ + """get a user's rating of a book""" rating = ( models.Review.objects.filter( user=user, @@ -48,33 +45,29 @@ def get_user_rating(book, user): @register.filter(name="username") def get_user_identifier(user): - """ use localname for local users, username for remote """ + """use localname for local users, username for remote""" return user.localname if user.localname else user.username @register.filter(name="notification_count") def get_notification_count(user): - """ how many UNREAD notifications are there """ + """how many UNREAD notifications are there""" return user.notification_set.filter(read=False).count() @register.filter(name="replies") def get_replies(status): - """ get all direct replies to a status """ + """get all direct replies to a status""" # TODO: this limit could cause problems - return ( - models.Status.objects.filter( - reply_parent=status, - deleted=False, - ) - .select_subclasses() - .all()[:10] - ) + return models.Status.objects.filter( + reply_parent=status, + deleted=False, + ).select_subclasses()[:10] @register.filter(name="parent") def get_parent(status): - """ get the reply parent for a status """ + """get the reply parent for a status""" return ( models.Status.objects.filter(id=status.reply_parent_id) .select_subclasses() @@ -84,7 +77,7 @@ def get_parent(status): @register.filter(name="liked") def get_user_liked(user, status): - """ did the given user fav a status? """ + """did the given user fav a status?""" try: models.Favorite.objects.get(user=user, status=status) return True @@ -94,13 +87,13 @@ def get_user_liked(user, status): @register.filter(name="boosted") def get_user_boosted(user, status): - """ did the given user fav a status? """ + """did the given user fav a status?""" return user.id in status.boosters.all().values_list("user", flat=True) @register.filter(name="follow_request_exists") def follow_request_exists(user, requester): - """ see if there is a pending follow request for a user """ + """see if there is a pending follow request for a user""" try: models.UserFollowRequest.objects.filter( user_subject=requester, @@ -113,7 +106,7 @@ def follow_request_exists(user, requester): @register.filter(name="boosted_status") def get_boosted(boost): - """ load a boosted status. have to do this or it wont get foregin keys """ + """load a boosted status. have to do this or it wont get foregin keys""" return ( models.Status.objects.select_subclasses() .filter(id=boost.boosted_status.id) @@ -123,41 +116,19 @@ def get_boosted(boost): @register.filter(name="book_description") def get_book_description(book): - """ use the work's text if the book doesn't have it """ + """use the work's text if the book doesn't have it""" return book.description or book.parent_work.description @register.filter(name="uuid") def get_uuid(identifier): - """ for avoiding clashing ids when there are many forms """ + """for avoiding clashing ids when there are many forms""" return "%s%s" % (identifier, uuid4()) -@register.filter(name="post_date") -def time_since(date): - """ concise time ago function """ - if not isinstance(date, datetime): - return "" - now = timezone.now() - - if date < (now - relativedelta(weeks=1)): - formatter = "%b %-d" - if date.year != now.year: - formatter += " %Y" - return date.strftime(formatter) - delta = relativedelta(now, date) - if delta.days: - return "%dd" % delta.days - if delta.hours: - return "%dh" % delta.hours - if delta.minutes: - return "%dm" % delta.minutes - return "%ds" % delta.seconds - - @register.filter(name="to_markdown") def get_markdown(content): - """ convert markdown to html """ + """convert markdown to html""" if content: return to_markdown(content) return None @@ -165,7 +136,7 @@ def get_markdown(content): @register.filter(name="mentions") def get_mentions(status, user): - """ people to @ in a reply: the parent and all mentions """ + """people to @ in a reply: the parent and all mentions""" mentions = set([status.user] + list(status.mention_users.all())) return ( " ".join("@" + get_user_identifier(m) for m in mentions if not m == user) + " " @@ -174,7 +145,7 @@ def get_mentions(status, user): @register.filter(name="status_preview_name") def get_status_preview_name(obj): - """ text snippet with book context for a status """ + """text snippet with book context for a status""" name = obj.__class__.__name__.lower() if name == "review": return "%s of %s" % (name, obj.book.title) @@ -187,7 +158,7 @@ def get_status_preview_name(obj): @register.filter(name="next_shelf") def get_next_shelf(current_shelf): - """ shelf you'd use to update reading progress """ + """shelf you'd use to update reading progress""" if current_shelf == "to-read": return "reading" if current_shelf == "reading": @@ -197,9 +168,20 @@ def get_next_shelf(current_shelf): return "to-read" +@register.filter(name="title") +def get_title(book): + """display the subtitle if the title is short""" + if not book: + return "" + title = book.title + if len(title) < 6 and book.subtitle: + title = "{:s}: {:s}".format(title, book.subtitle) + return title + + @register.simple_tag(takes_context=False) def related_status(notification): - """ for notifications """ + """for notifications""" if not notification.related_status: return None if hasattr(notification.related_status, "quotation"): @@ -213,7 +195,7 @@ def related_status(notification): @register.simple_tag(takes_context=True) def active_shelf(context, book): - """ check what shelf a user has a book on, if any """ + """check what shelf a user has a book on, if any""" shelf = models.ShelfBook.objects.filter( shelf__user=context["request"].user, book__in=book.parent_work.editions.all() ).first() @@ -222,7 +204,7 @@ def active_shelf(context, book): @register.simple_tag(takes_context=False) def latest_read_through(book, user): - """ the most recent read activity """ + """the most recent read activity""" return ( models.ReadThrough.objects.filter(user=user, book=book) .order_by("-start_date") @@ -232,7 +214,7 @@ def latest_read_through(book, user): @register.simple_tag(takes_context=False) def active_read_through(book, user): - """ the most recent read activity """ + """the most recent read activity""" return ( models.ReadThrough.objects.filter( user=user, book=book, finish_date__isnull=True @@ -244,5 +226,12 @@ def active_read_through(book, user): @register.simple_tag(takes_context=False) def comparison_bool(str1, str2): - """ idk why I need to write a tag for this, it reutrns a bool """ + """idk why I need to write a tag for this, it reutrns a bool""" return str1 == str2 + + +@register.simple_tag(takes_context=False) +def get_lang(): + """get current language, strip to the first two letters""" + language = utils.translation.get_language() + return language[0 : language.find("-")] diff --git a/bookwyrm/tests/activitypub/test_base_activity.py b/bookwyrm/tests/activitypub/test_base_activity.py index 8ec0b703f..77844a222 100644 --- a/bookwyrm/tests/activitypub/test_base_activity.py +++ b/bookwyrm/tests/activitypub/test_base_activity.py @@ -21,10 +21,10 @@ from bookwyrm import models @patch("bookwyrm.activitystreams.ActivityStream.add_status") class BaseActivity(TestCase): - """ the super class for model-linked activitypub dataclasses """ + """the super class for model-linked activitypub dataclasses""" def setUp(self): - """ we're probably going to re-use this so why copy/paste """ + """we're probably going to re-use this so why copy/paste""" self.user = models.User.objects.create_user( "mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" ) @@ -45,28 +45,28 @@ class BaseActivity(TestCase): self.image_data = output.getvalue() def test_init(self, _): - """ simple successfuly init """ + """simple successfuly init""" instance = ActivityObject(id="a", type="b") self.assertTrue(hasattr(instance, "id")) self.assertTrue(hasattr(instance, "type")) def test_init_missing(self, _): - """ init with missing required params """ + """init with missing required params""" with self.assertRaises(ActivitySerializerError): ActivityObject() def test_init_extra_fields(self, _): - """ init ignoring additional fields """ + """init ignoring additional fields""" instance = ActivityObject(id="a", type="b", fish="c") self.assertTrue(hasattr(instance, "id")) self.assertTrue(hasattr(instance, "type")) def test_init_default_field(self, _): - """ replace an existing required field with a default field """ + """replace an existing required field with a default field""" @dataclass(init=False) class TestClass(ActivityObject): - """ test class with default field """ + """test class with default field""" type: str = "TestObject" @@ -75,7 +75,7 @@ class BaseActivity(TestCase): self.assertEqual(instance.type, "TestObject") def test_serialize(self, _): - """ simple function for converting dataclass to dict """ + """simple function for converting dataclass to dict""" instance = ActivityObject(id="a", type="b") serialized = instance.serialize() self.assertIsInstance(serialized, dict) @@ -84,7 +84,7 @@ class BaseActivity(TestCase): @responses.activate def test_resolve_remote_id(self, _): - """ look up or load remote data """ + """look up or load remote data""" # existing item result = resolve_remote_id("http://example.com/a/b", model=models.User) self.assertEqual(result, self.user) @@ -106,14 +106,14 @@ class BaseActivity(TestCase): self.assertEqual(result.name, "MOUSE?? MOUSE!!") def test_to_model_invalid_model(self, _): - """ catch mismatch between activity type and model type """ + """catch mismatch between activity type and model type""" instance = ActivityObject(id="a", type="b") with self.assertRaises(ActivitySerializerError): instance.to_model(model=models.User) @responses.activate def test_to_model_image(self, _): - """ update an image field """ + """update an image field""" activity = activitypub.Person( id=self.user.remote_id, name="New Name", @@ -146,7 +146,7 @@ class BaseActivity(TestCase): self.assertEqual(self.user.key_pair.public_key, "hi") def test_to_model_many_to_many(self, _): - """ annoying that these all need special handling """ + """annoying that these all need special handling""" with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): status = models.Status.objects.create( content="test status", @@ -216,7 +216,7 @@ class BaseActivity(TestCase): @responses.activate def test_set_related_field(self, _): - """ celery task to add back-references to created objects """ + """celery task to add back-references to created objects""" with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): status = models.Status.objects.create( content="test status", diff --git a/bookwyrm/tests/activitypub/test_quotation.py b/bookwyrm/tests/activitypub/test_quotation.py index 1f429dd25..c90348bc3 100644 --- a/bookwyrm/tests/activitypub/test_quotation.py +++ b/bookwyrm/tests/activitypub/test_quotation.py @@ -8,10 +8,10 @@ from bookwyrm import activitypub, models class Quotation(TestCase): - """ we have hecka ways to create statuses """ + """we have hecka ways to create statuses""" def setUp(self): - """ model objects we'll need """ + """model objects we'll need""" with patch("bookwyrm.models.user.set_remote_server.delay"): self.user = models.User.objects.create_user( "mouse", @@ -30,7 +30,7 @@ class Quotation(TestCase): self.status_data = json.loads(datafile.read_bytes()) def test_quotation_activity(self): - """ create a Quoteation ap object from json """ + """create a Quoteation ap object from json""" quotation = activitypub.Quotation(**self.status_data) self.assertEqual(quotation.type, "Quotation") @@ -41,7 +41,7 @@ class Quotation(TestCase): self.assertEqual(quotation.published, "2020-05-10T02:38:31.150343+00:00") def test_activity_to_model(self): - """ create a model instance from an activity object """ + """create a model instance from an activity object""" activity = activitypub.Quotation(**self.status_data) quotation = activity.to_model(model=models.Quotation) diff --git a/bookwyrm/tests/connectors/test_abstract_connector.py b/bookwyrm/tests/connectors/test_abstract_connector.py index 97190c164..4497b4e5e 100644 --- a/bookwyrm/tests/connectors/test_abstract_connector.py +++ b/bookwyrm/tests/connectors/test_abstract_connector.py @@ -10,10 +10,10 @@ from bookwyrm.settings import DOMAIN class AbstractConnector(TestCase): - """ generic code for connecting to outside data sources """ + """generic code for connecting to outside data sources""" def setUp(self): - """ we need an example connector """ + """we need an example connector""" self.connector_info = models.Connector.objects.create( identifier="example.com", connector_file="openlibrary", @@ -38,7 +38,7 @@ class AbstractConnector(TestCase): self.edition_data = edition_data class TestConnector(abstract_connector.AbstractConnector): - """ nothing added here """ + """nothing added here""" def format_search_result(self, search_result): return search_result @@ -81,18 +81,18 @@ class AbstractConnector(TestCase): ) def test_abstract_connector_init(self): - """ barebones connector for search with defaults """ + """barebones connector for search with defaults""" self.assertIsInstance(self.connector.book_mappings, list) def test_is_available(self): - """ this isn't used.... """ + """this isn't used....""" self.assertTrue(self.connector.is_available()) self.connector.max_query_count = 1 self.connector.connector.query_count = 2 self.assertFalse(self.connector.is_available()) def test_get_or_create_book_existing(self): - """ find an existing book by remote/origin id """ + """find an existing book by remote/origin id""" self.assertEqual(models.Book.objects.count(), 1) self.assertEqual( self.book.remote_id, "https://%s/book/%d" % (DOMAIN, self.book.id) @@ -113,7 +113,7 @@ class AbstractConnector(TestCase): @responses.activate def test_get_or_create_book_deduped(self): - """ load remote data and deduplicate """ + """load remote data and deduplicate""" responses.add( responses.GET, "https://example.com/book/abcd", json=self.edition_data ) @@ -125,7 +125,7 @@ class AbstractConnector(TestCase): @responses.activate def test_get_or_create_author(self): - """ load an author """ + """load an author""" self.connector.author_mappings = [ Mapping("id"), Mapping("name"), @@ -142,7 +142,7 @@ class AbstractConnector(TestCase): self.assertEqual(result.origin_id, "https://www.example.com/author") def test_get_or_create_author_existing(self): - """ get an existing author """ + """get an existing author""" author = models.Author.objects.create(name="Test Author") result = self.connector.get_or_create_author(author.remote_id) self.assertEqual(author, result) diff --git a/bookwyrm/tests/connectors/test_abstract_minimal_connector.py b/bookwyrm/tests/connectors/test_abstract_minimal_connector.py index fa7c85f4a..bc5625c95 100644 --- a/bookwyrm/tests/connectors/test_abstract_minimal_connector.py +++ b/bookwyrm/tests/connectors/test_abstract_minimal_connector.py @@ -8,10 +8,10 @@ from bookwyrm.connectors.abstract_connector import Mapping, SearchResult class AbstractConnector(TestCase): - """ generic code for connecting to outside data sources """ + """generic code for connecting to outside data sources""" def setUp(self): - """ we need an example connector """ + """we need an example connector""" self.connector_info = models.Connector.objects.create( identifier="example.com", connector_file="openlibrary", @@ -23,7 +23,7 @@ class AbstractConnector(TestCase): ) class TestConnector(abstract_connector.AbstractMinimalConnector): - """ nothing added here """ + """nothing added here""" def format_search_result(self, search_result): return search_result @@ -43,7 +43,7 @@ class AbstractConnector(TestCase): self.test_connector = TestConnector("example.com") def test_abstract_minimal_connector_init(self): - """ barebones connector for search with defaults """ + """barebones connector for search with defaults""" connector = self.test_connector self.assertEqual(connector.connector, self.connector_info) self.assertEqual(connector.base_url, "https://example.com") @@ -58,7 +58,7 @@ class AbstractConnector(TestCase): @responses.activate def test_search(self): - """ makes an http request to the outside service """ + """makes an http request to the outside service""" responses.add( responses.GET, "https://example.com/search?q=a%20book%20title", @@ -73,7 +73,7 @@ class AbstractConnector(TestCase): @responses.activate def test_search_min_confidence(self): - """ makes an http request to the outside service """ + """makes an http request to the outside service""" responses.add( responses.GET, "https://example.com/search?q=a%20book%20title&min_confidence=1", @@ -85,7 +85,7 @@ class AbstractConnector(TestCase): @responses.activate def test_isbn_search(self): - """ makes an http request to the outside service """ + """makes an http request to the outside service""" responses.add( responses.GET, "https://example.com/isbn?q=123456", @@ -96,7 +96,7 @@ class AbstractConnector(TestCase): self.assertEqual(len(results), 10) def test_search_result(self): - """ a class that stores info about a search result """ + """a class that stores info about a search result""" result = SearchResult( title="Title", key="https://example.com/book/1", @@ -109,21 +109,21 @@ class AbstractConnector(TestCase): self.assertEqual(result.title, "Title") def test_create_mapping(self): - """ maps remote fields for book data to bookwyrm activitypub fields """ + """maps remote fields for book data to bookwyrm activitypub fields""" mapping = Mapping("isbn") self.assertEqual(mapping.local_field, "isbn") self.assertEqual(mapping.remote_field, "isbn") self.assertEqual(mapping.formatter("bb"), "bb") def test_create_mapping_with_remote(self): - """ the remote field is different than the local field """ + """the remote field is different than the local field""" mapping = Mapping("isbn", remote_field="isbn13") self.assertEqual(mapping.local_field, "isbn") self.assertEqual(mapping.remote_field, "isbn13") self.assertEqual(mapping.formatter("bb"), "bb") def test_create_mapping_with_formatter(self): - """ a function is provided to modify the data """ + """a function is provided to modify the data""" formatter = lambda x: "aa" + x mapping = Mapping("isbn", formatter=formatter) self.assertEqual(mapping.local_field, "isbn") diff --git a/bookwyrm/tests/connectors/test_bookwyrm_connector.py b/bookwyrm/tests/connectors/test_bookwyrm_connector.py index 386c13509..46ea54a91 100644 --- a/bookwyrm/tests/connectors/test_bookwyrm_connector.py +++ b/bookwyrm/tests/connectors/test_bookwyrm_connector.py @@ -9,10 +9,10 @@ from bookwyrm.connectors.abstract_connector import SearchResult class BookWyrmConnector(TestCase): - """ this connector doesn't do much, just search """ + """this connector doesn't do much, just search""" def setUp(self): - """ create the connector """ + """create the connector""" models.Connector.objects.create( identifier="example.com", connector_file="bookwyrm_connector", @@ -24,14 +24,14 @@ class BookWyrmConnector(TestCase): self.connector = Connector("example.com") def test_get_or_create_book_existing(self): - """ load book activity """ + """load book activity""" work = models.Work.objects.create(title="Test Work") book = models.Edition.objects.create(title="Test Edition", parent_work=work) result = self.connector.get_or_create_book(book.remote_id) self.assertEqual(book, result) def test_format_search_result(self): - """ create a SearchResult object from search response json """ + """create a SearchResult object from search response json""" datafile = pathlib.Path(__file__).parent.joinpath("../data/bw_search.json") search_data = json.loads(datafile.read_bytes()) results = self.connector.parse_search_data(search_data) @@ -46,7 +46,7 @@ class BookWyrmConnector(TestCase): self.assertEqual(result.connector, self.connector) def test_format_isbn_search_result(self): - """ just gotta attach the connector """ + """just gotta attach the connector""" datafile = pathlib.Path(__file__).parent.joinpath("../data/bw_search.json") search_data = json.loads(datafile.read_bytes()) results = self.connector.parse_isbn_search_data(search_data) diff --git a/bookwyrm/tests/connectors/test_connector_manager.py b/bookwyrm/tests/connectors/test_connector_manager.py index 52589323b..feded6168 100644 --- a/bookwyrm/tests/connectors/test_connector_manager.py +++ b/bookwyrm/tests/connectors/test_connector_manager.py @@ -8,10 +8,10 @@ from bookwyrm.connectors.self_connector import Connector as SelfConnector class ConnectorManager(TestCase): - """ interface between the app and various connectors """ + """interface between the app and various connectors""" def setUp(self): - """ we'll need some books and a connector info entry """ + """we'll need some books and a connector info entry""" self.work = models.Work.objects.create(title="Example Work") self.edition = models.Edition.objects.create( @@ -32,7 +32,7 @@ class ConnectorManager(TestCase): ) def test_get_or_create_connector(self): - """ loads a connector if the data source is known or creates one """ + """loads a connector if the data source is known or creates one""" remote_id = "https://example.com/object/1" connector = connector_manager.get_or_create_connector(remote_id) self.assertIsInstance(connector, BookWyrmConnector) @@ -43,7 +43,7 @@ class ConnectorManager(TestCase): self.assertEqual(connector.identifier, same_connector.identifier) def test_get_connectors(self): - """ load all connectors """ + """load all connectors""" remote_id = "https://example.com/object/1" connector_manager.get_or_create_connector(remote_id) connectors = list(connector_manager.get_connectors()) @@ -52,7 +52,7 @@ class ConnectorManager(TestCase): self.assertIsInstance(connectors[1], BookWyrmConnector) def test_search(self): - """ search all connectors """ + """search all connectors""" results = connector_manager.search("Example") self.assertEqual(len(results), 1) self.assertIsInstance(results[0]["connector"], SelfConnector) @@ -60,7 +60,7 @@ class ConnectorManager(TestCase): self.assertEqual(results[0]["results"][0].title, "Example Edition") def test_search_isbn(self): - """ special handling if a query resembles an isbn """ + """special handling if a query resembles an isbn""" results = connector_manager.search("0000000000") self.assertEqual(len(results), 1) self.assertIsInstance(results[0]["connector"], SelfConnector) @@ -68,20 +68,20 @@ class ConnectorManager(TestCase): self.assertEqual(results[0]["results"][0].title, "Example Edition") def test_local_search(self): - """ search only the local database """ + """search only the local database""" results = connector_manager.local_search("Example") self.assertEqual(len(results), 1) self.assertEqual(results[0].title, "Example Edition") def test_first_search_result(self): - """ only get one search result """ + """only get one search result""" result = connector_manager.first_search_result("Example") self.assertEqual(result.title, "Example Edition") no_result = connector_manager.first_search_result("dkjfhg") self.assertIsNone(no_result) def test_load_connector(self): - """ load a connector object from the database entry """ + """load a connector object from the database entry""" connector = connector_manager.load_connector(self.connector) self.assertIsInstance(connector, SelfConnector) self.assertEqual(connector.identifier, "test_connector") diff --git a/bookwyrm/tests/connectors/test_openlibrary_connector.py b/bookwyrm/tests/connectors/test_openlibrary_connector.py index 3cff4fb0a..699b26ed4 100644 --- a/bookwyrm/tests/connectors/test_openlibrary_connector.py +++ b/bookwyrm/tests/connectors/test_openlibrary_connector.py @@ -16,10 +16,10 @@ from bookwyrm.connectors.connector_manager import ConnectorException class Openlibrary(TestCase): - """ test loading data from openlibrary.org """ + """test loading data from openlibrary.org""" def setUp(self): - """ creates the connector we'll use """ + """creates the connector we'll use""" models.Connector.objects.create( identifier="openlibrary.org", name="OpenLibrary", @@ -42,7 +42,7 @@ class Openlibrary(TestCase): self.edition_list_data = json.loads(edition_list_file.read_bytes()) def test_get_remote_id_from_data(self): - """ format the remote id from the data """ + """format the remote id from the data""" data = {"key": "/work/OL1234W"} result = self.connector.get_remote_id_from_data(data) self.assertEqual(result, "https://openlibrary.org/work/OL1234W") @@ -51,13 +51,13 @@ class Openlibrary(TestCase): self.connector.get_remote_id_from_data({}) def test_is_work_data(self): - """ detect if the loaded json is a work """ + """detect if the loaded json is a work""" self.assertEqual(self.connector.is_work_data(self.work_data), True) self.assertEqual(self.connector.is_work_data(self.edition_data), False) @responses.activate def test_get_edition_from_work_data(self): - """ loads a list of editions """ + """loads a list of editions""" data = {"key": "/work/OL1234W"} responses.add( responses.GET, @@ -74,7 +74,7 @@ class Openlibrary(TestCase): @responses.activate def test_get_work_from_edition_data(self): - """ loads a list of editions """ + """loads a list of editions""" data = {"works": [{"key": "/work/OL1234W"}]} responses.add( responses.GET, @@ -87,7 +87,7 @@ class Openlibrary(TestCase): @responses.activate def test_get_authors_from_data(self): - """ find authors in data """ + """find authors in data""" responses.add( responses.GET, "https://openlibrary.org/authors/OL382982A", @@ -112,13 +112,13 @@ class Openlibrary(TestCase): self.assertEqual(result.openlibrary_key, "OL453734A") def test_get_cover_url(self): - """ formats a url that should contain the cover image """ + """formats a url that should contain the cover image""" blob = ["image"] result = self.connector.get_cover_url(blob) self.assertEqual(result, "https://covers.openlibrary.org/b/id/image-L.jpg") def test_parse_search_result(self): - """ extract the results from the search json response """ + """extract the results from the search json response""" datafile = pathlib.Path(__file__).parent.joinpath("../data/ol_search.json") search_data = json.loads(datafile.read_bytes()) result = self.connector.parse_search_data(search_data) @@ -126,7 +126,7 @@ class Openlibrary(TestCase): self.assertEqual(len(result), 2) def test_format_search_result(self): - """ translate json from openlibrary into SearchResult """ + """translate json from openlibrary into SearchResult""" datafile = pathlib.Path(__file__).parent.joinpath("../data/ol_search.json") search_data = json.loads(datafile.read_bytes()) results = self.connector.parse_search_data(search_data) @@ -141,7 +141,7 @@ class Openlibrary(TestCase): self.assertEqual(result.connector, self.connector) def test_parse_isbn_search_result(self): - """ extract the results from the search json response """ + """extract the results from the search json response""" datafile = pathlib.Path(__file__).parent.joinpath("../data/ol_isbn_search.json") search_data = json.loads(datafile.read_bytes()) result = self.connector.parse_isbn_search_data(search_data) @@ -149,7 +149,7 @@ class Openlibrary(TestCase): self.assertEqual(len(result), 1) def test_format_isbn_search_result(self): - """ translate json from openlibrary into SearchResult """ + """translate json from openlibrary into SearchResult""" datafile = pathlib.Path(__file__).parent.joinpath("../data/ol_isbn_search.json") search_data = json.loads(datafile.read_bytes()) results = self.connector.parse_isbn_search_data(search_data) @@ -165,7 +165,7 @@ class Openlibrary(TestCase): @responses.activate def test_load_edition_data(self): - """ format url from key and make request """ + """format url from key and make request""" key = "OL1234W" responses.add( responses.GET, @@ -177,7 +177,7 @@ class Openlibrary(TestCase): @responses.activate def test_expand_book_data(self): - """ given a book, get more editions """ + """given a book, get more editions""" work = models.Work.objects.create(title="Test Work", openlibrary_key="OL1234W") edition = models.Edition.objects.create(title="Test Edition", parent_work=work) @@ -194,29 +194,29 @@ class Openlibrary(TestCase): self.connector.expand_book_data(work) def test_get_description(self): - """ should do some cleanup on the description data """ + """should do some cleanup on the description data""" description = get_description(self.work_data["description"]) expected = "First in the Old Kingdom/Abhorsen series." self.assertEqual(description, expected) def test_get_openlibrary_key(self): - """ extracts the uuid """ + """extracts the uuid""" key = get_openlibrary_key("/books/OL27320736M") self.assertEqual(key, "OL27320736M") def test_get_languages(self): - """ looks up languages from a list """ + """looks up languages from a list""" languages = get_languages(self.edition_data["languages"]) self.assertEqual(languages, ["English"]) def test_pick_default_edition(self): - """ detect if the loaded json is an edition """ + """detect if the loaded json is an edition""" edition = pick_default_edition(self.edition_list_data["entries"]) self.assertEqual(edition["key"], "/books/OL9788823M") @responses.activate def test_create_edition_from_data(self): - """ okay but can it actually create an edition with proper metadata """ + """okay but can it actually create an edition with proper metadata""" work = models.Work.objects.create(title="Hello") responses.add( responses.GET, @@ -240,7 +240,7 @@ class Openlibrary(TestCase): self.assertEqual(result.physical_format, "Hardcover") def test_ignore_edition(self): - """ skip editions with poor metadata """ + """skip editions with poor metadata""" self.assertFalse(ignore_edition({"isbn_13": "hi"})) self.assertFalse(ignore_edition({"oclc_numbers": "hi"})) self.assertFalse(ignore_edition({"covers": "hi"})) diff --git a/bookwyrm/tests/connectors/test_self_connector.py b/bookwyrm/tests/connectors/test_self_connector.py index 9925f5943..eee7d00cf 100644 --- a/bookwyrm/tests/connectors/test_self_connector.py +++ b/bookwyrm/tests/connectors/test_self_connector.py @@ -9,10 +9,10 @@ from bookwyrm.settings import DOMAIN class SelfConnector(TestCase): - """ just uses local data """ + """just uses local data""" def setUp(self): - """ creating the connector """ + """creating the connector""" models.Connector.objects.create( identifier=DOMAIN, name="Local", @@ -27,7 +27,7 @@ class SelfConnector(TestCase): self.connector = Connector(DOMAIN) def test_format_search_result(self): - """ create a SearchResult """ + """create a SearchResult""" author = models.Author.objects.create(name="Anonymous") edition = models.Edition.objects.create( title="Edition of Example Work", @@ -42,7 +42,7 @@ class SelfConnector(TestCase): self.assertEqual(result.connector, self.connector) def test_search_rank(self): - """ prioritize certain results """ + """prioritize certain results""" author = models.Author.objects.create(name="Anonymous") edition = models.Edition.objects.create( title="Edition of Example Work", @@ -78,7 +78,7 @@ class SelfConnector(TestCase): self.assertEqual(results[2].title, "Edition of Example Work") def test_search_multiple_editions(self): - """ it should get rid of duplicate editions for the same work """ + """it should get rid of duplicate editions for the same work""" work = models.Work.objects.create(title="Work Title") edition_1 = models.Edition.objects.create( title="Edition 1 Title", parent_work=work diff --git a/bookwyrm/tests/data/ap_user_rat.json b/bookwyrm/tests/data/ap_user_rat.json new file mode 100644 index 000000000..0e36f1c62 --- /dev/null +++ b/bookwyrm/tests/data/ap_user_rat.json @@ -0,0 +1,39 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "schema": "http://schema.org#", + "PropertyValue": "schema:PropertyValue", + "value": "schema:value" + } + ], + "id": "https://example.com/users/rat", + "type": "Person", + "preferredUsername": "rat", + "name": "RAT???", + "inbox": "https://example.com/users/rat/inbox", + "outbox": "https://example.com/users/rat/outbox", + "followers": "https://example.com/users/rat/followers", + "following": "https://example.com/users/rat/following", + "summary": "", + "publicKey": { + "id": "https://example.com/users/rat/#main-key", + "owner": "https://example.com/users/rat", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC6QisDrjOQvkRo/MqNmSYPwqtt\nCxg/8rCW+9jKbFUKvqjTeKVotEE85122v/DCvobCCdfQuYIFdVMk+dB1xJ0iPGPg\nyU79QHY22NdV9mFKA2qtXVVxb5cxpA4PlwOHM6PM/k8B+H09OUrop2aPUAYwy+vg\n+MXyz8bAXrIS1kq6fQIDAQAB\n-----END PUBLIC KEY-----" + }, + "endpoints": { + "sharedInbox": "https://example.com/inbox" + }, + "bookwyrmUser": true, + "manuallyApprovesFollowers": false, + "discoverable": true, + "devices": "https://friend.camp/users/tripofmice/collections/devices", + "tag": [], + "icon": { + "type": "Image", + "mediaType": "image/png", + "url": "https://example.com/images/avatars/AL-2-crop-50.png" + } +} diff --git a/bookwyrm/tests/data/bw_edition.json b/bookwyrm/tests/data/bw_edition.json index 0cc17d29a..6194e4090 100644 --- a/bookwyrm/tests/data/bw_edition.json +++ b/bookwyrm/tests/data/bw_edition.json @@ -1,5 +1,6 @@ { "id": "https://bookwyrm.social/book/5989", + "lastEditedBy": "https://example.com/users/rat", "type": "Edition", "authors": [ "https://bookwyrm.social/author/417" diff --git a/bookwyrm/tests/importers/test_goodreads_import.py b/bookwyrm/tests/importers/test_goodreads_import.py index 6e9caaf4d..0e39d5ecc 100644 --- a/bookwyrm/tests/importers/test_goodreads_import.py +++ b/bookwyrm/tests/importers/test_goodreads_import.py @@ -14,10 +14,10 @@ from bookwyrm.settings import DOMAIN class GoodreadsImport(TestCase): - """ importing from goodreads csv """ + """importing from goodreads csv""" def setUp(self): - """ use a test csv """ + """use a test csv""" self.importer = GoodreadsImporter() datafile = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv") self.csv = open(datafile, "r", encoding=self.importer.encoding) @@ -44,7 +44,7 @@ class GoodreadsImport(TestCase): ) def test_create_job(self): - """ creates the import job entry and checks csv """ + """creates the import job entry and checks csv""" import_job = self.importer.create_job(self.user, self.csv, False, "public") self.assertEqual(import_job.user, self.user) self.assertEqual(import_job.include_reviews, False) @@ -60,7 +60,7 @@ class GoodreadsImport(TestCase): self.assertEqual(import_items[2].data["Book Id"], "28694510") def test_create_retry_job(self): - """ trying again with items that didn't import """ + """trying again with items that didn't import""" import_job = self.importer.create_job(self.user, self.csv, False, "unlisted") import_items = models.ImportItem.objects.filter(job=import_job).all()[:2] @@ -78,7 +78,7 @@ class GoodreadsImport(TestCase): self.assertEqual(retry_items[1].data["Book Id"], "52691223") def test_start_import(self): - """ begin loading books """ + """begin loading books""" import_job = self.importer.create_job(self.user, self.csv, False, "unlisted") MockTask = namedtuple("Task", ("id")) mock_task = MockTask(7) @@ -90,7 +90,7 @@ class GoodreadsImport(TestCase): @responses.activate def test_import_data(self): - """ resolve entry """ + """resolve entry""" import_job = self.importer.create_job(self.user, self.csv, False, "unlisted") book = models.Edition.objects.create(title="Test Book") @@ -105,7 +105,7 @@ class GoodreadsImport(TestCase): self.assertEqual(import_item.book.id, book.id) def test_handle_imported_book(self): - """ goodreads import added a book, this adds related connections """ + """goodreads import added a book, this adds related connections""" shelf = self.user.shelf_set.filter(identifier="read").first() self.assertIsNone(shelf.books.first()) @@ -138,7 +138,7 @@ class GoodreadsImport(TestCase): self.assertEqual(readthrough.finish_date.day, 25) def test_handle_imported_book_already_shelved(self): - """ goodreads import added a book, this adds related connections """ + """goodreads import added a book, this adds related connections""" with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): shelf = self.user.shelf_set.filter(identifier="to-read").first() models.ShelfBook.objects.create(shelf=shelf, user=self.user, book=self.book) @@ -171,7 +171,7 @@ class GoodreadsImport(TestCase): self.assertEqual(readthrough.finish_date.day, 25) def test_handle_import_twice(self): - """ re-importing books """ + """re-importing books""" shelf = self.user.shelf_set.filter(identifier="read").first() import_job = models.ImportJob.objects.create(user=self.user) datafile = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv") @@ -206,7 +206,7 @@ class GoodreadsImport(TestCase): @patch("bookwyrm.activitystreams.ActivityStream.add_status") def test_handle_imported_book_review(self, _): - """ goodreads review import """ + """goodreads review import""" import_job = models.ImportJob.objects.create(user=self.user) datafile = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv") csv_file = open(datafile, "r") @@ -229,7 +229,7 @@ class GoodreadsImport(TestCase): self.assertEqual(review.privacy, "unlisted") def test_handle_imported_book_reviews_disabled(self): - """ goodreads review import """ + """goodreads review import""" import_job = models.ImportJob.objects.create(user=self.user) datafile = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv") csv_file = open(datafile, "r") diff --git a/bookwyrm/tests/importers/test_librarything_import.py b/bookwyrm/tests/importers/test_librarything_import.py index 5e1d778e4..5ae0944c0 100644 --- a/bookwyrm/tests/importers/test_librarything_import.py +++ b/bookwyrm/tests/importers/test_librarything_import.py @@ -13,10 +13,10 @@ from bookwyrm.settings import DOMAIN class LibrarythingImport(TestCase): - """ importing from librarything tsv """ + """importing from librarything tsv""" def setUp(self): - """ use a test tsv """ + """use a test tsv""" self.importer = LibrarythingImporter() datafile = pathlib.Path(__file__).parent.joinpath("../data/librarything.tsv") @@ -45,7 +45,7 @@ class LibrarythingImport(TestCase): ) def test_create_job(self): - """ creates the import job entry and checks csv """ + """creates the import job entry and checks csv""" import_job = self.importer.create_job(self.user, self.csv, False, "public") self.assertEqual(import_job.user, self.user) self.assertEqual(import_job.include_reviews, False) @@ -61,7 +61,7 @@ class LibrarythingImport(TestCase): self.assertEqual(import_items[2].data["Book Id"], "5015399") def test_create_retry_job(self): - """ trying again with items that didn't import """ + """trying again with items that didn't import""" import_job = self.importer.create_job(self.user, self.csv, False, "unlisted") import_items = models.ImportItem.objects.filter(job=import_job).all()[:2] @@ -80,7 +80,7 @@ class LibrarythingImport(TestCase): @responses.activate def test_import_data(self): - """ resolve entry """ + """resolve entry""" import_job = self.importer.create_job(self.user, self.csv, False, "unlisted") book = models.Edition.objects.create(title="Test Book") @@ -95,7 +95,7 @@ class LibrarythingImport(TestCase): self.assertEqual(import_item.book.id, book.id) def test_handle_imported_book(self): - """ librarything import added a book, this adds related connections """ + """librarything import added a book, this adds related connections""" shelf = self.user.shelf_set.filter(identifier="read").first() self.assertIsNone(shelf.books.first()) @@ -130,7 +130,7 @@ class LibrarythingImport(TestCase): self.assertEqual(readthrough.finish_date.day, 8) def test_handle_imported_book_already_shelved(self): - """ librarything import added a book, this adds related connections """ + """librarything import added a book, this adds related connections""" with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): shelf = self.user.shelf_set.filter(identifier="to-read").first() models.ShelfBook.objects.create(shelf=shelf, user=self.user, book=self.book) @@ -165,7 +165,7 @@ class LibrarythingImport(TestCase): self.assertEqual(readthrough.finish_date.day, 8) def test_handle_import_twice(self): - """ re-importing books """ + """re-importing books""" shelf = self.user.shelf_set.filter(identifier="read").first() import_job = models.ImportJob.objects.create(user=self.user) datafile = pathlib.Path(__file__).parent.joinpath("../data/librarything.tsv") @@ -202,7 +202,7 @@ class LibrarythingImport(TestCase): @patch("bookwyrm.activitystreams.ActivityStream.add_status") def test_handle_imported_book_review(self, _): - """ librarything review import """ + """librarything review import""" import_job = models.ImportJob.objects.create(user=self.user) datafile = pathlib.Path(__file__).parent.joinpath("../data/librarything.tsv") csv_file = open(datafile, "r", encoding=self.importer.encoding) @@ -225,7 +225,7 @@ class LibrarythingImport(TestCase): self.assertEqual(review.privacy, "unlisted") def test_handle_imported_book_reviews_disabled(self): - """ librarything review import """ + """librarything review import""" import_job = models.ImportJob.objects.create(user=self.user) datafile = pathlib.Path(__file__).parent.joinpath("../data/librarything.tsv") csv_file = open(datafile, "r", encoding=self.importer.encoding) diff --git a/bookwyrm/tests/management/test_populate_streams.py b/bookwyrm/tests/management/test_populate_streams.py index 6a9b6b8ac..d187c054b 100644 --- a/bookwyrm/tests/management/test_populate_streams.py +++ b/bookwyrm/tests/management/test_populate_streams.py @@ -8,10 +8,10 @@ from bookwyrm.management.commands.populate_streams import populate_streams @patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") class Activitystreams(TestCase): - """ using redis to build activity streams """ + """using redis to build activity streams""" def setUp(self): - """ we need some stuff """ + """we need some stuff""" self.local_user = models.User.objects.create_user( "mouse", "mouse@mouse.mouse", "password", local=True, localname="mouse" ) @@ -31,7 +31,7 @@ class Activitystreams(TestCase): self.book = models.Edition.objects.create(title="test book") def test_populate_streams(self, _): - """ make sure the function on the redis manager gets called """ + """make sure the function on the redis manager gets called""" with patch("bookwyrm.activitystreams.ActivityStream.add_status"): models.Comment.objects.create( user=self.local_user, content="hi", book=self.book diff --git a/bookwyrm/tests/models/test_activitypub_mixin.py b/bookwyrm/tests/models/test_activitypub_mixin.py index 0d1acd978..1c0975c44 100644 --- a/bookwyrm/tests/models/test_activitypub_mixin.py +++ b/bookwyrm/tests/models/test_activitypub_mixin.py @@ -15,10 +15,10 @@ from bookwyrm.models.activitypub_mixin import ActivityMixin, ObjectMixin @patch("bookwyrm.activitystreams.ActivityStream.add_status") class ActivitypubMixins(TestCase): - """ functionality shared across models """ + """functionality shared across models""" def setUp(self): - """ shared data """ + """shared data""" self.local_user = models.User.objects.create_user( "mouse", "mouse@mouse.com", "mouseword", local=True, localname="mouse" ) @@ -46,16 +46,16 @@ class ActivitypubMixins(TestCase): # ActivitypubMixin def test_to_activity(self, _): - """ model to ActivityPub json """ + """model to ActivityPub json""" @dataclass(init=False) class TestActivity(ActivityObject): - """ real simple mock """ + """real simple mock""" type: str = "Test" class TestModel(ActivitypubMixin, base_model.BookWyrmModel): - """ real simple mock model because BookWyrmModel is abstract """ + """real simple mock model because BookWyrmModel is abstract""" instance = TestModel() instance.remote_id = "https://www.example.com/test" @@ -67,7 +67,7 @@ class ActivitypubMixins(TestCase): self.assertEqual(activity["type"], "Test") def test_find_existing_by_remote_id(self, _): - """ attempt to match a remote id to an object in the db """ + """attempt to match a remote id to an object in the db""" # uses a different remote id scheme # this isn't really part of this test directly but it's helpful to state book = models.Edition.objects.create( @@ -100,7 +100,7 @@ class ActivitypubMixins(TestCase): result = models.Status.find_existing_by_remote_id("https://comment.net") def test_find_existing(self, _): - """ match a blob of data to a model """ + """match a blob of data to a model""" book = models.Edition.objects.create( title="Test edition", openlibrary_key="OL1234", @@ -110,7 +110,7 @@ class ActivitypubMixins(TestCase): self.assertEqual(result, book) def test_get_recipients_public_object(self, _): - """ determines the recipients for an object's broadcast """ + """determines the recipients for an object's broadcast""" MockSelf = namedtuple("Self", ("privacy")) mock_self = MockSelf("public") recipients = ActivitypubMixin.get_recipients(mock_self) @@ -118,7 +118,7 @@ class ActivitypubMixins(TestCase): self.assertEqual(recipients[0], self.remote_user.inbox) def test_get_recipients_public_user_object_no_followers(self, _): - """ determines the recipients for a user's object broadcast """ + """determines the recipients for a user's object broadcast""" MockSelf = namedtuple("Self", ("privacy", "user")) mock_self = MockSelf("public", self.local_user) @@ -126,7 +126,7 @@ class ActivitypubMixins(TestCase): self.assertEqual(len(recipients), 0) def test_get_recipients_public_user_object(self, _): - """ determines the recipients for a user's object broadcast """ + """determines the recipients for a user's object broadcast""" MockSelf = namedtuple("Self", ("privacy", "user")) mock_self = MockSelf("public", self.local_user) self.local_user.followers.add(self.remote_user) @@ -136,7 +136,7 @@ class ActivitypubMixins(TestCase): self.assertEqual(recipients[0], self.remote_user.inbox) def test_get_recipients_public_user_object_with_mention(self, _): - """ determines the recipients for a user's object broadcast """ + """determines the recipients for a user's object broadcast""" MockSelf = namedtuple("Self", ("privacy", "user")) mock_self = MockSelf("public", self.local_user) self.local_user.followers.add(self.remote_user) @@ -155,11 +155,11 @@ class ActivitypubMixins(TestCase): recipients = ActivitypubMixin.get_recipients(mock_self) self.assertEqual(len(recipients), 2) - self.assertEqual(recipients[0], another_remote_user.inbox) - self.assertEqual(recipients[1], self.remote_user.inbox) + self.assertTrue(another_remote_user.inbox in recipients) + self.assertTrue(self.remote_user.inbox in recipients) def test_get_recipients_direct(self, _): - """ determines the recipients for a user's object broadcast """ + """determines the recipients for a user's object broadcast""" MockSelf = namedtuple("Self", ("privacy", "user")) mock_self = MockSelf("public", self.local_user) self.local_user.followers.add(self.remote_user) @@ -181,7 +181,7 @@ class ActivitypubMixins(TestCase): self.assertEqual(recipients[0], another_remote_user.inbox) def test_get_recipients_combine_inboxes(self, _): - """ should combine users with the same shared_inbox """ + """should combine users with the same shared_inbox""" self.remote_user.shared_inbox = "http://example.com/inbox" self.remote_user.save(broadcast=False) with patch("bookwyrm.models.user.set_remote_server.delay"): @@ -205,7 +205,7 @@ class ActivitypubMixins(TestCase): self.assertEqual(recipients[0], "http://example.com/inbox") def test_get_recipients_software(self, _): - """ should differentiate between bookwyrm and other remote users """ + """should differentiate between bookwyrm and other remote users""" with patch("bookwyrm.models.user.set_remote_server.delay"): another_remote_user = models.User.objects.create_user( "nutria", @@ -235,13 +235,13 @@ class ActivitypubMixins(TestCase): # ObjectMixin def test_object_save_create(self, _): - """ should save uneventufully when broadcast is disabled """ + """should save uneventufully when broadcast is disabled""" class Success(Exception): - """ this means we got to the right method """ + """this means we got to the right method""" class ObjectModel(ObjectMixin, base_model.BookWyrmModel): - """ real simple mock model because BookWyrmModel is abstract """ + """real simple mock model because BookWyrmModel is abstract""" user = models.fields.ForeignKey("User", on_delete=db.models.CASCADE) @@ -252,7 +252,7 @@ class ActivitypubMixins(TestCase): def broadcast( self, activity, sender, **kwargs ): # pylint: disable=arguments-differ - """ do something """ + """do something""" raise Success() def to_create_activity(self, user): # pylint: disable=arguments-differ @@ -266,13 +266,13 @@ class ActivitypubMixins(TestCase): ObjectModel(user=None).save() def test_object_save_update(self, _): - """ should save uneventufully when broadcast is disabled """ + """should save uneventufully when broadcast is disabled""" class Success(Exception): - """ this means we got to the right method """ + """this means we got to the right method""" class UpdateObjectModel(ObjectMixin, base_model.BookWyrmModel): - """ real simple mock model because BookWyrmModel is abstract """ + """real simple mock model because BookWyrmModel is abstract""" user = models.fields.ForeignKey("User", on_delete=db.models.CASCADE) last_edited_by = models.fields.ForeignKey( @@ -292,13 +292,13 @@ class ActivitypubMixins(TestCase): UpdateObjectModel(id=1, last_edited_by=self.local_user).save() def test_object_save_delete(self, _): - """ should create delete activities when objects are deleted by flag """ + """should create delete activities when objects are deleted by flag""" class ActivitySuccess(Exception): - """ this means we got to the right method """ + """this means we got to the right method""" class DeletableObjectModel(ObjectMixin, base_model.BookWyrmModel): - """ real simple mock model because BookWyrmModel is abstract """ + """real simple mock model because BookWyrmModel is abstract""" user = models.fields.ForeignKey("User", on_delete=db.models.CASCADE) deleted = models.fields.BooleanField() @@ -314,7 +314,7 @@ class ActivitypubMixins(TestCase): DeletableObjectModel(id=1, user=self.local_user, deleted=True).save() def test_to_delete_activity(self, _): - """ wrapper for Delete activity """ + """wrapper for Delete activity""" MockSelf = namedtuple("Self", ("remote_id", "to_activity")) mock_self = MockSelf( "https://example.com/status/1", lambda *args: self.object_mock @@ -329,7 +329,7 @@ class ActivitypubMixins(TestCase): ) def test_to_update_activity(self, _): - """ ditto above but for Update """ + """ditto above but for Update""" MockSelf = namedtuple("Self", ("remote_id", "to_activity")) mock_self = MockSelf( "https://example.com/status/1", lambda *args: self.object_mock @@ -347,7 +347,7 @@ class ActivitypubMixins(TestCase): # Activity mixin def test_to_undo_activity(self, _): - """ and again, for Undo """ + """and again, for Undo""" MockSelf = namedtuple("Self", ("remote_id", "to_activity", "user")) mock_self = MockSelf( "https://example.com/status/1", diff --git a/bookwyrm/tests/models/test_base_model.py b/bookwyrm/tests/models/test_base_model.py index 442f98ca1..5a8350b2e 100644 --- a/bookwyrm/tests/models/test_base_model.py +++ b/bookwyrm/tests/models/test_base_model.py @@ -8,10 +8,10 @@ from bookwyrm.settings import DOMAIN class BaseModel(TestCase): - """ functionality shared across models """ + """functionality shared across models""" def setUp(self): - """ shared data """ + """shared data""" self.local_user = models.User.objects.create_user( "mouse", "mouse@mouse.com", "mouseword", local=True, localname="mouse" ) @@ -27,14 +27,14 @@ class BaseModel(TestCase): ) def test_remote_id(self): - """ these should be generated """ + """these should be generated""" instance = base_model.BookWyrmModel() instance.id = 1 expected = instance.get_remote_id() self.assertEqual(expected, "https://%s/bookwyrmmodel/1" % DOMAIN) def test_remote_id_with_user(self): - """ format of remote id when there's a user object """ + """format of remote id when there's a user object""" instance = base_model.BookWyrmModel() instance.user = self.local_user instance.id = 1 @@ -42,7 +42,7 @@ class BaseModel(TestCase): self.assertEqual(expected, "https://%s/user/mouse/bookwyrmmodel/1" % DOMAIN) def test_set_remote_id(self): - """ this function sets remote ids after creation """ + """this function sets remote ids after creation""" # using Work because it BookWrymModel is abstract and this requires save # Work is a relatively not-fancy model. instance = models.Work.objects.create(title="work title") @@ -59,7 +59,7 @@ class BaseModel(TestCase): @patch("bookwyrm.activitystreams.ActivityStream.add_status") def test_object_visible_to_user(self, _): - """ does a user have permission to view an object """ + """does a user have permission to view an object""" obj = models.Status.objects.create( content="hi", user=self.remote_user, privacy="public" ) @@ -88,7 +88,7 @@ class BaseModel(TestCase): @patch("bookwyrm.activitystreams.ActivityStream.add_status") def test_object_visible_to_user_follower(self, _): - """ what you can see if you follow a user """ + """what you can see if you follow a user""" self.remote_user.followers.add(self.local_user) obj = models.Status.objects.create( content="hi", user=self.remote_user, privacy="followers" @@ -108,7 +108,7 @@ class BaseModel(TestCase): @patch("bookwyrm.activitystreams.ActivityStream.add_status") def test_object_visible_to_user_blocked(self, _): - """ you can't see it if they block you """ + """you can't see it if they block you""" self.remote_user.blocks.add(self.local_user) obj = models.Status.objects.create( content="hi", user=self.remote_user, privacy="public" diff --git a/bookwyrm/tests/models/test_book_model.py b/bookwyrm/tests/models/test_book_model.py index 14ab0c572..c80cc4a84 100644 --- a/bookwyrm/tests/models/test_book_model.py +++ b/bookwyrm/tests/models/test_book_model.py @@ -8,10 +8,10 @@ from bookwyrm.models.book import isbn_10_to_13, isbn_13_to_10 class Book(TestCase): - """ not too much going on in the books model but here we are """ + """not too much going on in the books model but here we are""" def setUp(self): - """ we'll need some books """ + """we'll need some books""" self.work = models.Work.objects.create( title="Example Work", remote_id="https://example.com/book/1" ) @@ -25,17 +25,17 @@ class Book(TestCase): ) def test_remote_id(self): - """ fanciness with remote/origin ids """ + """fanciness with remote/origin ids""" remote_id = "https://%s/book/%d" % (settings.DOMAIN, self.work.id) self.assertEqual(self.work.get_remote_id(), remote_id) self.assertEqual(self.work.remote_id, remote_id) def test_create_book(self): - """ you shouldn't be able to create Books (only editions and works) """ + """you shouldn't be able to create Books (only editions and works)""" self.assertRaises(ValueError, models.Book.objects.create, title="Invalid Book") def test_isbn_10_to_13(self): - """ checksums and so on """ + """checksums and so on""" isbn_10 = "178816167X" isbn_13 = isbn_10_to_13(isbn_10) self.assertEqual(isbn_13, "9781788161671") @@ -45,7 +45,7 @@ class Book(TestCase): self.assertEqual(isbn_13, "9781788161671") def test_isbn_13_to_10(self): - """ checksums and so on """ + """checksums and so on""" isbn_13 = "9781788161671" isbn_10 = isbn_13_to_10(isbn_13) self.assertEqual(isbn_10, "178816167X") @@ -55,7 +55,7 @@ class Book(TestCase): self.assertEqual(isbn_10, "178816167X") def test_get_edition_info(self): - """ text slug about an edition """ + """text slug about an edition""" book = models.Edition.objects.create(title="Test Edition") self.assertEqual(book.edition_info, "") @@ -77,7 +77,7 @@ class Book(TestCase): self.assertEqual(book.alt_text, "Test Edition (worm, Glorbish language, 2020)") def test_get_rank(self): - """ sets the data quality index for the book """ + """sets the data quality index for the book""" # basic rank self.assertEqual(self.first_edition.edition_rank, 0) diff --git a/bookwyrm/tests/models/test_federated_server.py b/bookwyrm/tests/models/test_federated_server.py index 4e9e8b686..43724568d 100644 --- a/bookwyrm/tests/models/test_federated_server.py +++ b/bookwyrm/tests/models/test_federated_server.py @@ -6,10 +6,10 @@ from bookwyrm import models class FederatedServer(TestCase): - """ federate server management """ + """federate server management""" def setUp(self): - """ we'll need a user """ + """we'll need a user""" self.server = models.FederatedServer.objects.create(server_name="test.server") with patch("bookwyrm.models.user.set_remote_server.delay"): self.remote_user = models.User.objects.create_user( @@ -36,7 +36,7 @@ class FederatedServer(TestCase): ) def test_block_unblock(self): - """ block a server and all users on it """ + """block a server and all users on it""" self.assertEqual(self.server.status, "federated") self.assertTrue(self.remote_user.is_active) self.assertFalse(self.inactive_remote_user.is_active) diff --git a/bookwyrm/tests/models/test_fields.py b/bookwyrm/tests/models/test_fields.py index 18bb028ff..ea692b625 100644 --- a/bookwyrm/tests/models/test_fields.py +++ b/bookwyrm/tests/models/test_fields.py @@ -25,10 +25,10 @@ from bookwyrm.models.activitypub_mixin import ActivitypubMixin # pylint: disable=too-many-public-methods class ActivitypubFields(TestCase): - """ overwrites standard model feilds to work with activitypub """ + """overwrites standard model feilds to work with activitypub""" def test_validate_remote_id(self): - """ should look like a url """ + """should look like a url""" self.assertIsNone(fields.validate_remote_id("http://www.example.com")) self.assertIsNone(fields.validate_remote_id("https://www.example.com")) self.assertIsNone(fields.validate_remote_id("http://exle.com/dlg-23/x")) @@ -45,7 +45,7 @@ class ActivitypubFields(TestCase): ) def test_activitypub_field_mixin(self): - """ generic mixin with super basic to and from functionality """ + """generic mixin with super basic to and from functionality""" instance = fields.ActivitypubFieldMixin() self.assertEqual(instance.field_to_activity("fish"), "fish") self.assertEqual(instance.field_from_activity("fish"), "fish") @@ -63,11 +63,11 @@ class ActivitypubFields(TestCase): self.assertEqual(instance.get_activitypub_field(), "snakeCaseName") def test_set_field_from_activity(self): - """ setter from entire json blob """ + """setter from entire json blob""" @dataclass class TestModel: - """ real simple mock """ + """real simple mock""" field_name: str @@ -82,11 +82,11 @@ class ActivitypubFields(TestCase): self.assertEqual(mock_model.field_name, "hi") def test_set_activity_from_field(self): - """ set json field given entire model """ + """set json field given entire model""" @dataclass class TestModel: - """ real simple mock """ + """real simple mock""" field_name: str unrelated: str @@ -100,7 +100,7 @@ class ActivitypubFields(TestCase): self.assertEqual(data["fieldName"], "bip") def test_remote_id_field(self): - """ just sets some defaults on charfield """ + """just sets some defaults on charfield""" instance = fields.RemoteIdField() self.assertEqual(instance.max_length, 255) self.assertTrue(instance.deduplication_field) @@ -109,7 +109,7 @@ class ActivitypubFields(TestCase): instance.run_validators("http://www.example.com/dlfjg 23/x") def test_username_field(self): - """ again, just setting defaults on username field """ + """again, just setting defaults on username field""" instance = fields.UsernameField() self.assertEqual(instance.activitypub_field, "preferredUsername") self.assertEqual(instance.max_length, 150) @@ -130,7 +130,7 @@ class ActivitypubFields(TestCase): self.assertEqual(instance.field_to_activity("test@example.com"), "test") def test_privacy_field_defaults(self): - """ post privacy field's many default values """ + """post privacy field's many default values""" instance = fields.PrivacyField() self.assertEqual(instance.max_length, 255) self.assertEqual( @@ -143,11 +143,11 @@ class ActivitypubFields(TestCase): ) def test_privacy_field_set_field_from_activity(self): - """ translate between to/cc fields and privacy """ + """translate between to/cc fields and privacy""" @dataclass(init=False) class TestActivity(ActivityObject): - """ real simple mock """ + """real simple mock""" to: List[str] cc: List[str] @@ -155,7 +155,7 @@ class ActivitypubFields(TestCase): type: str = "Test" class TestPrivacyModel(ActivitypubMixin, BookWyrmModel): - """ real simple mock model because BookWyrmModel is abstract """ + """real simple mock model because BookWyrmModel is abstract""" privacy_field = fields.PrivacyField() mention_users = fields.TagField(User) @@ -187,7 +187,7 @@ class ActivitypubFields(TestCase): @patch("bookwyrm.models.activitypub_mixin.ObjectMixin.broadcast") @patch("bookwyrm.activitystreams.ActivityStream.add_status") def test_privacy_field_set_activity_from_field(self, *_): - """ translate between to/cc fields and privacy """ + """translate between to/cc fields and privacy""" user = User.objects.create_user( "rat", "rat@rat.rat", "ratword", local=True, localname="rat" ) @@ -231,7 +231,7 @@ class ActivitypubFields(TestCase): self.assertEqual(activity["cc"], []) def test_foreign_key(self): - """ should be able to format a related model """ + """should be able to format a related model""" instance = fields.ForeignKey("User", on_delete=models.CASCADE) Serializable = namedtuple("Serializable", ("to_activity", "remote_id")) item = Serializable(lambda: {"a": "b"}, "https://e.b/c") @@ -240,7 +240,7 @@ class ActivitypubFields(TestCase): @responses.activate def test_foreign_key_from_activity_str(self): - """ create a new object from a foreign key """ + """create a new object from a foreign key""" instance = fields.ForeignKey(User, on_delete=models.CASCADE) datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json") userdata = json.loads(datafile.read_bytes()) @@ -264,7 +264,7 @@ class ActivitypubFields(TestCase): self.assertEqual(value.name, "MOUSE?? MOUSE!!") def test_foreign_key_from_activity_dict(self): - """ test recieving activity json """ + """test recieving activity json""" instance = fields.ForeignKey(User, on_delete=models.CASCADE) datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json") userdata = json.loads(datafile.read_bytes()) @@ -284,7 +284,7 @@ class ActivitypubFields(TestCase): # et cetera but we're not testing serializing user json def test_foreign_key_from_activity_dict_existing(self): - """ test receiving a dict of an existing object in the db """ + """test receiving a dict of an existing object in the db""" instance = fields.ForeignKey(User, on_delete=models.CASCADE) datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json") userdata = json.loads(datafile.read_bytes()) @@ -302,7 +302,7 @@ class ActivitypubFields(TestCase): self.assertEqual(value, user) def test_foreign_key_from_activity_str_existing(self): - """ test receiving a remote id of an existing object in the db """ + """test receiving a remote id of an existing object in the db""" instance = fields.ForeignKey(User, on_delete=models.CASCADE) user = User.objects.create_user( "mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" @@ -315,14 +315,14 @@ class ActivitypubFields(TestCase): self.assertEqual(value, user) def test_one_to_one_field(self): - """ a gussied up foreign key """ + """a gussied up foreign key""" instance = fields.OneToOneField("User", on_delete=models.CASCADE) Serializable = namedtuple("Serializable", ("to_activity", "remote_id")) item = Serializable(lambda: {"a": "b"}, "https://e.b/c") self.assertEqual(instance.field_to_activity(item), {"a": "b"}) def test_many_to_many_field(self): - """ lists! """ + """lists!""" instance = fields.ManyToManyField("User") Serializable = namedtuple("Serializable", ("to_activity", "remote_id")) @@ -340,7 +340,7 @@ class ActivitypubFields(TestCase): @responses.activate def test_many_to_many_field_from_activity(self): - """ resolve related fields for a list, takes a list of remote ids """ + """resolve related fields for a list, takes a list of remote ids""" instance = fields.ManyToManyField(User) datafile = pathlib.Path(__file__).parent.joinpath("../data/ap_user.json") userdata = json.loads(datafile.read_bytes()) @@ -360,7 +360,7 @@ class ActivitypubFields(TestCase): self.assertIsInstance(value[0], User) def test_tag_field(self): - """ a special type of many to many field """ + """a special type of many to many field""" instance = fields.TagField("User") Serializable = namedtuple( @@ -379,13 +379,13 @@ class ActivitypubFields(TestCase): self.assertEqual(result[0].type, "Serializable") def test_tag_field_from_activity(self): - """ loadin' a list of items from Links """ + """loadin' a list of items from Links""" # TODO @responses.activate @patch("bookwyrm.models.activitypub_mixin.ObjectMixin.broadcast") def test_image_field(self, _): - """ storing images """ + """storing images""" user = User.objects.create_user( "mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" ) @@ -423,7 +423,7 @@ class ActivitypubFields(TestCase): self.assertIsInstance(loaded_image[1], ContentFile) def test_datetime_field(self): - """ this one is pretty simple, it just has to use isoformat """ + """this one is pretty simple, it just has to use isoformat""" instance = fields.DateTimeField() now = timezone.now() self.assertEqual(instance.field_to_activity(now), now.isoformat()) @@ -431,12 +431,12 @@ class ActivitypubFields(TestCase): self.assertEqual(instance.field_from_activity("bip"), None) def test_array_field(self): - """ idk why it makes them strings but probably for a good reason """ + """idk why it makes them strings but probably for a good reason""" instance = fields.ArrayField(fields.IntegerField) self.assertEqual(instance.field_to_activity([0, 1]), ["0", "1"]) def test_html_field(self): - """ sanitizes html, the sanitizer has its own tests """ + """sanitizes html, the sanitizer has its own tests""" instance = fields.HtmlField() self.assertEqual( instance.field_from_activity("

    hi

    "), "

    hi

    " diff --git a/bookwyrm/tests/models/test_import_model.py b/bookwyrm/tests/models/test_import_model.py index 38c3b1ed3..76a914d1b 100644 --- a/bookwyrm/tests/models/test_import_model.py +++ b/bookwyrm/tests/models/test_import_model.py @@ -14,10 +14,10 @@ from bookwyrm.connectors.abstract_connector import SearchResult class ImportJob(TestCase): - """ this is a fancy one!!! """ + """this is a fancy one!!!""" def setUp(self): - """ data is from a goodreads export of The Raven Tower """ + """data is from a goodreads export of The Raven Tower""" read_data = { "Book Id": 39395857, "Title": "The Raven Tower", @@ -72,30 +72,30 @@ class ImportJob(TestCase): ) def test_isbn(self): - """ it unquotes the isbn13 field from data """ + """it unquotes the isbn13 field from data""" expected = "9780356506999" item = models.ImportItem.objects.get(index=1) self.assertEqual(item.isbn, expected) def test_shelf(self): - """ converts to the local shelf typology """ + """converts to the local shelf typology""" expected = "reading" self.assertEqual(self.item_1.shelf, expected) def test_date_added(self): - """ converts to the local shelf typology """ + """converts to the local shelf typology""" expected = datetime.datetime(2019, 4, 9, 0, 0, tzinfo=timezone.utc) item = models.ImportItem.objects.get(index=1) self.assertEqual(item.date_added, expected) def test_date_read(self): - """ converts to the local shelf typology """ + """converts to the local shelf typology""" expected = datetime.datetime(2019, 4, 12, 0, 0, tzinfo=timezone.utc) item = models.ImportItem.objects.get(index=2) self.assertEqual(item.date_read, expected) def test_currently_reading_reads(self): - """ infer currently reading dates where available """ + """infer currently reading dates where available""" expected = [ models.ReadThrough( start_date=datetime.datetime(2019, 4, 9, 0, 0, tzinfo=timezone.utc) @@ -106,7 +106,7 @@ class ImportJob(TestCase): self.assertEqual(actual.reads[0].finish_date, expected[0].finish_date) def test_read_reads(self): - """ infer read dates where available """ + """infer read dates where available""" actual = self.item_2 self.assertEqual( actual.reads[0].start_date, @@ -118,14 +118,14 @@ class ImportJob(TestCase): ) def test_unread_reads(self): - """ handle books with no read dates """ + """handle books with no read dates""" expected = [] actual = models.ImportItem.objects.get(index=3) self.assertEqual(actual.reads, expected) @responses.activate def test_get_book_from_isbn(self): - """ search and load books by isbn (9780356506999) """ + """search and load books by isbn (9780356506999)""" connector_info = models.Connector.objects.create( identifier="openlibrary.org", name="OpenLibrary", diff --git a/bookwyrm/tests/models/test_list.py b/bookwyrm/tests/models/test_list.py index d99e54656..8f5bd4976 100644 --- a/bookwyrm/tests/models/test_list.py +++ b/bookwyrm/tests/models/test_list.py @@ -7,10 +7,10 @@ from bookwyrm import models, settings @patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") class List(TestCase): - """ some activitypub oddness ahead """ + """some activitypub oddness ahead""" def setUp(self): - """ look, a list """ + """look, a list""" self.local_user = models.User.objects.create_user( "mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" ) @@ -18,7 +18,7 @@ class List(TestCase): self.book = models.Edition.objects.create(title="hi", parent_work=work) def test_remote_id(self, _): - """ shelves use custom remote ids """ + """shelves use custom remote ids""" with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): book_list = models.List.objects.create( name="Test List", user=self.local_user @@ -27,7 +27,7 @@ class List(TestCase): self.assertEqual(book_list.get_remote_id(), expected_id) def test_to_activity(self, _): - """ jsonify it """ + """jsonify it""" with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): book_list = models.List.objects.create( name="Test List", user=self.local_user @@ -41,7 +41,7 @@ class List(TestCase): self.assertEqual(activity_json["owner"], self.local_user.remote_id) def test_list_item(self, _): - """ a list entry """ + """a list entry""" with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): book_list = models.List.objects.create( name="Test List", user=self.local_user, privacy="unlisted" @@ -51,6 +51,7 @@ class List(TestCase): book_list=book_list, book=self.book, user=self.local_user, + order=1, ) self.assertTrue(item.approved) @@ -58,14 +59,18 @@ class List(TestCase): self.assertEqual(item.recipients, []) def test_list_item_pending(self, _): - """ a list entry """ + """a list entry""" with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): book_list = models.List.objects.create( name="Test List", user=self.local_user ) item = models.ListItem.objects.create( - book_list=book_list, book=self.book, user=self.local_user, approved=False + book_list=book_list, + book=self.book, + user=self.local_user, + approved=False, + order=1, ) self.assertFalse(item.approved) diff --git a/bookwyrm/tests/models/test_readthrough_model.py b/bookwyrm/tests/models/test_readthrough_model.py index f69e87790..93e9e654c 100644 --- a/bookwyrm/tests/models/test_readthrough_model.py +++ b/bookwyrm/tests/models/test_readthrough_model.py @@ -6,10 +6,10 @@ from bookwyrm import models, settings class ReadThrough(TestCase): - """ some activitypub oddness ahead """ + """some activitypub oddness ahead""" def setUp(self): - """ look, a shelf """ + """look, a shelf""" self.user = models.User.objects.create_user( "mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" ) @@ -27,7 +27,7 @@ class ReadThrough(TestCase): ) def test_progress_update(self): - """ Test progress updates """ + """Test progress updates""" self.readthrough.create_update() # No-op, no progress yet self.readthrough.progress = 10 self.readthrough.create_update() diff --git a/bookwyrm/tests/models/test_relationship_models.py b/bookwyrm/tests/models/test_relationship_models.py index 0e842b218..d629b5c7a 100644 --- a/bookwyrm/tests/models/test_relationship_models.py +++ b/bookwyrm/tests/models/test_relationship_models.py @@ -6,10 +6,10 @@ from bookwyrm import models class Relationship(TestCase): - """ following, blocking, stuff like that """ + """following, blocking, stuff like that""" def setUp(self): - """ we need some users for this """ + """we need some users for this""" with patch("bookwyrm.models.user.set_remote_server.delay"): self.remote_user = models.User.objects.create_user( "rat", @@ -27,11 +27,11 @@ class Relationship(TestCase): self.local_user.save(broadcast=False) def test_user_follows_from_request(self): - """ convert a follow request into a follow """ + """convert a follow request into a follow""" real_broadcast = models.UserFollowRequest.broadcast def mock_broadcast(_, activity, user): - """ introspect what's being sent out """ + """introspect what's being sent out""" self.assertEqual(user.remote_id, self.local_user.remote_id) self.assertEqual(activity["type"], "Follow") @@ -54,7 +54,7 @@ class Relationship(TestCase): models.UserFollowRequest.broadcast = real_broadcast def test_user_follows_from_request_custom_remote_id(self): - """ store a specific remote id for a relationship provided by remote """ + """store a specific remote id for a relationship provided by remote""" with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): request = models.UserFollowRequest.objects.create( user_subject=self.local_user, @@ -71,7 +71,7 @@ class Relationship(TestCase): self.assertEqual(rel.user_object, self.remote_user) def test_follow_request_activity(self): - """ accept a request and make it a relationship """ + """accept a request and make it a relationship""" real_broadcast = models.UserFollowRequest.broadcast def mock_broadcast(_, activity, user): @@ -88,7 +88,7 @@ class Relationship(TestCase): models.UserFollowRequest.broadcast = real_broadcast def test_follow_request_accept(self): - """ accept a request and make it a relationship """ + """accept a request and make it a relationship""" real_broadcast = models.UserFollowRequest.broadcast def mock_broadcast(_, activity, user): @@ -115,7 +115,7 @@ class Relationship(TestCase): models.UserFollowRequest.broadcast = real_broadcast def test_follow_request_reject(self): - """ accept a request and make it a relationship """ + """accept a request and make it a relationship""" real_broadcast = models.UserFollowRequest.broadcast def mock_reject(_, activity, user): diff --git a/bookwyrm/tests/models/test_shelf_model.py b/bookwyrm/tests/models/test_shelf_model.py index 45ae1fa13..911df059d 100644 --- a/bookwyrm/tests/models/test_shelf_model.py +++ b/bookwyrm/tests/models/test_shelf_model.py @@ -8,10 +8,10 @@ from bookwyrm import models, settings # pylint: disable=unused-argument class Shelf(TestCase): - """ some activitypub oddness ahead """ + """some activitypub oddness ahead""" def setUp(self): - """ look, a shelf """ + """look, a shelf""" self.local_user = models.User.objects.create_user( "mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" ) @@ -19,7 +19,7 @@ class Shelf(TestCase): self.book = models.Edition.objects.create(title="test book", parent_work=work) def test_remote_id(self): - """ shelves use custom remote ids """ + """shelves use custom remote ids""" with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): shelf = models.Shelf.objects.create( name="Test Shelf", identifier="test-shelf", user=self.local_user @@ -28,7 +28,7 @@ class Shelf(TestCase): self.assertEqual(shelf.get_remote_id(), expected_id) def test_to_activity(self): - """ jsonify it """ + """jsonify it""" with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): shelf = models.Shelf.objects.create( name="Test Shelf", identifier="test-shelf", user=self.local_user @@ -42,7 +42,7 @@ class Shelf(TestCase): self.assertEqual(activity_json["owner"], self.local_user.remote_id) def test_create_update_shelf(self): - """ create and broadcast shelf creation """ + """create and broadcast shelf creation""" with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock: shelf = models.Shelf.objects.create( @@ -63,7 +63,7 @@ class Shelf(TestCase): self.assertEqual(shelf.name, "arthur russel") def test_shelve(self): - """ create and broadcast shelf creation """ + """create and broadcast shelf creation""" with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): shelf = models.Shelf.objects.create( name="Test Shelf", identifier="test-shelf", user=self.local_user diff --git a/bookwyrm/tests/models/test_status_model.py b/bookwyrm/tests/models/test_status_model.py index 4263c4572..4c8930bc4 100644 --- a/bookwyrm/tests/models/test_status_model.py +++ b/bookwyrm/tests/models/test_status_model.py @@ -17,10 +17,10 @@ from bookwyrm import activitypub, models, settings @patch("bookwyrm.models.Status.broadcast") @patch("bookwyrm.activitystreams.ActivityStream.add_status") class Status(TestCase): - """ lotta types of statuses """ + """lotta types of statuses""" def setUp(self): - """ useful things for creating a status """ + """useful things for creating a status""" self.local_user = models.User.objects.create_user( "mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse" ) @@ -46,14 +46,14 @@ class Status(TestCase): self.book.cover.save("test.jpg", ContentFile(output.getvalue())) def test_status_generated_fields(self, *_): - """ setting remote id """ + """setting remote id""" status = models.Status.objects.create(content="bleh", user=self.local_user) expected_id = "https://%s/user/mouse/status/%d" % (settings.DOMAIN, status.id) self.assertEqual(status.remote_id, expected_id) self.assertEqual(status.privacy, "public") def test_replies(self, *_): - """ get a list of replies """ + """get a list of replies""" parent = models.Status.objects.create(content="hi", user=self.local_user) child = models.Status.objects.create( content="hello", reply_parent=parent, user=self.local_user @@ -72,7 +72,7 @@ class Status(TestCase): self.assertIsInstance(replies.last(), models.Review) def test_status_type(self, *_): - """ class name """ + """class name""" self.assertEqual(models.Status().status_type, "Note") self.assertEqual(models.Review().status_type, "Review") self.assertEqual(models.Quotation().status_type, "Quotation") @@ -80,14 +80,14 @@ class Status(TestCase): self.assertEqual(models.Boost().status_type, "Announce") def test_boostable(self, *_): - """ can a status be boosted, based on privacy """ + """can a status be boosted, based on privacy""" self.assertTrue(models.Status(privacy="public").boostable) self.assertTrue(models.Status(privacy="unlisted").boostable) self.assertFalse(models.Status(privacy="followers").boostable) self.assertFalse(models.Status(privacy="direct").boostable) def test_to_replies(self, *_): - """ activitypub replies collection """ + """activitypub replies collection""" parent = models.Status.objects.create(content="hi", user=self.local_user) child = models.Status.objects.create( content="hello", reply_parent=parent, user=self.local_user @@ -104,7 +104,7 @@ class Status(TestCase): self.assertEqual(replies["totalItems"], 2) def test_status_to_activity(self, *_): - """ subclass of the base model version with a "pure" serializer """ + """subclass of the base model version with a "pure" serializer""" status = models.Status.objects.create( content="test content", user=self.local_user ) @@ -115,7 +115,7 @@ class Status(TestCase): self.assertEqual(activity["sensitive"], False) def test_status_to_activity_tombstone(self, *_): - """ subclass of the base model version with a "pure" serializer """ + """subclass of the base model version with a "pure" serializer""" with patch( "bookwyrm.activitystreams.ActivityStream.remove_object_from_related_stores" ): @@ -131,7 +131,7 @@ class Status(TestCase): self.assertFalse(hasattr(activity, "content")) def test_status_to_pure_activity(self, *_): - """ subclass of the base model version with a "pure" serializer """ + """subclass of the base model version with a "pure" serializer""" status = models.Status.objects.create( content="test content", user=self.local_user ) @@ -143,7 +143,7 @@ class Status(TestCase): self.assertEqual(activity["attachment"], []) def test_generated_note_to_activity(self, *_): - """ subclass of the base model version with a "pure" serializer """ + """subclass of the base model version with a "pure" serializer""" status = models.GeneratedNote.objects.create( content="test content", user=self.local_user ) @@ -157,7 +157,7 @@ class Status(TestCase): self.assertEqual(len(activity["tag"]), 2) def test_generated_note_to_pure_activity(self, *_): - """ subclass of the base model version with a "pure" serializer """ + """subclass of the base model version with a "pure" serializer""" status = models.GeneratedNote.objects.create( content="test content", user=self.local_user ) @@ -181,7 +181,7 @@ class Status(TestCase): self.assertEqual(activity["attachment"][0].name, "Test Edition") def test_comment_to_activity(self, *_): - """ subclass of the base model version with a "pure" serializer """ + """subclass of the base model version with a "pure" serializer""" status = models.Comment.objects.create( content="test content", user=self.local_user, book=self.book ) @@ -192,7 +192,7 @@ class Status(TestCase): self.assertEqual(activity["inReplyToBook"], self.book.remote_id) def test_comment_to_pure_activity(self, *_): - """ subclass of the base model version with a "pure" serializer """ + """subclass of the base model version with a "pure" serializer""" status = models.Comment.objects.create( content="test content", user=self.local_user, book=self.book ) @@ -212,7 +212,7 @@ class Status(TestCase): self.assertEqual(activity["attachment"][0].name, "Test Edition") def test_quotation_to_activity(self, *_): - """ subclass of the base model version with a "pure" serializer """ + """subclass of the base model version with a "pure" serializer""" status = models.Quotation.objects.create( quote="a sickening sense", content="test content", @@ -227,7 +227,7 @@ class Status(TestCase): self.assertEqual(activity["inReplyToBook"], self.book.remote_id) def test_quotation_to_pure_activity(self, *_): - """ subclass of the base model version with a "pure" serializer """ + """subclass of the base model version with a "pure" serializer""" status = models.Quotation.objects.create( quote="a sickening sense", content="test content", @@ -250,7 +250,7 @@ class Status(TestCase): self.assertEqual(activity["attachment"][0].name, "Test Edition") def test_review_to_activity(self, *_): - """ subclass of the base model version with a "pure" serializer """ + """subclass of the base model version with a "pure" serializer""" status = models.Review.objects.create( name="Review name", content="test content", @@ -267,9 +267,9 @@ class Status(TestCase): self.assertEqual(activity["inReplyToBook"], self.book.remote_id) def test_review_to_pure_activity(self, *_): - """ subclass of the base model version with a "pure" serializer """ + """subclass of the base model version with a "pure" serializer""" status = models.Review.objects.create( - name="Review name", + name="Review's name", content="test content", rating=3.0, user=self.local_user, @@ -280,7 +280,7 @@ class Status(TestCase): self.assertEqual(activity["type"], "Article") self.assertEqual( activity["name"], - 'Review of "%s" (3 stars): Review name' % self.book.title, + 'Review of "%s" (3 stars): Review\'s name' % self.book.title, ) self.assertEqual(activity["content"], "test content") self.assertEqual(activity["attachment"][0].type, "Document") @@ -291,7 +291,7 @@ class Status(TestCase): self.assertEqual(activity["attachment"][0].name, "Test Edition") def test_review_to_pure_activity_no_rating(self, *_): - """ subclass of the base model version with a "pure" serializer """ + """subclass of the base model version with a "pure" serializer""" status = models.Review.objects.create( name="Review name", content="test content", @@ -313,7 +313,7 @@ class Status(TestCase): self.assertEqual(activity["attachment"][0].name, "Test Edition") def test_reviewrating_to_pure_activity(self, *_): - """ subclass of the base model version with a "pure" serializer """ + """subclass of the base model version with a "pure" serializer""" status = models.ReviewRating.objects.create( rating=3.0, user=self.local_user, @@ -335,11 +335,11 @@ class Status(TestCase): self.assertEqual(activity["attachment"][0].name, "Test Edition") def test_favorite(self, *_): - """ fav a status """ + """fav a status""" real_broadcast = models.Favorite.broadcast def fav_broadcast_mock(_, activity, user): - """ ok """ + """ok""" self.assertEqual(user.remote_id, self.local_user.remote_id) self.assertEqual(activity["type"], "Like") @@ -361,7 +361,7 @@ class Status(TestCase): models.Favorite.broadcast = real_broadcast def test_boost(self, *_): - """ boosting, this one's a bit fussy """ + """boosting, this one's a bit fussy""" status = models.Status.objects.create( content="test content", user=self.local_user ) @@ -373,7 +373,7 @@ class Status(TestCase): self.assertEqual(activity, boost.to_activity(pure=True)) def test_notification(self, *_): - """ a simple model """ + """a simple model""" notification = models.Notification.objects.create( user=self.local_user, notification_type="FAVORITE" ) @@ -385,7 +385,7 @@ class Status(TestCase): ) def test_create_broadcast(self, _, broadcast_mock): - """ should send out two verions of a status on create """ + """should send out two verions of a status on create""" models.Comment.objects.create( content="hi", user=self.local_user, book=self.book ) @@ -405,7 +405,7 @@ class Status(TestCase): self.assertEqual(args["object"]["type"], "Comment") def test_recipients_with_mentions(self, *_): - """ get recipients to broadcast a status """ + """get recipients to broadcast a status""" status = models.GeneratedNote.objects.create( content="test content", user=self.local_user ) @@ -414,7 +414,7 @@ class Status(TestCase): self.assertEqual(status.recipients, [self.remote_user]) def test_recipients_with_reply_parent(self, *_): - """ get recipients to broadcast a status """ + """get recipients to broadcast a status""" parent_status = models.GeneratedNote.objects.create( content="test content", user=self.remote_user ) @@ -425,7 +425,7 @@ class Status(TestCase): self.assertEqual(status.recipients, [self.remote_user]) def test_recipients_with_reply_parent_and_mentions(self, *_): - """ get recipients to broadcast a status """ + """get recipients to broadcast a status""" parent_status = models.GeneratedNote.objects.create( content="test content", user=self.remote_user ) @@ -438,7 +438,7 @@ class Status(TestCase): @responses.activate def test_ignore_activity_boost(self, *_): - """ don't bother with most remote statuses """ + """don't bother with most remote statuses""" activity = activitypub.Announce( id="http://www.faraway.com/boost/12", actor=self.remote_user.remote_id, diff --git a/bookwyrm/tests/models/test_user_model.py b/bookwyrm/tests/models/test_user_model.py index 883ef669e..b2791379d 100644 --- a/bookwyrm/tests/models/test_user_model.py +++ b/bookwyrm/tests/models/test_user_model.py @@ -22,7 +22,7 @@ class User(TestCase): ) def test_computed_fields(self): - """ username instead of id here """ + """username instead of id here""" expected_id = "https://%s/user/mouse" % DOMAIN self.assertEqual(self.user.remote_id, expected_id) self.assertEqual(self.user.username, "mouse@%s" % DOMAIN) @@ -155,7 +155,7 @@ class User(TestCase): self.assertIsNone(server.application_version) def test_delete_user(self): - """ deactivate a user """ + """deactivate a user""" self.assertTrue(self.user.is_active) with patch( "bookwyrm.models.activitypub_mixin.broadcast_task.delay" diff --git a/bookwyrm/tests/test_activitystreams.py b/bookwyrm/tests/test_activitystreams.py index b4efeba3f..59266383f 100644 --- a/bookwyrm/tests/test_activitystreams.py +++ b/bookwyrm/tests/test_activitystreams.py @@ -7,10 +7,10 @@ from bookwyrm import activitystreams, models @patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") @patch("bookwyrm.activitystreams.ActivityStream.add_status") class Activitystreams(TestCase): - """ using redis to build activity streams """ + """using redis to build activity streams""" def setUp(self): - """ use a test csv """ + """use a test csv""" self.local_user = models.User.objects.create_user( "mouse", "mouse@mouse.mouse", "password", local=True, localname="mouse" ) @@ -30,14 +30,14 @@ class Activitystreams(TestCase): self.book = models.Edition.objects.create(title="test book") class TestStream(activitystreams.ActivityStream): - """ test stream, don't have to do anything here """ + """test stream, don't have to do anything here""" key = "test" self.test_stream = TestStream() def test_activitystream_class_ids(self, *_): - """ the abstract base class for stream objects """ + """the abstract base class for stream objects""" self.assertEqual( self.test_stream.stream_id(self.local_user), "{}-test".format(self.local_user.id), @@ -48,7 +48,7 @@ class Activitystreams(TestCase): ) def test_abstractstream_get_audience(self, *_): - """ get a list of users that should see a status """ + """get a list of users that should see a status""" status = models.Status.objects.create( user=self.remote_user, content="hi", privacy="public" ) @@ -59,7 +59,7 @@ class Activitystreams(TestCase): self.assertTrue(self.another_user in users) def test_abstractstream_get_audience_direct(self, *_): - """ get a list of users that should see a status """ + """get a list of users that should see a status""" status = models.Status.objects.create( user=self.remote_user, content="hi", @@ -82,7 +82,7 @@ class Activitystreams(TestCase): self.assertFalse(self.remote_user in users) def test_abstractstream_get_audience_followers_remote_user(self, *_): - """ get a list of users that should see a status """ + """get a list of users that should see a status""" status = models.Status.objects.create( user=self.remote_user, content="hi", @@ -92,7 +92,7 @@ class Activitystreams(TestCase): self.assertFalse(users.exists()) def test_abstractstream_get_audience_followers_self(self, *_): - """ get a list of users that should see a status """ + """get a list of users that should see a status""" status = models.Comment.objects.create( user=self.local_user, content="hi", @@ -105,7 +105,7 @@ class Activitystreams(TestCase): self.assertFalse(self.remote_user in users) def test_abstractstream_get_audience_followers_with_mention(self, *_): - """ get a list of users that should see a status """ + """get a list of users that should see a status""" status = models.Comment.objects.create( user=self.remote_user, content="hi", @@ -120,7 +120,7 @@ class Activitystreams(TestCase): self.assertFalse(self.remote_user in users) def test_abstractstream_get_audience_followers_with_relationship(self, *_): - """ get a list of users that should see a status """ + """get a list of users that should see a status""" self.remote_user.followers.add(self.local_user) status = models.Comment.objects.create( user=self.remote_user, @@ -134,7 +134,7 @@ class Activitystreams(TestCase): self.assertFalse(self.remote_user in users) def test_homestream_get_audience(self, *_): - """ get a list of users that should see a status """ + """get a list of users that should see a status""" status = models.Status.objects.create( user=self.remote_user, content="hi", privacy="public" ) @@ -142,7 +142,7 @@ class Activitystreams(TestCase): self.assertFalse(users.exists()) def test_homestream_get_audience_with_mentions(self, *_): - """ get a list of users that should see a status """ + """get a list of users that should see a status""" status = models.Status.objects.create( user=self.remote_user, content="hi", privacy="public" ) @@ -152,7 +152,7 @@ class Activitystreams(TestCase): self.assertFalse(self.another_user in users) def test_homestream_get_audience_with_relationship(self, *_): - """ get a list of users that should see a status """ + """get a list of users that should see a status""" self.remote_user.followers.add(self.local_user) status = models.Status.objects.create( user=self.remote_user, content="hi", privacy="public" @@ -162,7 +162,7 @@ class Activitystreams(TestCase): self.assertFalse(self.another_user in users) def test_localstream_get_audience_remote_status(self, *_): - """ get a list of users that should see a status """ + """get a list of users that should see a status""" status = models.Status.objects.create( user=self.remote_user, content="hi", privacy="public" ) @@ -170,7 +170,7 @@ class Activitystreams(TestCase): self.assertEqual(users, []) def test_localstream_get_audience_local_status(self, *_): - """ get a list of users that should see a status """ + """get a list of users that should see a status""" status = models.Status.objects.create( user=self.local_user, content="hi", privacy="public" ) @@ -179,7 +179,7 @@ class Activitystreams(TestCase): self.assertTrue(self.another_user in users) def test_localstream_get_audience_unlisted(self, *_): - """ get a list of users that should see a status """ + """get a list of users that should see a status""" status = models.Status.objects.create( user=self.local_user, content="hi", privacy="unlisted" ) @@ -187,7 +187,7 @@ class Activitystreams(TestCase): self.assertEqual(users, []) def test_federatedstream_get_audience(self, *_): - """ get a list of users that should see a status """ + """get a list of users that should see a status""" status = models.Status.objects.create( user=self.remote_user, content="hi", privacy="public" ) @@ -196,7 +196,7 @@ class Activitystreams(TestCase): self.assertTrue(self.another_user in users) def test_federatedstream_get_audience_unlisted(self, *_): - """ get a list of users that should see a status """ + """get a list of users that should see a status""" status = models.Status.objects.create( user=self.remote_user, content="hi", privacy="unlisted" ) diff --git a/bookwyrm/tests/test_emailing.py b/bookwyrm/tests/test_emailing.py index 5d7d4894b..0f9cc3659 100644 --- a/bookwyrm/tests/test_emailing.py +++ b/bookwyrm/tests/test_emailing.py @@ -10,10 +10,10 @@ from bookwyrm import emailing, models @patch("bookwyrm.emailing.send_email.delay") class Emailing(TestCase): - """ every response to a get request, html or json """ + """every response to a get request, html or json""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -25,7 +25,7 @@ class Emailing(TestCase): models.SiteSettings.objects.create() def test_invite_email(self, email_mock): - """ load the invite email """ + """load the invite email""" invite_request = models.InviteRequest.objects.create( email="test@email.com", invite=models.SiteInvite.objects.create(user=self.local_user), @@ -40,7 +40,7 @@ class Emailing(TestCase): self.assertEqual(len(args), 4) def test_password_reset_email(self, email_mock): - """ load the password reset email """ + """load the password reset email""" reset = models.PasswordReset.objects.create(user=self.local_user) emailing.password_reset_email(reset) diff --git a/bookwyrm/tests/test_sanitize_html.py b/bookwyrm/tests/test_sanitize_html.py index 2b3d0378d..6c4053483 100644 --- a/bookwyrm/tests/test_sanitize_html.py +++ b/bookwyrm/tests/test_sanitize_html.py @@ -5,10 +5,10 @@ from bookwyrm.sanitize_html import InputHtmlParser class Sanitizer(TestCase): - """ sanitizer tests """ + """sanitizer tests""" def test_no_html(self): - """ just text """ + """just text""" input_text = "no html " parser = InputHtmlParser() parser.feed(input_text) @@ -16,7 +16,7 @@ class Sanitizer(TestCase): self.assertEqual(input_text, output) def test_valid_html(self): - """ leave the html untouched """ + """leave the html untouched""" input_text = "yes html" parser = InputHtmlParser() parser.feed(input_text) @@ -24,7 +24,7 @@ class Sanitizer(TestCase): self.assertEqual(input_text, output) def test_valid_html_attrs(self): - """ and don't remove attributes """ + """and don't remove attributes""" input_text = 'yes html' parser = InputHtmlParser() parser.feed(input_text) @@ -32,7 +32,7 @@ class Sanitizer(TestCase): self.assertEqual(input_text, output) def test_invalid_html(self): - """ remove all html when the html is malformed """ + """remove all html when the html is malformed""" input_text = "yes html" parser = InputHtmlParser() parser.feed(input_text) @@ -46,7 +46,7 @@ class Sanitizer(TestCase): self.assertEqual("yes html ", output) def test_disallowed_html(self): - """ remove disallowed html but keep allowed html """ + """remove disallowed html but keep allowed html""" input_text = "
    yes html
    " parser = InputHtmlParser() parser.feed(input_text) diff --git a/bookwyrm/tests/test_signing.py b/bookwyrm/tests/test_signing.py index d9cc411c0..758ba9bbc 100644 --- a/bookwyrm/tests/test_signing.py +++ b/bookwyrm/tests/test_signing.py @@ -20,7 +20,7 @@ from bookwyrm.signatures import create_key_pair, make_signature, make_digest def get_follow_activity(follower, followee): - """ generates a test activity """ + """generates a test activity""" return Follow( id="https://test.com/user/follow/id", actor=follower.remote_id, @@ -33,10 +33,10 @@ Sender = namedtuple("Sender", ("remote_id", "key_pair")) class Signature(TestCase): - """ signature test """ + """signature test""" def setUp(self): - """ create users and test data """ + """create users and test data""" self.mouse = models.User.objects.create_user( "mouse@%s" % DOMAIN, "mouse@example.com", "", local=True, localname="mouse" ) @@ -56,7 +56,7 @@ class Signature(TestCase): models.SiteSettings.objects.create() def send(self, signature, now, data, digest): - """ test request """ + """test request""" c = Client() return c.post( urlsplit(self.rat.inbox).path, @@ -74,7 +74,7 @@ class Signature(TestCase): def send_test_request( # pylint: disable=too-many-arguments self, sender, signer=None, send_data=None, digest=None, date=None ): - """ sends a follow request to the "rat" user """ + """sends a follow request to the "rat" user""" now = date or http_date() data = json.dumps(get_follow_activity(sender, self.rat)) digest = digest or make_digest(data) @@ -84,7 +84,7 @@ class Signature(TestCase): return self.send(signature, now, send_data or data, digest) def test_correct_signature(self): - """ this one should just work """ + """this one should just work""" response = self.send_test_request(sender=self.mouse) self.assertEqual(response.status_code, 200) @@ -96,7 +96,7 @@ class Signature(TestCase): @responses.activate def test_remote_signer(self): - """ signtures for remote users """ + """signtures for remote users""" datafile = pathlib.Path(__file__).parent.joinpath("data/ap_user.json") data = json.loads(datafile.read_bytes()) data["id"] = self.fake_remote.remote_id @@ -119,7 +119,7 @@ class Signature(TestCase): @responses.activate def test_key_needs_refresh(self): - """ an out of date key should be updated and the new key work """ + """an out of date key should be updated and the new key work""" datafile = pathlib.Path(__file__).parent.joinpath("data/ap_user.json") data = json.loads(datafile.read_bytes()) data["id"] = self.fake_remote.remote_id @@ -155,7 +155,7 @@ class Signature(TestCase): @responses.activate def test_nonexistent_signer(self): - """ fail when unable to look up signer """ + """fail when unable to look up signer""" responses.add( responses.GET, self.fake_remote.remote_id, @@ -177,7 +177,7 @@ class Signature(TestCase): @pytest.mark.integration def test_invalid_digest(self): - """ signature digest must be valid """ + """signature digest must be valid""" with patch("bookwyrm.activitypub.resolve_remote_id"): response = self.send_test_request( self.mouse, digest="SHA-256=AAAAAAAAAAAAAAAAAA" diff --git a/bookwyrm/tests/test_templatetags.py b/bookwyrm/tests/test_templatetags.py index b4dc517f1..a92e887a3 100644 --- a/bookwyrm/tests/test_templatetags.py +++ b/bookwyrm/tests/test_templatetags.py @@ -12,10 +12,10 @@ from bookwyrm.templatetags import bookwyrm_tags @patch("bookwyrm.activitystreams.ActivityStream.add_status") class TemplateTags(TestCase): - """ lotta different things here """ + """lotta different things here""" def setUp(self): - """ create some filler objects """ + """create some filler objects""" self.user = models.User.objects.create_user( "mouse@example.com", "mouse@mouse.mouse", @@ -34,34 +34,34 @@ class TemplateTags(TestCase): self.book = models.Edition.objects.create(title="Test Book") def test_dict_key(self, _): - """ just getting a value out of a dict """ + """just getting a value out of a dict""" test_dict = {"a": 1, "b": 3} self.assertEqual(bookwyrm_tags.dict_key(test_dict, "a"), 1) self.assertEqual(bookwyrm_tags.dict_key(test_dict, "c"), 0) def test_get_user_rating(self, _): - """ get a user's most recent rating of a book """ + """get a user's most recent rating of a book""" with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): models.Review.objects.create(user=self.user, book=self.book, rating=3) self.assertEqual(bookwyrm_tags.get_user_rating(self.book, self.user), 3) def test_get_user_rating_doesnt_exist(self, _): - """ there is no rating available """ + """there is no rating available""" self.assertEqual(bookwyrm_tags.get_user_rating(self.book, self.user), 0) def test_get_user_identifer_local(self, _): - """ fall back to the simplest uid available """ + """fall back to the simplest uid available""" self.assertNotEqual(self.user.username, self.user.localname) self.assertEqual(bookwyrm_tags.get_user_identifier(self.user), "mouse") def test_get_user_identifer_remote(self, _): - """ for a remote user, should be their full username """ + """for a remote user, should be their full username""" self.assertEqual( bookwyrm_tags.get_user_identifier(self.remote_user), "rat@example.com" ) def test_get_notification_count(self, _): - """ just countin' """ + """just countin'""" self.assertEqual(bookwyrm_tags.get_notification_count(self.user), 0) models.Notification.objects.create(user=self.user, notification_type="FAVORITE") @@ -74,7 +74,7 @@ class TemplateTags(TestCase): self.assertEqual(bookwyrm_tags.get_notification_count(self.user), 2) def test_get_replies(self, _): - """ direct replies to a status """ + """direct replies to a status""" with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): parent = models.Review.objects.create( user=self.user, book=self.book, content="hi" @@ -102,7 +102,7 @@ class TemplateTags(TestCase): self.assertFalse(third_child in replies) def test_get_parent(self, _): - """ get the reply parent of a status """ + """get the reply parent of a status""" with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): parent = models.Review.objects.create( user=self.user, book=self.book, content="hi" @@ -116,7 +116,7 @@ class TemplateTags(TestCase): self.assertIsInstance(result, models.Review) def test_get_user_liked(self, _): - """ did a user like a status """ + """did a user like a status""" status = models.Review.objects.create(user=self.remote_user, book=self.book) self.assertFalse(bookwyrm_tags.get_user_liked(self.user, status)) @@ -125,7 +125,7 @@ class TemplateTags(TestCase): self.assertTrue(bookwyrm_tags.get_user_liked(self.user, status)) def test_get_user_boosted(self, _): - """ did a user boost a status """ + """did a user boost a status""" status = models.Review.objects.create(user=self.remote_user, book=self.book) self.assertFalse(bookwyrm_tags.get_user_boosted(self.user, status)) @@ -134,7 +134,7 @@ class TemplateTags(TestCase): self.assertTrue(bookwyrm_tags.get_user_boosted(self.user, status)) def test_follow_request_exists(self, _): - """ does a user want to follow """ + """does a user want to follow""" self.assertFalse( bookwyrm_tags.follow_request_exists(self.user, self.remote_user) ) @@ -152,7 +152,7 @@ class TemplateTags(TestCase): ) def test_get_boosted(self, _): - """ load a boosted status """ + """load a boosted status""" with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): status = models.Review.objects.create(user=self.remote_user, book=self.book) boost = models.Boost.objects.create(user=self.user, boosted_status=status) @@ -161,7 +161,7 @@ class TemplateTags(TestCase): self.assertEqual(boosted, status) def test_get_book_description(self, _): - """ grab it from the edition or the parent """ + """grab it from the edition or the parent""" work = models.Work.objects.create(title="Test Work") self.book.parent_work = work self.book.save() @@ -177,42 +177,12 @@ class TemplateTags(TestCase): self.assertEqual(bookwyrm_tags.get_book_description(self.book), "hello") def test_get_uuid(self, _): - """ uuid functionality """ + """uuid functionality""" uuid = bookwyrm_tags.get_uuid("hi") self.assertTrue(re.match(r"hi[A-Za-z0-9\-]", uuid)) - def test_time_since(self, _): - """ ultraconcise timestamps """ - self.assertEqual(bookwyrm_tags.time_since("bleh"), "") - - now = timezone.now() - self.assertEqual(bookwyrm_tags.time_since(now), "0s") - - seconds_ago = now - relativedelta(seconds=4) - self.assertEqual(bookwyrm_tags.time_since(seconds_ago), "4s") - - minutes_ago = now - relativedelta(minutes=8) - self.assertEqual(bookwyrm_tags.time_since(minutes_ago), "8m") - - hours_ago = now - relativedelta(hours=9) - self.assertEqual(bookwyrm_tags.time_since(hours_ago), "9h") - - days_ago = now - relativedelta(days=3) - self.assertEqual(bookwyrm_tags.time_since(days_ago), "3d") - - # I am not going to figure out how to mock dates tonight. - months_ago = now - relativedelta(months=5) - self.assertTrue( - re.match(r"[A-Z][a-z]{2} \d?\d", bookwyrm_tags.time_since(months_ago)) - ) - - years_ago = now - relativedelta(years=10) - self.assertTrue( - re.match(r"[A-Z][a-z]{2} \d?\d \d{4}", bookwyrm_tags.time_since(years_ago)) - ) - def test_get_markdown(self, _): - """ mardown format data """ + """mardown format data""" result = bookwyrm_tags.get_markdown("_hi_") self.assertEqual(result, "

    hi

    ") @@ -220,13 +190,13 @@ class TemplateTags(TestCase): self.assertEqual(result, "

    hi

    ") def test_get_mentions(self, _): - """ list of people mentioned """ + """list of people mentioned""" status = models.Status.objects.create(content="hi", user=self.remote_user) result = bookwyrm_tags.get_mentions(status, self.user) self.assertEqual(result, "@rat@example.com ") def test_get_status_preview_name(self, _): - """ status context string """ + """status context string""" with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): status = models.Status.objects.create(content="hi", user=self.user) result = bookwyrm_tags.get_status_preview_name(status) @@ -251,7 +221,7 @@ class TemplateTags(TestCase): self.assertEqual(result, "quotation from Test Book") def test_related_status(self, _): - """ gets the subclass model for a notification status """ + """gets the subclass model for a notification status""" with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): status = models.Status.objects.create(content="hi", user=self.user) notification = models.Notification.objects.create( diff --git a/bookwyrm/tests/views/inbox/test_inbox.py b/bookwyrm/tests/views/inbox/test_inbox.py index c39a3fd2d..697f40100 100644 --- a/bookwyrm/tests/views/inbox/test_inbox.py +++ b/bookwyrm/tests/views/inbox/test_inbox.py @@ -12,10 +12,10 @@ from bookwyrm import models, views # pylint: disable=too-many-public-methods class Inbox(TestCase): - """ readthrough tests """ + """readthrough tests""" def setUp(self): - """ basic user and book data """ + """basic user and book data""" self.client = Client() self.factory = RequestFactory() local_user = models.User.objects.create_user( @@ -48,12 +48,12 @@ class Inbox(TestCase): models.SiteSettings.objects.create() def test_inbox_invalid_get(self): - """ shouldn't try to handle if the user is not found """ + """shouldn't try to handle if the user is not found""" result = self.client.get("/inbox", content_type="application/json") self.assertIsInstance(result, HttpResponseNotAllowed) def test_inbox_invalid_user(self): - """ shouldn't try to handle if the user is not found """ + """shouldn't try to handle if the user is not found""" result = self.client.post( "/user/bleh/inbox", '{"type": "Test", "object": "exists"}', @@ -62,7 +62,7 @@ class Inbox(TestCase): self.assertIsInstance(result, HttpResponseNotFound) def test_inbox_invalid_bad_signature(self): - """ bad request for invalid signature """ + """bad request for invalid signature""" with patch("bookwyrm.views.inbox.has_valid_signature") as mock_valid: mock_valid.return_value = False result = self.client.post( @@ -73,7 +73,7 @@ class Inbox(TestCase): self.assertEqual(result.status_code, 401) def test_inbox_invalid_bad_signature_delete(self): - """ invalid signature for Delete is okay though """ + """invalid signature for Delete is okay though""" with patch("bookwyrm.views.inbox.has_valid_signature") as mock_valid: mock_valid.return_value = False result = self.client.post( @@ -84,7 +84,7 @@ class Inbox(TestCase): self.assertEqual(result.status_code, 200) def test_inbox_unknown_type(self): - """ never heard of that activity type, don't have a handler for it """ + """never heard of that activity type, don't have a handler for it""" with patch("bookwyrm.views.inbox.has_valid_signature") as mock_valid: result = self.client.post( "/inbox", @@ -95,7 +95,7 @@ class Inbox(TestCase): self.assertIsInstance(result, HttpResponseNotFound) def test_inbox_success(self): - """ a known type, for which we start a task """ + """a known type, for which we start a task""" activity = self.create_json activity["object"] = { "id": "https://example.com/list/22", @@ -121,7 +121,7 @@ class Inbox(TestCase): self.assertEqual(result.status_code, 200) def test_is_blocked_user_agent(self): - """ check for blocked servers """ + """check for blocked servers""" request = self.factory.post( "", HTTP_USER_AGENT="http.rb/4.4.1 (Mastodon/3.3.0; +https://mastodon.social/)", @@ -134,7 +134,7 @@ class Inbox(TestCase): self.assertTrue(views.inbox.is_blocked_user_agent(request)) def test_is_blocked_activity(self): - """ check for blocked servers """ + """check for blocked servers""" activity = {"actor": "https://mastodon.social/user/whaatever/else"} self.assertFalse(views.inbox.is_blocked_activity(activity)) @@ -144,7 +144,7 @@ class Inbox(TestCase): self.assertTrue(views.inbox.is_blocked_activity(activity)) def test_create_by_deactivated_user(self): - """ don't let deactivated users post """ + """don't let deactivated users post""" self.remote_user.delete(broadcast=False) self.assertTrue(self.remote_user.deleted) datafile = pathlib.Path(__file__).parent.joinpath("../../data/ap_note.json") diff --git a/bookwyrm/tests/views/inbox/test_inbox_add.py b/bookwyrm/tests/views/inbox/test_inbox_add.py index b2b653381..9f237b6db 100644 --- a/bookwyrm/tests/views/inbox/test_inbox_add.py +++ b/bookwyrm/tests/views/inbox/test_inbox_add.py @@ -9,10 +9,10 @@ from bookwyrm import models, views # pylint: disable=too-many-public-methods class InboxAdd(TestCase): - """ inbox tests """ + """inbox tests""" def setUp(self): - """ basic user and book data """ + """basic user and book data""" local_user = models.User.objects.create_user( "mouse@example.com", "mouse@mouse.com", @@ -42,7 +42,7 @@ class InboxAdd(TestCase): models.SiteSettings.objects.create() def test_handle_add_book_to_shelf(self): - """ shelving a book """ + """shelving a book""" shelf = models.Shelf.objects.create(user=self.remote_user, name="Test Shelf") shelf.remote_id = "https://bookwyrm.social/user/mouse/shelf/to-read" shelf.save() @@ -65,7 +65,7 @@ class InboxAdd(TestCase): @responses.activate def test_handle_add_book_to_list(self): - """ listing a book """ + """listing a book""" responses.add( responses.GET, "https://bookwyrm.social/user/mouse/list/to-read", @@ -94,6 +94,7 @@ class InboxAdd(TestCase): "type": "ListItem", "book": self.book.remote_id, "id": "https://bookwyrm.social/listbook/6189", + "order": 1, }, "target": "https://bookwyrm.social/user/mouse/list/to-read", "@context": "https://www.w3.org/ns/activitystreams", diff --git a/bookwyrm/tests/views/inbox/test_inbox_announce.py b/bookwyrm/tests/views/inbox/test_inbox_announce.py index 954d4e647..4243dc78e 100644 --- a/bookwyrm/tests/views/inbox/test_inbox_announce.py +++ b/bookwyrm/tests/views/inbox/test_inbox_announce.py @@ -9,10 +9,10 @@ from bookwyrm import models, views # pylint: disable=too-many-public-methods class InboxActivities(TestCase): - """ inbox tests """ + """inbox tests""" def setUp(self): - """ basic user and book data """ + """basic user and book data""" self.local_user = models.User.objects.create_user( "mouse@example.com", "mouse@mouse.com", @@ -51,8 +51,8 @@ class InboxActivities(TestCase): models.SiteSettings.objects.create() @patch("bookwyrm.activitystreams.ActivityStream.add_status") - def test_handle_boost(self, _): - """ boost a status """ + def test_boost(self, redis_mock): + """boost a status""" self.assertEqual(models.Notification.objects.count(), 0) activity = { "type": "Announce", @@ -66,16 +66,23 @@ class InboxActivities(TestCase): with patch("bookwyrm.models.status.Status.ignore_activity") as discarder: discarder.return_value = False views.inbox.activity_task(activity) + + # boost added to redis activitystreams + self.assertTrue(redis_mock.called) + + # boost created of correct status boost = models.Boost.objects.get() self.assertEqual(boost.boosted_status, self.status) + + # notification sent to original poster notification = models.Notification.objects.get() self.assertEqual(notification.user, self.local_user) self.assertEqual(notification.related_status, self.status) @responses.activate @patch("bookwyrm.activitystreams.ActivityStream.add_status") - def test_handle_boost_remote_status(self, redis_mock): - """ boost a status """ + def test_boost_remote_status(self, redis_mock): + """boost a status from a remote server""" work = models.Work.objects.create(title="work title") book = models.Edition.objects.create( title="Test", @@ -123,8 +130,8 @@ class InboxActivities(TestCase): self.assertEqual(boost.boosted_status.comment.book, book) @responses.activate - def test_handle_discarded_boost(self): - """ test a boost of a mastodon status that will be discarded """ + def test_discarded_boost(self): + """test a boost of a mastodon status that will be discarded""" status = models.Status( content="hi", user=self.remote_user, @@ -146,8 +153,8 @@ class InboxActivities(TestCase): views.inbox.activity_task(activity) self.assertEqual(models.Boost.objects.count(), 0) - def test_handle_unboost(self): - """ undo a boost """ + def test_unboost(self): + """undo a boost""" with patch("bookwyrm.activitystreams.ActivityStream.add_status"): boost = models.Boost.objects.create( boosted_status=self.status, user=self.remote_user @@ -175,8 +182,8 @@ class InboxActivities(TestCase): self.assertTrue(redis_mock.called) self.assertFalse(models.Boost.objects.exists()) - def test_handle_unboost_unknown_boost(self): - """ undo a boost """ + def test_unboost_unknown_boost(self): + """undo a boost""" activity = { "type": "Undo", "actor": "hi", diff --git a/bookwyrm/tests/views/inbox/test_inbox_block.py b/bookwyrm/tests/views/inbox/test_inbox_block.py index e686c6b7d..956cf538a 100644 --- a/bookwyrm/tests/views/inbox/test_inbox_block.py +++ b/bookwyrm/tests/views/inbox/test_inbox_block.py @@ -8,10 +8,10 @@ from bookwyrm import models, views # pylint: disable=too-many-public-methods class InboxBlock(TestCase): - """ inbox tests """ + """inbox tests""" def setUp(self): - """ basic user and book data """ + """basic user and book data""" self.local_user = models.User.objects.create_user( "mouse@example.com", "mouse@mouse.com", @@ -35,7 +35,7 @@ class InboxBlock(TestCase): models.SiteSettings.objects.create() def test_handle_blocks(self): - """ create a "block" database entry from an activity """ + """create a "block" database entry from an activity""" self.local_user.followers.add(self.remote_user) with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): models.UserFollowRequest.objects.create( @@ -67,7 +67,7 @@ class InboxBlock(TestCase): self.assertFalse(models.UserFollowRequest.objects.exists()) def test_handle_unblock(self): - """ unblock a user """ + """unblock a user""" self.remote_user.blocks.add(self.local_user) block = models.UserBlocks.objects.get() diff --git a/bookwyrm/tests/views/inbox/test_inbox_create.py b/bookwyrm/tests/views/inbox/test_inbox_create.py index 3d2ce1c09..e7a120244 100644 --- a/bookwyrm/tests/views/inbox/test_inbox_create.py +++ b/bookwyrm/tests/views/inbox/test_inbox_create.py @@ -11,10 +11,10 @@ from bookwyrm.activitypub import ActivitySerializerError # pylint: disable=too-many-public-methods class InboxCreate(TestCase): - """ readthrough tests """ + """readthrough tests""" def setUp(self): - """ basic user and book data """ + """basic user and book data""" self.local_user = models.User.objects.create_user( "mouse@example.com", "mouse@mouse.com", @@ -53,7 +53,7 @@ class InboxCreate(TestCase): models.SiteSettings.objects.create() def test_create_status(self): - """ the "it justs works" mode """ + """the "it justs works" mode""" self.assertEqual(models.Status.objects.count(), 1) datafile = pathlib.Path(__file__).parent.joinpath( @@ -84,7 +84,7 @@ class InboxCreate(TestCase): self.assertEqual(models.Status.objects.count(), 2) def test_create_status_remote_note_with_mention(self): - """ should only create it under the right circumstances """ + """should only create it under the right circumstances""" self.assertEqual(models.Status.objects.count(), 1) self.assertFalse( models.Notification.objects.filter(user=self.local_user).exists() @@ -107,7 +107,7 @@ class InboxCreate(TestCase): self.assertEqual(models.Notification.objects.get().notification_type, "MENTION") def test_create_status_remote_note_with_reply(self): - """ should only create it under the right circumstances """ + """should only create it under the right circumstances""" self.assertEqual(models.Status.objects.count(), 1) self.assertFalse(models.Notification.objects.filter(user=self.local_user)) @@ -128,7 +128,7 @@ class InboxCreate(TestCase): self.assertEqual(models.Notification.objects.get().notification_type, "REPLY") def test_create_list(self): - """ a new list """ + """a new list""" activity = self.create_json activity["object"] = { "id": "https://example.com/list/22", @@ -152,7 +152,7 @@ class InboxCreate(TestCase): self.assertEqual(book_list.remote_id, "https://example.com/list/22") def test_create_unsupported_type(self): - """ ignore activities we know we can't handle """ + """ignore activities we know we can't handle""" activity = self.create_json activity["object"] = { "id": "https://example.com/status/887", @@ -162,7 +162,7 @@ class InboxCreate(TestCase): views.inbox.activity_task(activity) def test_create_unknown_type(self): - """ ignore activities we know we've never heard of """ + """ignore activities we know we've never heard of""" activity = self.create_json activity["object"] = { "id": "https://example.com/status/887", diff --git a/bookwyrm/tests/views/inbox/test_inbox_delete.py b/bookwyrm/tests/views/inbox/test_inbox_delete.py index 03598b88d..617dcf6f5 100644 --- a/bookwyrm/tests/views/inbox/test_inbox_delete.py +++ b/bookwyrm/tests/views/inbox/test_inbox_delete.py @@ -9,10 +9,10 @@ from bookwyrm import models, views # pylint: disable=too-many-public-methods class InboxActivities(TestCase): - """ inbox tests """ + """inbox tests""" def setUp(self): - """ basic user and book data """ + """basic user and book data""" self.local_user = models.User.objects.create_user( "mouse@example.com", "mouse@mouse.com", @@ -50,7 +50,7 @@ class InboxActivities(TestCase): models.SiteSettings.objects.create() def test_delete_status(self): - """ remove a status """ + """remove a status""" self.assertFalse(self.status.deleted) activity = { "type": "Delete", @@ -71,7 +71,7 @@ class InboxActivities(TestCase): self.assertIsInstance(status.deleted_date, datetime) def test_delete_status_notifications(self): - """ remove a status with related notifications """ + """remove a status with related notifications""" models.Notification.objects.create( related_status=self.status, user=self.local_user, @@ -106,7 +106,7 @@ class InboxActivities(TestCase): self.assertEqual(models.Notification.objects.get(), notif) def test_delete_user(self): - """ delete a user """ + """delete a user""" self.assertTrue(models.User.objects.get(username="rat@example.com").is_active) activity = { "@context": "https://www.w3.org/ns/activitystreams", @@ -121,7 +121,7 @@ class InboxActivities(TestCase): self.assertFalse(models.User.objects.get(username="rat@example.com").is_active) def test_delete_user_unknown(self): - """ don't worry about it if we don't know the user """ + """don't worry about it if we don't know the user""" self.assertEqual(models.User.objects.filter(is_active=True).count(), 2) activity = { "@context": "https://www.w3.org/ns/activitystreams", diff --git a/bookwyrm/tests/views/inbox/test_inbox_follow.py b/bookwyrm/tests/views/inbox/test_inbox_follow.py index c549c31bd..f5332b7a3 100644 --- a/bookwyrm/tests/views/inbox/test_inbox_follow.py +++ b/bookwyrm/tests/views/inbox/test_inbox_follow.py @@ -1,4 +1,5 @@ """ tests incoming activities""" +import json from unittest.mock import patch from django.test import TestCase @@ -8,10 +9,10 @@ from bookwyrm import models, views # pylint: disable=too-many-public-methods class InboxRelationships(TestCase): - """ inbox tests """ + """inbox tests""" def setUp(self): - """ basic user and book data """ + """basic user and book data""" self.local_user = models.User.objects.create_user( "mouse@example.com", "mouse@mouse.com", @@ -34,8 +35,8 @@ class InboxRelationships(TestCase): models.SiteSettings.objects.create() - def test_handle_follow(self): - """ remote user wants to follow local user """ + def test_follow(self): + """remote user wants to follow local user""" activity = { "@context": "https://www.w3.org/ns/activitystreams", "id": "https://example.com/users/rat/follows/123", @@ -48,6 +49,8 @@ class InboxRelationships(TestCase): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock: views.inbox.activity_task(activity) self.assertEqual(mock.call_count, 1) + response_activity = json.loads(mock.call_args[0][1]) + self.assertEqual(response_activity["type"], "Accept") # notification created notification = models.Notification.objects.get() @@ -61,8 +64,35 @@ class InboxRelationships(TestCase): follow = models.UserFollows.objects.get(user_object=self.local_user) self.assertEqual(follow.user_subject, self.remote_user) - def test_handle_follow_manually_approved(self): - """ needs approval before following """ + def test_follow_duplicate(self): + """remote user wants to follow local user twice""" + activity = { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://example.com/users/rat/follows/123", + "type": "Follow", + "actor": "https://example.com/users/rat", + "object": "https://example.com/user/mouse", + } + + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + views.inbox.activity_task(activity) + + # the follow relationship should exist + follow = models.UserFollows.objects.get(user_object=self.local_user) + self.assertEqual(follow.user_subject, self.remote_user) + + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock: + views.inbox.activity_task(activity) + self.assertEqual(mock.call_count, 1) + response_activity = json.loads(mock.call_args[0][1]) + self.assertEqual(response_activity["type"], "Accept") + + # the follow relationship should STILL exist + follow = models.UserFollows.objects.get(user_object=self.local_user) + self.assertEqual(follow.user_subject, self.remote_user) + + def test_follow_manually_approved(self): + """needs approval before following""" activity = { "@context": "https://www.w3.org/ns/activitystreams", "id": "https://example.com/users/rat/follows/123", @@ -91,8 +121,8 @@ class InboxRelationships(TestCase): follow = models.UserFollows.objects.all() self.assertEqual(list(follow), []) - def test_handle_undo_follow_request(self): - """ the requester cancels a follow request """ + def test_undo_follow_request(self): + """the requester cancels a follow request""" self.local_user.manually_approves_followers = True self.local_user.save(broadcast=False) with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): @@ -121,8 +151,8 @@ class InboxRelationships(TestCase): self.assertFalse(self.local_user.follower_requests.exists()) - def test_handle_unfollow(self): - """ remove a relationship """ + def test_unfollow(self): + """remove a relationship""" with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): rel = models.UserFollows.objects.create( user_subject=self.remote_user, user_object=self.local_user @@ -146,8 +176,8 @@ class InboxRelationships(TestCase): views.inbox.activity_task(activity) self.assertIsNone(self.local_user.followers.first()) - def test_handle_follow_accept(self): - """ a remote user approved a follow request from local """ + def test_follow_accept(self): + """a remote user approved a follow request from local""" with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): rel = models.UserFollowRequest.objects.create( user_subject=self.local_user, user_object=self.remote_user @@ -177,8 +207,8 @@ class InboxRelationships(TestCase): self.assertEqual(follows.count(), 1) self.assertEqual(follows.first(), self.local_user) - def test_handle_follow_reject(self): - """ turn down a follow request """ + def test_follow_reject(self): + """turn down a follow request""" with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): rel = models.UserFollowRequest.objects.create( user_subject=self.local_user, user_object=self.remote_user diff --git a/bookwyrm/tests/views/inbox/test_inbox_like.py b/bookwyrm/tests/views/inbox/test_inbox_like.py index 05105b75f..a5f3a9f09 100644 --- a/bookwyrm/tests/views/inbox/test_inbox_like.py +++ b/bookwyrm/tests/views/inbox/test_inbox_like.py @@ -8,10 +8,10 @@ from bookwyrm import models, views # pylint: disable=too-many-public-methods class InboxActivities(TestCase): - """ inbox tests """ + """inbox tests""" def setUp(self): - """ basic user and book data """ + """basic user and book data""" self.local_user = models.User.objects.create_user( "mouse@example.com", "mouse@mouse.com", @@ -50,7 +50,7 @@ class InboxActivities(TestCase): models.SiteSettings.objects.create() def test_handle_favorite(self): - """ fav a status """ + """fav a status""" activity = { "@context": "https://www.w3.org/ns/activitystreams", "id": "https://example.com/fav/1", @@ -68,7 +68,7 @@ class InboxActivities(TestCase): self.assertEqual(fav.user, self.remote_user) def test_ignore_favorite(self): - """ don't try to save an unknown status """ + """don't try to save an unknown status""" activity = { "@context": "https://www.w3.org/ns/activitystreams", "id": "https://example.com/fav/1", @@ -83,7 +83,7 @@ class InboxActivities(TestCase): self.assertFalse(models.Favorite.objects.exists()) def test_handle_unfavorite(self): - """ fav a status """ + """fav a status""" activity = { "id": "https://example.com/fav/1#undo", "type": "Undo", diff --git a/bookwyrm/tests/views/inbox/test_inbox_remove.py b/bookwyrm/tests/views/inbox/test_inbox_remove.py index a17154d11..a0c4cdcf9 100644 --- a/bookwyrm/tests/views/inbox/test_inbox_remove.py +++ b/bookwyrm/tests/views/inbox/test_inbox_remove.py @@ -8,10 +8,10 @@ from bookwyrm import models, views # pylint: disable=too-many-public-methods class InboxRemove(TestCase): - """ inbox tests """ + """inbox tests""" def setUp(self): - """ basic user and book data """ + """basic user and book data""" self.local_user = models.User.objects.create_user( "mouse@example.com", "mouse@mouse.com", @@ -41,7 +41,7 @@ class InboxRemove(TestCase): models.SiteSettings.objects.create() def test_handle_unshelve_book(self): - """ remove a book from a shelf """ + """remove a book from a shelf""" shelf = models.Shelf.objects.create(user=self.remote_user, name="Test Shelf") shelf.remote_id = "https://bookwyrm.social/user/mouse/shelf/to-read" shelf.save() @@ -70,7 +70,7 @@ class InboxRemove(TestCase): self.assertFalse(shelf.books.exists()) def test_handle_remove_book_from_list(self): - """ listing a book """ + """listing a book""" with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): booklist = models.List.objects.create( name="test list", @@ -80,6 +80,7 @@ class InboxRemove(TestCase): user=self.local_user, book=self.book, book_list=booklist, + order=1, ) self.assertEqual(booklist.books.count(), 1) diff --git a/bookwyrm/tests/views/inbox/test_inbox_update.py b/bookwyrm/tests/views/inbox/test_inbox_update.py index 012343e78..9fdc97922 100644 --- a/bookwyrm/tests/views/inbox/test_inbox_update.py +++ b/bookwyrm/tests/views/inbox/test_inbox_update.py @@ -10,10 +10,10 @@ from bookwyrm import models, views # pylint: disable=too-many-public-methods class InboxUpdate(TestCase): - """ inbox tests """ + """inbox tests""" def setUp(self): - """ basic user and book data """ + """basic user and book data""" self.local_user = models.User.objects.create_user( "mouse@example.com", "mouse@mouse.com", @@ -23,6 +23,16 @@ class InboxUpdate(TestCase): ) self.local_user.remote_id = "https://example.com/user/mouse" self.local_user.save(broadcast=False) + with patch("bookwyrm.models.user.set_remote_server.delay"): + self.remote_user = models.User.objects.create_user( + "rat", + "rat@rat.com", + "ratword", + local=False, + remote_id="https://example.com/users/rat", + inbox="https://example.com/users/rat/inbox", + outbox="https://example.com/users/rat/outbox", + ) self.create_json = { "id": "hi", @@ -34,8 +44,8 @@ class InboxUpdate(TestCase): } models.SiteSettings.objects.create() - def test_handle_update_list(self): - """ a new list """ + def test_update_list(self): + """a new list""" with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): book_list = models.List.objects.create( name="hi", remote_id="https://example.com/list/22", user=self.local_user @@ -68,16 +78,24 @@ class InboxUpdate(TestCase): self.assertEqual(book_list.description, "summary text") self.assertEqual(book_list.remote_id, "https://example.com/list/22") - def test_handle_update_user(self): - """ update an existing user """ - # we only do this with remote users - self.local_user.local = False - self.local_user.save() + def test_update_user(self): + """update an existing user""" + models.UserFollows.objects.create( + user_subject=self.local_user, + user_object=self.remote_user, + ) + models.UserFollows.objects.create( + user_subject=self.remote_user, + user_object=self.local_user, + ) + self.assertTrue(self.remote_user in self.local_user.followers.all()) + self.assertTrue(self.local_user in self.remote_user.followers.all()) - datafile = pathlib.Path(__file__).parent.joinpath("../../data/ap_user.json") + datafile = pathlib.Path(__file__).parent.joinpath("../../data/ap_user_rat.json") userdata = json.loads(datafile.read_bytes()) del userdata["icon"] - self.assertIsNone(self.local_user.name) + self.assertIsNone(self.remote_user.name) + self.assertFalse(self.remote_user.discoverable) views.inbox.activity_task( { "type": "Update", @@ -88,14 +106,17 @@ class InboxUpdate(TestCase): "object": userdata, } ) - user = models.User.objects.get(id=self.local_user.id) - self.assertEqual(user.name, "MOUSE?? MOUSE!!") - self.assertEqual(user.username, "mouse@example.com") - self.assertEqual(user.localname, "mouse") + user = models.User.objects.get(id=self.remote_user.id) + self.assertEqual(user.name, "RAT???") + self.assertEqual(user.username, "rat@example.com") self.assertTrue(user.discoverable) - def test_handle_update_edition(self): - """ update an existing edition """ + # make sure relationships aren't disrupted + self.assertTrue(self.remote_user in self.local_user.followers.all()) + self.assertTrue(self.local_user in self.remote_user.followers.all()) + + def test_update_edition(self): + """update an existing edition""" datafile = pathlib.Path(__file__).parent.joinpath("../../data/bw_edition.json") bookdata = json.loads(datafile.read_bytes()) @@ -122,9 +143,10 @@ class InboxUpdate(TestCase): ) book = models.Edition.objects.get(id=book.id) self.assertEqual(book.title, "Piranesi") + self.assertEqual(book.last_edited_by, self.remote_user) - def test_handle_update_work(self): - """ update an existing edition """ + def test_update_work(self): + """update an existing edition""" datafile = pathlib.Path(__file__).parent.joinpath("../../data/bw_work.json") bookdata = json.loads(datafile.read_bytes()) diff --git a/bookwyrm/tests/views/test_authentication.py b/bookwyrm/tests/views/test_authentication.py index f6d318615..c310b0a26 100644 --- a/bookwyrm/tests/views/test_authentication.py +++ b/bookwyrm/tests/views/test_authentication.py @@ -14,10 +14,10 @@ from bookwyrm.settings import DOMAIN # pylint: disable=too-many-public-methods class AuthenticationViews(TestCase): - """ login and password management """ + """login and password management""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -31,7 +31,7 @@ class AuthenticationViews(TestCase): self.settings = models.SiteSettings.objects.create(id=1) def test_login_get(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" login = views.Login.as_view() request = self.factory.get("") request.user = self.anonymous_user @@ -47,7 +47,7 @@ class AuthenticationViews(TestCase): self.assertEqual(result.status_code, 302) def test_register(self): - """ create a user """ + """create a user""" view = views.Register.as_view() self.assertEqual(models.User.objects.count(), 1) request = self.factory.post( @@ -68,7 +68,7 @@ class AuthenticationViews(TestCase): self.assertEqual(nutria.local, True) def test_register_trailing_space(self): - """ django handles this so weirdly """ + """django handles this so weirdly""" view = views.Register.as_view() request = self.factory.post( "register/", @@ -84,7 +84,7 @@ class AuthenticationViews(TestCase): self.assertEqual(nutria.local, True) def test_register_invalid_email(self): - """ gotta have an email """ + """gotta have an email""" view = views.Register.as_view() self.assertEqual(models.User.objects.count(), 1) request = self.factory.post( @@ -95,7 +95,7 @@ class AuthenticationViews(TestCase): response.render() def test_register_invalid_username(self): - """ gotta have an email """ + """gotta have an email""" view = views.Register.as_view() self.assertEqual(models.User.objects.count(), 1) request = self.factory.post( @@ -123,7 +123,7 @@ class AuthenticationViews(TestCase): response.render() def test_register_closed_instance(self): - """ you can't just register """ + """you can't just register""" view = views.Register.as_view() self.settings.allow_registration = False self.settings.save() @@ -135,7 +135,7 @@ class AuthenticationViews(TestCase): view(request) def test_register_invite(self): - """ you can't just register """ + """you can't just register""" view = views.Register.as_view() self.settings.allow_registration = False self.settings.save() diff --git a/bookwyrm/tests/views/test_author.py b/bookwyrm/tests/views/test_author.py index bb047b7c1..5dfbc3504 100644 --- a/bookwyrm/tests/views/test_author.py +++ b/bookwyrm/tests/views/test_author.py @@ -12,10 +12,10 @@ from bookwyrm.activitypub import ActivitypubResponse class AuthorViews(TestCase): - """ author views""" + """author views""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -42,7 +42,7 @@ class AuthorViews(TestCase): models.SiteSettings.objects.create() def test_author_page(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.Author.as_view() author = models.Author.objects.create(name="Jessica") request = self.factory.get("") @@ -62,7 +62,7 @@ class AuthorViews(TestCase): self.assertEqual(result.status_code, 200) def test_edit_author_page(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.EditAuthor.as_view() author = models.Author.objects.create(name="Test Author") request = self.factory.get("") @@ -76,7 +76,7 @@ class AuthorViews(TestCase): self.assertEqual(result.status_code, 200) def test_edit_author(self): - """ edit an author """ + """edit an author""" view = views.EditAuthor.as_view() author = models.Author.objects.create(name="Test Author") self.local_user.groups.add(self.group) @@ -93,7 +93,7 @@ class AuthorViews(TestCase): self.assertEqual(author.last_edited_by, self.local_user) def test_edit_author_non_editor(self): - """ edit an author with invalid post data""" + """edit an author with invalid post data""" view = views.EditAuthor.as_view() author = models.Author.objects.create(name="Test Author") form = forms.AuthorForm(instance=author) @@ -108,7 +108,7 @@ class AuthorViews(TestCase): self.assertEqual(author.name, "Test Author") def test_edit_author_invalid_form(self): - """ edit an author with invalid post data""" + """edit an author with invalid post data""" view = views.EditAuthor.as_view() author = models.Author.objects.create(name="Test Author") self.local_user.groups.add(self.group) diff --git a/bookwyrm/tests/views/test_block.py b/bookwyrm/tests/views/test_block.py index 71583d708..0b754689d 100644 --- a/bookwyrm/tests/views/test_block.py +++ b/bookwyrm/tests/views/test_block.py @@ -9,10 +9,10 @@ from bookwyrm import models, views @patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") class BlockViews(TestCase): - """ view user and edit profile """ + """view user and edit profile""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -34,7 +34,7 @@ class BlockViews(TestCase): models.SiteSettings.objects.create() def test_block_get(self, _): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.Block.as_view() request = self.factory.get("") request.user = self.local_user @@ -44,7 +44,7 @@ class BlockViews(TestCase): self.assertEqual(result.status_code, 200) def test_block_post(self, _): - """ create a "block" database entry from an activity """ + """create a "block" database entry from an activity""" view = views.Block.as_view() self.local_user.followers.add(self.remote_user) models.UserFollowRequest.objects.create( @@ -65,7 +65,7 @@ class BlockViews(TestCase): self.assertFalse(models.UserFollowRequest.objects.exists()) def test_unblock(self, _): - """ undo a block """ + """undo a block""" self.local_user.blocks.add(self.remote_user) request = self.factory.post("") request.user = self.local_user diff --git a/bookwyrm/tests/views/test_book.py b/bookwyrm/tests/views/test_book.py index a0fa03676..dce50868f 100644 --- a/bookwyrm/tests/views/test_book.py +++ b/bookwyrm/tests/views/test_book.py @@ -18,10 +18,10 @@ from bookwyrm.activitypub import ActivitypubResponse class BookViews(TestCase): - """ books books books """ + """books books books""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -81,7 +81,7 @@ class BookViews(TestCase): ) def test_book_page(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.Book.as_view() request = self.factory.get("") request.user = self.local_user @@ -100,7 +100,7 @@ class BookViews(TestCase): self.assertEqual(result.status_code, 200) def test_edit_book_page(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.EditBook.as_view() request = self.factory.get("") request.user = self.local_user @@ -111,7 +111,7 @@ class BookViews(TestCase): self.assertEqual(result.status_code, 200) def test_edit_book(self): - """ lets a user edit a book """ + """lets a user edit a book""" view = views.EditBook.as_view() self.local_user.groups.add(self.group) form = forms.EditionForm(instance=self.book) @@ -125,7 +125,7 @@ class BookViews(TestCase): self.assertEqual(self.book.title, "New Title") def test_edit_book_add_author(self): - """ lets a user edit a book with new authors """ + """lets a user edit a book with new authors""" view = views.EditBook.as_view() self.local_user.groups.add(self.group) form = forms.EditionForm(instance=self.book) @@ -143,7 +143,7 @@ class BookViews(TestCase): self.assertEqual(self.book.title, "Example Edition") def test_edit_book_add_new_author_confirm(self): - """ lets a user edit a book confirmed with new authors """ + """lets a user edit a book confirmed with new authors""" view = views.ConfirmEditBook.as_view() self.local_user.groups.add(self.group) form = forms.EditionForm(instance=self.book) @@ -162,7 +162,7 @@ class BookViews(TestCase): self.assertEqual(self.book.authors.first().name, "Sappho") def test_edit_book_remove_author(self): - """ remove an author from a book """ + """remove an author from a book""" author = models.Author.objects.create(name="Sappho") self.book.authors.add(author) form = forms.EditionForm(instance=self.book) @@ -182,7 +182,7 @@ class BookViews(TestCase): self.assertFalse(self.book.authors.exists()) def test_create_book(self): - """ create an entirely new book and work """ + """create an entirely new book and work""" view = views.ConfirmEditBook.as_view() self.local_user.groups.add(self.group) form = forms.EditionForm() @@ -196,7 +196,7 @@ class BookViews(TestCase): self.assertEqual(book.parent_work.title, "New Title") def test_create_book_existing_work(self): - """ create an entirely new book and work """ + """create an entirely new book and work""" view = views.ConfirmEditBook.as_view() self.local_user.groups.add(self.group) form = forms.EditionForm() @@ -211,7 +211,7 @@ class BookViews(TestCase): self.assertEqual(book.parent_work, self.work) def test_create_book_with_author(self): - """ create an entirely new book and work """ + """create an entirely new book and work""" view = views.ConfirmEditBook.as_view() self.local_user.groups.add(self.group) form = forms.EditionForm() @@ -229,7 +229,7 @@ class BookViews(TestCase): self.assertEqual(book.authors.first(), book.parent_work.authors.first()) def test_switch_edition(self): - """ updates user's relationships to a book """ + """updates user's relationships to a book""" work = models.Work.objects.create(title="test work") edition1 = models.Edition.objects.create(title="first ed", parent_work=work) edition2 = models.Edition.objects.create(title="second ed", parent_work=work) @@ -253,7 +253,7 @@ class BookViews(TestCase): self.assertEqual(models.ReadThrough.objects.get().book, edition2) def test_editions_page(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.Editions.as_view() request = self.factory.get("") with patch("bookwyrm.views.books.is_api_request") as is_api: @@ -271,7 +271,7 @@ class BookViews(TestCase): self.assertEqual(result.status_code, 200) def test_upload_cover_file(self): - """ add a cover via file upload """ + """add a cover via file upload""" self.assertFalse(self.book.cover) image_file = pathlib.Path(__file__).parent.joinpath( "../../static/images/default_avi.jpg" @@ -296,7 +296,7 @@ class BookViews(TestCase): @responses.activate def test_upload_cover_url(self): - """ add a cover via url """ + """add a cover via url""" self.assertFalse(self.book.cover) image_file = pathlib.Path(__file__).parent.joinpath( "../../static/images/default_avi.jpg" diff --git a/bookwyrm/tests/views/test_directory.py b/bookwyrm/tests/views/test_directory.py index 80d9eaf79..cada50bce 100644 --- a/bookwyrm/tests/views/test_directory.py +++ b/bookwyrm/tests/views/test_directory.py @@ -7,10 +7,10 @@ from bookwyrm import models, views # pylint: disable=unused-argument class DirectoryViews(TestCase): - """ tag views""" + """tag views""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -32,7 +32,7 @@ class DirectoryViews(TestCase): models.SiteSettings.objects.create() def test_directory_page(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.Directory.as_view() request = self.factory.get("") request.user = self.local_user diff --git a/bookwyrm/tests/views/test_federation.py b/bookwyrm/tests/views/test_federation.py index 2afdd6d3c..f17f7624b 100644 --- a/bookwyrm/tests/views/test_federation.py +++ b/bookwyrm/tests/views/test_federation.py @@ -10,10 +10,10 @@ from bookwyrm import forms, models, views class FederationViews(TestCase): - """ every response to a get request, html or json """ + """every response to a get request, html or json""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -35,7 +35,7 @@ class FederationViews(TestCase): models.SiteSettings.objects.create() def test_federation_page(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.Federation.as_view() request = self.factory.get("") request.user = self.local_user @@ -46,7 +46,7 @@ class FederationViews(TestCase): self.assertEqual(result.status_code, 200) def test_server_page(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" server = models.FederatedServer.objects.create(server_name="hi.there.com") view = views.FederatedServer.as_view() request = self.factory.get("") @@ -59,7 +59,7 @@ class FederationViews(TestCase): self.assertEqual(result.status_code, 200) def test_server_page_block(self): - """ block a server """ + """block a server""" server = models.FederatedServer.objects.create(server_name="hi.there.com") self.remote_user.federated_server = server self.remote_user.save() @@ -79,7 +79,7 @@ class FederationViews(TestCase): self.assertFalse(self.remote_user.is_active) def test_server_page_unblock(self): - """ unblock a server """ + """unblock a server""" server = models.FederatedServer.objects.create( server_name="hi.there.com", status="blocked" ) @@ -100,7 +100,7 @@ class FederationViews(TestCase): self.assertTrue(self.remote_user.is_active) def test_add_view_get(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" # create mode view = views.AddFederatedServer.as_view() request = self.factory.get("") @@ -113,7 +113,7 @@ class FederationViews(TestCase): self.assertEqual(result.status_code, 200) def test_add_view_post_create(self): - """ create a server entry """ + """create a server entry""" form = forms.ServerForm() form.data["server_name"] = "remote.server" form.data["application_type"] = "coolsoft" @@ -131,7 +131,7 @@ class FederationViews(TestCase): self.assertEqual(server.status, "blocked") def test_import_blocklist(self): - """ load a json file with a list of servers to block """ + """load a json file with a list of servers to block""" server = models.FederatedServer.objects.create(server_name="hi.there.com") self.remote_user.federated_server = server self.remote_user.save() diff --git a/bookwyrm/tests/views/test_feed.py b/bookwyrm/tests/views/test_feed.py index dd38a3ebe..a6a3d9677 100644 --- a/bookwyrm/tests/views/test_feed.py +++ b/bookwyrm/tests/views/test_feed.py @@ -17,10 +17,10 @@ from bookwyrm.activitypub import ActivitypubResponse @patch("bookwyrm.activitystreams.ActivityStream.get_activity_stream") @patch("bookwyrm.activitystreams.ActivityStream.add_status") class FeedViews(TestCase): - """ activity feed, statuses, dms """ + """activity feed, statuses, dms""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -37,7 +37,7 @@ class FeedViews(TestCase): models.SiteSettings.objects.create() def test_feed(self, *_): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.Feed.as_view() request = self.factory.get("") request.user = self.local_user @@ -47,7 +47,7 @@ class FeedViews(TestCase): self.assertEqual(result.status_code, 200) def test_status_page(self, *_): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.Status.as_view() with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): status = models.Status.objects.create(content="hi", user=self.local_user) @@ -67,7 +67,7 @@ class FeedViews(TestCase): self.assertEqual(result.status_code, 200) def test_status_page_not_found(self, *_): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.Status.as_view() request = self.factory.get("") @@ -79,7 +79,7 @@ class FeedViews(TestCase): self.assertEqual(result.status_code, 404) def test_status_page_with_image(self, *_): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.Status.as_view() image_file = pathlib.Path(__file__).parent.joinpath( @@ -115,7 +115,7 @@ class FeedViews(TestCase): self.assertEqual(result.status_code, 200) def test_replies_page(self, *_): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.Replies.as_view() with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): status = models.Status.objects.create(content="hi", user=self.local_user) @@ -135,7 +135,7 @@ class FeedViews(TestCase): self.assertEqual(result.status_code, 200) def test_direct_messages_page(self, *_): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" view = views.DirectMessage.as_view() request = self.factory.get("") request.user = self.local_user @@ -145,7 +145,7 @@ class FeedViews(TestCase): self.assertEqual(result.status_code, 200) def test_get_suggested_book(self, *_): - """ gets books the ~*~ algorithm ~*~ thinks you want to post about """ + """gets books the ~*~ algorithm ~*~ thinks you want to post about""" with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): models.ShelfBook.objects.create( book=self.book, diff --git a/bookwyrm/tests/views/test_follow.py b/bookwyrm/tests/views/test_follow.py index 6b4de05dd..45e60d3cb 100644 --- a/bookwyrm/tests/views/test_follow.py +++ b/bookwyrm/tests/views/test_follow.py @@ -11,10 +11,10 @@ from bookwyrm import models, views class BookViews(TestCase): - """ books books books """ + """books books books""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -50,7 +50,7 @@ class BookViews(TestCase): ) def test_handle_follow_remote(self): - """ send a follow request """ + """send a follow request""" request = self.factory.post("", {"user": self.remote_user.username}) request.user = self.local_user self.assertEqual(models.UserFollowRequest.objects.count(), 0) @@ -65,7 +65,7 @@ class BookViews(TestCase): self.assertEqual(rel.status, "follow_request") def test_handle_follow_local_manually_approves(self): - """ send a follow request """ + """send a follow request""" rat = models.User.objects.create_user( "rat@local.com", "rat@rat.com", @@ -88,7 +88,7 @@ class BookViews(TestCase): self.assertEqual(rel.status, "follow_request") def test_handle_follow_local(self): - """ send a follow request """ + """send a follow request""" rat = models.User.objects.create_user( "rat@local.com", "rat@rat.com", @@ -111,7 +111,7 @@ class BookViews(TestCase): self.assertEqual(rel.status, "follows") def test_handle_unfollow(self): - """ send an unfollow """ + """send an unfollow""" request = self.factory.post("", {"user": self.remote_user.username}) request.user = self.local_user self.remote_user.followers.add(self.local_user) @@ -125,7 +125,7 @@ class BookViews(TestCase): self.assertEqual(self.remote_user.followers.count(), 0) def test_handle_accept(self): - """ accept a follow request """ + """accept a follow request""" self.local_user.manually_approves_followers = True self.local_user.save(broadcast=False) request = self.factory.post("", {"user": self.remote_user.username}) @@ -142,7 +142,7 @@ class BookViews(TestCase): self.assertEqual(self.local_user.followers.first(), self.remote_user) def test_handle_reject(self): - """ reject a follow request """ + """reject a follow request""" self.local_user.manually_approves_followers = True self.local_user.save(broadcast=False) request = self.factory.post("", {"user": self.remote_user.username}) diff --git a/bookwyrm/tests/views/test_get_started.py b/bookwyrm/tests/views/test_get_started.py index 71eb4060f..1c55da086 100644 --- a/bookwyrm/tests/views/test_get_started.py +++ b/bookwyrm/tests/views/test_get_started.py @@ -8,10 +8,10 @@ from bookwyrm import forms, models, views class GetStartedViews(TestCase): - """ helping new users get oriented """ + """helping new users get oriented""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -31,7 +31,7 @@ class GetStartedViews(TestCase): models.SiteSettings.objects.create() def test_profile_view(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.GetStartedProfile.as_view() request = self.factory.get("") request.user = self.local_user @@ -43,7 +43,7 @@ class GetStartedViews(TestCase): self.assertEqual(result.status_code, 200) def test_profile_view_post(self): - """ save basic user details """ + """save basic user details""" view = views.GetStartedProfile.as_view() form = forms.LimitedEditUserForm(instance=self.local_user) form.data["name"] = "New Name" @@ -61,7 +61,7 @@ class GetStartedViews(TestCase): self.assertTrue(self.local_user.discoverable) def test_books_view(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.GetStartedBooks.as_view() request = self.factory.get("") request.user = self.local_user @@ -73,7 +73,7 @@ class GetStartedViews(TestCase): self.assertEqual(result.status_code, 200) def test_books_view_with_query(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.GetStartedBooks.as_view() request = self.factory.get("?query=Example") request.user = self.local_user @@ -85,7 +85,7 @@ class GetStartedViews(TestCase): self.assertEqual(result.status_code, 200) def test_books_view_post(self): - """ shelve some books """ + """shelve some books""" view = views.GetStartedBooks.as_view() data = {self.book.id: self.local_user.shelf_set.first().id} request = self.factory.post("", data) @@ -103,7 +103,7 @@ class GetStartedViews(TestCase): self.assertEqual(shelfbook.user, self.local_user) def test_users_view(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.GetStartedUsers.as_view() request = self.factory.get("") request.user = self.local_user @@ -115,7 +115,7 @@ class GetStartedViews(TestCase): self.assertEqual(result.status_code, 200) def test_users_view_with_query(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.GetStartedUsers.as_view() request = self.factory.get("?query=rat") request.user = self.local_user diff --git a/bookwyrm/tests/views/test_goal.py b/bookwyrm/tests/views/test_goal.py index cbe4fe015..4e8f6ee23 100644 --- a/bookwyrm/tests/views/test_goal.py +++ b/bookwyrm/tests/views/test_goal.py @@ -11,10 +11,10 @@ from bookwyrm import models, views class GoalViews(TestCase): - """ viewing and creating statuses """ + """viewing and creating statuses""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -41,7 +41,7 @@ class GoalViews(TestCase): models.SiteSettings.objects.create() def test_goal_page_no_goal(self): - """ view a reading goal page for another's unset goal """ + """view a reading goal page for another's unset goal""" view = views.Goal.as_view() request = self.factory.get("") request.user = self.rat @@ -50,7 +50,7 @@ class GoalViews(TestCase): self.assertEqual(result.status_code, 404) def test_goal_page_no_goal_self(self): - """ view a reading goal page for your own unset goal """ + """view a reading goal page for your own unset goal""" view = views.Goal.as_view() request = self.factory.get("") request.user = self.local_user @@ -60,7 +60,7 @@ class GoalViews(TestCase): self.assertIsInstance(result, TemplateResponse) def test_goal_page_anonymous(self): - """ can't view it without login """ + """can't view it without login""" view = views.Goal.as_view() request = self.factory.get("") request.user = self.anonymous_user @@ -69,7 +69,7 @@ class GoalViews(TestCase): self.assertEqual(result.status_code, 302) def test_goal_page_public(self): - """ view a user's public goal """ + """view a user's public goal""" models.ReadThrough.objects.create( finish_date=timezone.now(), user=self.local_user, @@ -91,7 +91,7 @@ class GoalViews(TestCase): self.assertIsInstance(result, TemplateResponse) def test_goal_page_private(self): - """ view a user's private goal """ + """view a user's private goal""" models.AnnualGoal.objects.create( user=self.local_user, year=2020, goal=15, privacy="followers" ) @@ -104,7 +104,7 @@ class GoalViews(TestCase): @patch("bookwyrm.activitystreams.ActivityStream.add_status") def test_create_goal(self, _): - """ create a new goal """ + """create a new goal""" view = views.Goal.as_view() request = self.factory.post( "", diff --git a/bookwyrm/tests/views/test_helpers.py b/bookwyrm/tests/views/test_helpers.py index 2e5ed82d4..e2e041e9a 100644 --- a/bookwyrm/tests/views/test_helpers.py +++ b/bookwyrm/tests/views/test_helpers.py @@ -13,10 +13,10 @@ from bookwyrm.settings import USER_AGENT @patch("bookwyrm.activitystreams.ActivityStream.add_status") class ViewsHelpers(TestCase): - """ viewing and creating statuses """ + """viewing and creating statuses""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -53,12 +53,12 @@ class ViewsHelpers(TestCase): ) def test_get_edition(self, _): - """ given an edition or a work, returns an edition """ + """given an edition or a work, returns an edition""" self.assertEqual(views.helpers.get_edition(self.book.id), self.book) self.assertEqual(views.helpers.get_edition(self.work.id), self.book) def test_get_user_from_username(self, _): - """ works for either localname or username """ + """works for either localname or username""" self.assertEqual( views.helpers.get_user_from_username(self.local_user, "mouse"), self.local_user, @@ -71,7 +71,7 @@ class ViewsHelpers(TestCase): views.helpers.get_user_from_username(self.local_user, "mojfse@example.com") def test_is_api_request(self, _): - """ should it return html or json """ + """should it return html or json""" request = self.factory.get("/path") request.headers = {"Accept": "application/json"} self.assertTrue(views.helpers.is_api_request(request)) @@ -85,12 +85,12 @@ class ViewsHelpers(TestCase): self.assertFalse(views.helpers.is_api_request(request)) def test_is_api_request_no_headers(self, _): - """ should it return html or json """ + """should it return html or json""" request = self.factory.get("/path") self.assertFalse(views.helpers.is_api_request(request)) def test_is_bookwyrm_request(self, _): - """ checks if a request came from a bookwyrm instance """ + """checks if a request came from a bookwyrm instance""" request = self.factory.get("", {"q": "Test Book"}) self.assertFalse(views.helpers.is_bookwyrm_request(request)) @@ -105,7 +105,7 @@ class ViewsHelpers(TestCase): self.assertTrue(views.helpers.is_bookwyrm_request(request)) def test_existing_user(self, _): - """ simple database lookup by username """ + """simple database lookup by username""" result = views.helpers.handle_remote_webfinger("@mouse@local.com") self.assertEqual(result, self.local_user) @@ -117,7 +117,7 @@ class ViewsHelpers(TestCase): @responses.activate def test_load_user(self, _): - """ find a remote user using webfinger """ + """find a remote user using webfinger""" username = "mouse@example.com" wellknown = { "subject": "acct:mouse@example.com", @@ -147,7 +147,7 @@ class ViewsHelpers(TestCase): self.assertEqual(result.username, "mouse@example.com") def test_user_on_blocked_server(self, _): - """ find a remote user using webfinger """ + """find a remote user using webfinger""" models.FederatedServer.objects.create( server_name="example.com", status="blocked" ) @@ -156,7 +156,7 @@ class ViewsHelpers(TestCase): self.assertIsNone(result) def test_handle_reading_status_to_read(self, _): - """ posts shelve activities """ + """posts shelve activities""" shelf = self.local_user.shelf_set.get(identifier="to-read") with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): views.helpers.handle_reading_status( @@ -168,7 +168,7 @@ class ViewsHelpers(TestCase): self.assertEqual(status.content, "wants to read") def test_handle_reading_status_reading(self, _): - """ posts shelve activities """ + """posts shelve activities""" shelf = self.local_user.shelf_set.get(identifier="reading") with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): views.helpers.handle_reading_status( @@ -180,7 +180,7 @@ class ViewsHelpers(TestCase): self.assertEqual(status.content, "started reading") def test_handle_reading_status_read(self, _): - """ posts shelve activities """ + """posts shelve activities""" shelf = self.local_user.shelf_set.get(identifier="read") with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): views.helpers.handle_reading_status( @@ -192,7 +192,7 @@ class ViewsHelpers(TestCase): self.assertEqual(status.content, "finished reading") def test_handle_reading_status_other(self, _): - """ posts shelve activities """ + """posts shelve activities""" with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): views.helpers.handle_reading_status( self.local_user, self.shelf, self.book, "public" @@ -200,7 +200,7 @@ class ViewsHelpers(TestCase): self.assertFalse(models.GeneratedNote.objects.exists()) def test_get_annotated_users(self, _): - """ list of people you might know """ + """list of people you might know""" user_1 = models.User.objects.create_user( "nutria@local.com", "nutria@nutria.com", @@ -247,7 +247,7 @@ class ViewsHelpers(TestCase): self.assertEqual(remote_user_annotated.shared_books, 0) def test_get_annotated_users_counts(self, _): - """ correct counting for multiple shared attributed """ + """correct counting for multiple shared attributed""" user_1 = models.User.objects.create_user( "nutria@local.com", "nutria@nutria.com", diff --git a/bookwyrm/tests/views/test_import.py b/bookwyrm/tests/views/test_import.py index 4de2cfb9c..22694623a 100644 --- a/bookwyrm/tests/views/test_import.py +++ b/bookwyrm/tests/views/test_import.py @@ -9,10 +9,10 @@ from bookwyrm import views class ImportViews(TestCase): - """ goodreads import views """ + """goodreads import views""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -24,7 +24,7 @@ class ImportViews(TestCase): models.SiteSettings.objects.create() def test_import_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.Import.as_view() request = self.factory.get("") request.user = self.local_user @@ -34,7 +34,7 @@ class ImportViews(TestCase): self.assertEqual(result.status_code, 200) def test_import_status(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.ImportStatus.as_view() import_job = models.ImportJob.objects.create(user=self.local_user) request = self.factory.get("") @@ -47,7 +47,7 @@ class ImportViews(TestCase): self.assertEqual(result.status_code, 200) def test_retry_import(self): - """ retry failed items """ + """retry failed items""" view = views.ImportStatus.as_view() import_job = models.ImportJob.objects.create( user=self.local_user, privacy="unlisted" diff --git a/bookwyrm/tests/views/test_interaction.py b/bookwyrm/tests/views/test_interaction.py index 8d2c63ffc..876d6053c 100644 --- a/bookwyrm/tests/views/test_interaction.py +++ b/bookwyrm/tests/views/test_interaction.py @@ -1,4 +1,5 @@ """ test for app action functionality """ +import json from unittest.mock import patch from django.test import TestCase from django.test.client import RequestFactory @@ -8,10 +9,10 @@ from bookwyrm import models, views @patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") class InteractionViews(TestCase): - """ viewing and creating statuses """ + """viewing and creating statuses""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -39,8 +40,8 @@ class InteractionViews(TestCase): parent_work=work, ) - def test_handle_favorite(self, _): - """ create and broadcast faving a status """ + def test_favorite(self, _): + """create and broadcast faving a status""" view = views.Favorite.as_view() request = self.factory.post("") request.user = self.remote_user @@ -57,8 +58,8 @@ class InteractionViews(TestCase): self.assertEqual(notification.user, self.local_user) self.assertEqual(notification.related_user, self.remote_user) - def test_handle_unfavorite(self, _): - """ unfav a status """ + def test_unfavorite(self, _): + """unfav a status""" view = views.Unfavorite.as_view() request = self.factory.post("") request.user = self.remote_user @@ -74,8 +75,8 @@ class InteractionViews(TestCase): self.assertEqual(models.Favorite.objects.count(), 0) self.assertEqual(models.Notification.objects.count(), 0) - def test_handle_boost(self, _): - """ boost a status """ + def test_boost(self, _): + """boost a status""" view = views.Boost.as_view() request = self.factory.post("") request.user = self.remote_user @@ -85,6 +86,7 @@ class InteractionViews(TestCase): view(request, status.id) boost = models.Boost.objects.get() + self.assertEqual(boost.boosted_status, status) self.assertEqual(boost.user, self.remote_user) self.assertEqual(boost.privacy, "public") @@ -95,15 +97,22 @@ class InteractionViews(TestCase): self.assertEqual(notification.related_user, self.remote_user) self.assertEqual(notification.related_status, status) - def test_handle_self_boost(self, _): - """ boost your own status """ + def test_self_boost(self, _): + """boost your own status""" view = views.Boost.as_view() request = self.factory.post("") request.user = self.local_user with patch("bookwyrm.activitystreams.ActivityStream.add_status"): status = models.Status.objects.create(user=self.local_user, content="hi") - view(request, status.id) + with patch( + "bookwyrm.models.activitypub_mixin.broadcast_task.delay" + ) as broadcast_mock: + view(request, status.id) + + self.assertEqual(broadcast_mock.call_count, 1) + activity = json.loads(broadcast_mock.call_args[0][1]) + self.assertEqual(activity["type"], "Announce") boost = models.Boost.objects.get() self.assertEqual(boost.boosted_status, status) @@ -112,8 +121,8 @@ class InteractionViews(TestCase): self.assertFalse(models.Notification.objects.exists()) - def test_handle_boost_unlisted(self, _): - """ boost a status """ + def test_boost_unlisted(self, _): + """boost a status""" view = views.Boost.as_view() request = self.factory.post("") request.user = self.local_user @@ -127,8 +136,8 @@ class InteractionViews(TestCase): boost = models.Boost.objects.get() self.assertEqual(boost.privacy, "unlisted") - def test_handle_boost_private(self, _): - """ boost a status """ + def test_boost_private(self, _): + """boost a status""" view = views.Boost.as_view() request = self.factory.post("") request.user = self.local_user @@ -140,8 +149,8 @@ class InteractionViews(TestCase): view(request, status.id) self.assertFalse(models.Boost.objects.exists()) - def test_handle_boost_twice(self, _): - """ boost a status """ + def test_boost_twice(self, _): + """boost a status""" view = views.Boost.as_view() request = self.factory.post("") request.user = self.local_user @@ -152,8 +161,8 @@ class InteractionViews(TestCase): view(request, status.id) self.assertEqual(models.Boost.objects.count(), 1) - def test_handle_unboost(self, _): - """ undo a boost """ + def test_unboost(self, _): + """undo a boost""" view = views.Unboost.as_view() request = self.factory.post("") request.user = self.remote_user diff --git a/bookwyrm/tests/views/test_invite.py b/bookwyrm/tests/views/test_invite.py index 7bfc8fe53..7b5071b36 100644 --- a/bookwyrm/tests/views/test_invite.py +++ b/bookwyrm/tests/views/test_invite.py @@ -11,10 +11,10 @@ from bookwyrm import views class InviteViews(TestCase): - """ every response to a get request, html or json """ + """every response to a get request, html or json""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -26,7 +26,7 @@ class InviteViews(TestCase): models.SiteSettings.objects.create() def test_invite_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.Invite.as_view() models.SiteInvite.objects.create(code="hi", user=self.local_user) request = self.factory.get("") @@ -41,7 +41,7 @@ class InviteViews(TestCase): self.assertEqual(result.status_code, 200) def test_manage_invites(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.ManageInvites.as_view() request = self.factory.get("") request.user = self.local_user @@ -52,7 +52,7 @@ class InviteViews(TestCase): self.assertEqual(result.status_code, 200) def test_invite_request(self): - """ request to join a server """ + """request to join a server""" form = forms.InviteRequestForm() form.data["email"] = "new@user.email" @@ -66,7 +66,7 @@ class InviteViews(TestCase): self.assertEqual(req.email, "new@user.email") def test_invite_request_email_taken(self): - """ request to join a server with an existing user email """ + """request to join a server with an existing user email""" form = forms.InviteRequestForm() form.data["email"] = "mouse@mouse.mouse" @@ -80,7 +80,7 @@ class InviteViews(TestCase): self.assertFalse(models.InviteRequest.objects.exists()) def test_manage_invite_requests(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.ManageInviteRequests.as_view() request = self.factory.get("") request.user = self.local_user @@ -98,7 +98,7 @@ class InviteViews(TestCase): self.assertEqual(result.status_code, 200) def test_manage_invite_requests_send(self): - """ send an invite """ + """send an invite""" req = models.InviteRequest.objects.create(email="fish@example.com") view = views.ManageInviteRequests.as_view() @@ -113,7 +113,7 @@ class InviteViews(TestCase): self.assertIsNotNone(req.invite) def test_ignore_invite_request(self): - """ don't invite that jerk """ + """don't invite that jerk""" req = models.InviteRequest.objects.create(email="fish@example.com") view = views.ignore_invite_request diff --git a/bookwyrm/tests/views/test_isbn.py b/bookwyrm/tests/views/test_isbn.py index 7f03a6109..2aedd3cea 100644 --- a/bookwyrm/tests/views/test_isbn.py +++ b/bookwyrm/tests/views/test_isbn.py @@ -11,10 +11,10 @@ from bookwyrm.settings import DOMAIN class IsbnViews(TestCase): - """ tag views""" + """tag views""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -37,7 +37,7 @@ class IsbnViews(TestCase): models.SiteSettings.objects.create() def test_isbn_json_response(self): - """ searches local data only and returns book data in json format """ + """searches local data only and returns book data in json format""" view = views.Isbn.as_view() request = self.factory.get("") with patch("bookwyrm.views.isbn.is_api_request") as is_api: diff --git a/bookwyrm/tests/views/test_landing.py b/bookwyrm/tests/views/test_landing.py index 2513fecc9..864e48f7f 100644 --- a/bookwyrm/tests/views/test_landing.py +++ b/bookwyrm/tests/views/test_landing.py @@ -10,10 +10,10 @@ from bookwyrm import views class LandingViews(TestCase): - """ pages you land on without really trying """ + """pages you land on without really trying""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -27,7 +27,7 @@ class LandingViews(TestCase): models.SiteSettings.objects.create() def test_home_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.Home.as_view() request = self.factory.get("") request.user = self.local_user @@ -43,7 +43,7 @@ class LandingViews(TestCase): result.render() def test_about_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.About.as_view() request = self.factory.get("") request.user = self.local_user @@ -53,7 +53,7 @@ class LandingViews(TestCase): self.assertEqual(result.status_code, 200) def test_discover(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.Discover.as_view() request = self.factory.get("") result = view(request) diff --git a/bookwyrm/tests/views/test_list.py b/bookwyrm/tests/views/test_list.py index d669307cc..3de35b1ed 100644 --- a/bookwyrm/tests/views/test_list.py +++ b/bookwyrm/tests/views/test_list.py @@ -12,10 +12,10 @@ from bookwyrm.activitypub import ActivitypubResponse # pylint: disable=unused-argument class ListViews(TestCase): - """ tag views""" + """tag views""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -39,6 +39,25 @@ class ListViews(TestCase): remote_id="https://example.com/book/1", parent_work=work, ) + work_two = models.Work.objects.create(title="Labori") + self.book_two = models.Edition.objects.create( + title="Example Edition 2", + remote_id="https://example.com/book/2", + parent_work=work_two, + ) + work_three = models.Work.objects.create(title="Trabajar") + self.book_three = models.Edition.objects.create( + title="Example Edition 3", + remote_id="https://example.com/book/3", + parent_work=work_three, + ) + work_four = models.Work.objects.create(title="Travailler") + self.book_four = models.Edition.objects.create( + title="Example Edition 4", + remote_id="https://example.com/book/4", + parent_work=work_four, + ) + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): self.list = models.List.objects.create( name="Test List", user=self.local_user @@ -48,7 +67,7 @@ class ListViews(TestCase): models.SiteSettings.objects.create() def test_lists_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.Lists.as_view() with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): models.List.objects.create(name="Public list", user=self.local_user) @@ -71,7 +90,7 @@ class ListViews(TestCase): self.assertEqual(result.status_code, 200) def test_lists_create(self): - """ create list view """ + """create list view""" view = views.Lists.as_view() request = self.factory.post( "", @@ -99,7 +118,7 @@ class ListViews(TestCase): self.assertEqual(new_list.curation, "open") def test_list_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.List.as_view() request = self.factory.get("") request.user = self.local_user @@ -134,7 +153,7 @@ class ListViews(TestCase): self.assertEqual(result.status_code, 200) def test_list_edit(self): - """ edit a list """ + """edit a list""" view = views.List.as_view() request = self.factory.post( "", @@ -166,7 +185,7 @@ class ListViews(TestCase): self.assertEqual(self.list.curation, "curated") def test_curate_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.Curate.as_view() with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): models.List.objects.create(name="Public list", user=self.local_user) @@ -186,7 +205,7 @@ class ListViews(TestCase): self.assertEqual(result.status_code, 302) def test_curate_approve(self): - """ approve a pending item """ + """approve a pending item""" view = views.Curate.as_view() with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): pending = models.ListItem.objects.create( @@ -194,6 +213,7 @@ class ListViews(TestCase): user=self.local_user, book=self.book, approved=False, + order=1, ) request = self.factory.post( @@ -208,7 +228,7 @@ class ListViews(TestCase): with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock: view(request, self.list.id) - self.assertEqual(mock.call_count, 1) + self.assertEqual(mock.call_count, 2) activity = json.loads(mock.call_args[0][1]) self.assertEqual(activity["type"], "Add") self.assertEqual(activity["actor"], self.local_user.remote_id) @@ -220,7 +240,7 @@ class ListViews(TestCase): self.assertTrue(pending.approved) def test_curate_reject(self): - """ approve a pending item """ + """approve a pending item""" view = views.Curate.as_view() with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): pending = models.ListItem.objects.create( @@ -228,6 +248,7 @@ class ListViews(TestCase): user=self.local_user, book=self.book, approved=False, + order=1, ) request = self.factory.post( @@ -245,7 +266,7 @@ class ListViews(TestCase): self.assertFalse(models.ListItem.objects.exists()) def test_add_book(self): - """ put a book on a list """ + """put a book on a list""" request = self.factory.post( "", { @@ -268,8 +289,263 @@ class ListViews(TestCase): self.assertEqual(item.user, self.local_user) self.assertTrue(item.approved) + def test_add_two_books(self): + """ + Putting two books on the list. The first should have an order value of + 1 and the second should have an order value of 2. + """ + request_one = self.factory.post( + "", + { + "book": self.book.id, + "list": self.list.id, + }, + ) + request_one.user = self.local_user + + request_two = self.factory.post( + "", + { + "book": self.book_two.id, + "list": self.list.id, + }, + ) + request_two.user = self.local_user + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + views.list.add_book(request_one) + views.list.add_book(request_two) + + items = self.list.listitem_set.order_by("order").all() + self.assertEqual(items[0].book, self.book) + self.assertEqual(items[1].book, self.book_two) + self.assertEqual(items[0].order, 1) + self.assertEqual(items[1].order, 2) + + def test_add_three_books_and_remove_second(self): + """ + Put three books on a list and then remove the one in the middle. The + ordering of the list should adjust to not have a gap. + """ + request_one = self.factory.post( + "", + { + "book": self.book.id, + "list": self.list.id, + }, + ) + request_one.user = self.local_user + + request_two = self.factory.post( + "", + { + "book": self.book_two.id, + "list": self.list.id, + }, + ) + request_two.user = self.local_user + + request_three = self.factory.post( + "", + { + "book": self.book_three.id, + "list": self.list.id, + }, + ) + request_three.user = self.local_user + + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + views.list.add_book(request_one) + views.list.add_book(request_two) + views.list.add_book(request_three) + + items = self.list.listitem_set.order_by("order").all() + self.assertEqual(items[0].book, self.book) + self.assertEqual(items[1].book, self.book_two) + self.assertEqual(items[2].book, self.book_three) + self.assertEqual(items[0].order, 1) + self.assertEqual(items[1].order, 2) + self.assertEqual(items[2].order, 3) + + remove_request = self.factory.post("", {"item": items[1].id}) + remove_request.user = self.local_user + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + views.list.remove_book(remove_request, self.list.id) + items = self.list.listitem_set.order_by("order").all() + self.assertEqual(items[0].book, self.book) + self.assertEqual(items[1].book, self.book_three) + self.assertEqual(items[0].order, 1) + self.assertEqual(items[1].order, 2) + + def test_adding_book_with_a_pending_book(self): + """ + When a list contains any pending books, the pending books should have + be at the end of the list by order. If a book is added while a book is + pending, its order should precede the pending books. + """ + request = self.factory.post( + "", + { + "book": self.book_three.id, + "list": self.list.id, + }, + ) + request.user = self.local_user + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + models.ListItem.objects.create( + book_list=self.list, + user=self.local_user, + book=self.book, + approved=True, + order=1, + ) + models.ListItem.objects.create( + book_list=self.list, + user=self.rat, + book=self.book_two, + approved=False, + order=2, + ) + views.list.add_book(request) + + items = self.list.listitem_set.order_by("order").all() + self.assertEqual(items[0].book, self.book) + self.assertEqual(items[0].order, 1) + self.assertTrue(items[0].approved) + + self.assertEqual(items[1].book, self.book_three) + self.assertEqual(items[1].order, 2) + self.assertTrue(items[1].approved) + + self.assertEqual(items[2].book, self.book_two) + self.assertEqual(items[2].order, 3) + self.assertFalse(items[2].approved) + + def test_approving_one_pending_book_from_multiple(self): + """ + When a list contains any pending books, the pending books should have + be at the end of the list by order. If a pending book is approved, then + its order should be at the end of the approved books and before the + remaining pending books. + """ + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + models.ListItem.objects.create( + book_list=self.list, + user=self.local_user, + book=self.book, + approved=True, + order=1, + ) + models.ListItem.objects.create( + book_list=self.list, + user=self.local_user, + book=self.book_two, + approved=True, + order=2, + ) + models.ListItem.objects.create( + book_list=self.list, + user=self.rat, + book=self.book_three, + approved=False, + order=3, + ) + to_be_approved = models.ListItem.objects.create( + book_list=self.list, + user=self.rat, + book=self.book_four, + approved=False, + order=4, + ) + + view = views.Curate.as_view() + request = self.factory.post( + "", + { + "item": to_be_approved.id, + "approved": "true", + }, + ) + request.user = self.local_user + + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + view(request, self.list.id) + + items = self.list.listitem_set.order_by("order").all() + self.assertEqual(items[0].book, self.book) + self.assertEqual(items[0].order, 1) + self.assertTrue(items[0].approved) + + self.assertEqual(items[1].book, self.book_two) + self.assertEqual(items[1].order, 2) + self.assertTrue(items[1].approved) + + self.assertEqual(items[2].book, self.book_four) + self.assertEqual(items[2].order, 3) + self.assertTrue(items[2].approved) + + self.assertEqual(items[3].book, self.book_three) + self.assertEqual(items[3].order, 4) + self.assertFalse(items[3].approved) + + def test_add_three_books_and_move_last_to_first(self): + """ + Put three books on the list and move the last book to the first + position. + """ + request_one = self.factory.post( + "", + { + "book": self.book.id, + "list": self.list.id, + }, + ) + request_one.user = self.local_user + + request_two = self.factory.post( + "", + { + "book": self.book_two.id, + "list": self.list.id, + }, + ) + request_two.user = self.local_user + + request_three = self.factory.post( + "", + { + "book": self.book_three.id, + "list": self.list.id, + }, + ) + request_three.user = self.local_user + + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + views.list.add_book(request_one) + views.list.add_book(request_two) + views.list.add_book(request_three) + + items = self.list.listitem_set.order_by("order").all() + self.assertEqual(items[0].book, self.book) + self.assertEqual(items[1].book, self.book_two) + self.assertEqual(items[2].book, self.book_three) + self.assertEqual(items[0].order, 1) + self.assertEqual(items[1].order, 2) + self.assertEqual(items[2].order, 3) + + set_position_request = self.factory.post("", {"position": 1}) + set_position_request.user = self.local_user + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + views.list.set_book_position(set_position_request, items[2].id) + items = self.list.listitem_set.order_by("order").all() + self.assertEqual(items[0].book, self.book_three) + self.assertEqual(items[1].book, self.book) + self.assertEqual(items[2].book, self.book_two) + self.assertEqual(items[0].order, 1) + self.assertEqual(items[1].order, 2) + self.assertEqual(items[2].order, 3) + def test_add_book_outsider(self): - """ put a book on a list """ + """put a book on a list""" self.list.curation = "open" self.list.save(broadcast=False) request = self.factory.post( @@ -295,7 +571,7 @@ class ListViews(TestCase): self.assertTrue(item.approved) def test_add_book_pending(self): - """ put a book on a list awaiting approval """ + """put a book on a list awaiting approval""" self.list.curation = "curated" self.list.save(broadcast=False) request = self.factory.post( @@ -325,7 +601,7 @@ class ListViews(TestCase): self.assertFalse(item.approved) def test_add_book_self_curated(self): - """ put a book on a list automatically approved """ + """put a book on a list automatically approved""" self.list.curation = "curated" self.list.save(broadcast=False) request = self.factory.post( @@ -351,13 +627,14 @@ class ListViews(TestCase): self.assertTrue(item.approved) def test_remove_book(self): - """ take an item off a list """ + """take an item off a list""" with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): item = models.ListItem.objects.create( book_list=self.list, user=self.local_user, book=self.book, + order=1, ) self.assertTrue(self.list.listitem_set.exists()) @@ -374,12 +651,10 @@ class ListViews(TestCase): self.assertFalse(self.list.listitem_set.exists()) def test_remove_book_unauthorized(self): - """ take an item off a list """ + """take an item off a list""" with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): item = models.ListItem.objects.create( - book_list=self.list, - user=self.local_user, - book=self.book, + book_list=self.list, user=self.local_user, book=self.book, order=1 ) self.assertTrue(self.list.listitem_set.exists()) request = self.factory.post( diff --git a/bookwyrm/tests/views/test_notifications.py b/bookwyrm/tests/views/test_notifications.py index 6d92485ef..182753f91 100644 --- a/bookwyrm/tests/views/test_notifications.py +++ b/bookwyrm/tests/views/test_notifications.py @@ -8,10 +8,10 @@ from bookwyrm import views class NotificationViews(TestCase): - """ notifications """ + """notifications""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -23,7 +23,7 @@ class NotificationViews(TestCase): models.SiteSettings.objects.create() def test_notifications_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.Notifications.as_view() request = self.factory.get("") request.user = self.local_user @@ -33,7 +33,7 @@ class NotificationViews(TestCase): self.assertEqual(result.status_code, 200) def test_clear_notifications(self): - """ erase notifications """ + """erase notifications""" models.Notification.objects.create( user=self.local_user, notification_type="FAVORITE" ) diff --git a/bookwyrm/tests/views/test_outbox.py b/bookwyrm/tests/views/test_outbox.py index 0bcfde693..f89258e5f 100644 --- a/bookwyrm/tests/views/test_outbox.py +++ b/bookwyrm/tests/views/test_outbox.py @@ -13,10 +13,10 @@ from bookwyrm.settings import USER_AGENT # pylint: disable=too-many-public-methods @patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") class OutboxView(TestCase): - """ sends out activities """ + """sends out activities""" def setUp(self): - """ we'll need some data """ + """we'll need some data""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -34,19 +34,19 @@ class OutboxView(TestCase): ) def test_outbox(self, _): - """ returns user's statuses """ + """returns user's statuses""" request = self.factory.get("") result = views.Outbox.as_view()(request, "mouse") self.assertIsInstance(result, JsonResponse) def test_outbox_bad_method(self, _): - """ can't POST to outbox """ + """can't POST to outbox""" request = self.factory.post("") result = views.Outbox.as_view()(request, "mouse") self.assertEqual(result.status_code, 405) def test_outbox_unknown_user(self, _): - """ should 404 for unknown and remote users """ + """should 404 for unknown and remote users""" request = self.factory.post("") result = views.Outbox.as_view()(request, "beepboop") self.assertEqual(result.status_code, 405) @@ -54,7 +54,7 @@ class OutboxView(TestCase): self.assertEqual(result.status_code, 405) def test_outbox_privacy(self, _): - """ don't show dms et cetera in outbox """ + """don't show dms et cetera in outbox""" with patch("bookwyrm.activitystreams.ActivityStream.add_status"): models.Status.objects.create( content="PRIVATE!!", user=self.local_user, privacy="direct" @@ -77,7 +77,7 @@ class OutboxView(TestCase): self.assertEqual(data["totalItems"], 2) def test_outbox_filter(self, _): - """ if we only care about reviews, only get reviews """ + """if we only care about reviews, only get reviews""" with patch("bookwyrm.activitystreams.ActivityStream.add_status"): models.Review.objects.create( content="look at this", @@ -103,7 +103,7 @@ class OutboxView(TestCase): self.assertEqual(data["totalItems"], 1) def test_outbox_bookwyrm_request_true(self, _): - """ should differentiate between bookwyrm and outside requests """ + """should differentiate between bookwyrm and outside requests""" with patch("bookwyrm.activitystreams.ActivityStream.add_status"): models.Review.objects.create( name="hi", @@ -121,7 +121,7 @@ class OutboxView(TestCase): self.assertEqual(data["orderedItems"][0]["type"], "Review") def test_outbox_bookwyrm_request_false(self, _): - """ should differentiate between bookwyrm and outside requests """ + """should differentiate between bookwyrm and outside requests""" with patch("bookwyrm.activitystreams.ActivityStream.add_status"): models.Review.objects.create( name="hi", diff --git a/bookwyrm/tests/views/test_password.py b/bookwyrm/tests/views/test_password.py index 53a9bcbdc..ec686db74 100644 --- a/bookwyrm/tests/views/test_password.py +++ b/bookwyrm/tests/views/test_password.py @@ -10,10 +10,10 @@ from bookwyrm import models, views class PasswordViews(TestCase): - """ view user and edit profile """ + """view user and edit profile""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -27,7 +27,7 @@ class PasswordViews(TestCase): models.SiteSettings.objects.create(id=1) def test_password_reset_request(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.PasswordResetRequest.as_view() request = self.factory.get("") request.user = self.local_user @@ -38,7 +38,7 @@ class PasswordViews(TestCase): self.assertEqual(result.status_code, 200) def test_password_reset_request_post(self): - """ send 'em an email """ + """send 'em an email""" request = self.factory.post("", {"email": "aa@bb.ccc"}) view = views.PasswordResetRequest.as_view() resp = view(request) @@ -53,7 +53,7 @@ class PasswordViews(TestCase): self.assertEqual(models.PasswordReset.objects.get().user, self.local_user) def test_password_reset(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.PasswordReset.as_view() code = models.PasswordReset.objects.create(user=self.local_user) request = self.factory.get("") @@ -64,7 +64,7 @@ class PasswordViews(TestCase): self.assertEqual(result.status_code, 200) def test_password_reset_post(self): - """ reset from code """ + """reset from code""" view = views.PasswordReset.as_view() code = models.PasswordReset.objects.create(user=self.local_user) request = self.factory.post("", {"password": "hi", "confirm-password": "hi"}) @@ -74,7 +74,7 @@ class PasswordViews(TestCase): self.assertFalse(models.PasswordReset.objects.exists()) def test_password_reset_wrong_code(self): - """ reset from code """ + """reset from code""" view = views.PasswordReset.as_view() models.PasswordReset.objects.create(user=self.local_user) request = self.factory.post("", {"password": "hi", "confirm-password": "hi"}) @@ -83,7 +83,7 @@ class PasswordViews(TestCase): self.assertTrue(models.PasswordReset.objects.exists()) def test_password_reset_mismatch(self): - """ reset from code """ + """reset from code""" view = views.PasswordReset.as_view() code = models.PasswordReset.objects.create(user=self.local_user) request = self.factory.post("", {"password": "hi", "confirm-password": "hihi"}) @@ -92,7 +92,7 @@ class PasswordViews(TestCase): self.assertTrue(models.PasswordReset.objects.exists()) def test_password_change_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.ChangePassword.as_view() request = self.factory.get("") request.user = self.local_user @@ -103,7 +103,7 @@ class PasswordViews(TestCase): self.assertEqual(result.status_code, 200) def test_password_change(self): - """ change password """ + """change password""" view = views.ChangePassword.as_view() password_hash = self.local_user.password request = self.factory.post("", {"password": "hi", "confirm-password": "hi"}) @@ -113,7 +113,7 @@ class PasswordViews(TestCase): self.assertNotEqual(self.local_user.password, password_hash) def test_password_change_mismatch(self): - """ change password """ + """change password""" view = views.ChangePassword.as_view() password_hash = self.local_user.password request = self.factory.post("", {"password": "hi", "confirm-password": "hihi"}) diff --git a/bookwyrm/tests/views/test_reading.py b/bookwyrm/tests/views/test_reading.py index b1ae6b88b..c591aa604 100644 --- a/bookwyrm/tests/views/test_reading.py +++ b/bookwyrm/tests/views/test_reading.py @@ -10,10 +10,10 @@ from bookwyrm import models, views @patch("bookwyrm.activitystreams.ActivityStream.add_status") class ReadingViews(TestCase): - """ viewing and creating statuses """ + """viewing and creating statuses""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -41,7 +41,7 @@ class ReadingViews(TestCase): ) def test_start_reading(self, _): - """ begin a book """ + """begin a book""" shelf = self.local_user.shelf_set.get(identifier=models.Shelf.READING) self.assertFalse(shelf.books.exists()) self.assertFalse(models.Status.objects.exists()) @@ -72,7 +72,7 @@ class ReadingViews(TestCase): self.assertEqual(readthrough.book, self.book) def test_start_reading_reshelf(self, _): - """ begin a book """ + """begin a book""" to_read_shelf = self.local_user.shelf_set.get(identifier=models.Shelf.TO_READ) with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): models.ShelfBook.objects.create( @@ -92,7 +92,7 @@ class ReadingViews(TestCase): self.assertEqual(shelf.books.get(), self.book) def test_finish_reading(self, _): - """ begin a book """ + """begin a book""" shelf = self.local_user.shelf_set.get(identifier=models.Shelf.READ_FINISHED) self.assertFalse(shelf.books.exists()) self.assertFalse(models.Status.objects.exists()) @@ -128,7 +128,7 @@ class ReadingViews(TestCase): self.assertEqual(readthrough.book, self.book) def test_edit_readthrough(self, _): - """ adding dates to an ongoing readthrough """ + """adding dates to an ongoing readthrough""" start = timezone.make_aware(dateutil.parser.parse("2021-01-03")) readthrough = models.ReadThrough.objects.create( book=self.book, user=self.local_user, start_date=start @@ -155,7 +155,7 @@ class ReadingViews(TestCase): self.assertEqual(readthrough.book, self.book) def test_delete_readthrough(self, _): - """ remove a readthrough """ + """remove a readthrough""" readthrough = models.ReadThrough.objects.create( book=self.book, user=self.local_user ) @@ -172,7 +172,7 @@ class ReadingViews(TestCase): self.assertFalse(models.ReadThrough.objects.filter(id=readthrough.id).exists()) def test_create_readthrough(self, _): - """ adding new read dates """ + """adding new read dates""" request = self.factory.post( "", { diff --git a/bookwyrm/tests/views/test_readthrough.py b/bookwyrm/tests/views/test_readthrough.py index 5399f673c..c9ebf2169 100644 --- a/bookwyrm/tests/views/test_readthrough.py +++ b/bookwyrm/tests/views/test_readthrough.py @@ -9,10 +9,10 @@ from bookwyrm import models @patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") class ReadThrough(TestCase): - """ readthrough tests """ + """readthrough tests""" def setUp(self): - """ basic user and book data """ + """basic user and book data""" self.client = Client() self.work = models.Work.objects.create(title="Example Work") @@ -52,7 +52,7 @@ class ReadThrough(TestCase): self.assertEqual(delay_mock.call_count, 1) def test_create_progress_readthrough(self, delay_mock): - """ a readthrough with progress """ + """a readthrough with progress""" self.assertEqual(self.edition.readthrough_set.count(), 0) self.client.post( diff --git a/bookwyrm/tests/views/test_reports.py b/bookwyrm/tests/views/test_reports.py index 1c56067ad..84539489d 100644 --- a/bookwyrm/tests/views/test_reports.py +++ b/bookwyrm/tests/views/test_reports.py @@ -1,5 +1,4 @@ """ test for app action functionality """ -from unittest.mock import patch from django.template.response import TemplateResponse from django.test import TestCase from django.test.client import RequestFactory @@ -8,10 +7,10 @@ from bookwyrm import forms, models, views class ReportViews(TestCase): - """ every response to a get request, html or json """ + """every response to a get request, html or json""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -30,7 +29,7 @@ class ReportViews(TestCase): models.SiteSettings.objects.create() def test_reports_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.Reports.as_view() request = self.factory.get("") request.user = self.local_user @@ -42,7 +41,7 @@ class ReportViews(TestCase): self.assertEqual(result.status_code, 200) def test_reports_page_with_data(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.Reports.as_view() request = self.factory.get("") request.user = self.local_user @@ -55,7 +54,7 @@ class ReportViews(TestCase): self.assertEqual(result.status_code, 200) def test_report_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.Report.as_view() request = self.factory.get("") request.user = self.local_user @@ -69,7 +68,7 @@ class ReportViews(TestCase): self.assertEqual(result.status_code, 200) def test_report_comment(self): - """ comment on a report """ + """comment on a report""" view = views.Report.as_view() request = self.factory.post("", {"note": "hi"}) request.user = self.local_user @@ -84,7 +83,7 @@ class ReportViews(TestCase): self.assertEqual(comment.report, report) def test_make_report(self): - """ a user reports another user """ + """a user reports another user""" form = forms.ReportForm() form.data["reporter"] = self.local_user.id form.data["user"] = self.rat.id @@ -98,7 +97,7 @@ class ReportViews(TestCase): self.assertEqual(report.user, self.rat) def test_resolve_report(self): - """ toggle report resolution status """ + """toggle report resolution status""" report = models.Report.objects.create(reporter=self.local_user, user=self.rat) self.assertFalse(report.resolved) request = self.factory.post("") @@ -115,22 +114,19 @@ class ReportViews(TestCase): report.refresh_from_db() self.assertFalse(report.resolved) - def test_deactivate_user(self): - """ toggle whether a user is able to log in """ + def test_suspend_user(self): + """toggle whether a user is able to log in""" self.assertTrue(self.rat.is_active) - report = models.Report.objects.create(reporter=self.local_user, user=self.rat) request = self.factory.post("") request.user = self.local_user request.user.is_superuser = True # de-activate - with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): - views.deactivate_user(request, report.id) + views.suspend_user(request, self.rat.id) self.rat.refresh_from_db() self.assertFalse(self.rat.is_active) # re-activate - with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): - views.deactivate_user(request, report.id) + views.suspend_user(request, self.rat.id) self.rat.refresh_from_db() self.assertTrue(self.rat.is_active) diff --git a/bookwyrm/tests/views/test_rss_feed.py b/bookwyrm/tests/views/test_rss_feed.py index 0230b4a96..eacb3c936 100644 --- a/bookwyrm/tests/views/test_rss_feed.py +++ b/bookwyrm/tests/views/test_rss_feed.py @@ -8,10 +8,10 @@ from bookwyrm.views import rss_feed class RssFeedView(TestCase): - """ rss feed behaves as expected """ + """rss feed behaves as expected""" def setUp(self): - """ test data """ + """test data""" self.site = models.SiteSettings.objects.create() self.user = models.User.objects.create_user( @@ -50,7 +50,7 @@ class RssFeedView(TestCase): @patch("bookwyrm.activitystreams.ActivityStream.get_activity_stream") def test_rss_feed(self, _): - """ load an rss feed """ + """load an rss feed""" view = rss_feed.RssFeed() request = self.factory.get("/user/rss_user/rss") request.user = self.user diff --git a/bookwyrm/tests/views/test_search.py b/bookwyrm/tests/views/test_search.py index 78c7a1037..777275222 100644 --- a/bookwyrm/tests/views/test_search.py +++ b/bookwyrm/tests/views/test_search.py @@ -13,10 +13,10 @@ from bookwyrm.settings import DOMAIN class ShelfViews(TestCase): - """ tag views""" + """tag views""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -38,7 +38,7 @@ class ShelfViews(TestCase): models.SiteSettings.objects.create() def test_search_json_response(self): - """ searches local data only and returns book data in json format """ + """searches local data only and returns book data in json format""" view = views.Search.as_view() # we need a connector for this, sorry request = self.factory.get("", {"q": "Test Book"}) @@ -53,11 +53,11 @@ class ShelfViews(TestCase): self.assertEqual(data[0]["key"], "https://%s/book/%d" % (DOMAIN, self.book.id)) def test_search_html_response(self): - """ searches remote connectors """ + """searches remote connectors""" view = views.Search.as_view() class TestConnector(abstract_connector.AbstractMinimalConnector): - """ nothing added here """ + """nothing added here""" def format_search_result(self, search_result): pass @@ -106,7 +106,7 @@ class ShelfViews(TestCase): ) def test_search_html_response_users(self): - """ searches remote connectors """ + """searches remote connectors""" view = views.Search.as_view() request = self.factory.get("", {"q": "mouse"}) request.user = self.local_user diff --git a/bookwyrm/tests/views/test_shelf.py b/bookwyrm/tests/views/test_shelf.py index 7ab015624..239b3318f 100644 --- a/bookwyrm/tests/views/test_shelf.py +++ b/bookwyrm/tests/views/test_shelf.py @@ -11,10 +11,10 @@ from bookwyrm.activitypub import ActivitypubResponse @patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") class ShelfViews(TestCase): - """ tag views""" + """tag views""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -37,7 +37,7 @@ class ShelfViews(TestCase): models.SiteSettings.objects.create() def test_shelf_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.Shelf.as_view() shelf = self.local_user.shelf_set.first() request = self.factory.get("") @@ -64,7 +64,7 @@ class ShelfViews(TestCase): self.assertEqual(result.status_code, 200) def test_edit_shelf_privacy(self, _): - """ set name or privacy on shelf """ + """set name or privacy on shelf""" view = views.Shelf.as_view() shelf = self.local_user.shelf_set.get(identifier="to-read") self.assertEqual(shelf.privacy, "public") @@ -84,7 +84,7 @@ class ShelfViews(TestCase): self.assertEqual(shelf.privacy, "unlisted") def test_edit_shelf_name(self, _): - """ change the name of an editable shelf """ + """change the name of an editable shelf""" view = views.Shelf.as_view() shelf = models.Shelf.objects.create(name="Test Shelf", user=self.local_user) self.assertEqual(shelf.privacy, "public") @@ -101,7 +101,7 @@ class ShelfViews(TestCase): self.assertEqual(shelf.identifier, "testshelf-%d" % shelf.id) def test_edit_shelf_name_not_editable(self, _): - """ can't change the name of an non-editable shelf """ + """can't change the name of an non-editable shelf""" view = views.Shelf.as_view() shelf = self.local_user.shelf_set.get(identifier="to-read") self.assertEqual(shelf.privacy, "public") @@ -116,7 +116,7 @@ class ShelfViews(TestCase): self.assertEqual(shelf.name, "To Read") def test_handle_shelve(self, _): - """ shelve a book """ + """shelve a book""" request = self.factory.post( "", {"book": self.book.id, "shelf": self.shelf.identifier} ) @@ -134,7 +134,7 @@ class ShelfViews(TestCase): self.assertEqual(self.shelf.books.get(), self.book) def test_handle_shelve_to_read(self, _): - """ special behavior for the to-read shelf """ + """special behavior for the to-read shelf""" shelf = models.Shelf.objects.get(identifier="to-read") request = self.factory.post( "", {"book": self.book.id, "shelf": shelf.identifier} @@ -147,7 +147,7 @@ class ShelfViews(TestCase): self.assertEqual(shelf.books.get(), self.book) def test_handle_shelve_reading(self, _): - """ special behavior for the reading shelf """ + """special behavior for the reading shelf""" shelf = models.Shelf.objects.get(identifier="reading") request = self.factory.post( "", {"book": self.book.id, "shelf": shelf.identifier} @@ -160,7 +160,7 @@ class ShelfViews(TestCase): self.assertEqual(shelf.books.get(), self.book) def test_handle_shelve_read(self, _): - """ special behavior for the read shelf """ + """special behavior for the read shelf""" shelf = models.Shelf.objects.get(identifier="read") request = self.factory.post( "", {"book": self.book.id, "shelf": shelf.identifier} @@ -173,7 +173,7 @@ class ShelfViews(TestCase): self.assertEqual(shelf.books.get(), self.book) def test_handle_unshelve(self, _): - """ remove a book from a shelf """ + """remove a book from a shelf""" with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): models.ShelfBook.objects.create( book=self.book, user=self.local_user, shelf=self.shelf diff --git a/bookwyrm/tests/views/test_status.py b/bookwyrm/tests/views/test_status.py index 5eb13b6b2..6f2fd30d4 100644 --- a/bookwyrm/tests/views/test_status.py +++ b/bookwyrm/tests/views/test_status.py @@ -10,10 +10,10 @@ from bookwyrm.settings import DOMAIN @patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") class StatusViews(TestCase): - """ viewing and creating statuses """ + """viewing and creating statuses""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -43,7 +43,7 @@ class StatusViews(TestCase): models.SiteSettings.objects.create() def test_handle_status(self, _): - """ create a status """ + """create a status""" view = views.CreateStatus.as_view() form = forms.CommentForm( { @@ -66,7 +66,7 @@ class StatusViews(TestCase): self.assertEqual(status.book, self.book) def test_handle_status_reply(self, _): - """ create a status in reply to an existing status """ + """create a status in reply to an existing status""" view = views.CreateStatus.as_view() user = models.User.objects.create_user( "rat", "rat@rat.com", "password", local=True @@ -96,7 +96,7 @@ class StatusViews(TestCase): self.assertEqual(models.Notification.objects.get().user, self.local_user) def test_handle_status_mentions(self, _): - """ @mention a user in a post """ + """@mention a user in a post""" view = views.CreateStatus.as_view() user = models.User.objects.create_user( "rat@%s" % DOMAIN, "rat@rat.com", "password", local=True, localname="rat" @@ -124,7 +124,7 @@ class StatusViews(TestCase): ) def test_handle_status_reply_with_mentions(self, _): - """ reply to a post with an @mention'ed user """ + """reply to a post with an @mention'ed user""" view = views.CreateStatus.as_view() user = models.User.objects.create_user( "rat", "rat@rat.com", "password", local=True, localname="rat" @@ -168,7 +168,7 @@ class StatusViews(TestCase): self.assertTrue(self.local_user in reply.mention_users.all()) def test_delete_and_redraft(self, _): - """ delete and re-draft a status """ + """delete and re-draft a status""" view = views.DeleteAndRedraft.as_view() request = self.factory.post("") request.user = self.local_user @@ -189,7 +189,7 @@ class StatusViews(TestCase): self.assertTrue(status.deleted) def test_delete_and_redraft_invalid_status_type_rating(self, _): - """ you can't redraft generated statuses """ + """you can't redraft generated statuses""" view = views.DeleteAndRedraft.as_view() request = self.factory.post("") request.user = self.local_user @@ -209,7 +209,7 @@ class StatusViews(TestCase): self.assertFalse(status.deleted) def test_delete_and_redraft_invalid_status_type_generated_note(self, _): - """ you can't redraft generated statuses """ + """you can't redraft generated statuses""" view = views.DeleteAndRedraft.as_view() request = self.factory.post("") request.user = self.local_user @@ -229,7 +229,7 @@ class StatusViews(TestCase): self.assertFalse(status.deleted) def test_find_mentions(self, _): - """ detect and look up @ mentions of users """ + """detect and look up @ mentions of users""" user = models.User.objects.create_user( "nutria@%s" % DOMAIN, "nutria@nutria.com", @@ -275,7 +275,7 @@ class StatusViews(TestCase): ) def test_format_links(self, _): - """ find and format urls into a tags """ + """find and format urls into a tags""" url = "http://www.fish.com/" self.assertEqual( views.status.format_links(url), 'www.fish.com/' % url @@ -298,7 +298,7 @@ class StatusViews(TestCase): ) def test_to_markdown(self, _): - """ this is mostly handled in other places, but nonetheless """ + """this is mostly handled in other places, but nonetheless""" text = "_hi_ and http://fish.com is rad" result = views.status.to_markdown(text) self.assertEqual( @@ -307,13 +307,13 @@ class StatusViews(TestCase): ) def test_to_markdown_link(self, _): - """ this is mostly handled in other places, but nonetheless """ + """this is mostly handled in other places, but nonetheless""" text = "[hi](http://fish.com) is rad" result = views.status.to_markdown(text) self.assertEqual(result, '

    hi ' "is rad

    ") def test_handle_delete_status(self, mock): - """ marks a status as deleted """ + """marks a status as deleted""" view = views.DeleteStatus.as_view() with patch("bookwyrm.activitystreams.ActivityStream.add_status"): status = models.Status.objects.create(user=self.local_user, content="hi") @@ -333,7 +333,7 @@ class StatusViews(TestCase): self.assertTrue(status.deleted) def test_handle_delete_status_permission_denied(self, _): - """ marks a status as deleted """ + """marks a status as deleted""" view = views.DeleteStatus.as_view() with patch("bookwyrm.activitystreams.ActivityStream.add_status"): status = models.Status.objects.create(user=self.local_user, content="hi") @@ -347,7 +347,7 @@ class StatusViews(TestCase): self.assertFalse(status.deleted) def test_handle_delete_status_moderator(self, mock): - """ marks a status as deleted """ + """marks a status as deleted""" view = views.DeleteStatus.as_view() with patch("bookwyrm.activitystreams.ActivityStream.add_status"): status = models.Status.objects.create(user=self.local_user, content="hi") diff --git a/bookwyrm/tests/views/test_tag.py b/bookwyrm/tests/views/test_tag.py deleted file mode 100644 index 6ad6ab254..000000000 --- a/bookwyrm/tests/views/test_tag.py +++ /dev/null @@ -1,119 +0,0 @@ -""" test for app action functionality """ -from unittest.mock import patch -from django.contrib.auth.models import Group, Permission -from django.contrib.contenttypes.models import ContentType -from django.template.response import TemplateResponse -from django.test import TestCase -from django.test.client import RequestFactory - -from bookwyrm import models, views -from bookwyrm.activitypub import ActivitypubResponse - - -class TagViews(TestCase): - """ tag views""" - - def setUp(self): - """ we need basic test data and mocks """ - self.factory = RequestFactory() - self.local_user = models.User.objects.create_user( - "mouse@local.com", - "mouse@mouse.com", - "mouseword", - local=True, - localname="mouse", - remote_id="https://example.com/users/mouse", - ) - self.group = Group.objects.create(name="editor") - self.group.permissions.add( - Permission.objects.create( - name="edit_book", - codename="edit_book", - content_type=ContentType.objects.get_for_model(models.User), - ).id - ) - self.work = models.Work.objects.create(title="Test Work") - self.book = models.Edition.objects.create( - title="Example Edition", - remote_id="https://example.com/book/1", - parent_work=self.work, - ) - models.SiteSettings.objects.create() - - def test_tag_page(self): - """ there are so many views, this just makes sure it LOADS """ - view = views.Tag.as_view() - with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): - tag = models.Tag.objects.create(name="hi there") - models.UserTag.objects.create(tag=tag, user=self.local_user, book=self.book) - request = self.factory.get("") - with patch("bookwyrm.views.tag.is_api_request") as is_api: - is_api.return_value = False - result = view(request, tag.identifier) - self.assertIsInstance(result, TemplateResponse) - result.render() - self.assertEqual(result.status_code, 200) - - request = self.factory.get("") - with patch("bookwyrm.views.tag.is_api_request") as is_api: - is_api.return_value = True - result = view(request, tag.identifier) - self.assertIsInstance(result, ActivitypubResponse) - self.assertEqual(result.status_code, 200) - - def test_tag_page_activitypub_page(self): - """ there are so many views, this just makes sure it LOADS """ - view = views.Tag.as_view() - with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): - tag = models.Tag.objects.create(name="hi there") - models.UserTag.objects.create(tag=tag, user=self.local_user, book=self.book) - request = self.factory.get("", {"page": 1}) - with patch("bookwyrm.views.tag.is_api_request") as is_api: - is_api.return_value = True - result = view(request, tag.identifier) - self.assertIsInstance(result, ActivitypubResponse) - self.assertEqual(result.status_code, 200) - - def test_tag(self): - """ add a tag to a book """ - view = views.AddTag.as_view() - request = self.factory.post( - "", - { - "name": "A Tag!?", - "book": self.book.id, - }, - ) - request.user = self.local_user - - with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): - view(request) - - tag = models.Tag.objects.get() - user_tag = models.UserTag.objects.get() - self.assertEqual(tag.name, "A Tag!?") - self.assertEqual(tag.identifier, "A+Tag%21%3F") - self.assertEqual(user_tag.user, self.local_user) - self.assertEqual(user_tag.book, self.book) - - def test_untag(self): - """ remove a tag from a book """ - view = views.RemoveTag.as_view() - with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): - tag = models.Tag.objects.create(name="A Tag!?") - models.UserTag.objects.create(user=self.local_user, book=self.book, tag=tag) - request = self.factory.post( - "", - { - "user": self.local_user.id, - "book": self.book.id, - "name": tag.name, - }, - ) - request.user = self.local_user - - with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): - view(request) - - self.assertTrue(models.Tag.objects.filter(name="A Tag!?").exists()) - self.assertFalse(models.UserTag.objects.exists()) diff --git a/bookwyrm/tests/views/test_updates.py b/bookwyrm/tests/views/test_updates.py index dff730e6d..fb003f8de 100644 --- a/bookwyrm/tests/views/test_updates.py +++ b/bookwyrm/tests/views/test_updates.py @@ -10,10 +10,10 @@ from bookwyrm import models, views class UpdateViews(TestCase): - """ lets the ui check for unread notification """ + """lets the ui check for unread notification""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -25,7 +25,7 @@ class UpdateViews(TestCase): models.SiteSettings.objects.create() def test_get_notification_count(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" request = self.factory.get("") request.user = self.local_user @@ -43,7 +43,7 @@ class UpdateViews(TestCase): self.assertEqual(data["count"], 1) def test_get_unread_status_count(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" request = self.factory.get("") request.user = self.local_user diff --git a/bookwyrm/tests/views/test_user.py b/bookwyrm/tests/views/test_user.py index 055edae25..3b431de1d 100644 --- a/bookwyrm/tests/views/test_user.py +++ b/bookwyrm/tests/views/test_user.py @@ -15,10 +15,10 @@ from bookwyrm.activitypub import ActivitypubResponse class UserViews(TestCase): - """ view user and edit profile """ + """view user and edit profile""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -30,12 +30,20 @@ class UserViews(TestCase): self.rat = models.User.objects.create_user( "rat@local.com", "rat@rat.rat", "password", local=True, localname="rat" ) + self.book = models.Edition.objects.create(title="test") + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + models.ShelfBook.objects.create( + book=self.book, + user=self.local_user, + shelf=self.local_user.shelf_set.first(), + ) + models.SiteSettings.objects.create() self.anonymous_user = AnonymousUser self.anonymous_user.is_authenticated = False def test_user_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.User.as_view() request = self.factory.get("") request.user = self.local_user @@ -61,7 +69,7 @@ class UserViews(TestCase): self.assertEqual(result.status_code, 200) def test_user_page_blocked(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.User.as_view() request = self.factory.get("") request.user = self.local_user @@ -72,7 +80,7 @@ class UserViews(TestCase): self.assertEqual(result.status_code, 404) def test_followers_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.Followers.as_view() request = self.factory.get("") request.user = self.local_user @@ -90,7 +98,7 @@ class UserViews(TestCase): self.assertEqual(result.status_code, 200) def test_followers_page_blocked(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.Followers.as_view() request = self.factory.get("") request.user = self.local_user @@ -101,7 +109,7 @@ class UserViews(TestCase): self.assertEqual(result.status_code, 404) def test_following_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.Following.as_view() request = self.factory.get("") request.user = self.local_user @@ -119,7 +127,7 @@ class UserViews(TestCase): self.assertEqual(result.status_code, 200) def test_following_page_blocked(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.Following.as_view() request = self.factory.get("") request.user = self.local_user @@ -130,7 +138,7 @@ class UserViews(TestCase): self.assertEqual(result.status_code, 404) def test_edit_user_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.EditUser.as_view() request = self.factory.get("") request.user = self.local_user @@ -140,7 +148,7 @@ class UserViews(TestCase): self.assertEqual(result.status_code, 200) def test_edit_user(self): - """ use a form to update a user """ + """use a form to update a user""" view = views.EditUser.as_view() form = forms.EditUserForm(instance=self.local_user) form.data["name"] = "New Name" @@ -160,7 +168,7 @@ class UserViews(TestCase): # idk how to mock the upload form, got tired of triyng to make it work def test_edit_user_avatar(self): - """ use a form to update a user """ + """use a form to update a user""" view = views.EditUser.as_view() form = forms.EditUserForm(instance=self.local_user) form.data["name"] = "New Name" @@ -187,7 +195,7 @@ class UserViews(TestCase): self.assertEqual(self.local_user.avatar.height, 120) def test_crop_avatar(self): - """ reduce that image size """ + """reduce that image size""" image_file = pathlib.Path(__file__).parent.joinpath( "../../static/images/no_cover.jpg" ) diff --git a/bookwyrm/tests/views/test_user_admin.py b/bookwyrm/tests/views/test_user_admin.py index dd20c1b64..a044a22c5 100644 --- a/bookwyrm/tests/views/test_user_admin.py +++ b/bookwyrm/tests/views/test_user_admin.py @@ -1,4 +1,6 @@ """ test for app action functionality """ +from unittest.mock import patch +from django.contrib.auth.models import Group from django.template.response import TemplateResponse from django.test import TestCase from django.test.client import RequestFactory @@ -7,10 +9,10 @@ from bookwyrm import models, views class UserAdminViews(TestCase): - """ every response to a get request, html or json """ + """every response to a get request, html or json""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -21,9 +23,9 @@ class UserAdminViews(TestCase): ) models.SiteSettings.objects.create() - def test_user_admin_page(self): - """ there are so many views, this just makes sure it LOADS """ - view = views.UserAdmin.as_view() + def test_user_admin_list_page(self): + """there are so many views, this just makes sure it LOADS""" + view = views.UserAdminList.as_view() request = self.factory.get("") request.user = self.local_user request.user.is_superuser = True @@ -31,3 +33,38 @@ class UserAdminViews(TestCase): self.assertIsInstance(result, TemplateResponse) result.render() self.assertEqual(result.status_code, 200) + + def test_user_admin_page(self): + """there are so many views, this just makes sure it LOADS""" + view = views.UserAdmin.as_view() + request = self.factory.get("") + request.user = self.local_user + request.user.is_superuser = True + + result = view(request, self.local_user.id) + + self.assertIsInstance(result, TemplateResponse) + result.render() + self.assertEqual(result.status_code, 200) + + def test_user_admin_page_post(self): + """set the user's group""" + group = Group.objects.create(name="editor") + self.assertEqual( + list(self.local_user.groups.values_list("name", flat=True)), [] + ) + + view = views.UserAdmin.as_view() + request = self.factory.post("", {"groups": [group.id]}) + request.user = self.local_user + request.user.is_superuser = True + + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + result = view(request, self.local_user.id) + + self.assertIsInstance(result, TemplateResponse) + result.render() + + self.assertEqual( + list(self.local_user.groups.values_list("name", flat=True)), ["editor"] + ) diff --git a/bookwyrm/tests/views/test_wellknown.py b/bookwyrm/tests/views/test_wellknown.py index f408460f5..244921d47 100644 --- a/bookwyrm/tests/views/test_wellknown.py +++ b/bookwyrm/tests/views/test_wellknown.py @@ -11,10 +11,10 @@ from bookwyrm import models, views class UserViews(TestCase): - """ view user and edit profile """ + """view user and edit profile""" def setUp(self): - """ we need basic test data and mocks """ + """we need basic test data and mocks""" self.factory = RequestFactory() self.local_user = models.User.objects.create_user( "mouse@local.com", @@ -41,7 +41,7 @@ class UserViews(TestCase): self.anonymous_user.is_authenticated = False def test_webfinger(self): - """ there are so many views, this just makes sure it LOADS """ + """there are so many views, this just makes sure it LOADS""" request = self.factory.get("", {"resource": "acct:mouse@local.com"}) request.user = self.anonymous_user @@ -51,7 +51,7 @@ class UserViews(TestCase): self.assertEqual(data["subject"], "acct:mouse@local.com") def test_nodeinfo_pointer(self): - """ just tells you where nodeinfo is """ + """just tells you where nodeinfo is""" request = self.factory.get("") request.user = self.anonymous_user @@ -61,7 +61,7 @@ class UserViews(TestCase): self.assertTrue("href" in data["links"][0]) def test_nodeinfo(self): - """ info about the instance """ + """info about the instance""" request = self.factory.get("") request.user = self.anonymous_user @@ -73,7 +73,7 @@ class UserViews(TestCase): self.assertEqual(models.User.objects.count(), 3) def test_instanceinfo(self): - """ about the instance's user activity """ + """about the instance's user activity""" request = self.factory.get("") request.user = self.anonymous_user diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index cf3f877b9..53ceeaa83 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -51,13 +51,20 @@ urlpatterns = [ r"^password-reset/(?P[A-Za-z0-9]+)/?$", views.PasswordReset.as_view() ), # admin - re_path(r"^settings/site-settings", views.Site.as_view(), name="settings-site"), + re_path(r"^settings/site-settings/?$", views.Site.as_view(), name="settings-site"), re_path( - r"^settings/email-preview", + r"^settings/email-preview/?$", views.site.email_preview, name="settings-email-preview", ), - re_path(r"^settings/users", views.UserAdmin.as_view(), name="settings-users"), + re_path( + r"^settings/users/?$", views.UserAdminList.as_view(), name="settings-users" + ), + re_path( + r"^settings/users/(?P\d+)/?$", + views.UserAdmin.as_view(), + name="settings-user", + ), re_path( r"^settings/federation/?$", views.Federation.as_view(), @@ -113,9 +120,9 @@ urlpatterns = [ name="settings-report", ), re_path( - r"^settings/reports/(?P\d+)/deactivate/?$", - views.deactivate_user, - name="settings-report-deactivate", + r"^settings/reports/(?P\d+)/suspend/?$", + views.suspend_user, + name="settings-report-suspend", ), re_path( r"^settings/reports/(?P\d+)/resolve/?$", @@ -184,6 +191,11 @@ urlpatterns = [ views.list.remove_book, name="list-remove-book", ), + re_path( + r"^list-item/(?P\d+)/set-position$", + views.list.set_book_position, + name="list-set-book-position", + ), re_path( r"^list/(?P\d+)/curate/?$", views.Curate.as_view(), name="list-curate" ), @@ -248,7 +260,12 @@ urlpatterns = [ re_path(r"^boost/(?P\d+)/?$", views.Boost.as_view()), re_path(r"^unboost/(?P\d+)/?$", views.Unboost.as_view()), # books - re_path(r"%s(.json)?/?$" % book_path, views.Book.as_view()), + re_path(r"%s(.json)?/?$" % book_path, views.Book.as_view(), name="book"), + re_path( + r"%s/(?Preview|comment|quote)/?$" % book_path, + views.Book.as_view(), + name="book-user-statuses", + ), re_path(r"%s/edit/?$" % book_path, views.EditBook.as_view()), re_path(r"%s/confirm/?$" % book_path, views.ConfirmEditBook.as_view()), re_path(r"^create-book/?$", views.EditBook.as_view()), @@ -265,11 +282,6 @@ urlpatterns = [ # author re_path(r"^author/(?P\d+)(.json)?/?$", views.Author.as_view()), re_path(r"^author/(?P\d+)/edit/?$", views.EditAuthor.as_view()), - # tags - re_path(r"^tag/(?P.+)\.json/?$", views.Tag.as_view()), - re_path(r"^tag/(?P.+)/?$", views.Tag.as_view()), - re_path(r"^tag/?$", views.AddTag.as_view()), - re_path(r"^untag/?$", views.RemoveTag.as_view()), # reading progress re_path(r"^edit-readthrough/?$", views.edit_readthrough, name="edit-readthrough"), re_path(r"^delete-readthrough/?$", views.delete_readthrough), diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py index 9f8463b40..bcd914e10 100644 --- a/bookwyrm/views/__init__.py +++ b/bookwyrm/views/__init__.py @@ -25,7 +25,7 @@ from .notifications import Notifications from .outbox import Outbox from .reading import edit_readthrough, create_readthrough, delete_readthrough from .reading import start_reading, finish_reading, delete_progressupdate -from .reports import Report, Reports, make_report, resolve_report, deactivate_user +from .reports import Report, Reports, make_report, resolve_report, suspend_user from .rss_feed import RssFeed from .password import PasswordResetRequest, PasswordReset, ChangePassword from .search import Search @@ -34,8 +34,7 @@ from .shelf import create_shelf, delete_shelf from .shelf import shelve, unshelve from .site import Site from .status import CreateStatus, DeleteStatus, DeleteAndRedraft -from .tag import Tag, AddTag, RemoveTag from .updates import get_notification_count, get_unread_status_count from .user import User, EditUser, Followers, Following -from .user_admin import UserAdmin +from .user_admin import UserAdmin, UserAdminList from .wellknown import * diff --git a/bookwyrm/views/authentication.py b/bookwyrm/views/authentication.py index 22689a28c..bfb492480 100644 --- a/bookwyrm/views/authentication.py +++ b/bookwyrm/views/authentication.py @@ -16,10 +16,10 @@ from bookwyrm.settings import DOMAIN # pylint: disable= no-self-use @method_decorator(csrf_exempt, name="dispatch") class Login(View): - """ authenticate an existing user """ + """authenticate an existing user""" def get(self, request): - """ login page """ + """login page""" if request.user.is_authenticated: return redirect("/") # sene user to the login page @@ -30,7 +30,7 @@ class Login(View): return TemplateResponse(request, "login.html", data) def post(self, request): - """ authentication action """ + """authentication action""" if request.user.is_authenticated: return redirect("/") login_form = forms.LoginForm(request.POST) @@ -61,10 +61,10 @@ class Login(View): class Register(View): - """ register a user """ + """register a user""" def post(self, request): - """ join the server """ + """join the server""" if not models.SiteSettings.get().allow_registration: invite_code = request.POST.get("invite_code") @@ -117,9 +117,9 @@ class Register(View): @method_decorator(login_required, name="dispatch") class Logout(View): - """ log out """ + """log out""" def get(self, request): - """ done with this place! outa here! """ + """done with this place! outa here!""" logout(request) return redirect("/") diff --git a/bookwyrm/views/author.py b/bookwyrm/views/author.py index 50a3588de..0bd7b0e04 100644 --- a/bookwyrm/views/author.py +++ b/bookwyrm/views/author.py @@ -13,10 +13,10 @@ from .helpers import is_api_request # pylint: disable= no-self-use class Author(View): - """ this person wrote a book """ + """this person wrote a book""" def get(self, request, author_id): - """ landing page for an author """ + """landing page for an author""" author = get_object_or_404(models.Author, id=author_id) if is_api_request(request): @@ -37,16 +37,16 @@ class Author(View): permission_required("bookwyrm.edit_book", raise_exception=True), name="dispatch" ) class EditAuthor(View): - """ edit author info """ + """edit author info""" def get(self, request, author_id): - """ info about a book """ + """info about a book""" author = get_object_or_404(models.Author, id=author_id) data = {"author": author, "form": forms.AuthorForm(instance=author)} return TemplateResponse(request, "edit_author.html", data) def post(self, request, author_id): - """ edit a author cool """ + """edit a author cool""" author = get_object_or_404(models.Author, id=author_id) form = forms.AuthorForm(request.POST, request.FILES, instance=author) diff --git a/bookwyrm/views/block.py b/bookwyrm/views/block.py index 6d6a8a58c..99014a937 100644 --- a/bookwyrm/views/block.py +++ b/bookwyrm/views/block.py @@ -12,14 +12,14 @@ from bookwyrm import models # pylint: disable= no-self-use @method_decorator(login_required, name="dispatch") class Block(View): - """ blocking users """ + """blocking users""" def get(self, request): - """ list of blocked users? """ + """list of blocked users?""" return TemplateResponse(request, "preferences/blocks.html") def post(self, request, user_id): - """ block a user """ + """block a user""" to_block = get_object_or_404(models.User, id=user_id) models.UserBlocks.objects.create( user_subject=request.user, user_object=to_block @@ -30,7 +30,7 @@ class Block(View): @require_POST @login_required def unblock(request, user_id): - """ undo a block """ + """undo a block""" to_unblock = get_object_or_404(models.User, id=user_id) try: block = models.UserBlocks.objects.get( diff --git a/bookwyrm/views/books.py b/bookwyrm/views/books.py index c3ac4f492..448cf9929 100644 --- a/bookwyrm/views/books.py +++ b/bookwyrm/views/books.py @@ -26,15 +26,10 @@ from .helpers import is_api_request, get_edition, privacy_filter # pylint: disable= no-self-use class Book(View): - """ a book! this is the stuff """ - - def get(self, request, book_id): - """ info about a book """ - try: - page = int(request.GET.get("page", 1)) - except ValueError: - page = 1 + """a book! this is the stuff""" + def get(self, request, book_id, user_statuses=False): + """info about a book""" try: book = models.Book.objects.select_subclasses().get(id=book_id) except models.Book.DoesNotExist: @@ -45,29 +40,41 @@ class Book(View): if isinstance(book, models.Work): book = book.get_default_edition() - if not book: + if not book or not book.parent_work: return HttpResponseNotFound() work = book.parent_work - if not work: - return HttpResponseNotFound() # all reviews for the book - reviews = models.Review.objects.filter(book__in=work.editions.all()) - reviews = privacy_filter(request.user, reviews) + reviews = privacy_filter( + request.user, models.Review.objects.filter(book__in=work.editions.all()) + ) # the reviews to show - paginated = Paginator( - reviews.exclude(Q(content__isnull=True) | Q(content="")), PAGE_LENGTH - ) - reviews_page = paginated.get_page(page) + if user_statuses and request.user.is_authenticated: + if user_statuses == "review": + queryset = book.review_set + elif user_statuses == "comment": + queryset = book.comment_set + else: + queryset = book.quotation_set + queryset = queryset.filter(user=request.user) + else: + queryset = reviews.exclude(Q(content__isnull=True) | Q(content="")) + paginated = Paginator(queryset, PAGE_LENGTH) + + data = { + "book": book, + "statuses": paginated.get_page(request.GET.get("page")), + "review_count": reviews.count(), + "ratings": reviews.filter(Q(content__isnull=True) | Q(content="")), + "rating": reviews.aggregate(Avg("rating"))["rating__avg"], + "lists": privacy_filter( + request.user, book.list_set.filter(listitem__approved=True) + ), + } - user_tags = readthroughs = user_shelves = other_edition_shelves = [] if request.user.is_authenticated: - user_tags = models.UserTag.objects.filter( - book=book, user=request.user - ).values_list("tag__identifier", flat=True) - readthroughs = models.ReadThrough.objects.filter( user=request.user, book=book, @@ -77,31 +84,24 @@ class Book(View): readthrough.progress_updates = ( readthrough.progressupdate_set.all().order_by("-updated_date") ) + data["readthroughs"] = readthroughs - user_shelves = models.ShelfBook.objects.filter(user=request.user, book=book) + data["user_shelves"] = models.ShelfBook.objects.filter( + user=request.user, book=book + ) - other_edition_shelves = models.ShelfBook.objects.filter( + data["other_edition_shelves"] = models.ShelfBook.objects.filter( ~Q(book=book), user=request.user, book__parent_work=book.parent_work, ) - data = { - "book": book, - "reviews": reviews_page, - "review_count": reviews.count(), - "ratings": reviews.filter(Q(content__isnull=True) | Q(content="")), - "rating": reviews.aggregate(Avg("rating"))["rating__avg"], - "tags": models.UserTag.objects.filter(book=book), - "lists": privacy_filter( - request.user, book.list_set.filter(listitem__approved=True) - ), - "user_tags": user_tags, - "user_shelves": user_shelves, - "other_edition_shelves": other_edition_shelves, - "readthroughs": readthroughs, - "path": "/book/%s" % book_id, - } + data["user_statuses"] = { + "review_count": book.review_set.filter(user=request.user).count(), + "comment_count": book.comment_set.filter(user=request.user).count(), + "quotation_count": book.quotation_set.filter(user=request.user).count(), + } + return TemplateResponse(request, "book/book.html", data) @@ -110,10 +110,10 @@ class Book(View): permission_required("bookwyrm.edit_book", raise_exception=True), name="dispatch" ) class EditBook(View): - """ edit a book """ + """edit a book""" def get(self, request, book_id=None): - """ info about a book """ + """info about a book""" book = None if book_id: book = get_edition(book_id) @@ -123,7 +123,7 @@ class EditBook(View): return TemplateResponse(request, "book/edit_book.html", data) def post(self, request, book_id=None): - """ edit a book cool """ + """edit a book cool""" # returns None if no match is found book = models.Edition.objects.filter(id=book_id).first() form = forms.EditionForm(request.POST, request.FILES, instance=book) @@ -209,10 +209,10 @@ class EditBook(View): permission_required("bookwyrm.edit_book", raise_exception=True), name="dispatch" ) class ConfirmEditBook(View): - """ confirm edits to a book """ + """confirm edits to a book""" def post(self, request, book_id=None): - """ edit a book cool """ + """edit a book cool""" # returns None if no match is found book = models.Edition.objects.filter(id=book_id).first() form = forms.EditionForm(request.POST, request.FILES, instance=book) @@ -260,17 +260,12 @@ class ConfirmEditBook(View): class Editions(View): - """ list of editions """ + """list of editions""" def get(self, request, book_id): - """ list of editions of a book """ + """list of editions of a book""" work = get_object_or_404(models.Work, id=book_id) - try: - page = int(request.GET.get("page", 1)) - except ValueError: - page = 1 - if is_api_request(request): return ActivitypubResponse(work.to_edition_list(**request.GET)) filters = {} @@ -280,12 +275,12 @@ class Editions(View): if request.GET.get("format"): filters["physical_format__iexact"] = request.GET.get("format") - editions = work.editions.order_by("-edition_rank").all() + editions = work.editions.order_by("-edition_rank") languages = set(sum([e.languages for e in editions], [])) - paginated = Paginator(editions.filter(**filters).all(), PAGE_LENGTH) + paginated = Paginator(editions.filter(**filters), PAGE_LENGTH) data = { - "editions": paginated.get_page(page), + "editions": paginated.get_page(request.GET.get("page")), "work": work, "languages": languages, "formats": set( @@ -298,7 +293,7 @@ class Editions(View): @login_required @require_POST def upload_cover(request, book_id): - """ upload a new cover """ + """upload a new cover""" book = get_object_or_404(models.Edition, id=book_id) book.last_edited_by = request.user @@ -321,7 +316,7 @@ def upload_cover(request, book_id): def set_cover_from_url(url): - """ load it from a url """ + """load it from a url""" image_file = get_image(url) if not image_file: return None @@ -334,7 +329,7 @@ def set_cover_from_url(url): @require_POST @permission_required("bookwyrm.edit_book", raise_exception=True) def add_description(request, book_id): - """ upload a new cover """ + """upload a new cover""" if not request.method == "POST": return redirect("/") @@ -351,7 +346,7 @@ def add_description(request, book_id): @require_POST def resolve_book(request): - """ figure out the local path to a book from a remote_id """ + """figure out the local path to a book from a remote_id""" remote_id = request.POST.get("remote_id") connector = connector_manager.get_or_create_connector(remote_id) book = connector.get_or_create_book(remote_id) @@ -363,7 +358,7 @@ def resolve_book(request): @require_POST @transaction.atomic def switch_edition(request): - """ switch your copy of a book to a different edition """ + """switch your copy of a book to a different edition""" edition_id = request.POST.get("edition") new_edition = get_object_or_404(models.Edition, id=edition_id) shelfbooks = models.ShelfBook.objects.filter( diff --git a/bookwyrm/views/directory.py b/bookwyrm/views/directory.py index 2565f4ec5..a5786f740 100644 --- a/bookwyrm/views/directory.py +++ b/bookwyrm/views/directory.py @@ -11,16 +11,10 @@ from .helpers import get_annotated_users # pylint: disable=no-self-use @method_decorator(login_required, name="dispatch") class Directory(View): - """ display of known bookwyrm users """ + """display of known bookwyrm users""" def get(self, request): - """ lets see your cute faces """ - try: - page = int(request.GET.get("page", 1)) - except ValueError: - page = 1 - - # filters + """lets see your cute faces""" filters = {} software = request.GET.get("software") if not software or software == "bookwyrm": @@ -39,12 +33,12 @@ class Directory(View): paginated = Paginator(users, 12) data = { - "users": paginated.get_page(page), + "users": paginated.get_page(request.GET.get("page")), } return TemplateResponse(request, "directory/directory.html", data) def post(self, request): - """ join the directory """ + """join the directory""" request.user.discoverable = True request.user.save() return redirect("directory") diff --git a/bookwyrm/views/federation.py b/bookwyrm/views/federation.py index 8712c463c..d4a1af127 100644 --- a/bookwyrm/views/federation.py +++ b/bookwyrm/views/federation.py @@ -20,15 +20,10 @@ from bookwyrm.settings import PAGE_LENGTH name="dispatch", ) class Federation(View): - """ what servers do we federate with """ + """what servers do we federate with""" def get(self, request): - """ list of servers """ - try: - page = int(request.GET.get("page", 1)) - except ValueError: - page = 1 - + """list of servers""" servers = models.FederatedServer.objects sort = request.GET.get("sort") @@ -40,7 +35,7 @@ class Federation(View): paginated = Paginator(servers, PAGE_LENGTH) data = { - "servers": paginated.get_page(page), + "servers": paginated.get_page(request.GET.get("page")), "sort": sort, "form": forms.ServerForm(), } @@ -48,15 +43,15 @@ class Federation(View): class AddFederatedServer(View): - """ manually add a server """ + """manually add a server""" def get(self, request): - """ add server form """ + """add server form""" data = {"form": forms.ServerForm()} return TemplateResponse(request, "settings/edit_server.html", data) def post(self, request): - """ add a server from the admin panel """ + """add a server from the admin panel""" form = forms.ServerForm(request.POST) if not form.is_valid(): data = {"form": form} @@ -71,14 +66,14 @@ class AddFederatedServer(View): name="dispatch", ) class ImportServerBlocklist(View): - """ manually add a server """ + """manually add a server""" def get(self, request): - """ add server form """ + """add server form""" return TemplateResponse(request, "settings/server_blocklist.html") def post(self, request): - """ add a server from the admin panel """ + """add a server from the admin panel""" json_data = json.load(request.FILES["json_file"]) failed = [] success_count = 0 @@ -107,10 +102,10 @@ class ImportServerBlocklist(View): name="dispatch", ) class FederatedServer(View): - """ views for handling a specific federated server """ + """views for handling a specific federated server""" def get(self, request, server): - """ load a server """ + """load a server""" server = get_object_or_404(models.FederatedServer, id=server) users = server.user_set data = { @@ -126,7 +121,7 @@ class FederatedServer(View): return TemplateResponse(request, "settings/federated_server.html", data) def post(self, request, server): # pylint: disable=unused-argument - """ update note """ + """update note""" server = get_object_or_404(models.FederatedServer, id=server) server.notes = request.POST.get("notes") server.save() @@ -138,7 +133,7 @@ class FederatedServer(View): @permission_required("bookwyrm.control_federation", raise_exception=True) # pylint: disable=unused-argument def block_server(request, server): - """ block a server """ + """block a server""" server = get_object_or_404(models.FederatedServer, id=server) server.block() return redirect("settings-federated-server", server.id) @@ -149,7 +144,7 @@ def block_server(request, server): @permission_required("bookwyrm.control_federation", raise_exception=True) # pylint: disable=unused-argument def unblock_server(request, server): - """ unblock a server """ + """unblock a server""" server = get_object_or_404(models.FederatedServer, id=server) server.unblock() return redirect("settings-federated-server", server.id) diff --git a/bookwyrm/views/feed.py b/bookwyrm/views/feed.py index cd2792823..98f365ea4 100644 --- a/bookwyrm/views/feed.py +++ b/bookwyrm/views/feed.py @@ -18,15 +18,10 @@ from .helpers import is_api_request, is_bookwyrm_request # pylint: disable= no-self-use @method_decorator(login_required, name="dispatch") class Feed(View): - """ activity stream """ + """activity stream""" def get(self, request, tab): - """ user's homepage with activity feed """ - try: - page = int(request.GET.get("page", 1)) - except ValueError: - page = 1 - + """user's homepage with activity feed""" if not tab in STREAMS: tab = "home" @@ -39,7 +34,7 @@ class Feed(View): **feed_page_data(request.user), **{ "user": request.user, - "activities": paginated.get_page(page), + "activities": paginated.get_page(request.GET.get("page")), "suggested_users": suggested_users, "tab": tab, "goal_form": forms.GoalForm(), @@ -51,15 +46,10 @@ class Feed(View): @method_decorator(login_required, name="dispatch") class DirectMessage(View): - """ dm view """ + """dm view""" def get(self, request, username=None): - """ like a feed but for dms only """ - try: - page = int(request.GET.get("page", 1)) - except ValueError: - page = 1 - + """like a feed but for dms only""" # remove fancy subclasses of status, keep just good ol' notes queryset = models.Status.objects.filter( review__isnull=True, @@ -82,13 +72,12 @@ class DirectMessage(View): ).order_by("-published_date") paginated = Paginator(activities, PAGE_LENGTH) - activity_page = paginated.get_page(page) data = { **feed_page_data(request.user), **{ "user": request.user, "partner": user, - "activities": activity_page, + "activities": paginated.get_page(request.GET.get("page")), "path": "/direct-messages", }, } @@ -96,10 +85,10 @@ class DirectMessage(View): class Status(View): - """ get posting """ + """get posting""" def get(self, request, username, status_id): - """ display a particular status (and replies, etc) """ + """display a particular status (and replies, etc)""" try: user = get_user_from_username(request.user, username) status = models.Status.objects.select_subclasses().get( @@ -131,10 +120,10 @@ class Status(View): class Replies(View): - """ replies page (a json view of status) """ + """replies page (a json view of status)""" def get(self, request, username, status_id): - """ ordered collection of replies to a status """ + """ordered collection of replies to a status""" # the html view is the same as Status if not is_api_request(request): status_view = Status.as_view() @@ -149,7 +138,7 @@ class Replies(View): def feed_page_data(user): - """ info we need for every feed page """ + """info we need for every feed page""" if not user.is_authenticated: return {} @@ -162,7 +151,7 @@ def feed_page_data(user): def get_suggested_books(user, max_books=5): - """ helper to get a user's recent books """ + """helper to get a user's recent books""" book_count = 0 preset_shelves = [("reading", max_books), ("read", 2), ("to-read", max_books)] suggested_books = [] @@ -174,7 +163,7 @@ def get_suggested_books(user, max_books=5): ) shelf = user.shelf_set.get(identifier=preset) - shelf_books = shelf.shelfbook_set.order_by("-updated_date").all()[:limit] + shelf_books = shelf.shelfbook_set.order_by("-updated_date")[:limit] if not shelf_books: continue shelf_preview = { diff --git a/bookwyrm/views/follow.py b/bookwyrm/views/follow.py index d9f455ebb..09c2d53af 100644 --- a/bookwyrm/views/follow.py +++ b/bookwyrm/views/follow.py @@ -12,7 +12,7 @@ from .helpers import get_user_from_username @login_required @require_POST def follow(request): - """ follow another user, here or abroad """ + """follow another user, here or abroad""" username = request.POST["user"] try: to_follow = get_user_from_username(request.user, username) @@ -33,7 +33,7 @@ def follow(request): @login_required @require_POST def unfollow(request): - """ unfollow a user """ + """unfollow a user""" username = request.POST["user"] try: to_unfollow = get_user_from_username(request.user, username) @@ -61,7 +61,7 @@ def unfollow(request): @login_required @require_POST def accept_follow_request(request): - """ a user accepts a follow request """ + """a user accepts a follow request""" username = request.POST["user"] try: requester = get_user_from_username(request.user, username) @@ -83,7 +83,7 @@ def accept_follow_request(request): @login_required @require_POST def delete_follow_request(request): - """ a user rejects a follow request """ + """a user rejects a follow request""" username = request.POST["user"] try: requester = get_user_from_username(request.user, username) diff --git a/bookwyrm/views/get_started.py b/bookwyrm/views/get_started.py index a21723a38..5573bf199 100644 --- a/bookwyrm/views/get_started.py +++ b/bookwyrm/views/get_started.py @@ -20,12 +20,12 @@ from .user import save_user_form # pylint: disable= no-self-use @method_decorator(login_required, name="dispatch") class GetStartedProfile(View): - """ tell us about yourself """ + """tell us about yourself""" next_view = "get-started-books" def get(self, request): - """ basic profile info """ + """basic profile info""" data = { "form": forms.LimitedEditUserForm(instance=request.user), "next": self.next_view, @@ -33,7 +33,7 @@ class GetStartedProfile(View): return TemplateResponse(request, "get_started/profile.html", data) def post(self, request): - """ update your profile """ + """update your profile""" form = forms.LimitedEditUserForm( request.POST, request.FILES, instance=request.user ) @@ -46,12 +46,12 @@ class GetStartedProfile(View): @method_decorator(login_required, name="dispatch") class GetStartedBooks(View): - """ name a book, any book, we gotta start somewhere """ + """name a book, any book, we gotta start somewhere""" next_view = "get-started-users" def get(self, request): - """ info about a book """ + """info about a book""" query = request.GET.get("query") book_results = popular_books = [] if query: @@ -82,7 +82,7 @@ class GetStartedBooks(View): return TemplateResponse(request, "get_started/books.html", data) def post(self, request): - """ shelve some books """ + """shelve some books""" shelve_actions = [ (k, v) for k, v in request.POST.items() @@ -100,10 +100,10 @@ class GetStartedBooks(View): @method_decorator(login_required, name="dispatch") class GetStartedUsers(View): - """ find friends """ + """find friends""" def get(self, request): - """ basic profile info """ + """basic profile info""" query = request.GET.get("query") user_results = ( models.User.viewer_aware_objects(request.user) diff --git a/bookwyrm/views/goal.py b/bookwyrm/views/goal.py index 1627d3da3..84091fe35 100644 --- a/bookwyrm/views/goal.py +++ b/bookwyrm/views/goal.py @@ -16,10 +16,10 @@ from .helpers import get_user_from_username # pylint: disable= no-self-use @method_decorator(login_required, name="dispatch") class Goal(View): - """ track books for the year """ + """track books for the year""" def get(self, request, username, year): - """ reading goal page """ + """reading goal page""" user = get_user_from_username(request.user, username) year = int(year) goal = models.AnnualGoal.objects.filter(year=year, user=user).first() @@ -39,7 +39,7 @@ class Goal(View): return TemplateResponse(request, "goal.html", data) def post(self, request, username, year): - """ update or create an annual goal """ + """update or create an annual goal""" user = get_user_from_username(request.user, username) if user != request.user: return HttpResponseNotFound() @@ -71,7 +71,7 @@ class Goal(View): @require_POST @login_required def hide_goal(request): - """ don't keep bugging people to set a goal """ + """don't keep bugging people to set a goal""" request.user.show_goal = False request.user.save(broadcast=False) return redirect(request.headers.get("Referer", "/")) diff --git a/bookwyrm/views/helpers.py b/bookwyrm/views/helpers.py index 57c334377..8a60b54c7 100644 --- a/bookwyrm/views/helpers.py +++ b/bookwyrm/views/helpers.py @@ -11,7 +11,7 @@ from bookwyrm.utils import regex def get_user_from_username(viewer, username): - """ helper function to resolve a localname or a username to a user """ + """helper function to resolve a localname or a username to a user""" # raises DoesNotExist if user is now found try: return models.User.viewer_aware_objects(viewer).get(localname=username) @@ -20,12 +20,12 @@ def get_user_from_username(viewer, username): def is_api_request(request): - """ check whether a request is asking for html or data """ + """check whether a request is asking for html or data""" return "json" in request.headers.get("Accept", "") or request.path[-5:] == ".json" def is_bookwyrm_request(request): - """ check if the request is coming from another bookwyrm instance """ + """check if the request is coming from another bookwyrm instance""" user_agent = request.headers.get("User-Agent") if user_agent is None or re.search(regex.bookwyrm_user_agent, user_agent) is None: return False @@ -33,7 +33,7 @@ def is_bookwyrm_request(request): def privacy_filter(viewer, queryset, privacy_levels=None, following_only=False): - """ filter objects that have "user" and "privacy" fields """ + """filter objects that have "user" and "privacy" fields""" privacy_levels = privacy_levels or ["public", "unlisted", "followers", "direct"] # if there'd a deleted field, exclude deleted items try: @@ -84,7 +84,7 @@ def privacy_filter(viewer, queryset, privacy_levels=None, following_only=False): def handle_remote_webfinger(query): - """ webfingerin' other servers """ + """webfingerin' other servers""" user = None # usernames could be @user@domain or user@domain @@ -120,7 +120,7 @@ def handle_remote_webfinger(query): def get_edition(book_id): - """ look up a book in the db and return an edition """ + """look up a book in the db and return an edition""" book = models.Book.objects.select_subclasses().get(id=book_id) if isinstance(book, models.Work): book = book.get_default_edition() @@ -128,7 +128,7 @@ def get_edition(book_id): def handle_reading_status(user, shelf, book, privacy): - """ post about a user reading a book """ + """post about a user reading a book""" # tell the world about this cool thing that happened try: message = { @@ -145,14 +145,14 @@ def handle_reading_status(user, shelf, book, privacy): def is_blocked(viewer, user): - """ is this viewer blocked by the user? """ + """is this viewer blocked by the user?""" if viewer.is_authenticated and viewer in user.blocks.all(): return True return False def get_discover_books(): - """ list of books for the discover page """ + """list of books for the discover page""" return list( set( models.Edition.objects.filter( @@ -169,7 +169,7 @@ def get_discover_books(): def get_suggested_users(user): - """ bookwyrm users you don't already know """ + """bookwyrm users you don't already know""" return ( get_annotated_users( user, @@ -184,7 +184,7 @@ def get_suggested_users(user): def get_annotated_users(user, *args, **kwargs): - """ Users, annotated with things they have in common """ + """Users, annotated with things they have in common""" return ( models.User.objects.filter(discoverable=True, is_active=True, *args, **kwargs) .exclude(Q(id__in=user.blocks.all()) | Q(blocks=user)) diff --git a/bookwyrm/views/import_data.py b/bookwyrm/views/import_data.py index 5bdbe9151..a2abbc695 100644 --- a/bookwyrm/views/import_data.py +++ b/bookwyrm/views/import_data.py @@ -16,10 +16,10 @@ from bookwyrm.tasks import app # pylint: disable= no-self-use @method_decorator(login_required, name="dispatch") class Import(View): - """ import view """ + """import view""" def get(self, request): - """ load import page """ + """load import page""" return TemplateResponse( request, "import.html", @@ -32,7 +32,7 @@ class Import(View): ) def post(self, request): - """ ingest a goodreads csv """ + """ingest a goodreads csv""" form = forms.ImportForm(request.POST, request.FILES) if form.is_valid(): include_reviews = request.POST.get("include_reviews") == "on" @@ -66,10 +66,10 @@ class Import(View): @method_decorator(login_required, name="dispatch") class ImportStatus(View): - """ status of an existing import """ + """status of an existing import""" def get(self, request, job_id): - """ status of an import job """ + """status of an import job""" job = models.ImportJob.objects.get(id=job_id) if job.user != request.user: raise PermissionDenied @@ -84,7 +84,7 @@ class ImportStatus(View): ) def post(self, request, job_id): - """ retry lines from an import """ + """retry lines from an import""" job = get_object_or_404(models.ImportJob, id=job_id) items = [] for item in request.POST.getlist("import_item"): diff --git a/bookwyrm/views/inbox.py b/bookwyrm/views/inbox.py index c701956d2..a558c571e 100644 --- a/bookwyrm/views/inbox.py +++ b/bookwyrm/views/inbox.py @@ -19,10 +19,10 @@ from bookwyrm.utils import regex @method_decorator(csrf_exempt, name="dispatch") # pylint: disable=no-self-use class Inbox(View): - """ requests sent by outside servers""" + """requests sent by outside servers""" def post(self, request, username=None): - """ only works as POST request """ + """only works as POST request""" # first check if this server is on our shitlist if is_blocked_user_agent(request): return HttpResponseForbidden() @@ -65,7 +65,7 @@ class Inbox(View): def is_blocked_user_agent(request): - """ check if a request is from a blocked server based on user agent """ + """check if a request is from a blocked server based on user agent""" # check user agent user_agent = request.headers.get("User-Agent") if not user_agent: @@ -78,7 +78,7 @@ def is_blocked_user_agent(request): def is_blocked_activity(activity_json): - """ get the sender out of activity json and check if it's blocked """ + """get the sender out of activity json and check if it's blocked""" actor = activity_json.get("actor") # check if the user is banned/deleted @@ -94,7 +94,7 @@ def is_blocked_activity(activity_json): @app.task def activity_task(activity_json): - """ do something with this json we think is legit """ + """do something with this json we think is legit""" # lets see if the activitypub module can make sense of this json activity = activitypub.parse(activity_json) @@ -104,7 +104,7 @@ def activity_task(activity_json): def has_valid_signature(request, activity): - """ verify incoming signature """ + """verify incoming signature""" try: signature = Signature.parse(request) diff --git a/bookwyrm/views/interaction.py b/bookwyrm/views/interaction.py index e337f2ef6..e138e41cf 100644 --- a/bookwyrm/views/interaction.py +++ b/bookwyrm/views/interaction.py @@ -12,10 +12,10 @@ from bookwyrm import models # pylint: disable= no-self-use @method_decorator(login_required, name="dispatch") class Favorite(View): - """ like a status """ + """like a status""" def post(self, request, status_id): - """ create a like """ + """create a like""" status = models.Status.objects.get(id=status_id) try: models.Favorite.objects.create(status=status, user=request.user) @@ -28,10 +28,10 @@ class Favorite(View): @method_decorator(login_required, name="dispatch") class Unfavorite(View): - """ take back a fav """ + """take back a fav""" def post(self, request, status_id): - """ unlike a status """ + """unlike a status""" status = models.Status.objects.get(id=status_id) try: favorite = models.Favorite.objects.get(status=status, user=request.user) @@ -45,10 +45,10 @@ class Unfavorite(View): @method_decorator(login_required, name="dispatch") class Boost(View): - """ boost a status """ + """boost a status""" def post(self, request, status_id): - """ boost a status """ + """boost a status""" status = models.Status.objects.get(id=status_id) # is it boostable? if not status.boostable: @@ -70,10 +70,10 @@ class Boost(View): @method_decorator(login_required, name="dispatch") class Unboost(View): - """ boost a status """ + """boost a status""" def post(self, request, status_id): - """ boost a status """ + """boost a status""" status = models.Status.objects.get(id=status_id) boost = models.Boost.objects.filter( boosted_status=status, user=request.user diff --git a/bookwyrm/views/invite.py b/bookwyrm/views/invite.py index 03b31b7b5..92f930f45 100644 --- a/bookwyrm/views/invite.py +++ b/bookwyrm/views/invite.py @@ -26,15 +26,10 @@ from . import helpers name="dispatch", ) class ManageInvites(View): - """ create invites """ + """create invites""" def get(self, request): - """ invite management page """ - try: - page = int(request.GET.get("page", 1)) - except ValueError: - page = 1 - + """invite management page""" paginated = Paginator( models.SiteInvite.objects.filter(user=request.user).order_by( "-created_date" @@ -43,13 +38,13 @@ class ManageInvites(View): ) data = { - "invites": paginated.get_page(page), + "invites": paginated.get_page(request.GET.get("page")), "form": forms.CreateInviteForm(), } return TemplateResponse(request, "settings/manage_invites.html", data) def post(self, request): - """ creates an invite database entry """ + """creates an invite database entry""" form = forms.CreateInviteForm(request.POST) if not form.is_valid(): return HttpResponseBadRequest("ERRORS : %s" % (form.errors,)) @@ -69,10 +64,10 @@ class ManageInvites(View): class Invite(View): - """ use an invite to register """ + """use an invite to register""" def get(self, request, code): - """ endpoint for using an invites """ + """endpoint for using an invites""" if request.user.is_authenticated: return redirect("/") invite = get_object_or_404(models.SiteInvite, code=code) @@ -88,16 +83,11 @@ class Invite(View): class ManageInviteRequests(View): - """ grant invites like the benevolent lord you are """ + """grant invites like the benevolent lord you are""" def get(self, request): - """ view a list of requests """ + """view a list of requests""" ignored = request.GET.get("ignored", False) - try: - page = int(request.GET.get("page", 1)) - except ValueError: - page = 1 - sort = request.GET.get("sort") sort_fields = [ "created_date", @@ -136,13 +126,13 @@ class ManageInviteRequests(View): data = { "ignored": ignored, "count": paginated.count, - "requests": paginated.get_page(page), + "requests": paginated.get_page(request.GET.get("page")), "sort": sort, } return TemplateResponse(request, "settings/manage_invite_requests.html", data) def post(self, request): - """ send out an invite """ + """send out an invite""" invite_request = get_object_or_404( models.InviteRequest, id=request.POST.get("invite-request") ) @@ -162,10 +152,10 @@ class ManageInviteRequests(View): class InviteRequest(View): - """ prospective users sign up here """ + """prospective users sign up here""" def post(self, request): - """ create a request """ + """create a request""" form = forms.InviteRequestForm(request.POST) received = False if form.is_valid(): @@ -182,7 +172,7 @@ class InviteRequest(View): @require_POST def ignore_invite_request(request): - """ hide an invite request """ + """hide an invite request""" invite_request = get_object_or_404( models.InviteRequest, id=request.POST.get("invite-request") ) diff --git a/bookwyrm/views/isbn.py b/bookwyrm/views/isbn.py index b7ba02dd8..197088bab 100644 --- a/bookwyrm/views/isbn.py +++ b/bookwyrm/views/isbn.py @@ -13,10 +13,10 @@ from .helpers import is_api_request # pylint: disable= no-self-use class Isbn(View): - """ search a book by isbn """ + """search a book by isbn""" def get(self, request, isbn): - """ info about a book """ + """info about a book""" book_results = connector_manager.isbn_local_search(isbn) if is_api_request(request): diff --git a/bookwyrm/views/landing.py b/bookwyrm/views/landing.py index 407451fb8..1361935ee 100644 --- a/bookwyrm/views/landing.py +++ b/bookwyrm/views/landing.py @@ -9,18 +9,18 @@ from . import helpers # pylint: disable= no-self-use class About(View): - """ create invites """ + """create invites""" def get(self, request): - """ more information about the instance """ + """more information about the instance""" return TemplateResponse(request, "discover/about.html") class Home(View): - """ discover page or home feed depending on auth """ + """discover page or home feed depending on auth""" def get(self, request): - """ this is the same as the feed on the home tab """ + """this is the same as the feed on the home tab""" if request.user.is_authenticated: feed_view = Feed.as_view() return feed_view(request, "home") @@ -29,10 +29,10 @@ class Home(View): class Discover(View): - """ preview of recently reviewed books """ + """preview of recently reviewed books""" def get(self, request): - """ tiled book activity page """ + """tiled book activity page""" data = { "register_form": forms.RegisterForm(), "request_form": forms.InviteRequestForm(), diff --git a/bookwyrm/views/list.py b/bookwyrm/views/list.py index 27e36dc5f..992ea4f7e 100644 --- a/bookwyrm/views/list.py +++ b/bookwyrm/views/list.py @@ -1,9 +1,12 @@ """ book list views""" +from typing import Optional + from django.contrib.auth.decorators import login_required from django.core.paginator import Paginator -from django.db import IntegrityError -from django.db.models import Count, Q -from django.http import HttpResponseNotFound, HttpResponseBadRequest +from django.db import IntegrityError, transaction +from django.db.models import Avg, Count, Q, Max +from django.db.models.functions import Coalesce +from django.http import HttpResponseNotFound, HttpResponseBadRequest, HttpResponse from django.shortcuts import get_object_or_404, redirect from django.template.response import TemplateResponse from django.utils.decorators import method_decorator @@ -16,17 +19,13 @@ from bookwyrm.connectors import connector_manager from .helpers import is_api_request, privacy_filter from .helpers import get_user_from_username + # pylint: disable=no-self-use class Lists(View): - """ book list page """ + """book list page""" def get(self, request): - """ display a book list """ - try: - page = int(request.GET.get("page", 1)) - except ValueError: - page = 1 - + """display a book list""" # hide lists with no approved books lists = ( models.List.objects.annotate( @@ -35,7 +34,6 @@ class Lists(View): .filter(item_count__gt=0) .order_by("-updated_date") .distinct() - .all() ) lists = privacy_filter( @@ -44,7 +42,7 @@ class Lists(View): paginated = Paginator(lists, 12) data = { - "lists": paginated.get_page(page), + "lists": paginated.get_page(request.GET.get("page")), "list_form": forms.ListForm(), "path": "/list", } @@ -53,7 +51,7 @@ class Lists(View): @method_decorator(login_required, name="dispatch") # pylint: disable=unused-argument def post(self, request): - """ create a book_list """ + """create a book_list""" form = forms.ListForm(request.POST) if not form.is_valid(): return redirect("lists") @@ -63,23 +61,19 @@ class Lists(View): class UserLists(View): - """ a user's book list page """ + """a user's book list page""" def get(self, request, username): - """ display a book list """ - try: - page = int(request.GET.get("page", 1)) - except ValueError: - page = 1 + """display a book list""" user = get_user_from_username(request.user, username) - lists = models.List.objects.filter(user=user).all() + lists = models.List.objects.filter(user=user) lists = privacy_filter(request.user, lists) paginated = Paginator(lists, 12) data = { "user": user, "is_self": request.user.id == user.id, - "lists": paginated.get_page(page), + "lists": paginated.get_page(request.GET.get("page")), "list_form": forms.ListForm(), "path": user.local_path + "/lists", } @@ -87,10 +81,10 @@ class UserLists(View): class List(View): - """ book list page """ + """book list page""" def get(self, request, list_id): - """ display a book list """ + """display a book list""" book_list = get_object_or_404(models.List, id=list_id) if not book_list.visible_to_user(request.user): return HttpResponseNotFound() @@ -100,6 +94,45 @@ class List(View): query = request.GET.get("q") suggestions = None + + # sort_by shall be "order" unless a valid alternative is given + sort_by = request.GET.get("sort_by", "order") + if sort_by not in ("order", "title", "rating"): + sort_by = "order" + + # direction shall be "ascending" unless a valid alternative is given + direction = request.GET.get("direction", "ascending") + if direction not in ("ascending", "descending"): + direction = "ascending" + + internal_sort_by = { + "order": "order", + "title": "book__title", + "rating": "average_rating", + } + directional_sort_by = internal_sort_by[sort_by] + if direction == "descending": + directional_sort_by = "-" + directional_sort_by + + if sort_by == "order": + items = book_list.listitem_set.filter(approved=True).order_by( + directional_sort_by + ) + elif sort_by == "title": + items = book_list.listitem_set.filter(approved=True).order_by( + directional_sort_by + ) + elif sort_by == "rating": + items = ( + book_list.listitem_set.annotate( + average_rating=Avg(Coalesce("book__review__rating", 0)) + ) + .filter(approved=True) + .order_by(directional_sort_by) + ) + + paginated = Paginator(items, 12) + if query and request.user.is_authenticated: # search for books suggestions = connector_manager.local_search(query, raw=True) @@ -119,18 +152,21 @@ class List(View): data = { "list": book_list, - "items": book_list.listitem_set.filter(approved=True), + "items": paginated.get_page(request.GET.get("page")), "pending_count": book_list.listitem_set.filter(approved=False).count(), "suggested_books": suggestions, "list_form": forms.ListForm(instance=book_list), "query": query or "", + "sort_form": forms.SortListForm( + {"direction": direction, "sort_by": sort_by} + ), } return TemplateResponse(request, "lists/list.html", data) @method_decorator(login_required, name="dispatch") # pylint: disable=unused-argument def post(self, request, list_id): - """ edit a list """ + """edit a list""" book_list = get_object_or_404(models.List, id=list_id) form = forms.ListForm(request.POST, instance=book_list) if not form.is_valid(): @@ -140,11 +176,11 @@ class List(View): class Curate(View): - """ approve or discard list suggestsions """ + """approve or discard list suggestsions""" @method_decorator(login_required, name="dispatch") def get(self, request, list_id): - """ display a pending list """ + """display a pending list""" book_list = get_object_or_404(models.List, id=list_id) if not book_list.user == request.user: # only the creater can curate the list @@ -160,21 +196,33 @@ class Curate(View): @method_decorator(login_required, name="dispatch") # pylint: disable=unused-argument def post(self, request, list_id): - """ edit a book_list """ + """edit a book_list""" book_list = get_object_or_404(models.List, id=list_id) suggestion = get_object_or_404(models.ListItem, id=request.POST.get("item")) approved = request.POST.get("approved") == "true" if approved: + # update the book and set it to be the last in the order of approved books, + # before any pending books suggestion.approved = True + order_max = ( + book_list.listitem_set.filter(approved=True).aggregate(Max("order"))[ + "order__max" + ] + or 0 + ) + 1 + suggestion.order = order_max + increment_order_in_reverse(book_list.id, order_max) suggestion.save() else: + deleted_order = suggestion.order suggestion.delete(broadcast=False) + normalize_book_list_ordering(book_list.id, start=deleted_order) return redirect("list-curate", book_list.id) @require_POST def add_book(request): - """ put a book on a list """ + """put a book on a list""" book_list = get_object_or_404(models.List, id=request.POST.get("list")) if not book_list.visible_to_user(request.user): return HttpResponseNotFound() @@ -183,19 +231,30 @@ def add_book(request): # do you have permission to add to the list? try: if request.user == book_list.user or book_list.curation == "open": - # go ahead and add it + # add the book at the latest order of approved books, before pending books + order_max = ( + book_list.listitem_set.filter(approved=True).aggregate(Max("order"))[ + "order__max" + ] + ) or 0 + increment_order_in_reverse(book_list.id, order_max + 1) models.ListItem.objects.create( book=book, book_list=book_list, user=request.user, + order=order_max + 1, ) elif book_list.curation == "curated": - # make a pending entry + # make a pending entry at the end of the list + order_max = ( + book_list.listitem_set.aggregate(Max("order"))["order__max"] + ) or 0 models.ListItem.objects.create( approved=False, book=book, book_list=book_list, user=request.user, + order=order_max + 1, ) else: # you can't add to this list, what were you THINKING @@ -209,12 +268,113 @@ def add_book(request): @require_POST def remove_book(request, list_id): - """ put a book on a list """ - book_list = get_object_or_404(models.List, id=list_id) - item = get_object_or_404(models.ListItem, id=request.POST.get("item")) + """remove a book from a list""" + with transaction.atomic(): + book_list = get_object_or_404(models.List, id=list_id) + item = get_object_or_404(models.ListItem, id=request.POST.get("item")) - if not book_list.user == request.user and not item.user == request.user: - return HttpResponseNotFound() + if not book_list.user == request.user and not item.user == request.user: + return HttpResponseNotFound() - item.delete() + deleted_order = item.order + item.delete() + normalize_book_list_ordering(book_list.id, start=deleted_order) return redirect("list", list_id) + + +@require_POST +def set_book_position(request, list_item_id): + """ + Action for when the list user manually specifies a list position, takes + special care with the unique ordering per list. + """ + with transaction.atomic(): + list_item = get_object_or_404(models.ListItem, id=list_item_id) + try: + int_position = int(request.POST.get("position")) + except ValueError: + return HttpResponseBadRequest( + "bad value for position. should be an integer" + ) + + if int_position < 1: + return HttpResponseBadRequest("position cannot be less than 1") + + book_list = list_item.book_list + + # the max position to which a book may be set is the highest order for + # books which are approved + order_max = book_list.listitem_set.filter(approved=True).aggregate( + Max("order") + )["order__max"] + + if int_position > order_max: + int_position = order_max + + if request.user not in (book_list.user, list_item.user): + return HttpResponseNotFound() + + original_order = list_item.order + if original_order == int_position: + return HttpResponse(status=204) + if original_order > int_position: + list_item.order = -1 + list_item.save() + increment_order_in_reverse(book_list.id, int_position, original_order) + else: + list_item.order = -1 + list_item.save() + decrement_order(book_list.id, original_order, int_position) + + list_item.order = int_position + list_item.save() + + return redirect("list", book_list.id) + + +@transaction.atomic +def increment_order_in_reverse( + book_list_id: int, start: int, end: Optional[int] = None +): + """increase the order nu,ber for every item in a list""" + try: + book_list = models.List.objects.get(id=book_list_id) + except models.List.DoesNotExist: + return + items = book_list.listitem_set.filter(order__gte=start) + if end is not None: + items = items.filter(order__lt=end) + items = items.order_by("-order") + for item in items: + item.order += 1 + item.save() + + +@transaction.atomic +def decrement_order(book_list_id, start, end): + """decrement the order value for every item in a list""" + try: + book_list = models.List.objects.get(id=book_list_id) + except models.List.DoesNotExist: + return + items = book_list.listitem_set.filter(order__gt=start, order__lte=end).order_by( + "order" + ) + for item in items: + item.order -= 1 + item.save() + + +@transaction.atomic +def normalize_book_list_ordering(book_list_id, start=0, add_offset=0): + """gives each book in a list the proper sequential order number""" + try: + book_list = models.List.objects.get(id=book_list_id) + except models.List.DoesNotExist: + return + items = book_list.listitem_set.filter(order__gt=start).order_by("order") + for i, item in enumerate(items, start): + effective_order = i + add_offset + if item.order != effective_order: + item.order = effective_order + item.save() diff --git a/bookwyrm/views/notifications.py b/bookwyrm/views/notifications.py index 7a62ec01e..e0e2102d7 100644 --- a/bookwyrm/views/notifications.py +++ b/bookwyrm/views/notifications.py @@ -9,10 +9,10 @@ from django.views import View # pylint: disable= no-self-use @method_decorator(login_required, name="dispatch") class Notifications(View): - """ notifications view """ + """notifications view""" def get(self, request): - """ people are interacting with you, get hyped """ + """people are interacting with you, get hyped""" notifications = request.user.notification_set.all().order_by("-created_date") unread = [n.id for n in notifications.filter(read=False)] data = { @@ -23,6 +23,6 @@ class Notifications(View): return TemplateResponse(request, "notifications.html", data) def post(self, request): - """ permanently delete notification for user """ + """permanently delete notification for user""" request.user.notification_set.filter(read=True).delete() return redirect("/notifications") diff --git a/bookwyrm/views/outbox.py b/bookwyrm/views/outbox.py index ec6f5cd39..4bc2d2b98 100644 --- a/bookwyrm/views/outbox.py +++ b/bookwyrm/views/outbox.py @@ -9,10 +9,10 @@ from .helpers import is_bookwyrm_request # pylint: disable= no-self-use class Outbox(View): - """ outbox """ + """outbox""" def get(self, request, username): - """ outbox for the requested user """ + """outbox for the requested user""" user = get_object_or_404(models.User, localname=username) filter_type = request.GET.get("type") if filter_type not in models.status_models: diff --git a/bookwyrm/views/password.py b/bookwyrm/views/password.py index 67010974e..933817a8e 100644 --- a/bookwyrm/views/password.py +++ b/bookwyrm/views/password.py @@ -14,17 +14,17 @@ from bookwyrm.emailing import password_reset_email # pylint: disable= no-self-use class PasswordResetRequest(View): - """ forgot password flow """ + """forgot password flow""" def get(self, request): - """ password reset page """ + """password reset page""" return TemplateResponse( request, "password_reset_request.html", ) def post(self, request): - """ create a password reset token """ + """create a password reset token""" email = request.POST.get("email") try: user = models.User.objects.get(email=email) @@ -43,10 +43,10 @@ class PasswordResetRequest(View): class PasswordReset(View): - """ set new password """ + """set new password""" def get(self, request, code): - """ endpoint for sending invites """ + """endpoint for sending invites""" if request.user.is_authenticated: return redirect("/") try: @@ -59,7 +59,7 @@ class PasswordReset(View): return TemplateResponse(request, "password_reset.html", {"code": code}) def post(self, request, code): - """ allow a user to change their password through an emailed token """ + """allow a user to change their password through an emailed token""" try: reset_code = models.PasswordReset.objects.get(code=code) except models.PasswordReset.DoesNotExist: @@ -84,15 +84,15 @@ class PasswordReset(View): @method_decorator(login_required, name="dispatch") class ChangePassword(View): - """ change password as logged in user """ + """change password as logged in user""" def get(self, request): - """ change password page """ + """change password page""" data = {"user": request.user} return TemplateResponse(request, "preferences/change_password.html", data) def post(self, request): - """ allow a user to change their password """ + """allow a user to change their password""" new_password = request.POST.get("password") confirm_password = request.POST.get("confirm-password") diff --git a/bookwyrm/views/reading.py b/bookwyrm/views/reading.py index b780dd2fd..65ca717d4 100644 --- a/bookwyrm/views/reading.py +++ b/bookwyrm/views/reading.py @@ -18,7 +18,7 @@ from .shelf import handle_unshelve @login_required @require_POST def start_reading(request, book_id): - """ begin reading a book """ + """begin reading a book""" book = get_edition(book_id) reading_shelf = models.Shelf.objects.filter( identifier=models.Shelf.READING, user=request.user @@ -60,7 +60,7 @@ def start_reading(request, book_id): @login_required @require_POST def finish_reading(request, book_id): - """ a user completed a book, yay """ + """a user completed a book, yay""" book = get_edition(book_id) finished_read_shelf = models.Shelf.objects.filter( identifier=models.Shelf.READ_FINISHED, user=request.user @@ -101,7 +101,7 @@ def finish_reading(request, book_id): @login_required @require_POST def edit_readthrough(request): - """ can't use the form because the dates are too finnicky """ + """can't use the form because the dates are too finnicky""" readthrough = update_readthrough(request, create=False) if not readthrough: return HttpResponseNotFound() @@ -121,7 +121,7 @@ def edit_readthrough(request): @login_required @require_POST def delete_readthrough(request): - """ remove a readthrough """ + """remove a readthrough""" readthrough = get_object_or_404(models.ReadThrough, id=request.POST.get("id")) # don't let people edit other people's data @@ -135,7 +135,7 @@ def delete_readthrough(request): @login_required @require_POST def create_readthrough(request): - """ can't use the form because the dates are too finnicky """ + """can't use the form because the dates are too finnicky""" book = get_object_or_404(models.Edition, id=request.POST.get("book")) readthrough = update_readthrough(request, create=True, book=book) if not readthrough: @@ -145,13 +145,14 @@ def create_readthrough(request): def load_date_in_user_tz_as_utc(date_str: str, user: models.User) -> datetime: + """ensures that data is stored consistently in the UTC timezone""" user_tz = dateutil.tz.gettz(user.preferred_timezone) start_date = dateutil.parser.parse(date_str, ignoretz=True) return start_date.replace(tzinfo=user_tz).astimezone(dateutil.tz.UTC) def update_readthrough(request, book=None, create=True): - """ updates but does not save dates on a readthrough """ + """updates but does not save dates on a readthrough""" try: read_id = request.POST.get("id") if not read_id: @@ -208,7 +209,7 @@ def update_readthrough(request, book=None, create=True): @login_required @require_POST def delete_progressupdate(request): - """ remove a progress update """ + """remove a progress update""" update = get_object_or_404(models.ProgressUpdate, id=request.POST.get("id")) # don't let people edit other people's data diff --git a/bookwyrm/views/reports.py b/bookwyrm/views/reports.py index 3dd53cb96..46c238844 100644 --- a/bookwyrm/views/reports.py +++ b/bookwyrm/views/reports.py @@ -20,10 +20,10 @@ from bookwyrm import forms, models name="dispatch", ) class Reports(View): - """ list of reports """ + """list of reports""" def get(self, request): - """ view current reports """ + """view current reports""" filters = {} resolved = request.GET.get("resolved") == "true" @@ -52,17 +52,17 @@ class Reports(View): name="dispatch", ) class Report(View): - """ view a specific report """ + """view a specific report""" def get(self, request, report_id): - """ load a report """ + """load a report""" data = { "report": get_object_or_404(models.Report, id=report_id), } return TemplateResponse(request, "moderation/report.html", data) def post(self, request, report_id): - """ comment on a report """ + """comment on a report""" report = get_object_or_404(models.Report, id=report_id) models.ReportComment.objects.create( user=request.user, @@ -74,18 +74,19 @@ class Report(View): @login_required @permission_required("bookwyrm_moderate_user") -def deactivate_user(_, report_id): - """ mark an account as inactive """ - report = get_object_or_404(models.Report, id=report_id) - report.user.is_active = not report.user.is_active - report.user.save() - return redirect("settings-report", report.id) +def suspend_user(_, user_id): + """mark an account as inactive""" + user = get_object_or_404(models.User, id=user_id) + user.is_active = not user.is_active + # this isn't a full deletion, so we don't want to tell the world + user.save(broadcast=False) + return redirect("settings-user", user.id) @login_required @permission_required("bookwyrm_moderate_post") def resolve_report(_, report_id): - """ mark a report as (un)resolved """ + """mark a report as (un)resolved""" report = get_object_or_404(models.Report, id=report_id) report.resolved = not report.resolved report.save() @@ -97,7 +98,7 @@ def resolve_report(_, report_id): @login_required @require_POST def make_report(request): - """ a user reports something """ + """a user reports something""" form = forms.ReportForm(request.POST) if not form.is_valid(): raise ValueError(form.errors) diff --git a/bookwyrm/views/rss_feed.py b/bookwyrm/views/rss_feed.py index ed3e84f47..f1678b7f3 100644 --- a/bookwyrm/views/rss_feed.py +++ b/bookwyrm/views/rss_feed.py @@ -5,25 +5,25 @@ from .helpers import get_user_from_username, privacy_filter # pylint: disable=no-self-use, unused-argument class RssFeed(Feed): - """ serialize user's posts in rss feed """ + """serialize user's posts in rss feed""" description_template = "snippets/rss_content.html" title_template = "snippets/rss_title.html" def get_object(self, request, username): - """ the user who's posts get serialized """ + """the user who's posts get serialized""" return get_user_from_username(request.user, username) def link(self, obj): - """ link to the user's profile """ + """link to the user's profile""" return obj.local_path def title(self, obj): - """ title of the rss feed entry """ + """title of the rss feed entry""" return f"Status updates from {obj.display_name}" def items(self, obj): - """ the user's activity feed """ + """the user's activity feed""" return privacy_filter( obj, obj.status_set.select_subclasses(), @@ -31,5 +31,5 @@ class RssFeed(Feed): ) def item_link(self, item): - """ link to the status """ + """link to the status""" return item.local_path diff --git a/bookwyrm/views/search.py b/bookwyrm/views/search.py index 9e7df9f4d..4543b55ee 100644 --- a/bookwyrm/views/search.py +++ b/bookwyrm/views/search.py @@ -16,10 +16,10 @@ from .helpers import handle_remote_webfinger # pylint: disable= no-self-use class Search(View): - """ search users or books """ + """search users or books""" def get(self, request): - """ that search bar up top """ + """that search bar up top""" query = request.GET.get("q") min_confidence = request.GET.get("min_confidence", 0.1) diff --git a/bookwyrm/views/shelf.py b/bookwyrm/views/shelf.py index 740439db6..9bcf0a4ac 100644 --- a/bookwyrm/views/shelf.py +++ b/bookwyrm/views/shelf.py @@ -21,20 +21,15 @@ from .helpers import handle_reading_status, privacy_filter # pylint: disable= no-self-use class Shelf(View): - """ shelf page """ + """shelf page""" def get(self, request, username, shelf_identifier=None): - """ display a shelf """ + """display a shelf""" try: user = get_user_from_username(request.user, username) except models.User.DoesNotExist: return HttpResponseNotFound() - try: - page = int(request.GET.get("page", 1)) - except ValueError: - page = 1 - shelves = privacy_filter(request.user, user.shelf_set) # get the shelf and make sure the logged in user should be able to see it @@ -61,7 +56,7 @@ class Shelf(View): return ActivitypubResponse(shelf.to_activity(**request.GET)) paginated = Paginator( - shelf.books.order_by("-updated_date").all(), + shelf.books.order_by("-updated_date"), PAGE_LENGTH, ) @@ -70,7 +65,7 @@ class Shelf(View): "is_self": is_self, "shelves": shelves.all(), "shelf": shelf, - "books": paginated.get_page(page), + "books": paginated.get_page(request.GET.get("page")), } return TemplateResponse(request, "user/shelf.html", data) @@ -78,7 +73,7 @@ class Shelf(View): @method_decorator(login_required, name="dispatch") # pylint: disable=unused-argument def post(self, request, username, shelf_identifier): - """ edit a shelf """ + """edit a shelf""" try: shelf = request.user.shelf_set.get(identifier=shelf_identifier) except models.Shelf.DoesNotExist: @@ -99,7 +94,7 @@ class Shelf(View): @login_required @require_POST def create_shelf(request): - """ user generated shelves """ + """user generated shelves""" form = forms.ShelfForm(request.POST) if not form.is_valid(): return redirect(request.headers.get("Referer", "/")) @@ -111,7 +106,7 @@ def create_shelf(request): @login_required @require_POST def delete_shelf(request, shelf_id): - """ user generated shelves """ + """user generated shelves""" shelf = get_object_or_404(models.Shelf, id=shelf_id) if request.user != shelf.user or not shelf.editable: return HttpResponseBadRequest() @@ -123,7 +118,7 @@ def delete_shelf(request, shelf_id): @login_required @require_POST def shelve(request): - """ put a book on a user's shelf """ + """put a book on a user's shelf""" book = get_edition(request.POST.get("book")) desired_shelf = models.Shelf.objects.filter( @@ -182,7 +177,7 @@ def shelve(request): @login_required @require_POST def unshelve(request): - """ put a on a user's shelf """ + """put a on a user's shelf""" book = models.Edition.objects.get(id=request.POST["book"]) current_shelf = models.Shelf.objects.get(id=request.POST["shelf"]) @@ -192,6 +187,6 @@ def unshelve(request): # pylint: disable=unused-argument def handle_unshelve(book, shelf): - """ unshelve a book """ + """unshelve a book""" row = models.ShelfBook.objects.get(book=book, shelf=shelf) row.delete() diff --git a/bookwyrm/views/site.py b/bookwyrm/views/site.py index e58976607..46bdf7226 100644 --- a/bookwyrm/views/site.py +++ b/bookwyrm/views/site.py @@ -15,16 +15,16 @@ from bookwyrm import emailing, forms, models name="dispatch", ) class Site(View): - """ manage things like the instance name """ + """manage things like the instance name""" def get(self, request): - """ edit form """ + """edit form""" site = models.SiteSettings.objects.get() data = {"site_form": forms.SiteForm(instance=site)} return TemplateResponse(request, "settings/site.html", data) def post(self, request): - """ edit the site settings """ + """edit the site settings""" site = models.SiteSettings.objects.get() form = forms.SiteForm(request.POST, request.FILES, instance=site) if not form.is_valid(): @@ -38,7 +38,7 @@ class Site(View): @login_required @permission_required("bookwyrm.edit_instance_settings", raise_exception=True) def email_preview(request): - """ for development, renders and example email template """ + """for development, renders and example email template""" template = request.GET.get("email") data = emailing.email_data() data["subject_path"] = "email/{}/subject.html".format(template) diff --git a/bookwyrm/views/status.py b/bookwyrm/views/status.py index f0119e0e0..2295c8ccf 100644 --- a/bookwyrm/views/status.py +++ b/bookwyrm/views/status.py @@ -19,16 +19,16 @@ from .reading import edit_readthrough # pylint: disable= no-self-use @method_decorator(login_required, name="dispatch") class CreateStatus(View): - """ the view for *posting* """ + """the view for *posting*""" def get(self, request): - """ compose view (used for delete-and-redraft """ + """compose view (used for delete-and-redraft""" book = get_object_or_404(models.Edition, id=request.GET.get("book")) data = {"book": book} return TemplateResponse(request, "compose.html", data) def post(self, request, status_type): - """ create status of whatever type """ + """create status of whatever type""" status_type = status_type[0].upper() + status_type[1:] try: @@ -80,10 +80,10 @@ class CreateStatus(View): @method_decorator(login_required, name="dispatch") class DeleteStatus(View): - """ tombstone that bad boy """ + """tombstone that bad boy""" def post(self, request, status_id): - """ delete and tombstone a status """ + """delete and tombstone a status""" status = get_object_or_404(models.Status, id=status_id) # don't let people delete other people's statuses @@ -97,10 +97,10 @@ class DeleteStatus(View): @method_decorator(login_required, name="dispatch") class DeleteAndRedraft(View): - """ delete a status but let the user re-create it """ + """delete a status but let the user re-create it""" def post(self, request, status_id): - """ delete and tombstone a status """ + """delete and tombstone a status""" status = get_object_or_404( models.Status.objects.select_subclasses(), id=status_id ) @@ -130,7 +130,7 @@ class DeleteAndRedraft(View): def find_mentions(content): - """ detect @mentions in raw status content """ + """detect @mentions in raw status content""" if not content: return for match in re.finditer(regex.strict_username, content): @@ -148,7 +148,7 @@ def find_mentions(content): def format_links(content): - """ detect and format links """ + """detect and format links""" return re.sub( r'([^(href=")]|^|\()(https?:\/\/(%s([\w\.\-_\/+&\?=:;,])*))' % regex.domain, r'\g<1>\g<3>', @@ -157,7 +157,7 @@ def format_links(content): def to_markdown(content): - """ catch links and convert to markdown """ + """catch links and convert to markdown""" content = markdown(content) content = format_links(content) # sanitize resulting html diff --git a/bookwyrm/views/tag.py b/bookwyrm/views/tag.py deleted file mode 100644 index a6bdf05a2..000000000 --- a/bookwyrm/views/tag.py +++ /dev/null @@ -1,73 +0,0 @@ -""" tagging views""" -from django.contrib.auth.decorators import login_required -from django.shortcuts import get_object_or_404, redirect -from django.template.response import TemplateResponse -from django.utils.decorators import method_decorator -from django.views import View - -from bookwyrm import models -from bookwyrm.activitypub import ActivitypubResponse -from .helpers import is_api_request - - -# pylint: disable= no-self-use -class Tag(View): - """ tag page """ - - def get(self, request, tag_id): - """ see books related to a tag """ - tag_obj = get_object_or_404(models.Tag, identifier=tag_id) - - if is_api_request(request): - return ActivitypubResponse(tag_obj.to_activity(**request.GET)) - - books = models.Edition.objects.filter( - usertag__tag__identifier=tag_id - ).distinct() - data = { - "books": books, - "tag": tag_obj, - } - return TemplateResponse(request, "tag.html", data) - - -@method_decorator(login_required, name="dispatch") -class AddTag(View): - """ add a tag to a book """ - - def post(self, request): - """ tag a book """ - # I'm not using a form here because sometimes "name" is sent as a hidden - # field which doesn't validate - name = request.POST.get("name") - book_id = request.POST.get("book") - book = get_object_or_404(models.Edition, id=book_id) - tag_obj, _ = models.Tag.objects.get_or_create( - name=name, - ) - models.UserTag.objects.get_or_create( - user=request.user, - book=book, - tag=tag_obj, - ) - - return redirect("/book/%s" % book_id) - - -@method_decorator(login_required, name="dispatch") -class RemoveTag(View): - """ remove a user's tag from a book """ - - def post(self, request): - """ untag a book """ - name = request.POST.get("name") - tag_obj = get_object_or_404(models.Tag, name=name) - book_id = request.POST.get("book") - book = get_object_or_404(models.Edition, id=book_id) - - user_tag = get_object_or_404( - models.UserTag, tag=tag_obj, book=book, user=request.user - ) - user_tag.delete() - - return redirect("/book/%s" % book_id) diff --git a/bookwyrm/views/updates.py b/bookwyrm/views/updates.py index cc5fc4199..349022724 100644 --- a/bookwyrm/views/updates.py +++ b/bookwyrm/views/updates.py @@ -7,7 +7,7 @@ from bookwyrm import activitystreams @login_required def get_notification_count(request): - """ any notifications waiting? """ + """any notifications waiting?""" return JsonResponse( { "count": request.user.notification_set.filter(read=False).count(), @@ -17,7 +17,7 @@ def get_notification_count(request): @login_required def get_unread_status_count(request, stream="home"): - """ any unread statuses for this feed? """ + """any unread statuses for this feed?""" stream = activitystreams.streams.get(stream) if not stream: return JsonResponse({}) diff --git a/bookwyrm/views/user.py b/bookwyrm/views/user.py index 26117a928..05fdb6069 100644 --- a/bookwyrm/views/user.py +++ b/bookwyrm/views/user.py @@ -22,10 +22,10 @@ from .helpers import is_blocked, privacy_filter # pylint: disable= no-self-use class User(View): - """ user profile page """ + """user profile page""" def get(self, request, username): - """ profile page for a user """ + """profile page for a user""" try: user = get_user_from_username(request.user, username) except models.User.DoesNotExist: @@ -40,11 +40,6 @@ class User(View): return ActivitypubResponse(user.to_activity()) # otherwise we're at a UI view - try: - page = int(request.GET.get("page", 1)) - except ValueError: - page = 1 - shelf_preview = [] # only show other shelves that should be visible @@ -87,7 +82,7 @@ class User(View): "is_self": is_self, "shelves": shelf_preview, "shelf_count": shelves.count(), - "activities": paginated.get_page(page), + "activities": paginated.get_page(request.GET.get("page", 1)), "goal": goal, } @@ -95,10 +90,10 @@ class User(View): class Followers(View): - """ list of followers view """ + """list of followers view""" def get(self, request, username): - """ list of followers """ + """list of followers""" try: user = get_user_from_username(request.user, username) except models.User.DoesNotExist: @@ -120,10 +115,10 @@ class Followers(View): class Following(View): - """ list of following view """ + """list of following view""" def get(self, request, username): - """ list of followers """ + """list of followers""" try: user = get_user_from_username(request.user, username) except models.User.DoesNotExist: @@ -146,10 +141,10 @@ class Following(View): @method_decorator(login_required, name="dispatch") class EditUser(View): - """ edit user view """ + """edit user view""" def get(self, request): - """ edit profile page for a user """ + """edit profile page for a user""" data = { "form": forms.EditUserForm(instance=request.user), "user": request.user, @@ -157,7 +152,7 @@ class EditUser(View): return TemplateResponse(request, "preferences/edit_user.html", data) def post(self, request): - """ les get fancy with images """ + """les get fancy with images""" form = forms.EditUserForm(request.POST, request.FILES, instance=request.user) if not form.is_valid(): data = {"form": form, "user": request.user} @@ -169,7 +164,7 @@ class EditUser(View): def save_user_form(form): - """ special handling for the user form """ + """special handling for the user form""" user = form.save(commit=False) if "avatar" in form.files: @@ -186,7 +181,7 @@ def save_user_form(form): def crop_avatar(image): - """ reduce the size and make an avatar square """ + """reduce the size and make an avatar square""" target_size = 120 width, height = image.size thumbnail_scale = ( diff --git a/bookwyrm/views/user_admin.py b/bookwyrm/views/user_admin.py index cc3ff8411..9d08e9309 100644 --- a/bookwyrm/views/user_admin.py +++ b/bookwyrm/views/user_admin.py @@ -1,11 +1,12 @@ """ manage user """ from django.contrib.auth.decorators import login_required, permission_required from django.core.paginator import Paginator +from django.shortcuts import get_object_or_404 from django.template.response import TemplateResponse from django.utils.decorators import method_decorator from django.views import View -from bookwyrm import models +from bookwyrm import forms, models from bookwyrm.settings import PAGE_LENGTH @@ -15,16 +16,11 @@ from bookwyrm.settings import PAGE_LENGTH permission_required("bookwyrm.moderate_users", raise_exception=True), name="dispatch", ) -class UserAdmin(View): - """ admin view of users on this server """ +class UserAdminList(View): + """admin view of users on this server""" def get(self, request): - """ list of users """ - try: - page = int(request.GET.get("page", 1)) - except ValueError: - page = 1 - + """list of users""" filters = {} server = request.GET.get("server") if server: @@ -50,8 +46,32 @@ class UserAdmin(View): paginated = Paginator(users, PAGE_LENGTH) data = { - "users": paginated.get_page(page), + "users": paginated.get_page(request.GET.get("page")), "sort": sort, "server": server, } - return TemplateResponse(request, "settings/user_admin.html", data) + return TemplateResponse(request, "user_admin/user_admin.html", data) + + +@method_decorator(login_required, name="dispatch") +@method_decorator( + permission_required("bookwyrm.moderate_users", raise_exception=True), + name="dispatch", +) +class UserAdmin(View): + """moderate an individual user""" + + def get(self, request, user): + """user view""" + user = get_object_or_404(models.User, id=user) + data = {"user": user, "group_form": forms.UserGroupForm()} + return TemplateResponse(request, "user_admin/user.html", data) + + def post(self, request, user): + """update user group""" + user = get_object_or_404(models.User, id=user) + form = forms.UserGroupForm(request.POST, instance=user) + if form.is_valid(): + form.save() + data = {"user": user, "group_form": form} + return TemplateResponse(request, "user_admin/user.html", data) diff --git a/bookwyrm/views/wellknown.py b/bookwyrm/views/wellknown.py index 178d558e6..2462c5a49 100644 --- a/bookwyrm/views/wellknown.py +++ b/bookwyrm/views/wellknown.py @@ -13,7 +13,7 @@ from bookwyrm.settings import DOMAIN, VERSION @require_GET def webfinger(request): - """ allow other servers to ask about a user """ + """allow other servers to ask about a user""" resource = request.GET.get("resource") if not resource or not resource.startswith("acct:"): return HttpResponseNotFound() @@ -40,7 +40,7 @@ def webfinger(request): @require_GET def nodeinfo_pointer(_): - """ direct servers to nodeinfo """ + """direct servers to nodeinfo""" return JsonResponse( { "links": [ @@ -55,7 +55,7 @@ def nodeinfo_pointer(_): @require_GET def nodeinfo(_): - """ basic info about the server """ + """basic info about the server""" status_count = models.Status.objects.filter(user__local=True).count() user_count = models.User.objects.filter(local=True).count() @@ -90,7 +90,7 @@ def nodeinfo(_): @require_GET def instance_info(_): - """ let's talk about your cool unique instance """ + """let's talk about your cool unique instance""" user_count = models.User.objects.filter(local=True).count() status_count = models.Status.objects.filter(user__local=True).count() @@ -116,12 +116,12 @@ def instance_info(_): @require_GET def peers(_): - """ list of federated servers this instance connects with """ + """list of federated servers this instance connects with""" names = models.FederatedServer.objects.values_list("server_name", flat=True) return JsonResponse(list(names), safe=False) @require_GET def host_meta(request): - """ meta of the host """ + """meta of the host""" return TemplateResponse(request, "host_meta.xml", {"DOMAIN": DOMAIN}) diff --git a/bw-dev b/bw-dev index 42fb4a2e7..c2b63bc17 100755 --- a/bw-dev +++ b/bw-dev @@ -90,10 +90,10 @@ case "$CMD" in runweb python manage.py collectstatic --no-input ;; makemessages) - runweb django-admin makemessages --no-wrap --ignore=venv3 $@ + runweb django-admin makemessages --no-wrap --ignore=venv $@ ;; compilemessages) - runweb django-admin compilemessages --ignore venv3 $@ + runweb django-admin compilemessages --ignore venv $@ ;; build) docker-compose build diff --git a/fr-dev b/fr-dev deleted file mode 120000 index 9947871eb..000000000 --- a/fr-dev +++ /dev/null @@ -1 +0,0 @@ -bw-dev \ No newline at end of file diff --git a/locale/es/LC_MESSAGES/django.mo b/locale/es/LC_MESSAGES/django.mo index ea8eac7ee..64baee4f2 100644 Binary files a/locale/es/LC_MESSAGES/django.mo and b/locale/es/LC_MESSAGES/django.mo differ diff --git a/locale/es/LC_MESSAGES/django.po b/locale/es/LC_MESSAGES/django.po index 64921d5e5..87f443a05 100644 --- a/locale/es/LC_MESSAGES/django.po +++ b/locale/es/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: 0.0.1\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-04-01 13:14-0700\n" +"POT-Creation-Date: 2021-04-25 04:01+0000\n" "PO-Revision-Date: 2021-03-19 11:49+0800\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,37 +18,60 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: bookwyrm/forms.py:226 -#, fuzzy -#| msgid "A user with that username already exists." +#: bookwyrm/forms.py:224 msgid "A user with this email already exists." -msgstr "Ya existe un usuario con ese nombre." +msgstr "Ya existe un usuario con ese correo electrónico." -#: bookwyrm/forms.py:240 +#: bookwyrm/forms.py:238 msgid "One Day" msgstr "Un día" -#: bookwyrm/forms.py:241 +#: bookwyrm/forms.py:239 msgid "One Week" msgstr "Una semana" -#: bookwyrm/forms.py:242 +#: bookwyrm/forms.py:240 msgid "One Month" msgstr "Un mes" -#: bookwyrm/forms.py:243 +#: bookwyrm/forms.py:241 msgid "Does Not Expire" msgstr "Nunca se vence" -#: bookwyrm/forms.py:248 +#: bookwyrm/forms.py:246 #, python-format msgid "%(count)d uses" msgstr "%(count)d usos" -#: bookwyrm/forms.py:251 +#: bookwyrm/forms.py:249 msgid "Unlimited" msgstr "Sin límite" +#: bookwyrm/forms.py:293 +msgid "List Order" +msgstr "Orden de la lista" + +#: bookwyrm/forms.py:294 +msgid "Book Title" +msgstr "Título" + +#: bookwyrm/forms.py:295 bookwyrm/templates/snippets/create_status_form.html:31 +#: bookwyrm/templates/user/shelf.html:81 +msgid "Rating" +msgstr "Calificación" + +#: bookwyrm/forms.py:297 bookwyrm/templates/lists/list.html:72 +msgid "Sort By" +msgstr "Ordenar por" + +#: bookwyrm/forms.py:301 +msgid "Ascending" +msgstr "Ascendente" + +#: bookwyrm/forms.py:302 +msgid "Descending" +msgstr "Descendente" + #: bookwyrm/models/fields.py:24 #, python-format msgid "%(value)s is not a valid remote_id" @@ -59,7 +82,7 @@ msgstr "%(value)s no es un remote_id válido" msgid "%(value)s is not a valid username" msgstr "%(value)s no es un usuario válido" -#: bookwyrm/models/fields.py:165 bookwyrm/templates/layout.html:157 +#: bookwyrm/models/fields.py:165 bookwyrm/templates/layout.html:153 msgid "username" msgstr "nombre de usuario" @@ -67,23 +90,23 @@ msgstr "nombre de usuario" msgid "A user with that username already exists." msgstr "Ya existe un usuario con ese nombre." -#: bookwyrm/settings.py:150 +#: bookwyrm/settings.py:152 msgid "English" msgstr "Inglés" -#: bookwyrm/settings.py:151 +#: bookwyrm/settings.py:153 msgid "German" msgstr "Aléman" -#: bookwyrm/settings.py:152 +#: bookwyrm/settings.py:154 msgid "Spanish" msgstr "Español" -#: bookwyrm/settings.py:153 +#: bookwyrm/settings.py:155 msgid "French" msgstr "Francés" -#: bookwyrm/settings.py:154 +#: bookwyrm/settings.py:156 msgid "Simplified Chinese" msgstr "Chino simplificado" @@ -120,82 +143,70 @@ msgstr "Wikipedia" msgid "Books by %(name)s" msgstr "Libros de %(name)s" -#: bookwyrm/templates/book/book.html:21 +#: bookwyrm/templates/book/book.html:33 #: bookwyrm/templates/discover/large-book.html:12 #: bookwyrm/templates/discover/small-book.html:9 msgid "by" msgstr "por" -#: bookwyrm/templates/book/book.html:29 bookwyrm/templates/book/book.html:30 +#: bookwyrm/templates/book/book.html:41 bookwyrm/templates/book/book.html:42 msgid "Edit Book" msgstr "Editar Libro" -#: bookwyrm/templates/book/book.html:49 +#: bookwyrm/templates/book/book.html:61 #: bookwyrm/templates/book/cover_modal.html:5 msgid "Add cover" msgstr "Agregar portada" -#: bookwyrm/templates/book/book.html:53 -#, fuzzy -#| msgid "Failed to load" +#: bookwyrm/templates/book/book.html:65 msgid "Failed to load cover" -msgstr "Se falló a cargar" +msgstr "No se pudo cargar la portada" -#: bookwyrm/templates/book/book.html:62 -msgid "ISBN:" -msgstr "ISBN:" - -#: bookwyrm/templates/book/book.html:69 -#: bookwyrm/templates/book/edit_book.html:211 -msgid "OCLC Number:" -msgstr "Número OCLC:" - -#: bookwyrm/templates/book/book.html:76 -#: bookwyrm/templates/book/edit_book.html:215 -msgid "ASIN:" -msgstr "ASIN:" - -#: bookwyrm/templates/book/book.html:85 +#: bookwyrm/templates/book/book.html:82 msgid "View on OpenLibrary" msgstr "Ver en OpenLibrary" -#: bookwyrm/templates/book/book.html:94 +#: bookwyrm/templates/book/book.html:102 #, python-format msgid "(%(review_count)s review)" msgid_plural "(%(review_count)s reviews)" msgstr[0] "(%(review_count)s reseña)" msgstr[1] "(%(review_count)s reseñas)" -#: bookwyrm/templates/book/book.html:100 +#: bookwyrm/templates/book/book.html:114 msgid "Add Description" msgstr "Agregar descripción" -#: bookwyrm/templates/book/book.html:107 -#: bookwyrm/templates/book/edit_book.html:101 +#: bookwyrm/templates/book/book.html:121 +#: bookwyrm/templates/book/edit_book.html:107 #: bookwyrm/templates/lists/form.html:12 msgid "Description:" msgstr "Descripción:" -#: bookwyrm/templates/book/book.html:111 -#: bookwyrm/templates/book/edit_book.html:225 +#: bookwyrm/templates/book/book.html:125 +#: bookwyrm/templates/book/edit_book.html:240 #: bookwyrm/templates/edit_author.html:78 bookwyrm/templates/lists/form.html:42 #: bookwyrm/templates/preferences/edit_user.html:70 +#: bookwyrm/templates/settings/edit_server.html:68 +#: bookwyrm/templates/settings/federated_server.html:93 #: bookwyrm/templates/settings/site.html:93 -#: bookwyrm/templates/snippets/readthrough.html:65 +#: bookwyrm/templates/snippets/readthrough.html:75 #: bookwyrm/templates/snippets/shelve_button/finish_reading_modal.html:42 #: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:42 #: bookwyrm/templates/snippets/shelve_button/start_reading_modal.html:34 +#: bookwyrm/templates/user_admin/user_moderation_actions.html:38 msgid "Save" msgstr "Guardar" -#: bookwyrm/templates/book/book.html:112 bookwyrm/templates/book/book.html:161 +#: bookwyrm/templates/book/book.html:126 bookwyrm/templates/book/book.html:175 #: bookwyrm/templates/book/cover_modal.html:32 -#: bookwyrm/templates/book/edit_book.html:226 +#: bookwyrm/templates/book/edit_book.html:241 #: bookwyrm/templates/edit_author.html:79 -#: bookwyrm/templates/moderation/report_modal.html:32 +#: bookwyrm/templates/moderation/report_modal.html:34 +#: bookwyrm/templates/settings/federated_server.html:94 #: bookwyrm/templates/snippets/delete_readthrough_modal.html:17 #: bookwyrm/templates/snippets/goal_form.html:32 -#: bookwyrm/templates/snippets/readthrough.html:66 +#: bookwyrm/templates/snippets/readthrough.html:76 #: bookwyrm/templates/snippets/shelve_button/finish_reading_modal.html:43 #: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:43 #: bookwyrm/templates/snippets/shelve_button/start_reading_modal.html:35 @@ -203,91 +214,98 @@ msgstr "Guardar" msgid "Cancel" msgstr "Cancelar" -#: bookwyrm/templates/book/book.html:121 +#: bookwyrm/templates/book/book.html:135 #, python-format msgid "%(count)s editions" msgstr "%(count)s ediciones" -#: bookwyrm/templates/book/book.html:129 +#: bookwyrm/templates/book/book.html:143 #, python-format msgid "This edition is on your %(shelf_name)s shelf." msgstr "Esta edición está en tu %(shelf_name)s estante." -#: bookwyrm/templates/book/book.html:135 +#: bookwyrm/templates/book/book.html:149 #, python-format msgid "A different edition of this book is on your %(shelf_name)s shelf." msgstr "Una edición diferente de este libro está en tu %(shelf_name)s estante." -#: bookwyrm/templates/book/book.html:144 +#: bookwyrm/templates/book/book.html:158 msgid "Your reading activity" msgstr "Tu actividad de lectura" -#: bookwyrm/templates/book/book.html:146 +#: bookwyrm/templates/book/book.html:160 msgid "Add read dates" msgstr "Agregar fechas de lectura" -#: bookwyrm/templates/book/book.html:151 +#: bookwyrm/templates/book/book.html:165 msgid "You don't have any reading activity for this book." msgstr "No tienes ninguna actividad de lectura para este libro." -#: bookwyrm/templates/book/book.html:158 +#: bookwyrm/templates/book/book.html:172 msgid "Create" msgstr "Crear" -#: bookwyrm/templates/book/book.html:180 +#: bookwyrm/templates/book/book.html:194 msgid "Subjects" msgstr "Sujetos" -#: bookwyrm/templates/book/book.html:191 +#: bookwyrm/templates/book/book.html:206 msgid "Places" msgstr "Lugares" -#: bookwyrm/templates/book/book.html:202 bookwyrm/templates/layout.html:64 +#: bookwyrm/templates/book/book.html:217 bookwyrm/templates/layout.html:65 #: bookwyrm/templates/lists/lists.html:5 bookwyrm/templates/lists/lists.html:12 #: bookwyrm/templates/search_results.html:91 #: bookwyrm/templates/user/user_layout.html:62 msgid "Lists" msgstr "Listas" -#: bookwyrm/templates/book/book.html:213 -#, fuzzy -#| msgid "Go to list" +#: bookwyrm/templates/book/book.html:228 msgid "Add to list" -msgstr "Irse a lista" +msgstr "Agregar a lista" -#: bookwyrm/templates/book/book.html:223 +#: bookwyrm/templates/book/book.html:238 #: bookwyrm/templates/book/cover_modal.html:31 -#: bookwyrm/templates/lists/list.html:90 +#: bookwyrm/templates/lists/list.html:123 msgid "Add" msgstr "Agregar" -#: bookwyrm/templates/book/book.html:251 +#: bookwyrm/templates/book/book.html:276 msgid "rated it" msgstr "lo calificó con" +#: bookwyrm/templates/book/book_identifiers.html:8 +msgid "ISBN:" +msgstr "ISBN:" + +#: bookwyrm/templates/book/book_identifiers.html:15 +#: bookwyrm/templates/book/edit_book.html:226 +msgid "OCLC Number:" +msgstr "Número OCLC:" + +#: bookwyrm/templates/book/book_identifiers.html:22 +#: bookwyrm/templates/book/edit_book.html:230 +msgid "ASIN:" +msgstr "ASIN:" + #: bookwyrm/templates/book/cover_modal.html:17 -#: bookwyrm/templates/book/edit_book.html:163 -#, fuzzy -#| msgid "Add cover" +#: bookwyrm/templates/book/edit_book.html:178 msgid "Upload cover:" -msgstr "Agregar portada" +msgstr "Subir portada:" #: bookwyrm/templates/book/cover_modal.html:23 -#: bookwyrm/templates/book/edit_book.html:169 +#: bookwyrm/templates/book/edit_book.html:184 msgid "Load cover from url:" -msgstr "" +msgstr "Agregar portada de url:" #: bookwyrm/templates/book/edit_book.html:5 #: bookwyrm/templates/book/edit_book.html:11 -#, fuzzy, python-format -#| msgid "Editions of %(book_title)s" +#, python-format msgid "Edit \"%(book_title)s\"" -msgstr "Ediciones de %(book_title)s" +msgstr "Editar \"%(book_title)s\"" #: bookwyrm/templates/book/edit_book.html:5 #: bookwyrm/templates/book/edit_book.html:13 -#, fuzzy -#| msgid "Add Books" msgid "Add Book" msgstr "Agregar libro" @@ -308,35 +326,34 @@ msgstr "Editado más recientemente por:" #: bookwyrm/templates/book/edit_book.html:40 msgid "Confirm Book Info" -msgstr "" +msgstr "Confirmar información de libro" #: bookwyrm/templates/book/edit_book.html:47 #, python-format msgid "Is \"%(name)s\" an existing author?" -msgstr "" +msgstr "¿Es \"%(name)s\" un autor ya existente?" #: bookwyrm/templates/book/edit_book.html:52 -#, fuzzy, python-format -#| msgid "Start \"%(book_title)s\"" +#, python-format msgid "Author of %(book_title)s" -msgstr "Empezar \"%(book_title)s\"" +msgstr "Autor de %(book_title)s" #: bookwyrm/templates/book/edit_book.html:55 msgid "This is a new author" -msgstr "" +msgstr "Este es un autor nuevo" #: bookwyrm/templates/book/edit_book.html:61 #, python-format msgid "Creating a new author: %(name)s" -msgstr "" +msgstr "Creando un autor nuevo: %(name)s" #: bookwyrm/templates/book/edit_book.html:67 msgid "Is this an edition of an existing work?" -msgstr "" +msgstr "¿Es esta una edición de una obra ya existente?" #: bookwyrm/templates/book/edit_book.html:71 msgid "This is a new work" -msgstr "" +msgstr "Esta es una obra nueva" #: bookwyrm/templates/book/edit_book.html:77 #: bookwyrm/templates/password_reset.html:30 @@ -353,93 +370,86 @@ msgstr "Volver" msgid "Metadata" msgstr "Metadatos" -#: bookwyrm/templates/book/edit_book.html:91 +#: bookwyrm/templates/book/edit_book.html:92 msgid "Title:" msgstr "Título:" -#: bookwyrm/templates/book/edit_book.html:96 +#: bookwyrm/templates/book/edit_book.html:100 msgid "Subtitle:" msgstr "Subtítulo:" -#: bookwyrm/templates/book/edit_book.html:106 +#: bookwyrm/templates/book/edit_book.html:113 msgid "Series:" msgstr "Serie:" -#: bookwyrm/templates/book/edit_book.html:111 +#: bookwyrm/templates/book/edit_book.html:120 msgid "Series number:" msgstr "Número de serie:" -#: bookwyrm/templates/book/edit_book.html:117 -#, fuzzy -#| msgid "Published" +#: bookwyrm/templates/book/edit_book.html:126 msgid "Publisher:" -msgstr "Publicado" +msgstr "Editorial:" -#: bookwyrm/templates/book/edit_book.html:119 +#: bookwyrm/templates/book/edit_book.html:128 msgid "Separate multiple publishers with commas." -msgstr "" +msgstr "Separar varios editores con comas." -#: bookwyrm/templates/book/edit_book.html:125 +#: bookwyrm/templates/book/edit_book.html:135 msgid "First published date:" msgstr "Fecha de primera publicación:" -#: bookwyrm/templates/book/edit_book.html:130 +#: bookwyrm/templates/book/edit_book.html:143 msgid "Published date:" msgstr "Fecha de publicación:" -#: bookwyrm/templates/book/edit_book.html:137 -#, fuzzy -#| msgid "Author" +#: bookwyrm/templates/book/edit_book.html:152 msgid "Authors" -msgstr "Autor/Autora" +msgstr "Autores" -#: bookwyrm/templates/book/edit_book.html:143 -#, fuzzy, python-format -#| msgid "Added by %(username)s" +#: bookwyrm/templates/book/edit_book.html:158 +#, python-format msgid "Remove %(name)s" -msgstr "Agregado por %(username)s" +msgstr "Eliminar %(name)s" -#: bookwyrm/templates/book/edit_book.html:148 -#, fuzzy -#| msgid "Edit Author" +#: bookwyrm/templates/book/edit_book.html:163 msgid "Add Authors:" -msgstr "Editar Autor/Autora" +msgstr "Agregar Autores:" -#: bookwyrm/templates/book/edit_book.html:149 +#: bookwyrm/templates/book/edit_book.html:164 msgid "John Doe, Jane Smith" -msgstr "" +msgstr "Juan Nadie, Natalia Natalia" -#: bookwyrm/templates/book/edit_book.html:155 +#: bookwyrm/templates/book/edit_book.html:170 #: bookwyrm/templates/user/shelf.html:75 msgid "Cover" msgstr "Portada:" -#: bookwyrm/templates/book/edit_book.html:182 +#: bookwyrm/templates/book/edit_book.html:197 msgid "Physical Properties" msgstr "Propiedades físicas:" -#: bookwyrm/templates/book/edit_book.html:183 +#: bookwyrm/templates/book/edit_book.html:198 #: bookwyrm/templates/book/format_filter.html:5 msgid "Format:" msgstr "Formato:" -#: bookwyrm/templates/book/edit_book.html:191 +#: bookwyrm/templates/book/edit_book.html:206 msgid "Pages:" msgstr "Páginas:" -#: bookwyrm/templates/book/edit_book.html:198 +#: bookwyrm/templates/book/edit_book.html:213 msgid "Book Identifiers" msgstr "Identificadores de libro" -#: bookwyrm/templates/book/edit_book.html:199 +#: bookwyrm/templates/book/edit_book.html:214 msgid "ISBN 13:" msgstr "ISBN 13:" -#: bookwyrm/templates/book/edit_book.html:203 +#: bookwyrm/templates/book/edit_book.html:218 msgid "ISBN 10:" msgstr "ISBN 10:" -#: bookwyrm/templates/book/edit_book.html:207 +#: bookwyrm/templates/book/edit_book.html:222 #: bookwyrm/templates/edit_author.html:59 msgid "Openlibrary key:" msgstr "Clave OpenLibrary:" @@ -457,85 +467,85 @@ msgstr "Ediciones de \"%(work_title)s\"" #: bookwyrm/templates/book/format_filter.html:8 #: bookwyrm/templates/book/language_filter.html:8 msgid "Any" -msgstr "" +msgstr "Cualquier" #: bookwyrm/templates/book/language_filter.html:5 msgid "Language:" -msgstr "" +msgstr "Idioma:" -#: bookwyrm/templates/book/publisher_info.html:6 +#: bookwyrm/templates/book/publisher_info.html:22 +#, python-format +msgid "%(format)s" +msgstr "%(format)s" + +#: bookwyrm/templates/book/publisher_info.html:24 #, python-format msgid "%(format)s, %(pages)s pages" msgstr "%(format)s, %(pages)s páginas" -#: bookwyrm/templates/book/publisher_info.html:8 +#: bookwyrm/templates/book/publisher_info.html:26 #, python-format msgid "%(pages)s pages" msgstr "%(pages)s páginas" -#: bookwyrm/templates/book/publisher_info.html:13 -#, fuzzy, python-format -#| msgid "%(pages)s pages" +#: bookwyrm/templates/book/publisher_info.html:38 +#, python-format msgid "%(languages)s language" -msgstr "%(pages)s páginas" +msgstr "idioma %(languages)s" -#: bookwyrm/templates/book/publisher_info.html:18 +#: bookwyrm/templates/book/publisher_info.html:64 #, python-format msgid "Published %(date)s by %(publisher)s." -msgstr "" +msgstr "Publicado %(date)s por %(publisher)s." -#: bookwyrm/templates/book/publisher_info.html:20 -#, fuzzy, python-format -#| msgid "Published date:" +#: bookwyrm/templates/book/publisher_info.html:66 +#, python-format msgid "Published %(date)s" -msgstr "Fecha de publicación:" +msgstr "Publicado %(date)s" -#: bookwyrm/templates/book/publisher_info.html:22 +#: bookwyrm/templates/book/publisher_info.html:68 #, python-format msgid "Published by %(publisher)s." -msgstr "" +msgstr "Publicado por %(publisher)s." #: bookwyrm/templates/components/inline_form.html:8 #: bookwyrm/templates/components/modal.html:11 -#: bookwyrm/templates/feed/feed_layout.html:57 +#: bookwyrm/templates/feed/feed_layout.html:70 #: bookwyrm/templates/get_started/layout.html:19 #: bookwyrm/templates/get_started/layout.html:52 msgid "Close" msgstr "Cerrar" +#: bookwyrm/templates/compose.html:5 bookwyrm/templates/compose.html:8 +msgid "Compose status" +msgstr "Componer status" + #: bookwyrm/templates/directory/community_filter.html:5 -#, fuzzy -#| msgid "Comment" msgid "Community" -msgstr "Comentario" +msgstr "Comunidad" #: bookwyrm/templates/directory/community_filter.html:8 -#, fuzzy -#| msgid "Max uses" msgid "Local users" -msgstr "Número máximo de usos" +msgstr "Usuarios locales" #: bookwyrm/templates/directory/community_filter.html:12 -#, fuzzy -#| msgid "Federated" msgid "Federated community" -msgstr "Federalizado" +msgstr "Comunidad federalizada" #: bookwyrm/templates/directory/directory.html:6 #: bookwyrm/templates/directory/directory.html:11 -#: bookwyrm/templates/layout.html:97 +#: bookwyrm/templates/layout.html:93 msgid "Directory" -msgstr "" +msgstr "Directorio" #: bookwyrm/templates/directory/directory.html:19 msgid "Make your profile discoverable to other BookWyrm users." -msgstr "" +msgstr "Haz que tu perfil sea reconocible a otros usarios de BookWyrm." #: bookwyrm/templates/directory/directory.html:26 -#, fuzzy, python-format -#| msgid "You can set or change your reading goal any time from your profile page" +#, python-format msgid "You can opt-out at any time in your profile settings." -msgstr "Puedes establecer o cambiar tu meta de lectura en cualquier momento que desees desde tu perfil" +msgstr "Puedes optar por no en cualquier hora en tus configuraciones de perfil." #: bookwyrm/templates/directory/directory.html:31 #: bookwyrm/templates/snippets/goal_card.html:22 @@ -543,56 +553,48 @@ msgid "Dismiss message" msgstr "Desechar mensaje" #: bookwyrm/templates/directory/directory.html:71 -#, fuzzy -#| msgid "followed you" msgid "follower you follow" msgid_plural "followers you follow" -msgstr[0] "te siguió" -msgstr[1] "te siguió" +msgstr[0] "seguidor que tu sigues" +msgstr[1] "seguidores que tu sigues" #: bookwyrm/templates/directory/directory.html:78 -#, fuzzy -#| msgid "Your shelves" msgid "book on your shelves" msgid_plural "books on your shelves" -msgstr[0] "Tus estantes" -msgstr[1] "Tus estantes" +msgstr[0] "libro en tus estantes" +msgstr[1] "libro en tus estantes" #: bookwyrm/templates/directory/directory.html:86 msgid "posts" -msgstr "" +msgstr "publicaciones" #: bookwyrm/templates/directory/directory.html:92 msgid "last active" -msgstr "" +msgstr "actividad reciente" #: bookwyrm/templates/directory/sort_filter.html:5 msgid "Order by" -msgstr "" +msgstr "Ordenar por" #: bookwyrm/templates/directory/sort_filter.html:8 -#, fuzzy -#| msgid "Suggest" msgid "Suggested" -msgstr "Sugerir" +msgstr "Sugerido" #: bookwyrm/templates/directory/sort_filter.html:9 msgid "Recently active" -msgstr "" +msgstr "Activ@ recientemente" #: bookwyrm/templates/directory/user_type_filter.html:5 -#, fuzzy -#| msgid "User Activity" msgid "User type" -msgstr "Actividad de usuario" +msgstr "Tipo de usuario" #: bookwyrm/templates/directory/user_type_filter.html:8 msgid "BookWyrm users" -msgstr "" +msgstr "Usuarios de BookWyrm" #: bookwyrm/templates/directory/user_type_filter.html:12 msgid "All known users" -msgstr "" +msgstr "Todos los usuarios conocidos" #: bookwyrm/templates/discover/about.html:7 #, python-format @@ -642,11 +644,11 @@ msgstr "Esta instancia está cerrada." #: bookwyrm/templates/discover/landing_layout.html:57 msgid "Thank you! Your request has been received." -msgstr "" +msgstr "¡Gracias! Tu solicitud ha sido recibido." #: bookwyrm/templates/discover/landing_layout.html:60 msgid "Request an Invitation" -msgstr "" +msgstr "Solicitar una invitación" #: bookwyrm/templates/discover/landing_layout.html:64 #: bookwyrm/templates/password_reset_request.html:18 @@ -656,19 +658,17 @@ msgid "Email address:" msgstr "Dirección de correo electrónico:" #: bookwyrm/templates/discover/landing_layout.html:70 -#: bookwyrm/templates/moderation/report_modal.html:31 +#: bookwyrm/templates/moderation/report_modal.html:33 msgid "Submit" -msgstr "" +msgstr "Enviar" #: bookwyrm/templates/discover/landing_layout.html:79 msgid "Your Account" msgstr "Tu cuenta" #: bookwyrm/templates/edit_author.html:5 -#, fuzzy -#| msgid "Edit Author" msgid "Edit Author:" -msgstr "Editar Autor/Autora" +msgstr "Editar Autor/Autora/Autore:" #: bookwyrm/templates/edit_author.html:32 bookwyrm/templates/lists/form.html:8 #: bookwyrm/templates/user/create_shelf_form.html:13 @@ -707,49 +707,46 @@ msgstr "Clave Goodreads:" #: bookwyrm/templates/email/html_layout.html:15 #: bookwyrm/templates/email/text_layout.html:2 msgid "Hi there," -msgstr "" +msgstr "Hola, " #: bookwyrm/templates/email/html_layout.html:21 #, python-format msgid "BookWyrm hosted on %(site_name)s" -msgstr "" +msgstr "BookWyrm alojado en %(site_name)s" #: bookwyrm/templates/email/html_layout.html:23 msgid "Email preference" -msgstr "" +msgstr "Preferencia de correo electrónico" #: bookwyrm/templates/email/invite/html_content.html:6 #: bookwyrm/templates/email/invite/subject.html:2 -#, fuzzy, python-format -#| msgid "About %(site_name)s" +#, python-format msgid "You're invited to join %(site_name)s!" -msgstr "Sobre %(site_name)s" +msgstr "¡Estás invitado a unirse con %(site_name)s!" #: bookwyrm/templates/email/invite/html_content.html:9 msgid "Join Now" -msgstr "" +msgstr "Únete ahora" #: bookwyrm/templates/email/invite/html_content.html:15 #, python-format msgid "Learn more about this instance." -msgstr "" +msgstr "Aprenda más sobre esta instancia." #: bookwyrm/templates/email/invite/text_content.html:4 #, python-format msgid "You're invited to join %(site_name)s! Click the link below to create an account." -msgstr "" +msgstr "Estás invitado a unirte con %(site_name)s! Haz clic en el enlace a continuación para crear una cuenta." #: bookwyrm/templates/email/invite/text_content.html:8 -#, fuzzy -#| msgid "More about this site" msgid "Learn more about this instance:" -msgstr "Más sobre este sitio" +msgstr "Aprende más sobre esta intancia:" #: bookwyrm/templates/email/password_reset/html_content.html:6 #: bookwyrm/templates/email/password_reset/text_content.html:4 #, python-format msgid "You requested to reset your %(site_name)s password. Click the link below to set a new password and log in to your account." -msgstr "" +msgstr "Tú solicitaste reestablecer tu %(site_name)s contraseña. Haz clic en el enlace a continuación para establecer una nueva contraseña e ingresar a tu cuenta." #: bookwyrm/templates/email/password_reset/html_content.html:9 #: bookwyrm/templates/password_reset.html:4 @@ -762,13 +759,12 @@ msgstr "Restablecer contraseña" #: bookwyrm/templates/email/password_reset/html_content.html:13 #: bookwyrm/templates/email/password_reset/text_content.html:8 msgid "If you didn't request to reset your password, you can ignore this email." -msgstr "" +msgstr "Si no solicitaste reestablecer tu contraseña, puedes ignorar este mensaje." #: bookwyrm/templates/email/password_reset/subject.html:2 -#, fuzzy, python-format -#| msgid "About %(site_name)s" +#, python-format msgid "Reset your %(site_name)s password" -msgstr "Sobre %(site_name)s" +msgstr "Reestablece tu contraseña de %(site_name)s" #: bookwyrm/templates/feed/direct_messages.html:8 #, python-format @@ -776,7 +772,7 @@ msgid "Direct Messages with %(username)s" msgstr "Mensajes directos con %(username)s" #: bookwyrm/templates/feed/direct_messages.html:10 -#: bookwyrm/templates/layout.html:87 +#: bookwyrm/templates/layout.html:88 msgid "Direct Messages" msgstr "Mensajes directos" @@ -790,19 +786,15 @@ msgstr "No tienes ningún mensaje en este momento." #: bookwyrm/templates/feed/feed.html:9 msgid "Home Timeline" -msgstr "" +msgstr "Línea temporal de hogar" #: bookwyrm/templates/feed/feed.html:11 -#, fuzzy -#| msgid "%(tab_title)s Timeline" msgid "Local Timeline" -msgstr "%(tab_title)s Línea temporal" +msgstr "Línea temporal local" #: bookwyrm/templates/feed/feed.html:13 -#, fuzzy -#| msgid "Federated Servers" msgid "Federated Timeline" -msgstr "Servidores federalizados" +msgstr "Línea temporal federalizado" #: bookwyrm/templates/feed/feed.html:19 msgid "Home" @@ -813,29 +805,30 @@ msgid "Local" msgstr "Local" #: bookwyrm/templates/feed/feed.html:25 +#: bookwyrm/templates/settings/edit_server.html:40 msgid "Federated" msgstr "Federalizado" #: bookwyrm/templates/feed/feed.html:33 #, python-format msgid "load 0 unread status(es)" -msgstr "" +msgstr "cargar 0 status(es) no leídos" #: bookwyrm/templates/feed/feed.html:48 msgid "There aren't any activities right now! Try following a user to get started" -msgstr "¡No hay actividades en este momento! Sigue a otro usuario para empezar" +msgstr "¡No hay actividad ahora mismo! Sigue a otro usuario para empezar" #: bookwyrm/templates/feed/feed.html:56 #: bookwyrm/templates/get_started/users.html:6 msgid "Who to follow" -msgstr "" +msgstr "A quién seguir" #: bookwyrm/templates/feed/feed_layout.html:5 msgid "Updates" msgstr "Actualizaciones" #: bookwyrm/templates/feed/feed_layout.html:11 -#: bookwyrm/templates/layout.html:58 +#: bookwyrm/templates/layout.html:59 #: bookwyrm/templates/user/books_header.html:3 msgid "Your books" msgstr "Tus libros" @@ -844,23 +837,23 @@ msgstr "Tus libros" msgid "There are no books here right now! Try searching for a book to get started" msgstr "¡No hay ningún libro aqui ahorita! Busca a un libro para empezar" -#: bookwyrm/templates/feed/feed_layout.html:23 +#: bookwyrm/templates/feed/feed_layout.html:24 #: bookwyrm/templates/user/shelf.html:28 msgid "To Read" msgstr "Para leer" -#: bookwyrm/templates/feed/feed_layout.html:24 +#: bookwyrm/templates/feed/feed_layout.html:25 #: bookwyrm/templates/user/shelf.html:28 msgid "Currently Reading" msgstr "Leyendo actualmente" -#: bookwyrm/templates/feed/feed_layout.html:25 +#: bookwyrm/templates/feed/feed_layout.html:26 #: bookwyrm/templates/snippets/shelve_button/shelve_button_options.html:11 #: bookwyrm/templates/user/shelf.html:28 msgid "Read" -msgstr "Leer" +msgstr "Leido" -#: bookwyrm/templates/feed/feed_layout.html:74 bookwyrm/templates/goal.html:26 +#: bookwyrm/templates/feed/feed_layout.html:88 bookwyrm/templates/goal.html:26 #: bookwyrm/templates/snippets/goal_card.html:6 #, python-format msgid "%(year)s Reading Goal" @@ -870,30 +863,27 @@ msgstr "%(year)s Meta de lectura" #, python-format msgid "%(mutuals)s follower you follow" msgid_plural "%(mutuals)s followers you follow" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "%(mutuals)s seguidor que sigues" +msgstr[1] "%(mutuals)s seguidores que sigues" #: bookwyrm/templates/feed/suggested_users.html:19 #, python-format msgid "%(shared_books)s book on your shelves" msgid_plural "%(shared_books)s books on your shelves" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "%(shared_books)s libro en tus estantes" +msgstr[1] "%(shared_books)s libros en tus estantes" #: bookwyrm/templates/get_started/book_preview.html:6 -#, fuzzy, python-format -#| msgid "Want to Read \"%(book_title)s\"" +#, python-format msgid "Have you read %(book_title)s?" -msgstr "Quiero leer \"%(book_title)s\"" +msgstr "¿Has leído %(book_title)s?" #: bookwyrm/templates/get_started/books.html:6 -#, fuzzy -#| msgid "Started reading" msgid "What are you reading?" -msgstr "Lectura se empezó" +msgstr "¿Qué estás leyendo?" #: bookwyrm/templates/get_started/books.html:9 -#: bookwyrm/templates/lists/list.html:58 +#: bookwyrm/templates/lists/list.html:91 msgid "Search for a book" msgstr "Buscar libros" @@ -907,77 +897,65 @@ msgstr "No se encontró ningún libro correspondiente a \"%(query)s\"" #: bookwyrm/templates/get_started/books.html:11 #, python-format msgid "You can add books when you start using %(site_name)s." -msgstr "" +msgstr "Puedes agregar libros cuando comiences a usar %(site_name)s." #: bookwyrm/templates/get_started/books.html:16 #: bookwyrm/templates/get_started/books.html:17 #: bookwyrm/templates/get_started/users.html:18 #: bookwyrm/templates/get_started/users.html:19 -#: bookwyrm/templates/layout.html:37 bookwyrm/templates/layout.html:38 -#: bookwyrm/templates/lists/list.html:62 +#: bookwyrm/templates/layout.html:38 bookwyrm/templates/layout.html:39 +#: bookwyrm/templates/lists/list.html:95 msgid "Search" msgstr "Buscar" #: bookwyrm/templates/get_started/books.html:26 -#, fuzzy -#| msgid "Suggest Books" msgid "Suggested Books" -msgstr "Sugerir libros" +msgstr "Libros sugeridos" #: bookwyrm/templates/get_started/books.html:41 -#, fuzzy, python-format -#| msgid "About %(site_name)s" +#, python-format msgid "Popular on %(site_name)s" -msgstr "Sobre %(site_name)s" +msgstr "Popular en %(site_name)s" #: bookwyrm/templates/get_started/books.html:51 -#: bookwyrm/templates/lists/list.html:75 +#: bookwyrm/templates/lists/list.html:108 msgid "No books found" msgstr "No se encontró ningún libro" #: bookwyrm/templates/get_started/books.html:54 #: bookwyrm/templates/get_started/profile.html:54 msgid "Save & continue" -msgstr "" +msgstr "Guardar & continuar" #: bookwyrm/templates/get_started/layout.html:14 -#, fuzzy, python-format -#| msgid "About %(site_name)s" +#, python-format msgid "Welcome to %(site_name)s!" -msgstr "Sobre %(site_name)s" +msgstr "¡Bienvenido a %(site_name)s!" #: bookwyrm/templates/get_started/layout.html:16 msgid "These are some first steps to get you started." -msgstr "" +msgstr "Estos son unos primeros pasos para empezar." #: bookwyrm/templates/get_started/layout.html:30 #: bookwyrm/templates/get_started/profile.html:6 -#, fuzzy -#| msgid "User Profile" msgid "Create your profile" -msgstr "Perfil de usuario" +msgstr "Crear tu perfil" #: bookwyrm/templates/get_started/layout.html:34 -#, fuzzy -#| msgid "Add Books" msgid "Add books" msgstr "Agregar libros" #: bookwyrm/templates/get_started/layout.html:38 -#, fuzzy -#| msgid "Friendly" msgid "Find friends" -msgstr "Amigable" +msgstr "Encontrar amigos" #: bookwyrm/templates/get_started/layout.html:44 msgid "Skip this step" -msgstr "" +msgstr "Saltar este paso" #: bookwyrm/templates/get_started/layout.html:48 -#, fuzzy -#| msgid "Finished" msgid "Finish" -msgstr "Terminado" +msgstr "Terminar" #: bookwyrm/templates/get_started/profile.html:15 #: bookwyrm/templates/preferences/edit_user.html:24 @@ -991,7 +969,7 @@ msgstr "Resumen:" #: bookwyrm/templates/get_started/profile.html:23 msgid "A little bit about you" -msgstr "" +msgstr "Un poco sobre ti" #: bookwyrm/templates/get_started/profile.html:32 #: bookwyrm/templates/preferences/edit_user.html:17 @@ -1006,17 +984,15 @@ msgstr "Aprobar seguidores a mano:" #: bookwyrm/templates/get_started/profile.html:48 #: bookwyrm/templates/preferences/edit_user.html:58 msgid "Show this account in suggested users:" -msgstr "" +msgstr "Mostrar esta cuenta en los usuarios sugeridos:" #: bookwyrm/templates/get_started/profile.html:52 msgid "Your account will show up in the directory, and may be recommended to other BookWyrm users." -msgstr "" +msgstr "Tu cuenta se aparecerá en el directorio, y puede ser recomendado a otros usuarios de BookWyrm." #: bookwyrm/templates/get_started/users.html:11 -#, fuzzy -#| msgid "Search for a book or user" msgid "Search for a user" -msgstr "Buscar un libro o un usuario" +msgstr "Buscar un usuario" #: bookwyrm/templates/get_started/users.html:13 #: bookwyrm/templates/search_results.html:76 @@ -1055,19 +1031,17 @@ msgid "%(username)s's %(year)s Books" msgstr "Los libros de %(username)s para %(year)s" #: bookwyrm/templates/import.html:5 bookwyrm/templates/import.html:9 -#: bookwyrm/templates/layout.html:102 +#: bookwyrm/templates/layout.html:98 msgid "Import Books" msgstr "Importar libros" #: bookwyrm/templates/import.html:16 -#, fuzzy -#| msgid "Data source" msgid "Data source:" -msgstr "Fuente de datos" +msgstr "Fuente de datos:" #: bookwyrm/templates/import.html:29 msgid "Data file:" -msgstr "" +msgstr "Archivo de datos:" #: bookwyrm/templates/import.html:37 msgid "Include reviews" @@ -1078,6 +1052,7 @@ msgid "Privacy setting for imported reviews:" msgstr "Configuración de privacidad para las reseñas importadas:" #: bookwyrm/templates/import.html:48 +#: bookwyrm/templates/settings/server_blocklist.html:64 msgid "Import" msgstr "Importar" @@ -1116,12 +1091,12 @@ msgstr "(¡Refresca para actualizar!)" #: bookwyrm/templates/import_status.html:35 msgid "Failed to load" -msgstr "Se falló a cargar" +msgstr "No se pudo cargar" #: bookwyrm/templates/import_status.html:44 #, python-format msgid "Jump to the bottom of the list to select the %(failed_count)s items which failed to import." -msgstr "" +msgstr "Saltar al final de la lista para seleccionar los %(failed_count)s artículos que no se pudieron importar." #: bookwyrm/templates/import_status.html:79 msgid "Select all" @@ -1184,105 +1159,98 @@ msgstr "Resultados de búsqueda por \"%(query)s\"" msgid "Matching Books" msgstr "Libros correspondientes" -#: bookwyrm/templates/layout.html:33 +#: bookwyrm/templates/layout.html:34 msgid "Search for a book or user" msgstr "Buscar un libro o un usuario" -#: bookwyrm/templates/layout.html:47 bookwyrm/templates/layout.html:48 +#: bookwyrm/templates/layout.html:48 bookwyrm/templates/layout.html:49 msgid "Main navigation menu" msgstr "Menú de navigación central" -#: bookwyrm/templates/layout.html:61 +#: bookwyrm/templates/layout.html:62 msgid "Feed" msgstr "Actividad" -#: bookwyrm/templates/layout.html:92 -#: bookwyrm/templates/preferences/preferences_layout.html:14 -msgid "Profile" -msgstr "Perfil" - -#: bookwyrm/templates/layout.html:107 +#: bookwyrm/templates/layout.html:103 msgid "Settings" msgstr "Configuración" -#: bookwyrm/templates/layout.html:116 -#: bookwyrm/templates/settings/admin_layout.html:24 +#: bookwyrm/templates/layout.html:112 +#: bookwyrm/templates/settings/admin_layout.html:31 #: bookwyrm/templates/settings/manage_invite_requests.html:15 #: bookwyrm/templates/settings/manage_invites.html:3 #: bookwyrm/templates/settings/manage_invites.html:15 msgid "Invites" msgstr "Invitaciones" -#: bookwyrm/templates/layout.html:123 +#: bookwyrm/templates/layout.html:119 msgid "Admin" -msgstr "" +msgstr "Admin" -#: bookwyrm/templates/layout.html:130 +#: bookwyrm/templates/layout.html:126 msgid "Log out" msgstr "Cerrar sesión" -#: bookwyrm/templates/layout.html:138 bookwyrm/templates/layout.html:139 +#: bookwyrm/templates/layout.html:134 bookwyrm/templates/layout.html:135 #: bookwyrm/templates/notifications.html:6 #: bookwyrm/templates/notifications.html:10 msgid "Notifications" msgstr "Notificaciones" -#: bookwyrm/templates/layout.html:156 bookwyrm/templates/layout.html:160 +#: bookwyrm/templates/layout.html:152 bookwyrm/templates/layout.html:156 #: bookwyrm/templates/login.html:17 #: bookwyrm/templates/snippets/register_form.html:4 msgid "Username:" msgstr "Nombre de usuario:" -#: bookwyrm/templates/layout.html:161 +#: bookwyrm/templates/layout.html:157 msgid "password" msgstr "contraseña" -#: bookwyrm/templates/layout.html:162 bookwyrm/templates/login.html:36 +#: bookwyrm/templates/layout.html:158 bookwyrm/templates/login.html:36 msgid "Forgot your password?" msgstr "¿Olvidaste tu contraseña?" -#: bookwyrm/templates/layout.html:165 bookwyrm/templates/login.html:10 +#: bookwyrm/templates/layout.html:161 bookwyrm/templates/login.html:10 #: bookwyrm/templates/login.html:33 msgid "Log in" msgstr "Iniciar sesión" -#: bookwyrm/templates/layout.html:173 +#: bookwyrm/templates/layout.html:169 msgid "Join" -msgstr "" +msgstr "Unirse" -#: bookwyrm/templates/layout.html:196 +#: bookwyrm/templates/layout.html:195 msgid "About this server" msgstr "Sobre este servidor" -#: bookwyrm/templates/layout.html:200 +#: bookwyrm/templates/layout.html:199 msgid "Contact site admin" msgstr "Contactarse con administradores del sitio" -#: bookwyrm/templates/layout.html:207 +#: bookwyrm/templates/layout.html:206 #, python-format msgid "Support %(site_name)s on %(support_title)s" -msgstr "" +msgstr "Apoyar %(site_name)s en %(support_title)s" -#: bookwyrm/templates/layout.html:211 +#: bookwyrm/templates/layout.html:210 msgid "BookWyrm is open source software. You can contribute or report issues on GitHub." msgstr "BookWyrm es software de código abierto. Puedes contribuir o reportar problemas en GitHub." #: bookwyrm/templates/lists/create_form.html:5 -#: bookwyrm/templates/lists/lists.html:19 +#: bookwyrm/templates/lists/lists.html:20 msgid "Create List" msgstr "Crear lista" #: bookwyrm/templates/lists/created_text.html:5 -#, fuzzy, python-format -#| msgid "Added by %(username)s" +#, python-format msgid "Created and curated by %(username)s" -msgstr "Agregado por %(username)s" +msgstr "Agregado y comisariado por %(username)s" #: bookwyrm/templates/lists/created_text.html:7 -#, fuzzy, python-format -#| msgid "Added by %(username)s" +#, python-format msgid "Created by %(username)s" -msgstr "Agregado por %(username)s" +msgstr "Creado por %(username)s" #: bookwyrm/templates/lists/curate.html:6 msgid "Pending Books" @@ -1334,7 +1302,7 @@ msgid "Anyone can suggest books, subject to your approval" msgstr "Cualquier usuario puede sugerir libros, en cuanto lo hayas aprobado" #: bookwyrm/templates/lists/form.html:31 -#: bookwyrm/templates/moderation/reports.html:24 +#: bookwyrm/templates/moderation/reports.html:25 msgid "Open" msgstr "Abierto" @@ -1346,41 +1314,61 @@ msgstr "Cualquer usuario puede agregar libros a esta lista" msgid "This list is currently empty" msgstr "Esta lista está vacia" -#: bookwyrm/templates/lists/list.html:35 +#: bookwyrm/templates/lists/list.html:36 #, python-format msgid "Added by %(username)s" msgstr "Agregado por %(username)s" -#: bookwyrm/templates/lists/list.html:41 -#: bookwyrm/templates/snippets/shelf_selector.html:28 +#: bookwyrm/templates/lists/list.html:48 +msgid "Set" +msgstr "Establecido" + +#: bookwyrm/templates/lists/list.html:51 +msgid "List position" +msgstr "Posición" + +#: bookwyrm/templates/lists/list.html:57 +#: bookwyrm/templates/snippets/shelf_selector.html:26 msgid "Remove" msgstr "Quitar" -#: bookwyrm/templates/lists/list.html:54 +#: bookwyrm/templates/lists/list.html:70 bookwyrm/templates/lists/list.html:82 +msgid "Sort List" +msgstr "Ordena la lista" + +#: bookwyrm/templates/lists/list.html:76 +msgid "Direction" +msgstr "Dirección" + +#: bookwyrm/templates/lists/list.html:87 msgid "Add Books" msgstr "Agregar libros" -#: bookwyrm/templates/lists/list.html:54 +#: bookwyrm/templates/lists/list.html:87 msgid "Suggest Books" msgstr "Sugerir libros" -#: bookwyrm/templates/lists/list.html:63 +#: bookwyrm/templates/lists/list.html:96 msgid "search" msgstr "buscar" -#: bookwyrm/templates/lists/list.html:69 +#: bookwyrm/templates/lists/list.html:102 msgid "Clear search" msgstr "Borrar búsqueda" -#: bookwyrm/templates/lists/list.html:74 +#: bookwyrm/templates/lists/list.html:107 #, python-format msgid "No books found matching the query \"%(query)s\"" msgstr "No se encontró ningún libro correspondiente a la búsqueda: \"%(query)s\"" -#: bookwyrm/templates/lists/list.html:90 +#: bookwyrm/templates/lists/list.html:123 msgid "Suggest" msgstr "Sugerir" +#: bookwyrm/templates/lists/lists.html:14 bookwyrm/templates/user/lists.html:9 +msgid "Your Lists" +msgstr "Tus listas" + #: bookwyrm/templates/login.html:4 msgid "Login" msgstr "Iniciar sesión" @@ -1398,133 +1386,93 @@ msgstr "Contactar a unx administradorx para recibir una invitación" msgid "More about this site" msgstr "Más sobre este sitio" -#: bookwyrm/templates/moderation/report.html:5 #: bookwyrm/templates/moderation/report.html:6 +#: bookwyrm/templates/moderation/report.html:7 #: bookwyrm/templates/moderation/report_preview.html:6 #, python-format msgid "Report #%(report_id)s: %(username)s" -msgstr "" +msgstr "Reportar #%(report_id)s: %(username)s" -#: bookwyrm/templates/moderation/report.html:10 +#: bookwyrm/templates/moderation/report.html:11 msgid "Back to reports" -msgstr "" +msgstr "Volver a los informes" -#: bookwyrm/templates/moderation/report.html:18 -#, fuzzy -#| msgid "Notifications" -msgid "Actions" -msgstr "Notificaciones" - -#: bookwyrm/templates/moderation/report.html:19 -#, fuzzy -#| msgid "User Profile" -msgid "View user profile" -msgstr "Perfil de usuario" - -#: bookwyrm/templates/moderation/report.html:22 -#: bookwyrm/templates/snippets/status/status_options.html:25 -#: bookwyrm/templates/snippets/user_options.html:13 -msgid "Send direct message" -msgstr "Enviar mensaje directo" - -#: bookwyrm/templates/moderation/report.html:27 -msgid "Deactivate user" -msgstr "" - -#: bookwyrm/templates/moderation/report.html:29 -msgid "Reactivate user" -msgstr "" - -#: bookwyrm/templates/moderation/report.html:36 +#: bookwyrm/templates/moderation/report.html:23 msgid "Moderator Comments" -msgstr "" +msgstr "Comentarios de moderador" -#: bookwyrm/templates/moderation/report.html:54 -#: bookwyrm/templates/snippets/create_status.html:12 -#: bookwyrm/templates/snippets/create_status_form.html:52 +#: bookwyrm/templates/moderation/report.html:41 +#: bookwyrm/templates/snippets/create_status.html:28 +#: bookwyrm/templates/snippets/create_status_form.html:53 msgid "Comment" msgstr "Comentario" -#: bookwyrm/templates/moderation/report.html:59 -#, fuzzy -#| msgid "Delete status" +#: bookwyrm/templates/moderation/report.html:46 msgid "Reported statuses" -msgstr "Eliminar status" +msgstr "Statuses reportados" -#: bookwyrm/templates/moderation/report.html:61 +#: bookwyrm/templates/moderation/report.html:48 msgid "No statuses reported" -msgstr "" +msgstr "Ningún estatus reportado" -#: bookwyrm/templates/moderation/report.html:67 -msgid "Statuses has been deleted" -msgstr "" +#: bookwyrm/templates/moderation/report.html:54 +msgid "Status has been deleted" +msgstr "Status ha sido eliminado" #: bookwyrm/templates/moderation/report_modal.html:6 -#, fuzzy, python-format -#| msgid "Lists: %(username)s" +#, python-format msgid "Report @%(username)s" -msgstr "Listas: %(username)s" +msgstr "Reportar @%(username)s" -#: bookwyrm/templates/moderation/report_modal.html:21 +#: bookwyrm/templates/moderation/report_modal.html:23 #, python-format msgid "This report will be sent to %(site_name)s's moderators for review." -msgstr "" +msgstr "Este informe se enviará a los moderadores de %(site_name)s para la revisión." -#: bookwyrm/templates/moderation/report_modal.html:22 -#, fuzzy -#| msgid "More about this site" +#: bookwyrm/templates/moderation/report_modal.html:24 msgid "More info about this report:" -msgstr "Más sobre este sitio" +msgstr "Más información sobre este informe:" #: bookwyrm/templates/moderation/report_preview.html:13 msgid "No notes provided" -msgstr "" +msgstr "No se proporcionó notas" #: bookwyrm/templates/moderation/report_preview.html:20 -#, fuzzy, python-format -#| msgid "Added by %(username)s" +#, python-format msgid "Reported by %(username)s" -msgstr "Agregado por %(username)s" +msgstr "Reportado por %(username)s" #: bookwyrm/templates/moderation/report_preview.html:30 msgid "Re-open" -msgstr "" +msgstr "Reabrir" #: bookwyrm/templates/moderation/report_preview.html:32 msgid "Resolve" -msgstr "" +msgstr "Resolver" #: bookwyrm/templates/moderation/reports.html:6 -#, fuzzy, python-format -#| msgid "Lists: %(username)s" +#, python-format msgid "Reports: %(server_name)s" -msgstr "Listas: %(username)s" +msgstr "Informes: %(server_name)s" #: bookwyrm/templates/moderation/reports.html:8 -#: bookwyrm/templates/moderation/reports.html:16 -#: bookwyrm/templates/settings/admin_layout.html:28 -#, fuzzy -#| msgid "Recent Imports" +#: bookwyrm/templates/moderation/reports.html:17 +#: bookwyrm/templates/settings/admin_layout.html:35 msgid "Reports" -msgstr "Importaciones recientes" +msgstr "Informes" -#: bookwyrm/templates/moderation/reports.html:13 -#, fuzzy, python-format -#| msgid "Lists: %(username)s" +#: bookwyrm/templates/moderation/reports.html:14 +#, python-format msgid "Reports: %(server_name)s" -msgstr "Listas: %(username)s" +msgstr "Informes: %(server_name)s" -#: bookwyrm/templates/moderation/reports.html:27 -#, fuzzy -#| msgid "Shelved" +#: bookwyrm/templates/moderation/reports.html:28 msgid "Resolved" -msgstr "Archivado" +msgstr "Resuelto" -#: bookwyrm/templates/moderation/reports.html:34 -#, fuzzy -#| msgid "No books found" +#: bookwyrm/templates/moderation/reports.html:37 msgid "No reports found." -msgstr "No se encontró ningún libro" +msgstr "No se encontró ningún informe." #: bookwyrm/templates/notifications.html:14 msgid "Delete notifications" @@ -1636,7 +1584,7 @@ msgstr "Tu importación ha terminado." #: bookwyrm/templates/notifications.html:113 #, python-format msgid "A new report needs moderation." -msgstr "" +msgstr "Un informe nuevo se requiere moderación." #: bookwyrm/templates/notifications.html:139 msgid "You're all caught up!" @@ -1683,12 +1631,12 @@ msgstr "Editar perfil" #: bookwyrm/templates/preferences/edit_user.html:46 msgid "Show set reading goal prompt in feed:" -msgstr "" +msgstr "Mostrar meta de lectura en el feed:" #: bookwyrm/templates/preferences/edit_user.html:62 #, python-format msgid "Your account will show up in the directory, and may be recommended to other BookWyrm users." -msgstr "" +msgstr "Tu cuenta se aparecerá en el directorio, y puede ser recomendado a otros usuarios de BookWyrm." #: bookwyrm/templates/preferences/edit_user.html:65 msgid "Preferred Timezone: " @@ -1698,6 +1646,10 @@ msgstr "Huso horario preferido" msgid "Account" msgstr "Cuenta" +#: bookwyrm/templates/preferences/preferences_layout.html:14 +msgid "Profile" +msgstr "Perfil" + #: bookwyrm/templates/preferences/preferences_layout.html:20 msgid "Relationships" msgstr "Relaciones" @@ -1727,135 +1679,187 @@ msgstr "No se encontró ningúna lista correspondiente a \"%(query)s\"" msgid "Administration" msgstr "Adminstración" -#: bookwyrm/templates/settings/admin_layout.html:15 +#: bookwyrm/templates/settings/admin_layout.html:22 msgid "Manage Users" msgstr "Administrar usuarios" -#: bookwyrm/templates/settings/admin_layout.html:19 -#: bookwyrm/templates/settings/user_admin.html:3 -#: bookwyrm/templates/settings/user_admin.html:10 +#: bookwyrm/templates/settings/admin_layout.html:26 +#: bookwyrm/templates/user_admin/user_admin.html:3 +#: bookwyrm/templates/user_admin/user_admin.html:10 msgid "Users" -msgstr "" +msgstr "Usuarios" -#: bookwyrm/templates/settings/admin_layout.html:32 +#: bookwyrm/templates/settings/admin_layout.html:39 #: bookwyrm/templates/settings/federation.html:3 #: bookwyrm/templates/settings/federation.html:5 msgid "Federated Servers" msgstr "Servidores federalizados" -#: bookwyrm/templates/settings/admin_layout.html:37 +#: bookwyrm/templates/settings/admin_layout.html:44 msgid "Instance Settings" msgstr "Configuración de instancia" -#: bookwyrm/templates/settings/admin_layout.html:41 +#: bookwyrm/templates/settings/admin_layout.html:48 #: bookwyrm/templates/settings/site.html:4 #: bookwyrm/templates/settings/site.html:6 msgid "Site Settings" msgstr "Configuración de sitio" -#: bookwyrm/templates/settings/admin_layout.html:44 +#: bookwyrm/templates/settings/admin_layout.html:51 #: bookwyrm/templates/settings/site.html:13 msgid "Instance Info" msgstr "Información de instancia" -#: bookwyrm/templates/settings/admin_layout.html:45 +#: bookwyrm/templates/settings/admin_layout.html:52 #: bookwyrm/templates/settings/site.html:39 msgid "Images" msgstr "Imagenes" -#: bookwyrm/templates/settings/admin_layout.html:46 +#: bookwyrm/templates/settings/admin_layout.html:53 #: bookwyrm/templates/settings/site.html:59 msgid "Footer Content" msgstr "Contenido del pie de página" -#: bookwyrm/templates/settings/admin_layout.html:47 +#: bookwyrm/templates/settings/admin_layout.html:54 #: bookwyrm/templates/settings/site.html:77 msgid "Registration" msgstr "Registración" -#: bookwyrm/templates/settings/federated_server.html:7 -msgid "Back to server list" -msgstr "" +#: bookwyrm/templates/settings/edit_server.html:3 +#: bookwyrm/templates/settings/edit_server.html:6 +#: bookwyrm/templates/settings/edit_server.html:20 +#: bookwyrm/templates/settings/federation.html:9 +#: bookwyrm/templates/settings/federation.html:10 +#: bookwyrm/templates/settings/server_blocklist.html:3 +#: bookwyrm/templates/settings/server_blocklist.html:20 +msgid "Add server" +msgstr "Agregar servidor" +#: bookwyrm/templates/settings/edit_server.html:7 #: bookwyrm/templates/settings/federated_server.html:12 -msgid "Details" -msgstr "" +#: bookwyrm/templates/settings/server_blocklist.html:7 +msgid "Back to server list" +msgstr "Volver a la lista de servidores" -#: bookwyrm/templates/settings/federated_server.html:15 -#, fuzzy -#| msgid "Software" -msgid "Software:" -msgstr "Software" +#: bookwyrm/templates/settings/edit_server.html:16 +#: bookwyrm/templates/settings/server_blocklist.html:16 +msgid "Import block list" +msgstr "Importar lista de bloqueo" -#: bookwyrm/templates/settings/federated_server.html:19 -#, fuzzy -#| msgid "Description:" -msgid "Version:" -msgstr "Descripción:" +#: bookwyrm/templates/settings/edit_server.html:30 +msgid "Instance:" +msgstr "Instancia:" -#: bookwyrm/templates/settings/federated_server.html:23 -#, fuzzy -#| msgid "Status" +#: bookwyrm/templates/settings/edit_server.html:37 +#: bookwyrm/templates/settings/federated_server.html:29 +#: bookwyrm/templates/user_admin/user_info.html:34 msgid "Status:" -msgstr "Status" +msgstr "Status:" -#: bookwyrm/templates/settings/federated_server.html:30 +#: bookwyrm/templates/settings/edit_server.html:41 +#: bookwyrm/templates/settings/federated_server.html:9 +msgid "Blocked" +msgstr "Bloqueado" + +#: bookwyrm/templates/settings/edit_server.html:48 +#: bookwyrm/templates/settings/federated_server.html:21 +#: bookwyrm/templates/user_admin/user_info.html:26 +msgid "Software:" +msgstr "Software:" + +#: bookwyrm/templates/settings/edit_server.html:55 +#: bookwyrm/templates/settings/federated_server.html:25 +#: bookwyrm/templates/user_admin/user_info.html:30 +msgid "Version:" +msgstr "Versión:" + +#: bookwyrm/templates/settings/edit_server.html:64 +msgid "Notes:" +msgstr "Notas:" + +#: bookwyrm/templates/settings/federated_server.html:18 +msgid "Details" +msgstr "Detalles" + +#: bookwyrm/templates/settings/federated_server.html:36 #: bookwyrm/templates/user/user_layout.html:50 msgid "Activity" msgstr "Actividad" -#: bookwyrm/templates/settings/federated_server.html:33 -#, fuzzy -#| msgid "Username:" +#: bookwyrm/templates/settings/federated_server.html:39 msgid "Users:" -msgstr "Nombre de usuario:" +msgstr "Usuarios:" -#: bookwyrm/templates/settings/federated_server.html:36 -#: bookwyrm/templates/settings/federated_server.html:43 +#: bookwyrm/templates/settings/federated_server.html:42 +#: bookwyrm/templates/settings/federated_server.html:49 msgid "View all" -msgstr "" +msgstr "Ver todos" -#: bookwyrm/templates/settings/federated_server.html:40 -#, fuzzy -#| msgid "Recent Imports" +#: bookwyrm/templates/settings/federated_server.html:46 msgid "Reports:" -msgstr "Importaciones recientes" - -#: bookwyrm/templates/settings/federated_server.html:47 -#, fuzzy -#| msgid "followed you" -msgid "Followed by us:" -msgstr "te siguió" +msgstr "Informes:" #: bookwyrm/templates/settings/federated_server.html:53 -#, fuzzy -#| msgid "followed you" -msgid "Followed by them:" -msgstr "te siguió" +msgid "Followed by us:" +msgstr "Seguido por nosotros:" #: bookwyrm/templates/settings/federated_server.html:59 -#, fuzzy -#| msgid "Blocked Users" -msgid "Blocked by us:" -msgstr "Usuarios bloqueados" +msgid "Followed by them:" +msgstr "Seguido por ellos:" -#: bookwyrm/templates/settings/federation.html:13 +#: bookwyrm/templates/settings/federated_server.html:65 +msgid "Blocked by us:" +msgstr "Bloqueado por nosotros:" + +#: bookwyrm/templates/settings/federated_server.html:77 +#: bookwyrm/templates/user_admin/user_info.html:39 +msgid "Notes" +msgstr "Notas" + +#: bookwyrm/templates/settings/federated_server.html:80 +msgid "Edit" +msgstr "Editar" + +#: bookwyrm/templates/settings/federated_server.html:100 +#: bookwyrm/templates/user_admin/user_moderation_actions.html:3 +msgid "Actions" +msgstr "Acciones" + +#: bookwyrm/templates/settings/federated_server.html:104 +#: bookwyrm/templates/snippets/block_button.html:5 +msgid "Block" +msgstr "Bloquear" + +#: bookwyrm/templates/settings/federated_server.html:105 +msgid "All users from this instance will be deactivated." +msgstr "Todos los usuarios en esta instancia serán desactivados." + +#: bookwyrm/templates/settings/federated_server.html:110 +#: bookwyrm/templates/snippets/block_button.html:10 +msgid "Un-block" +msgstr "Desbloquear" + +#: bookwyrm/templates/settings/federated_server.html:111 +msgid "All users from this instance will be re-activated." +msgstr "Todos los usuarios en esta instancia serán re-activados." + +#: bookwyrm/templates/settings/federation.html:20 +#: bookwyrm/templates/user_admin/server_filter.html:5 msgid "Server name" msgstr "Nombre de servidor" -#: bookwyrm/templates/settings/federation.html:17 -#, fuzzy -#| msgid "Federated" +#: bookwyrm/templates/settings/federation.html:24 msgid "Date federated" -msgstr "Federalizado" +msgstr "Fecha de federalización" -#: bookwyrm/templates/settings/federation.html:21 +#: bookwyrm/templates/settings/federation.html:28 msgid "Software" msgstr "Software" -#: bookwyrm/templates/settings/federation.html:24 -#: bookwyrm/templates/settings/manage_invite_requests.html:33 -#: bookwyrm/templates/settings/user_admin.html:32 +#: bookwyrm/templates/settings/federation.html:31 +#: bookwyrm/templates/settings/manage_invite_requests.html:44 +#: bookwyrm/templates/settings/status_filter.html:5 +#: bookwyrm/templates/user_admin/user_admin.html:34 msgid "Status" msgstr "Status" @@ -1863,72 +1867,71 @@ msgstr "Status" #: bookwyrm/templates/settings/manage_invite_requests.html:11 #: bookwyrm/templates/settings/manage_invite_requests.html:25 #: bookwyrm/templates/settings/manage_invites.html:11 -#, fuzzy -#| msgid "Invites" msgid "Invite Requests" -msgstr "Invitaciones" +msgstr "Solicitudes de invitación" #: bookwyrm/templates/settings/manage_invite_requests.html:23 msgid "Ignored Invite Requests" -msgstr "" +msgstr "Solicitudes de invitación ignoradas" -#: bookwyrm/templates/settings/manage_invite_requests.html:31 -msgid "Date" -msgstr "" +#: bookwyrm/templates/settings/manage_invite_requests.html:35 +msgid "Date requested" +msgstr "Fecha solicitada" -#: bookwyrm/templates/settings/manage_invite_requests.html:32 +#: bookwyrm/templates/settings/manage_invite_requests.html:39 +msgid "Date accepted" +msgstr "Fecha de aceptación" + +#: bookwyrm/templates/settings/manage_invite_requests.html:42 msgid "Email" -msgstr "" - -#: bookwyrm/templates/settings/manage_invite_requests.html:34 -#, fuzzy -#| msgid "Notifications" -msgid "Action" -msgstr "Notificaciones" - -#: bookwyrm/templates/settings/manage_invite_requests.html:37 -#, fuzzy -#| msgid "Follow Requests" -msgid "No requests" -msgstr "Solicitudes de seguidor" - -#: bookwyrm/templates/settings/manage_invite_requests.html:45 -#, fuzzy -#| msgid "Accept" -msgid "Accepted" -msgstr "Aceptar" +msgstr "Correo electronico" #: bookwyrm/templates/settings/manage_invite_requests.html:47 -msgid "Sent" -msgstr "" +msgid "Action" +msgstr "Acción" -#: bookwyrm/templates/settings/manage_invite_requests.html:49 -msgid "Requested" -msgstr "" - -#: bookwyrm/templates/settings/manage_invite_requests.html:57 -msgid "Send invite" -msgstr "" +#: bookwyrm/templates/settings/manage_invite_requests.html:50 +msgid "No requests" +msgstr "No solicitudes" #: bookwyrm/templates/settings/manage_invite_requests.html:59 +#: bookwyrm/templates/settings/status_filter.html:16 +msgid "Accepted" +msgstr "Aceptado" + +#: bookwyrm/templates/settings/manage_invite_requests.html:61 +#: bookwyrm/templates/settings/status_filter.html:12 +msgid "Sent" +msgstr "Enviado" + +#: bookwyrm/templates/settings/manage_invite_requests.html:63 +#: bookwyrm/templates/settings/status_filter.html:8 +msgid "Requested" +msgstr "Solicitado" + +#: bookwyrm/templates/settings/manage_invite_requests.html:73 +msgid "Send invite" +msgstr "Enviar invitación" + +#: bookwyrm/templates/settings/manage_invite_requests.html:75 msgid "Re-send invite" -msgstr "" +msgstr "Re-enviar invitación" -#: bookwyrm/templates/settings/manage_invite_requests.html:70 +#: bookwyrm/templates/settings/manage_invite_requests.html:95 msgid "Ignore" -msgstr "" +msgstr "Ignorar" -#: bookwyrm/templates/settings/manage_invite_requests.html:72 -msgid "Un-gnore" -msgstr "" +#: bookwyrm/templates/settings/manage_invite_requests.html:97 +msgid "Un-ignore" +msgstr "Des-ignorar" -#: bookwyrm/templates/settings/manage_invite_requests.html:83 +#: bookwyrm/templates/settings/manage_invite_requests.html:108 msgid "Back to pending requests" -msgstr "" +msgstr "Volver a las solicitudes pendientes" -#: bookwyrm/templates/settings/manage_invite_requests.html:85 +#: bookwyrm/templates/settings/manage_invite_requests.html:110 msgid "View ignored requests" -msgstr "" +msgstr "Ver solicitudes ignoradas" #: bookwyrm/templates/settings/manage_invites.html:21 msgid "Generate New Invite" @@ -1966,6 +1969,23 @@ msgstr "Número de usos" msgid "No active invites" msgstr "No invitaciónes activas" +#: bookwyrm/templates/settings/server_blocklist.html:6 +msgid "Import Blocklist" +msgstr "Importar lista de bloqueo" + +#: bookwyrm/templates/settings/server_blocklist.html:26 +#: bookwyrm/templates/snippets/goal_progress.html:5 +msgid "Success!" +msgstr "¡Meta logrado!" + +#: bookwyrm/templates/settings/server_blocklist.html:30 +msgid "Successfully blocked:" +msgstr "Se bloqueó exitosamente:" + +#: bookwyrm/templates/settings/server_blocklist.html:32 +msgid "Failed:" +msgstr "Falló:" + #: bookwyrm/templates/settings/site.html:15 msgid "Instance Name:" msgstr "Nombre de instancia:" @@ -2015,73 +2035,27 @@ msgid "Allow registration:" msgstr "Permitir registración:" #: bookwyrm/templates/settings/site.html:83 -#, fuzzy -#| msgid "Follow Requests" msgid "Allow invite requests:" -msgstr "Solicitudes de seguidor" +msgstr "Permitir solicitudes de invitación:" #: bookwyrm/templates/settings/site.html:87 msgid "Registration closed text:" msgstr "Texto de registración cerrada:" -#: bookwyrm/templates/settings/user_admin.html:7 -#, python-format -msgid "Users: %(server_name)s" -msgstr "" +#: bookwyrm/templates/snippets/book_cover.html:20 +#: bookwyrm/templates/snippets/search_result_text.html:10 +msgid "No cover" +msgstr "Sin portada" -#: bookwyrm/templates/settings/user_admin.html:20 -#, fuzzy -#| msgid "Username:" -msgid "Username" -msgstr "Nombre de usuario:" - -#: bookwyrm/templates/settings/user_admin.html:24 -#, fuzzy -#| msgid "Added:" -msgid "Date Added" -msgstr "Agregado:" - -#: bookwyrm/templates/settings/user_admin.html:28 -msgid "Last Active" -msgstr "" - -#: bookwyrm/templates/settings/user_admin.html:36 -#, fuzzy -#| msgid "Remove" -msgid "Remote server" -msgstr "Quitar" - -#: bookwyrm/templates/settings/user_admin.html:45 -#, fuzzy -#| msgid "Activity" -msgid "Active" -msgstr "Actividad" - -#: bookwyrm/templates/settings/user_admin.html:45 -msgid "Inactive" -msgstr "" - -#: bookwyrm/templates/settings/user_admin.html:50 -msgid "Not set" -msgstr "" - -#: bookwyrm/templates/snippets/block_button.html:5 -msgid "Block" -msgstr "Bloquear" - -#: bookwyrm/templates/snippets/block_button.html:10 -msgid "Un-block" -msgstr "Desbloquear" - -#: bookwyrm/templates/snippets/book_titleby.html:3 +#: bookwyrm/templates/snippets/book_titleby.html:4 #, python-format msgid "%(title)s by " msgstr "%(title)s por " #: bookwyrm/templates/snippets/boost_button.html:8 #: bookwyrm/templates/snippets/boost_button.html:9 -#: bookwyrm/templates/snippets/status/status_body.html:51 -#: bookwyrm/templates/snippets/status/status_body.html:52 +#: bookwyrm/templates/snippets/status/layout.html:47 +#: bookwyrm/templates/snippets/status/layout.html:48 msgid "Boost status" msgstr "Status de respaldo" @@ -2094,82 +2068,72 @@ msgstr "Status de des-respaldo" msgid "Spoiler alert:" msgstr "Alerta de spoiler:" -#: bookwyrm/templates/snippets/content_warning_field.html:4 +#: bookwyrm/templates/snippets/content_warning_field.html:10 msgid "Spoilers ahead!" msgstr "¡Advertencia, ya vienen spoilers!" -#: bookwyrm/templates/snippets/create_status.html:9 +#: bookwyrm/templates/snippets/create_status.html:17 msgid "Review" msgstr "Reseña" -#: bookwyrm/templates/snippets/create_status.html:15 +#: bookwyrm/templates/snippets/create_status.html:39 msgid "Quote" msgstr "Cita" -#: bookwyrm/templates/snippets/create_status_form.html:18 -#, fuzzy -#| msgid "Comment" -msgid "Comment:" -msgstr "Comentario" - #: bookwyrm/templates/snippets/create_status_form.html:20 -#, fuzzy -#| msgid "Quote" -msgid "Quote:" -msgstr "Cita" +msgid "Comment:" +msgstr "Comentario:" #: bookwyrm/templates/snippets/create_status_form.html:22 -#, fuzzy -#| msgid "Review" +msgid "Quote:" +msgstr "Cita:" + +#: bookwyrm/templates/snippets/create_status_form.html:24 msgid "Review:" -msgstr "Reseña" +msgstr "Reseña:" -#: bookwyrm/templates/snippets/create_status_form.html:29 -#: bookwyrm/templates/user/shelf.html:81 -msgid "Rating" -msgstr "Calificación" +#: bookwyrm/templates/snippets/create_status_form.html:42 +#: bookwyrm/templates/snippets/status/layout.html:30 +#: bookwyrm/templates/snippets/status/layout.html:43 +#: bookwyrm/templates/snippets/status/layout.html:44 +msgid "Reply" +msgstr "Respuesta" -#: bookwyrm/templates/snippets/create_status_form.html:31 -#: bookwyrm/templates/snippets/rate_action.html:14 -#: bookwyrm/templates/snippets/stars.html:3 -msgid "No rating" -msgstr "No calificación" - -#: bookwyrm/templates/snippets/create_status_form.html:64 +#: bookwyrm/templates/snippets/create_status_form.html:67 #: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:16 msgid "Progress:" msgstr "Progreso:" -#: bookwyrm/templates/snippets/create_status_form.html:71 +#: bookwyrm/templates/snippets/create_status_form.html:75 #: bookwyrm/templates/snippets/readthrough_form.html:22 #: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:30 msgid "pages" msgstr "páginas" -#: bookwyrm/templates/snippets/create_status_form.html:72 +#: bookwyrm/templates/snippets/create_status_form.html:76 #: bookwyrm/templates/snippets/readthrough_form.html:23 #: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:31 msgid "percent" msgstr "por ciento" -#: bookwyrm/templates/snippets/create_status_form.html:77 +#: bookwyrm/templates/snippets/create_status_form.html:82 #: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:36 #, python-format msgid "of %(pages)s pages" msgstr "de %(pages)s páginas" -#: bookwyrm/templates/snippets/create_status_form.html:89 +#: bookwyrm/templates/snippets/create_status_form.html:97 msgid "Include spoiler alert" msgstr "Incluir alerta de spoiler" -#: bookwyrm/templates/snippets/create_status_form.html:95 +#: bookwyrm/templates/snippets/create_status_form.html:104 #: bookwyrm/templates/snippets/privacy-icons.html:15 #: bookwyrm/templates/snippets/privacy-icons.html:16 #: bookwyrm/templates/snippets/privacy_select.html:19 msgid "Private" msgstr "Privada" -#: bookwyrm/templates/snippets/create_status_form.html:102 +#: bookwyrm/templates/snippets/create_status_form.html:115 msgid "Post" msgstr "Compartir" @@ -2189,8 +2153,8 @@ msgstr "Eliminar" #: bookwyrm/templates/snippets/fav_button.html:7 #: bookwyrm/templates/snippets/fav_button.html:8 -#: bookwyrm/templates/snippets/status/status_body.html:55 -#: bookwyrm/templates/snippets/status/status_body.html:56 +#: bookwyrm/templates/snippets/status/layout.html:51 +#: bookwyrm/templates/snippets/status/layout.html:52 msgid "Like status" msgstr "Me gusta status" @@ -2200,34 +2164,28 @@ msgid "Un-like status" msgstr "Quitar me gusta de status" #: bookwyrm/templates/snippets/filters_panel/filters_panel.html:7 -#, fuzzy -#| msgid "Show less" msgid "Show filters" -msgstr "Mostrar menos" +msgstr "Mostrar filtros" #: bookwyrm/templates/snippets/filters_panel/filters_panel.html:9 msgid "Hide filters" -msgstr "" +msgstr "Ocultar filtros" -#: bookwyrm/templates/snippets/filters_panel/filters_panel.html:19 +#: bookwyrm/templates/snippets/filters_panel/filters_panel.html:22 msgid "Apply filters" -msgstr "" +msgstr "Aplicar filtros" -#: bookwyrm/templates/snippets/filters_panel/filters_panel.html:23 -#, fuzzy -#| msgid "Clear search" +#: bookwyrm/templates/snippets/filters_panel/filters_panel.html:26 msgid "Clear filters" -msgstr "Borrar búsqueda" +msgstr "Borrar filtros" #: bookwyrm/templates/snippets/follow_button.html:12 msgid "Follow" msgstr "Seguir" #: bookwyrm/templates/snippets/follow_button.html:18 -#, fuzzy -#| msgid "Send follow request" msgid "Undo follow request" -msgstr "Envia solicitud de seguidor" +msgstr "Des-enviar solicitud de seguidor" #: bookwyrm/templates/snippets/follow_button.html:20 msgid "Unfollow" @@ -2237,6 +2195,19 @@ msgstr "Dejar de seguir" msgid "Accept" msgstr "Aceptar" +#: bookwyrm/templates/snippets/form_rate_stars.html:20 +#: bookwyrm/templates/snippets/stars.html:13 +msgid "No rating" +msgstr "No calificación" + +#: bookwyrm/templates/snippets/form_rate_stars.html:45 +#: bookwyrm/templates/snippets/stars.html:7 +#, python-format +msgid "%(rating)s star" +msgid_plural "%(rating)s stars" +msgstr[0] "%(rating)s estrella" +msgstr[1] "%(rating)s estrellas" + #: bookwyrm/templates/snippets/generated_status/goal.html:1 #, python-format msgid "set a goal to read %(counter)s book in %(year)s" @@ -2245,24 +2216,23 @@ msgstr[0] "estableció una meta de leer %(counter)s libro en %(year)s" msgstr[1] "estableció una meta de leer %(counter)s libros en %(year)s" #: bookwyrm/templates/snippets/generated_status/rating.html:3 -#, fuzzy, python-format -#| msgid "%(title)s by " +#, python-format msgid "Rated %(title)s: %(display_rating)s star" msgid_plural "Rated %(title)s: %(display_rating)s stars" -msgstr[0] "%(title)s por " -msgstr[1] "%(title)s por " +msgstr[0] "Reseño %(title)s: %(display_rating)s estrella" +msgstr[1] "Reseño %(title)s: %(display_rating)s estrellas" #: bookwyrm/templates/snippets/generated_status/review_pure_name.html:4 #, python-format msgid "Review of \"%(book_title)s\" (%(display_rating)s star): %(review_title)s" msgid_plural "Review of \"%(book_title)s\" (%(display_rating)s stars): %(review_title)s" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "Reseña de \"%(book_title)s\" (%(display_rating)s estrella): %(review_title)s" +msgstr[1] "Reseña de \"%(book_title)s\" (%(display_rating)s estrellas): %(review_title)s" #: bookwyrm/templates/snippets/generated_status/review_pure_name.html:8 #, python-format msgid "Review of \"%(book_title)s\": %(review_title)s" -msgstr "" +msgstr "Reseña de \"%(book_title)s\": %(review_title)s" #: bookwyrm/templates/snippets/goal_card.html:23 #, python-format @@ -2292,10 +2262,6 @@ msgstr "Compartir con tu feed" msgid "Set goal" msgstr "Establecer meta" -#: bookwyrm/templates/snippets/goal_progress.html:5 -msgid "Success!" -msgstr "¡Meta logrado!" - #: bookwyrm/templates/snippets/goal_progress.html:7 #, python-format msgid "%(percent)s%% complete!" @@ -2311,11 +2277,20 @@ msgstr "Has leído %(read_count)s de %(goal_count)s libros< msgid "%(username)s has read %(read_count)s of %(goal_count)s books." msgstr "%(username)s ha leído %(read_count)s de %(goal_count)s libros." -#: bookwyrm/templates/snippets/pagination.html:7 +#: bookwyrm/templates/snippets/page_text.html:4 +#, python-format +msgid "page %(page)s of %(total_pages)s" +msgstr "página %(page)s de %(total_pages)s" + +#: bookwyrm/templates/snippets/page_text.html:6 +msgid "page %(page)s" +msgstr "página %(pages)s" + +#: bookwyrm/templates/snippets/pagination.html:12 msgid "Previous" msgstr "Anterior" -#: bookwyrm/templates/snippets/pagination.html:15 +#: bookwyrm/templates/snippets/pagination.html:23 msgid "Next" msgstr "Siguiente" @@ -2348,7 +2323,7 @@ msgstr "Seguidores" msgid "Leave a rating" msgstr "Da una calificación" -#: bookwyrm/templates/snippets/rate_action.html:29 +#: bookwyrm/templates/snippets/rate_action.html:19 msgid "Rate" msgstr "Calificar" @@ -2356,28 +2331,28 @@ msgstr "Calificar" msgid "Progress Updates:" msgstr "Actualizaciones de progreso:" -#: bookwyrm/templates/snippets/readthrough.html:12 +#: bookwyrm/templates/snippets/readthrough.html:14 msgid "finished" msgstr "terminado" -#: bookwyrm/templates/snippets/readthrough.html:15 +#: bookwyrm/templates/snippets/readthrough.html:25 msgid "Show all updates" msgstr "Mostrar todas las actualizaciones" -#: bookwyrm/templates/snippets/readthrough.html:31 +#: bookwyrm/templates/snippets/readthrough.html:41 msgid "Delete this progress update" msgstr "Eliminar esta actualización de progreso" -#: bookwyrm/templates/snippets/readthrough.html:41 +#: bookwyrm/templates/snippets/readthrough.html:51 msgid "started" msgstr "empezado" -#: bookwyrm/templates/snippets/readthrough.html:47 -#: bookwyrm/templates/snippets/readthrough.html:61 +#: bookwyrm/templates/snippets/readthrough.html:57 +#: bookwyrm/templates/snippets/readthrough.html:71 msgid "Edit read dates" msgstr "Editar fechas de lectura" -#: bookwyrm/templates/snippets/readthrough.html:51 +#: bookwyrm/templates/snippets/readthrough.html:61 msgid "Delete these read dates" msgstr "Eliminar estas fechas de lectura" @@ -2401,37 +2376,29 @@ msgid "Sign Up" msgstr "Inscribirse" #: bookwyrm/templates/snippets/report_button.html:5 -#, fuzzy -#| msgid "Import" msgid "Report" -msgstr "Importar" +msgstr "Reportar" #: bookwyrm/templates/snippets/rss_title.html:5 -#: bookwyrm/templates/snippets/status/status_header.html:11 +#: bookwyrm/templates/snippets/status/status_header.html:21 msgid "rated" msgstr "calificó" #: bookwyrm/templates/snippets/rss_title.html:7 -#: bookwyrm/templates/snippets/status/status_header.html:13 +#: bookwyrm/templates/snippets/status/status_header.html:23 msgid "reviewed" msgstr "reseñó" #: bookwyrm/templates/snippets/rss_title.html:9 -#: bookwyrm/templates/snippets/status/status_header.html:15 +#: bookwyrm/templates/snippets/status/status_header.html:25 msgid "commented on" msgstr "comentó en" #: bookwyrm/templates/snippets/rss_title.html:11 -#: bookwyrm/templates/snippets/status/status_header.html:17 +#: bookwyrm/templates/snippets/status/status_header.html:27 msgid "quoted" msgstr "citó" -#: bookwyrm/templates/snippets/search_result_text.html:10 -#, fuzzy -#| msgid "Add cover" -msgid "No cover" -msgstr "Agregar portada" - #: bookwyrm/templates/snippets/search_result_text.html:22 #, python-format msgid "by %(author)s" @@ -2442,10 +2409,8 @@ msgid "Import book" msgstr "Importar libro" #: bookwyrm/templates/snippets/shelf_selector.html:4 -#, fuzzy -#| msgid "Your books" msgid "Move book" -msgstr "Tus libros" +msgstr "Mover libro" #: bookwyrm/templates/snippets/shelve_button/finish_reading_modal.html:5 #, python-format @@ -2453,11 +2418,9 @@ msgid "Finish \"%(book_title)s\"" msgstr "Terminar \"%(book_title)s\"" #: bookwyrm/templates/snippets/shelve_button/progress_update_modal.html:5 -#: bookwyrm/templates/snippets/shelve_button/shelve_button_options.html:35 -#, fuzzy -#| msgid "Updates" +#: bookwyrm/templates/snippets/shelve_button/shelve_button_options.html:34 msgid "Update progress" -msgstr "Actualizaciones" +msgstr "Progreso de actualización" #: bookwyrm/templates/snippets/shelve_button/shelve_button_dropdown.html:5 msgid "More shelves" @@ -2476,11 +2439,10 @@ msgstr "Terminar de leer" msgid "Want to read" msgstr "Quiero leer" -#: bookwyrm/templates/snippets/shelve_button/shelve_button_options.html:48 -#, fuzzy, python-format -#| msgid "Lists: %(username)s" +#: bookwyrm/templates/snippets/shelve_button/shelve_button_options.html:45 +#, python-format msgid "Remove from %(name)s" -msgstr "Listas: %(username)s" +msgstr "Quitar de %(name)s" #: bookwyrm/templates/snippets/shelve_button/start_reading_modal.html:5 #, python-format @@ -2492,98 +2454,83 @@ msgstr "Empezar \"%(book_title)s\"" msgid "Want to Read \"%(book_title)s\"" msgstr "Quiero leer \"%(book_title)s\"" -#: bookwyrm/templates/snippets/status/status.html:9 -msgid "boosted" -msgstr "respaldó" +#: bookwyrm/templates/snippets/status/content_status.html:60 +#: bookwyrm/templates/snippets/status/status_content.html:50 +#: bookwyrm/templates/snippets/trimmed_text.html:14 +msgid "Show more" +msgstr "Mostrar más" -#: bookwyrm/templates/snippets/status/status_body.html:27 +#: bookwyrm/templates/snippets/status/content_status.html:75 +#: bookwyrm/templates/snippets/status/status_content.html:65 +#: bookwyrm/templates/snippets/trimmed_text.html:29 +msgid "Show less" +msgstr "Mostrar menos" + +#: bookwyrm/templates/snippets/status/content_status.html:105 +#: bookwyrm/templates/snippets/status/status_content.html:95 +msgid "Open image in new window" +msgstr "Abrir imagen en una nueva ventana" + +#: bookwyrm/templates/snippets/status/layout.html:22 #: bookwyrm/templates/snippets/status/status_options.html:18 msgid "Delete status" msgstr "Eliminar status" -#: bookwyrm/templates/snippets/status/status_body.html:34 -#: bookwyrm/templates/snippets/status/status_body.html:47 -#: bookwyrm/templates/snippets/status/status_body.html:48 -msgid "Reply" -msgstr "Respuesta" +#: bookwyrm/templates/snippets/status/status.html:9 +msgid "boosted" +msgstr "respaldó" -#: bookwyrm/templates/snippets/status/status_content.html:18 -#: bookwyrm/templates/snippets/trimmed_text.html:15 -msgid "Show more" -msgstr "Mostrar más" - -#: bookwyrm/templates/snippets/status/status_content.html:25 -#: bookwyrm/templates/snippets/trimmed_text.html:25 -msgid "Show less" -msgstr "Mostrar menos" - -#: bookwyrm/templates/snippets/status/status_content.html:46 -msgid "Open image in new window" -msgstr "Abrir imagen en una nueva ventana" - -#: bookwyrm/templates/snippets/status/status_header.html:22 -#, fuzzy, python-format -#| msgid "Added by %(username)s" +#: bookwyrm/templates/snippets/status/status_header.html:32 +#, python-format msgid "replied to %(username)s's review" -msgstr "Agregado por %(username)s" +msgstr "respondió a la reseña de %(username)s " -#: bookwyrm/templates/snippets/status/status_header.html:24 -#, fuzzy, python-format -#| msgid "replied to your status" +#: bookwyrm/templates/snippets/status/status_header.html:34 +#, python-format msgid "replied to %(username)s's comment" -msgstr "respondió a tu status" +msgstr "respondió al comentario de %(username)s " -#: bookwyrm/templates/snippets/status/status_header.html:26 -#, fuzzy, python-format -#| msgid "replied to your status" +#: bookwyrm/templates/snippets/status/status_header.html:36 +#, python-format msgid "replied to %(username)s's quote" -msgstr "respondió a tu status" +msgstr "respondió a la cita de %(username)s " -#: bookwyrm/templates/snippets/status/status_header.html:28 -#, fuzzy, python-format -#| msgid "replied to your status" +#: bookwyrm/templates/snippets/status/status_header.html:38 +#, python-format msgid "replied to %(username)s's status" -msgstr "respondió a tu status" +msgstr "respondió al status de %(username)s " #: bookwyrm/templates/snippets/status/status_options.html:7 #: bookwyrm/templates/snippets/user_options.html:7 msgid "More options" msgstr "Más opciones" +#: bookwyrm/templates/snippets/status/status_options.html:27 +msgid "Delete & re-draft" +msgstr "Eliminar y recomponer" + +#: bookwyrm/templates/snippets/status/status_options.html:36 +#: bookwyrm/templates/snippets/user_options.html:13 +#: bookwyrm/templates/user_admin/user_moderation_actions.html:6 +msgid "Send direct message" +msgstr "Enviar mensaje directo" + #: bookwyrm/templates/snippets/switch_edition_button.html:5 msgid "Switch to this edition" msgstr "Cambiar a esta edición" #: bookwyrm/templates/snippets/table-sort-header.html:6 -#, fuzzy -#| msgid "Started reading" msgid "Sorted ascending" -msgstr "Lectura se empezó" +msgstr "En orden ascendente" #: bookwyrm/templates/snippets/table-sort-header.html:10 -#, fuzzy -#| msgid "Started reading" msgid "Sorted descending" -msgstr "Lectura se empezó" - -#: bookwyrm/templates/snippets/tag.html:14 -msgid "Remove tag" -msgstr "Eliminar etiqueta" - -#: bookwyrm/templates/snippets/tag.html:18 -msgid "Add tag" -msgstr "Agregar etiqueta" - -#: bookwyrm/templates/tag.html:9 -#, python-format -msgid "Books tagged \"%(tag.name)s\"" -msgstr "Libros etiquetados con \"%(tag.name)s\"" +msgstr "En orden descendente" #: bookwyrm/templates/user/books_header.html:5 -#, fuzzy, python-format -#| msgid "%(username)s's %(year)s Books" +#, python-format msgid "%(username)s's books" -msgstr "Los libros de %(username)s para %(year)s" +msgstr "Los libros de %(username)s" #: bookwyrm/templates/user/create_shelf_form.html:5 #: bookwyrm/templates/user/create_shelf_form.html:22 @@ -2618,10 +2565,6 @@ msgstr "Siguiendo" msgid "%(username)s isn't following any users" msgstr "%(username)s no sigue a nadie" -#: bookwyrm/templates/user/lists.html:9 -msgid "Your Lists" -msgstr "Tus listas" - #: bookwyrm/templates/user/lists.html:11 #, python-format msgid "Lists: %(username)s" @@ -2631,11 +2574,9 @@ msgstr "Listas: %(username)s" msgid "Create list" msgstr "Crear lista" -#: bookwyrm/templates/user/shelf.html:24 bookwyrm/views/shelf.py:56 -#, fuzzy -#| msgid "books" +#: bookwyrm/templates/user/shelf.html:24 bookwyrm/views/shelf.py:51 msgid "All books" -msgstr "libros" +msgstr "Todos los libros" #: bookwyrm/templates/user/shelf.html:37 msgid "Create shelf" @@ -2657,11 +2598,11 @@ msgstr "Empezado" msgid "Finished" msgstr "Terminado" -#: bookwyrm/templates/user/shelf.html:127 +#: bookwyrm/templates/user/shelf.html:129 msgid "This shelf is empty." msgstr "Este estante está vacio." -#: bookwyrm/templates/user/shelf.html:133 +#: bookwyrm/templates/user/shelf.html:135 msgid "Delete shelf" msgstr "Eliminar estante" @@ -2670,14 +2611,13 @@ msgid "Edit profile" msgstr "Editar perfil" #: bookwyrm/templates/user/user.html:34 -#, fuzzy, python-format -#| msgid "See all %(size)s" +#, python-format msgid "View all %(size)s" -msgstr "Ver %(size)s" +msgstr "Ver todos los %(size)s" #: bookwyrm/templates/user/user.html:47 msgid "View all books" -msgstr "" +msgstr "Ver todos los libros" #: bookwyrm/templates/user/user.html:59 #, python-format @@ -2705,10 +2645,8 @@ msgid "Reading Goal" msgstr "Meta de lectura" #: bookwyrm/templates/user/user_layout.html:68 -#, fuzzy -#| msgid "Book" msgid "Books" -msgstr "Libro" +msgstr "Libros" #: bookwyrm/templates/user/user_preview.html:13 #, python-format @@ -2727,23 +2665,964 @@ msgstr[1] "%(counter)s seguidores" msgid "%(counter)s following" msgstr "%(counter)s siguiendo" +#: bookwyrm/templates/user_admin/user.html:11 +msgid "Back to users" +msgstr "Volver a usuarios" + +#: bookwyrm/templates/user_admin/user_admin.html:7 +#, python-format +msgid "Users: %(server_name)s" +msgstr "Usuarios %(server_name)s" + +#: bookwyrm/templates/user_admin/user_admin.html:22 +#: bookwyrm/templates/user_admin/username_filter.html:5 +msgid "Username" +msgstr "Nombre de usuario" + +#: bookwyrm/templates/user_admin/user_admin.html:26 +msgid "Date Added" +msgstr "Fecha agregada" + +#: bookwyrm/templates/user_admin/user_admin.html:30 +msgid "Last Active" +msgstr "Actividad reciente" + +#: bookwyrm/templates/user_admin/user_admin.html:38 +msgid "Remote server" +msgstr "Quitar servidor" + +#: bookwyrm/templates/user_admin/user_admin.html:47 +msgid "Active" +msgstr "Activ@" + +#: bookwyrm/templates/user_admin/user_admin.html:47 +msgid "Inactive" +msgstr "Inactiv@" + +#: bookwyrm/templates/user_admin/user_admin.html:52 +#: bookwyrm/templates/user_admin/user_info.html:49 +msgid "Not set" +msgstr "No establecido" + +#: bookwyrm/templates/user_admin/user_info.html:5 +msgid "User details" +msgstr "Detalles" + +#: bookwyrm/templates/user_admin/user_info.html:14 +msgid "View user profile" +msgstr "Ver perfil de usuario" + +#: bookwyrm/templates/user_admin/user_info.html:20 +msgid "Instance details" +msgstr "Detalles de instancia" + +#: bookwyrm/templates/user_admin/user_info.html:46 +msgid "View instance" +msgstr "Ver instancia" + +#: bookwyrm/templates/user_admin/user_moderation_actions.html:11 +msgid "Suspend user" +msgstr "Suspender usuario" + +#: bookwyrm/templates/user_admin/user_moderation_actions.html:13 +msgid "Un-suspend user" +msgstr "Des-suspender usuario" + +#: bookwyrm/templates/user_admin/user_moderation_actions.html:21 +msgid "Access level:" +msgstr "Nivel de acceso:" + #: bookwyrm/views/password.py:32 -#, fuzzy -#| msgid "A user with that username already exists." msgid "No user with that email address was found." -msgstr "Ya existe un usuario con ese nombre." +msgstr "No se pudo encontrar un usuario con esa dirección de correo electrónico." #: bookwyrm/views/password.py:41 #, python-format msgid "A password reset link sent to %s" -msgstr "" +msgstr "Un enlace para reestablecer tu contraseña se enviará a %s" + +#~ msgid "Remove tag" +#~ msgstr "Eliminar etiqueta" + +#~ msgid "Add tag" +#~ msgstr "Agregar etiqueta" + +#~ msgid "Books tagged \"%(tag.name)s\"" +#~ msgstr "Libros etiquetados con \"%(tag.name)s\"" + +#~ msgid "Deactivate user" +#~ msgstr "Desactivar usuario" + +#~ msgid "Reactivate user" +#~ msgstr "Reactivar usuario" + +#~ msgid "ambiguous option: %(option)s could match %(matches)s" +#~ msgstr "opción ambiguo: %(option)s pudiera coincidir con %(matches)s" + +#~ msgid "Messages" +#~ msgstr "Mensajes" + +#~ msgid "Site Maps" +#~ msgstr "Mapas de sitio" + +#~ msgid "Static Files" +#~ msgstr "Archivos estáticos" + +#~ msgid "Syndication" +#~ msgstr "Sindicación" + +#~ msgid "That page number is not an integer" +#~ msgstr "Ese numero de pagina no es un entero" + +#~ msgid "That page number is less than 1" +#~ msgstr "Ese numero de pagina es menos que uno" + +#~ msgid "That page contains no results" +#~ msgstr "Esa pagina no contiene resultados" + +#~ msgid "Enter a valid value." +#~ msgstr "Ingrese un valor válido." + +#~ msgid "Enter a valid URL." +#~ msgstr "Ingrese una URL válida." + +#~ msgid "Enter a valid integer." +#~ msgstr "Ingrese un entero válido." + +#~ msgid "Enter a valid email address." +#~ msgstr "Ingrese una dirección de correo electrónico válida." + +#~ msgid "Enter a valid “slug” consisting of letters, numbers, underscores or hyphens." +#~ msgstr "Ingrese un “slug” válido que consiste de letras, numeros, guiones bajos, o guiones" + +#~ msgid "Enter a valid “slug” consisting of Unicode letters, numbers, underscores, or hyphens." +#~ msgstr "Ingrese un “slug” válido que consiste de letras Unicode, numeros, guiones bajos, o guiones" + +#~ msgid "Enter a valid IPv4 address." +#~ msgstr "Ingrese una dirección IPv4 válida." + +#~ msgid "Enter a valid IPv6 address." +#~ msgstr "Ingrese una dirección IPv6 válida." + +#~ msgid "Enter a valid IPv4 or IPv6 address." +#~ msgstr "Ingrese una dirección IPv4 o IPv6 válida." + +#~ msgid "Enter only digits separated by commas." +#~ msgstr "Ingrese solo digitos separados por comas." + +#~ msgid "Ensure this value is %(limit_value)s (it is %(show_value)s)." +#~ msgstr "Asegura que este valor es %(limit_value)s (es %(show_value)s)." + +#~ msgid "Ensure this value is less than or equal to %(limit_value)s." +#~ msgstr "Asegura que este valor es menor que o iguala a %(limit_value)s." + +#~ msgid "Ensure this value is greater than or equal to %(limit_value)s." +#~ msgstr "Asegura que este valor es más que o que iguala a %(limit_value)s." + +#~ msgid "Ensure this value has at least %(limit_value)d character (it has %(show_value)d)." +#~ msgid_plural "Ensure this value has at least %(limit_value)d characters (it has %(show_value)d)." +#~ msgstr[0] "Verifica que este valor tiene por lo menos %(limit_value)d carácter. (Tiene %(show_value)d).)" +#~ msgstr[1] "Verifica que este valor tiene por lo menos %(limit_value)d caracteres. (Tiene %(show_value)d).)" + +#~ msgid "Ensure this value has at most %(limit_value)d character (it has %(show_value)d)." +#~ msgid_plural "Ensure this value has at most %(limit_value)d characters (it has %(show_value)d)." +#~ msgstr[0] "Verifica que este valor tiene a lo sumo %(limit_value)d carácter. (Tiene %(show_value)d).)" +#~ msgstr[1] "Verifica que este valor tiene a lo sumo %(limit_value)d caracteres. (Tiene %(show_value)d).)" + +#~ msgid "Enter a number." +#~ msgstr "Ingrese un número." + +#~ msgid "Ensure that there are no more than %(max)s digit in total." +#~ msgid_plural "Ensure that there are no more than %(max)s digits in total." +#~ msgstr[0] "Verifica que no hay más que %(max)s digito en total." +#~ msgstr[1] "Verifica que no hay más que %(max)s digitos en total." + +# is +#~ msgid "Ensure that there are no more than %(max)s decimal place." +#~ msgid_plural "Ensure that there are no more than %(max)s decimal places." +#~ msgstr[0] "Verifica que no hay más que %(max)s cifra decimal." +#~ msgstr[1] "Verifica que no hay más que %(max)s cifras decimales." + +#~ msgid "Ensure that there are no more than %(max)s digit before the decimal point." +#~ msgid_plural "Ensure that there are no more than %(max)s digits before the decimal point." +#~ msgstr[0] "Verifica que no hay más que %(max)s digito antes de la coma decimal." +#~ msgstr[1] "Verifica que no hay más que %(max)s digitos antes de la coma decimal." + +#~ msgid "File extension “%(extension)s” is not allowed. Allowed extensions are: %(allowed_extensions)s." +#~ msgstr "No se permite la extensión de archivo “%(extension)s”. Extensiones permitidas son: %(allowed_extensions)s." + +#~ msgid "Null characters are not allowed." +#~ msgstr "No se permiten caracteres nulos" + +#~ msgid "and" +#~ msgstr "y" + +#~ msgid "%(model_name)s with this %(field_labels)s already exists." +#~ msgstr "Ya existe %(model_name)s con este %(field_labels)s." + +#~ msgid "Value %(value)r is not a valid choice." +#~ msgstr "El valor %(value)s no es una opción válida." + +#~ msgid "This field cannot be null." +#~ msgstr "Este campo no puede ser nulo." + +#~ msgid "This field cannot be blank." +#~ msgstr "Este campo no puede ser vacio." + +#~ msgid "%(model_name)s with this %(field_label)s already exists." +#~ msgstr "Ya existe %(model_name)s con este %(field_labels)s." + +#~ msgid "%(field_label)s must be unique for %(date_field_label)s %(lookup_type)s." +#~ msgstr "%(field_label)s deben ser unicos por %(date_field_label)s %(lookup_type)s." + +#~ msgid "Field of type: %(field_type)s" +#~ msgstr "Campo de tipo: %(field_type)s" + +#~ msgid "“%(value)s” value must be either True or False." +#~ msgstr "“%(value)s” valor debe ser o verdadero o falso." + +#~ msgid "“%(value)s” value must be either True, False, or None." +#~ msgstr "%(value)s” valor debe ser o True, False, o None." + +#~ msgid "Boolean (Either True or False)" +#~ msgstr "Booleano (O True O False)" + +#~ msgid "String (up to %(max_length)s)" +#~ msgstr "Cadena (máximo de %(max_length)s caracteres)" + +#~ msgid "Comma-separated integers" +#~ msgstr "Enteros separados por comas" + +#~ msgid "“%(value)s” value has an invalid date format. It must be in YYYY-MM-DD format." +#~ msgstr "“%(value)s” valor tiene un formato de fecha inválido. Hay que estar de formato YYYY-MM-DD." + +#~ msgid "“%(value)s” value has the correct format (YYYY-MM-DD) but it is an invalid date." +#~ msgstr "“%(value)s” valor tiene el formato correcto (YYYY-MM-DD) pero la fecha es invalida." + +#~ msgid "Date (without time)" +#~ msgstr "Fecha (sin la hora)" + +#~ msgid "“%(value)s” value has an invalid format. It must be in YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ] format." +#~ msgstr "“%(value)s” valor tiene un formato invalido. Debe estar en formato YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ]." + +#~ msgid "“%(value)s” value has the correct format (YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ]) but it is an invalid date/time." +#~ msgstr "“%(value)s” valor tiene el formato correcto (YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ]) pero es una fecha/hora invalida." + +#~ msgid "Date (with time)" +#~ msgstr "Fecha (con la hora)" + +#~ msgid "“%(value)s” value must be a decimal number." +#~ msgstr "El valor de “%(value)s” debe ser un número decimal." + +#~ msgid "Decimal number" +#~ msgstr "Número decimal" + +#~ msgid "“%(value)s” value has an invalid format. It must be in [DD] [[HH:]MM:]ss[.uuuuuu] format." +#~ msgstr "“%(value)s” valor tiene un formato invalido. Debe estar en formato [DD] [[HH:]MM:]ss[.uuuuuu]." + +#~ msgid "Duration" +#~ msgstr "Duración" + +#~ msgid "Email address" +#~ msgstr "Dirección de correo electrónico" + +#~ msgid "File path" +#~ msgstr "Ruta de archivo" + +#~ msgid "“%(value)s” value must be a float." +#~ msgstr "%(value)s no es un usuario válido" + +#~ msgid "Floating point number" +#~ msgstr "Número de coma flotante" + +#~ msgid "“%(value)s” value must be an integer." +#~ msgstr "“%(value)s” valor debe ser un entero." + +#~ msgid "Integer" +#~ msgstr "Entero" + +#~ msgid "Big (8 byte) integer" +#~ msgstr "Entero grande (8 byte)" + +#~ msgid "IPv4 address" +#~ msgstr "Dirección IPv4" + +#~ msgid "IP address" +#~ msgstr "Dirección IP" + +#~ msgid "“%(value)s” value must be either None, True or False." +#~ msgstr "Valor “%(value)s” debe ser o None, True, o False." + +#~ msgid "Boolean (Either True, False or None)" +#~ msgstr "Booleano (O True, Falso, o None)" + +#~ msgid "Positive big integer" +#~ msgstr "Entero positivo grande" + +#~ msgid "Positive integer" +#~ msgstr "Entero positivo" + +#~ msgid "Positive small integer" +#~ msgstr "Entero positivo pequeño " + +#~ msgid "Slug (up to %(max_length)s)" +#~ msgstr "Slug (máximo de %(max_length)s)" + +#~ msgid "Small integer" +#~ msgstr "Entero pequeño" + +#~ msgid "Text" +#~ msgstr "Texto" + +#~ msgid "“%(value)s” value has an invalid format. It must be in HH:MM[:ss[.uuuuuu]] format." +#~ msgstr "“%(value)s” valor tiene un formato invalido. Debe estar en formato HH:MM[:ss[.uuuuuu]]." + +#~ msgid "“%(value)s” value has the correct format (HH:MM[:ss[.uuuuuu]]) but it is an invalid time." +#~ msgstr "“%(value)s” valor tiene el formato correcto (HH:MM[:ss[.uuuuuu]]) pero es una hora invalida." + +#~ msgid "Time" +#~ msgstr "Tiempo" + +#~ msgid "URL" +#~ msgstr "URL" + +#~ msgid "Raw binary data" +#~ msgstr "Datos binarios sin procesar" + +#~ msgid "“%(value)s” is not a valid UUID." +#~ msgstr "%(value)s no es una UUID válida." + +#~ msgid "Universally unique identifier" +#~ msgstr "Identificador universalmente único" + +#~ msgid "File" +#~ msgstr "Archivo" + +#~ msgid "Image" +#~ msgstr "Imágen" + +#~ msgid "A JSON object" +#~ msgstr "Un objeto JSON" + +#~ msgid "Value must be valid JSON." +#~ msgstr "Valor debe ser JSON válido." + +#~ msgid "%(model)s instance with %(field)s %(value)r does not exist." +#~ msgstr "%(model)s instancia con %(field)s %(value)r no existe." + +#~ msgid "Foreign Key (type determined by related field)" +#~ msgstr "Clave externa (tipo determinado por campo relacionado)" + +#~ msgid "One-to-one relationship" +#~ msgstr "Relación uno-a-uno" + +#~ msgid "%(from)s-%(to)s relationship" +#~ msgstr "relación %(from)s-%(to)s" + +#~ msgid "%(from)s-%(to)s relationships" +#~ msgstr "relaciones %(from)s-%(to)s" + +#~ msgid "Many-to-many relationship" +#~ msgstr "Relaciones mucho-a-mucho" + +#~ msgid "This field is required." +#~ msgstr "Este campo es requerido." + +#~ msgid "Enter a whole number." +#~ msgstr "Ingrese un número entero." + +#~ msgid "Enter a valid date." +#~ msgstr "Ingrese una fecha válida." + +#~ msgid "Enter a valid time." +#~ msgstr "Ingrese una hora válida." + +#~ msgid "Enter a valid date/time." +#~ msgstr "Ingrese una fecha/hora válida." + +#~ msgid "Enter a valid duration." +#~ msgstr "Ingrese una duración válida." + +#~ msgid "The number of days must be between {min_days} and {max_days}." +#~ msgstr "El número de dias debe ser entre {min_days} y {max_days}." + +#~ msgid "No file was submitted. Check the encoding type on the form." +#~ msgstr "No se aceptó ningun archivo. Verfica el tipo de codificación en el formulario." + +#~ msgid "No file was submitted." +#~ msgstr "No se aceptó ningun archivo." + +#~ msgid "The submitted file is empty." +#~ msgstr "El archivo enviado está vacio." + +#~ msgid "Ensure this filename has at most %(max)d character (it has %(length)d)." +#~ msgid_plural "Ensure this filename has at most %(max)d characters (it has %(length)d)." +#~ msgstr[0] "Verifica que este nombre de archivo no tiene más que %(max)d carácter. (Tiene %(length)d)." +#~ msgstr[1] "Verifica que este nombre de archivo no tiene más que %(max)d caracteres. (Tiene %(length)d)." + +#~ msgid "Please either submit a file or check the clear checkbox, not both." +#~ msgstr "Por favor, o envia un archivo o marca la casilla vacia, no los dos." + +#~ msgid "Upload a valid image. The file you uploaded was either not an image or a corrupted image." +#~ msgstr "Subir una imagen válida. El archivo que subiste o no fue imagen o fue corrupto." + +#~ msgid "Select a valid choice. %(value)s is not one of the available choices." +#~ msgstr "Selecciona una opción válida. %(value)s no es una de las opciones disponibles." + +#~ msgid "Enter a list of values." +#~ msgstr "Ingrese una lista de valores." + +#~ msgid "Enter a complete value." +#~ msgstr "Ingresa un valor completo." + +#~ msgid "Enter a valid UUID." +#~ msgstr "Ingrese una UUID válida." + +#~ msgid "Enter a valid JSON." +#~ msgstr "Ingrese una JSON válida." + +#~ msgid ":" +#~ msgstr ":" + +#~ msgid "(Hidden field %(name)s) %(error)s" +#~ msgstr "(Campo oculto %(name)s) %(error)s" + +#~ msgid "ManagementForm data is missing or has been tampered with" +#~ msgstr "Datos de ManagementForm está ausento o ha sido corrompido" + +#~ msgid "Please submit %d or fewer forms." +#~ msgid_plural "Please submit %d or fewer forms." +#~ msgstr[0] "Por favor, enviar %d o menos formularios." +#~ msgstr[1] "Por favor, enviar %d o menos formularios." + +#~ msgid "Please submit %d or more forms." +#~ msgid_plural "Please submit %d or more forms." +#~ msgstr[0] "Por favor, enviar %d o más formularios." +#~ msgstr[1] "Por favor, enviar %d o más formularios." + +# TODO cc @mouse is this a verb or noun +#, fuzzy +#~ msgid "Order" +#~ msgstr "Pedir" + +# if verb +# msgstr "Pedido" # if noun +#~ msgid "Please correct the duplicate data for %(field)s." +#~ msgstr "Por favor corrige los datos duplicados en %(field)s." + +#~ msgid "Please correct the duplicate data for %(field)s, which must be unique." +#~ msgstr "Por favor corrige los datos duplicados en %(field)s, los cuales deben ser unicos." + +#~ msgid "Please correct the duplicate data for %(field_name)s which must be unique for the %(lookup)s in %(date_field)s." +#~ msgstr "Por favor corrige los datos duplicados en %(field_name)s los cuales deben ser unicos por el %(lookup)s en %(date_field)s." + +#~ msgid "Please correct the duplicate values below." +#~ msgstr "Por favor corrige los valores duplicados a continuación." + +#~ msgid "The inline value did not match the parent instance." +#~ msgstr "El valor en línea no empareja la instancia progenitor." + +#~ msgid "Select a valid choice. That choice is not one of the available choices." +#~ msgstr "Selecciona una opción válida. Esa opción no es una de las opciones disponibles." + +#~ msgid "“%(pk)s” is not a valid value." +#~ msgstr "“%(pk)s” no es un valor válido." + +#~ msgid "%(datetime)s couldn’t be interpreted in time zone %(current_timezone)s; it may be ambiguous or it may not exist." +#~ msgstr "%(datetime)s no se pudo interpretar en la zona horaria %(current_timezone)s; puede ser ambiguo o puede que no exista." + +#~ msgid "Clear" +#~ msgstr "Borrar" + +#~ msgid "Currently" +#~ msgstr "Actualmente" + +#~ msgid "Change" +#~ msgstr "Cambiar" + +#~ msgid "Unknown" +#~ msgstr "Desconocido" + +#~ msgid "Yes" +#~ msgstr "Sí" + +#~ msgid "No" +#~ msgstr "No" + +#~ msgid "yes,no,maybe" +#~ msgstr "sí,no,quizás" + +#~ msgid "%(size)d byte" +#~ msgid_plural "%(size)d bytes" +#~ msgstr[0] "%(size)d byte" +#~ msgstr[1] "%(size)d bytes" + +#~ msgid "%s KB" +#~ msgstr "%s KB" + +#~ msgid "%s MB" +#~ msgstr "%s MB" + +#~ msgid "%s GB" +#~ msgstr "%s GB" + +#~ msgid "%s TB" +#~ msgstr "%s TB" + +#~ msgid "%s PB" +#~ msgstr "%s PB" + +#~ msgid "p.m." +#~ msgstr "p.m." + +#~ msgid "a.m." +#~ msgstr "a.m." + +#~ msgid "PM" +#~ msgstr "PM" + +#~ msgid "AM" +#~ msgstr "AM" + +#~ msgid "midnight" +#~ msgstr "medianoche" + +#~ msgid "noon" +#~ msgstr "mediodia" + +#~ msgid "Monday" +#~ msgstr "Lunes" + +#~ msgid "Tuesday" +#~ msgstr "Martes" + +#~ msgid "Wednesday" +#~ msgstr "Miercoles" + +#~ msgid "Thursday" +#~ msgstr "Jueves" + +#~ msgid "Friday" +#~ msgstr "Viernes" + +#~ msgid "Saturday" +#~ msgstr "Sábado" + +#~ msgid "Sunday" +#~ msgstr "Domino" + +#~ msgid "Mon" +#~ msgstr "Lun" + +#~ msgid "Tue" +#~ msgstr "Mar" + +#~ msgid "Wed" +#~ msgstr "Mie" + +#~ msgid "Thu" +#~ msgstr "Jue" + +#~ msgid "Fri" +#~ msgstr "Vie" + +#~ msgid "Sat" +#~ msgstr "Sáb" + +#~ msgid "Sun" +#~ msgstr "Dom" + +#~ msgid "January" +#~ msgstr "Enero" + +#~ msgid "February" +#~ msgstr "Febrero" + +#~ msgid "March" +#~ msgstr "Marzo" + +#~ msgid "April" +#~ msgstr "Abril" + +#~ msgid "May" +#~ msgstr "Mayo" + +#~ msgid "June" +#~ msgstr "Junio" + +#~ msgid "July" +#~ msgstr "Julio" + +#~ msgid "August" +#~ msgstr "Agosto" + +#~ msgid "September" +#~ msgstr "Septiembre" + +#~ msgid "October" +#~ msgstr "Octubre" + +#~ msgid "November" +#~ msgstr "Noviembre" + +#~ msgid "December" +#~ msgstr "Diciembre" + +#~ msgid "jan" +#~ msgstr "ene" + +#~ msgid "feb" +#~ msgstr "feb" + +#~ msgid "mar" +#~ msgstr "mar" + +#~ msgid "apr" +#~ msgstr "abr" + +#~ msgid "may" +#~ msgstr "may" + +#~ msgid "jun" +#~ msgstr "jun" + +#~ msgid "jul" +#~ msgstr "jul" + +#~ msgid "aug" +#~ msgstr "ago" + +#~ msgid "sep" +#~ msgstr "sep" + +#~ msgid "oct" +#~ msgstr "oct" + +#~ msgid "nov" +#~ msgstr "nov" + +#~ msgid "dec" +#~ msgstr "dic" + +#~ msgctxt "abbrev. month" +#~ msgid "Jan." +#~ msgstr "en." + +#~ msgctxt "abbrev. month" +#~ msgid "Feb." +#~ msgstr "feb." + +#~ msgctxt "abbrev. month" +#~ msgid "March" +#~ msgstr "mzo." + +#~ msgctxt "abbrev. month" +#~ msgid "April" +#~ msgstr "abr." + +#~ msgctxt "abbrev. month" +#~ msgid "May" +#~ msgstr "my." + +#~ msgctxt "abbrev. month" +#~ msgid "June" +#~ msgstr "jun." + +#~ msgctxt "abbrev. month" +#~ msgid "July" +#~ msgstr "jul." + +#~ msgctxt "abbrev. month" +#~ msgid "Aug." +#~ msgstr "agto." + +#~ msgctxt "abbrev. month" +#~ msgid "Sept." +#~ msgstr "set." + +#~ msgctxt "abbrev. month" +#~ msgid "Oct." +#~ msgstr "oct." + +#~ msgctxt "abbrev. month" +#~ msgid "Nov." +#~ msgstr "nov." + +#~ msgctxt "abbrev. month" +#~ msgid "Dec." +#~ msgstr "dic." + +#~ msgctxt "alt. month" +#~ msgid "January" +#~ msgstr "Enero" + +#~ msgctxt "alt. month" +#~ msgid "February" +#~ msgstr "Febrero" + +#~ msgctxt "alt. month" +#~ msgid "March" +#~ msgstr "Marzo" + +#~ msgctxt "alt. month" +#~ msgid "April" +#~ msgstr "Abril" + +#~ msgctxt "alt. month" +#~ msgid "May" +#~ msgstr "Mayo" + +#~ msgctxt "alt. month" +#~ msgid "June" +#~ msgstr "Junio" + +#~ msgctxt "alt. month" +#~ msgid "July" +#~ msgstr "Julio" + +#~ msgctxt "alt. month" +#~ msgid "August" +#~ msgstr "Agosto" + +#~ msgctxt "alt. month" +#~ msgid "September" +#~ msgstr "Septiembre" + +#~ msgctxt "alt. month" +#~ msgid "October" +#~ msgstr "Octubre" + +#~ msgctxt "alt. month" +#~ msgid "November" +#~ msgstr "Noviembre" + +#~ msgctxt "alt. month" +#~ msgid "December" +#~ msgstr "Diciembre" + +#~ msgid "This is not a valid IPv6 address." +#~ msgstr "Esta no es una dirección IPv6 válida." + +#~ msgctxt "String to return when truncating text" +#~ msgid "%(truncated_text)s…" +#~ msgstr "%(truncated_text)s…" + +#~ msgid "or" +#~ msgstr "o" + +#~ msgid ", " +#~ msgstr ", " + +#~ msgid "%d year" +#~ msgid_plural "%d years" +#~ msgstr[0] "%d año" +#~ msgstr[1] "%d años" + +#~ msgid "%d month" +#~ msgid_plural "%d months" +#~ msgstr[0] "%d mes" +#~ msgstr[1] "%d meses" + +#~ msgid "%d week" +#~ msgid_plural "%d weeks" +#~ msgstr[0] "%d semana" +#~ msgstr[1] "%d semanas" + +#~ msgid "%d day" +#~ msgid_plural "%d days" +#~ msgstr[0] "%d día" +#~ msgstr[1] "%d días" + +#~ msgid "%d hour" +#~ msgid_plural "%d hours" +#~ msgstr[0] "%d hora" +#~ msgstr[1] "%d horas" + +#~ msgid "%d minute" +#~ msgid_plural "%d minutes" +#~ msgstr[0] "%d minuto" +#~ msgstr[1] "%d minutos" + +#~ msgid "Forbidden" +#~ msgstr "Prohibido" + +#~ msgid "CSRF verification failed. Request aborted." +#~ msgstr "Se falló la verificación CSRF. Se abortó la solicitud." + +#~ msgid "You are seeing this message because this HTTPS site requires a “Referer header” to be sent by your Web browser, but none was sent. This header is required for security reasons, to ensure that your browser is not being hijacked by third parties." +#~ msgstr "Estás viendo este mensaje porque este sitio HTTPS requiere que tu navegador Web envie un “Referer header”, pero no se la envió. Esta cabecedera se requiere por razones de seguridad, para asegurar que tu navegador no sea secuestrado por terceros." + +#~ msgid "If you have configured your browser to disable “Referer” headers, please re-enable them, at least for this site, or for HTTPS connections, or for “same-origin” requests." +#~ msgstr "Si has configurado su navegador para deshabilitar las cabecederas “Referer”, vuelva a habilitarlos, al menos para este sitio, o para conexiones HTTPS, o para solicitudes del “same-origin”. " + +#~ msgid "If you are using the tag or including the “Referrer-Policy: no-referrer” header, please remove them. The CSRF protection requires the “Referer” header to do strict referer checking. If you’re concerned about privacy, use alternatives like for links to third-party sites." +#~ msgstr "Si estás usando la eqtigueta o estás incluyendo la cabecedera “Referrer-Policy: no-referrer”, quitalas por favor. La protección CSRF require la cabecedera “Referer” para hacer verficación “strict referer“. Si te preocupa la privacidad, utiliza alternativas como para sitios de terceros." + +#~ msgid "You are seeing this message because this site requires a CSRF cookie when submitting forms. This cookie is required for security reasons, to ensure that your browser is not being hijacked by third parties." +#~ msgstr "Estás viendo este mensaje porque este sitio requiere un cookie CSRF cuando se envie formularios. Este cookie se requiere por razones de seguridad, para asegurar que tu navegador no sea secuestrado por terceros." + +#~ msgid "If you have configured your browser to disable cookies, please re-enable them, at least for this site, or for “same-origin” requests." +#~ msgstr "Si has configurado su navegador para deshabilitar los cookies, vuelva a habilitarlos, al menos para este sitio, o para conexiones HTTPS, o para solicitudes del “same-origin”. " + +#~ msgid "More information is available with DEBUG=True." +#~ msgstr "Más información es disponible con DEBUG=True." + +#~ msgid "No year specified" +#~ msgstr "Ningun año fue especificado" + +#~ msgid "Date out of range" +#~ msgstr "Fecha fuera de rango" + +#~ msgid "No month specified" +#~ msgstr "Ningun mes fue especificado" + +#~ msgid "No day specified" +#~ msgstr "Ningun día fue especificado" + +#~ msgid "No week specified" +#~ msgstr "Ninguna semana fue especificado" + +#~ msgid "No %(verbose_name_plural)s available" +#~ msgstr "No %(verbose_name_plural)s disponible" + +#~ msgid "Future %(verbose_name_plural)s not available because %(class_name)s.allow_future is False." +#~ msgstr "%(verbose_name_plural)s del futuro no está disponible porque %(class_name)s.allow_future es False." + +#~ msgid "Invalid date string “%(datestr)s” given format “%(format)s”" +#~ msgstr "Cadena de fecha invalida “%(datestr)s” dado el formato “%(format)s”" + +#~ msgid "No %(verbose_name)s found matching the query" +#~ msgstr "No se encontró ningún %(verbose_name)s correspondiente a la búsqueda" + +#~ msgid "Page is not “last”, nor can it be converted to an int." +#~ msgstr "Página no es “last”, ni puede ser convertido en un int." + +#~ msgid "Invalid page (%(page_number)s): %(message)s" +#~ msgstr "Página invalida (%(page_number)s): %(message)s" + +#~ msgid "Empty list and “%(class_name)s.allow_empty” is False." +#~ msgstr "Lista vacia y “%(class_name)s.allow_empty” es False." + +#~ msgid "Directory indexes are not allowed here." +#~ msgstr "Indices directorios no se permiten aquí." + +#~ msgid "“%(path)s” does not exist" +#~ msgstr "“%(path)s” no existe" + +#~ msgid "Index of %(directory)s" +#~ msgstr "Indice de %(directory)s" + +#~ msgid "Django: the Web framework for perfectionists with deadlines." +#~ msgstr "Django: el estructura Web para perfeccionistas con fechas límites." + +#~ msgid "View release notes for Django %(version)s" +#~ msgstr "Ver notas de lanzamiento por Django %(version)s" + +#~ msgid "The install worked successfully! Congratulations!" +#~ msgstr "¡La instalación fue exitoso! ¡Felicidades!" + +#~ msgid "You are seeing this page because DEBUG=True is in your settings file and you have not configured any URLs." +#~ msgstr "Estás viendo esta pagina porque DEBUG=True está en tu archivo de configuración y no has configurado ningún URL." + +#~ msgid "Django Documentation" +#~ msgstr "Documentación de Django" + +#~ msgid "Topics, references, & how-to’s" +#~ msgstr "Tópicos, referencias, & instrucciones paso-a-paso" + +#~ msgid "Tutorial: A Polling App" +#~ msgstr "Tutorial: Una aplicación polling" + +#~ msgid "Get started with Django" +#~ msgstr "Empezar con Django" + +#~ msgid "Django Community" +#~ msgstr "Comunidad Django" + +#~ msgid "Connect, get help, or contribute" +#~ msgstr "Conectarse, encontrar ayuda, o contribuir" + +#~ msgid "Attempting to connect to qpid with SASL mechanism %s" +#~ msgstr "Intentando conectar con qpid con mecanismo SASL %s" + +#~ msgid "Connected to qpid with SASL mechanism %s" +#~ msgstr "Conectado con qpid con mecanismo SASL %s" + +#~ msgid "Unable to connect to qpid with SASL mechanism %s" +#~ msgstr "No se pudo conectar con qpid con mecanismo SASL %s" + +#~ msgid "1 second ago" +#~ msgstr "Hace 1 segundo" + +#~ msgid "1 minute ago" +#~ msgstr "Hace 1 minuto" + +#~ msgid "1 hour ago" +#~ msgstr "Hace 1 hora" + +#~ msgid "%(time)s" +#~ msgstr "%(time)s" + +#~ msgid "yesterday" +#~ msgstr "ayer" + +# TODO cc @mouse this could be grammatically incorrect if the time said 1 o'clock +# a working clock is broken twice a day! +#~ msgid "yesterday at %(time)s" +#~ msgstr "ayer a las %(time)s" + +#~ msgid "%(weekday)s" +#~ msgstr "%(weekday)s" + +# TODO cc @mouse this could be grammatically incorrect if the time said 1 o'clock +# a working clock is broken twice a day! +#~ msgid "%(weekday)s at %(time)s" +#~ msgstr "%(weekday)s a las %(time)s" + +#~ msgid "%(month_name)s %(day)s" +#~ msgstr "%(day)s %(month_name)s" + +# TODO cc @mouse this could be grammatically incorrect if the time said 1 o'clock +# a working clock is broken twice a day! +#~ msgid "%(month_name)s %(day)s at %(time)s" +#~ msgstr "%(day)s %(month_name)s a las %(time)s" + +#~ msgid "%(month_name)s %(day)s, %(year)s" +#~ msgstr "%(day)s %(month_name)s, %(year)s" + +# TODO cc @mouse this could be grammatically incorrect if the time said 1 o'clock +# a working clock is broken twice a day! +#~ msgid "%(month_name)s %(day)s, %(year)s at %(time)s" +#~ msgstr "%(day)s %(month_name)s, %(year)s a las %(time)s" + +#~ msgid "%(weekday)s, %(month_name)s %(day)s" +#~ msgstr "%(weekday)s, %(day)s %(month_name)s" + +#~ msgid "%(commas)s and %(last)s" +#~ msgstr "%(commas)s y %(last)s" + +#~ msgctxt "law" +#~ msgid "right" +#~ msgstr "justo" + +#~ msgctxt "good" +#~ msgid "right" +#~ msgstr "correcto" + +#~ msgctxt "organization" +#~ msgid "club" +#~ msgstr "club" + +#~ msgctxt "stick" +#~ msgid "club" +#~ msgstr "garrote" #, fuzzy #~| msgid "Started" #~ msgid "Getting Started" #~ msgstr "Empezado" -#, fuzzy, python-format +#, fuzzy #~| msgid "No users found for \"%(query)s\"" #~ msgid "No users were found for \"%(query)s\"" #~ msgstr "No se encontró ningún usuario correspondiente a \"%(query)s\"" @@ -2757,7 +3636,6 @@ msgstr "" #~ msgid "Your lists" #~ msgstr "Tus listas" -#, python-format #~ msgid "See all %(size)s lists" #~ msgstr "Ver las %(size)s listas" @@ -2782,148 +3660,18 @@ msgstr "" #~ msgid "Your Shelves" #~ msgstr "Tus estantes" -#, python-format #~ msgid "%(username)s: Shelves" #~ msgstr "%(username)s: Estantes" #~ msgid "Shelves" #~ msgstr "Estantes" -#, python-format #~ msgid "See all %(shelf_count)s shelves" #~ msgstr "Ver los %(shelf_count)s estantes" #~ msgid "Send follow request" #~ msgstr "Envia solicitud de seguidor" -#, fuzzy -#~| msgid "All messages" -#~ msgid "Messages" -#~ msgstr "Todos los mensajes" - -#, fuzzy -#~| msgid "Email address:" -#~ msgid "Enter a valid email address." -#~ msgstr "Dirección de correo electrónico:" - -#, fuzzy -#~| msgid "Series number:" -#~ msgid "Enter a number." -#~ msgstr "Número de serie:" - -#, fuzzy -#~| msgid "%(value)s is not a valid remote_id" -#~ msgid "Value %(value)r is not a valid choice." -#~ msgstr "%(value)s no es un remote_id válido" - -#, fuzzy -#~| msgid "Series number:" -#~ msgid "Decimal number" -#~ msgstr "Número de serie:" - -#, fuzzy -#~| msgid "List curation:" -#~ msgid "Duration" -#~ msgstr "Enumerar lista de comisariado:" - -#, fuzzy -#~| msgid "Email address:" -#~ msgid "Email address" -#~ msgstr "Dirección de correo electrónico:" - -#, fuzzy -#~| msgid "Email address:" -#~ msgid "IPv4 address" -#~ msgstr "Dirección de correo electrónico:" - -#, fuzzy -#~| msgid "Email address:" -#~ msgid "IP address" -#~ msgstr "Dirección de correo electrónico:" - -#, fuzzy -#~| msgid "No active invites" -#~ msgid "Positive integer" -#~ msgstr "No invitaciónes activas" - -#, fuzzy -#~| msgid "%(value)s is not a valid username" -#~ msgid "“%(value)s” is not a valid UUID." -#~ msgstr "%(value)s no es un usuario válido" - -#, fuzzy -#~| msgid "Images" -#~ msgid "Image" -#~ msgstr "Imagenes" - -#, fuzzy -#~| msgid "Relationships" -#~ msgid "One-to-one relationship" -#~ msgstr "Relaciones" - -#, fuzzy -#~| msgid "This shelf is empty." -#~ msgid "This field is required." -#~ msgstr "Este estante está vacio." - -#, fuzzy -#~| msgid "This shelf is empty." -#~ msgid "The submitted file is empty." -#~ msgstr "Este estante está vacio." - -#, fuzzy -#~| msgid "%(value)s is not a valid username" -#~ msgid "“%(pk)s” is not a valid value." -#~ msgstr "%(value)s no es un usuario válido" - -#, fuzzy -#~| msgid "Currently Reading" -#~ msgid "Currently" -#~ msgstr "Leyendo actualmente" - -#, fuzzy -#~| msgid "Change shelf" -#~ msgid "Change" -#~ msgstr "Cambiar estante" - -#, fuzzy -#~| msgid "Status" -#~ msgid "Sat" -#~ msgstr "Status" - -#, fuzzy -#~| msgid "Search" -#~ msgid "March" -#~ msgstr "Buscar" - -#, fuzzy -#~| msgid "Series number:" -#~ msgid "September" -#~ msgstr "Número de serie:" - -#, fuzzy -#~| msgid "Search" -#~ msgctxt "abbrev. month" -#~ msgid "March" -#~ msgstr "Buscar" - -#, fuzzy -#~| msgid "Search" -#~ msgctxt "alt. month" -#~ msgid "March" -#~ msgstr "Buscar" - -#, fuzzy -#~| msgid "Series number:" -#~ msgctxt "alt. month" -#~ msgid "September" -#~ msgstr "Número de serie:" - -#, fuzzy -#~| msgid "No books found matching the query \"%(query)s\"" -#~ msgid "No %(verbose_name)s found matching the query" -#~ msgstr "No se encontró ningún libro correspondiente a la búsqueda: \"%(query)s\"" - #~ msgid "Announcements" #~ msgstr "Anuncios" diff --git a/locale/fr_FR/LC_MESSAGES/django.mo b/locale/fr_FR/LC_MESSAGES/django.mo index 09e90f22c..b91a542db 100644 Binary files a/locale/fr_FR/LC_MESSAGES/django.mo and b/locale/fr_FR/LC_MESSAGES/django.mo differ diff --git a/locale/zh_CN/LC_MESSAGES/django.mo b/locale/zh_Hans/LC_MESSAGES/django.mo similarity index 100% rename from locale/zh_CN/LC_MESSAGES/django.mo rename to locale/zh_Hans/LC_MESSAGES/django.mo diff --git a/locale/zh_CN/LC_MESSAGES/django.po b/locale/zh_Hans/LC_MESSAGES/django.po similarity index 100% rename from locale/zh_CN/LC_MESSAGES/django.po rename to locale/zh_Hans/LC_MESSAGES/django.po diff --git a/requirements.txt b/requirements.txt index 0bcc85993..289d6fe68 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,7 +15,7 @@ django-rename-app==0.1.2 pytz>=2021.1 # Dev -black==20.8b1 +black==21.4b0 coverage==5.1 pytest-django==4.1.0 pytest==6.1.2
    {{ user.username }}{{ user.username }} {{ user.created_date }} {{ user.last_active_date }} {% if user.is_active %}{% trans "Active" %}{% else %}{% trans "Inactive" %}{% endif %}