New version of black, new whitespace
This commit is contained in:
@ -31,18 +31,18 @@ PropertyField = namedtuple("PropertyField", ("set_activity_from_field"))
|
||||
|
||||
|
||||
def set_activity_from_property_field(activity, obj, field):
|
||||
""" assign a model property value to the activity json """
|
||||
"""assign a model property value to the activity json"""
|
||||
activity[field[1]] = getattr(obj, field[0])
|
||||
|
||||
|
||||
class ActivitypubMixin:
|
||||
""" add this mixin for models that are AP serializable """
|
||||
"""add this mixin for models that are AP serializable"""
|
||||
|
||||
activity_serializer = lambda: {}
|
||||
reverse_unfurl = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
""" collect some info on model fields """
|
||||
"""collect some info on model fields"""
|
||||
self.image_fields = []
|
||||
self.many_to_many_fields = []
|
||||
self.simple_fields = [] # "simple"
|
||||
@ -85,7 +85,7 @@ class ActivitypubMixin:
|
||||
|
||||
@classmethod
|
||||
def find_existing_by_remote_id(cls, remote_id):
|
||||
""" look up a remote id in the db """
|
||||
"""look up a remote id in the db"""
|
||||
return cls.find_existing({"id": remote_id})
|
||||
|
||||
@classmethod
|
||||
@ -126,7 +126,7 @@ class ActivitypubMixin:
|
||||
return match.first()
|
||||
|
||||
def broadcast(self, activity, sender, software=None):
|
||||
""" send out an activity """
|
||||
"""send out an activity"""
|
||||
broadcast_task.delay(
|
||||
sender.id,
|
||||
json.dumps(activity, cls=activitypub.ActivityEncoder),
|
||||
@ -134,7 +134,7 @@ class ActivitypubMixin:
|
||||
)
|
||||
|
||||
def get_recipients(self, software=None):
|
||||
""" figure out which inbox urls to post to """
|
||||
"""figure out which inbox urls to post to"""
|
||||
# first we have to figure out who should receive this activity
|
||||
privacy = self.privacy if hasattr(self, "privacy") else "public"
|
||||
# is this activity owned by a user (statuses, lists, shelves), or is it
|
||||
@ -182,20 +182,20 @@ class ActivitypubMixin:
|
||||
return list(set(recipients))
|
||||
|
||||
def to_activity_dataclass(self):
|
||||
""" convert from a model to an activity """
|
||||
"""convert from a model to an activity"""
|
||||
activity = generate_activity(self)
|
||||
return self.activity_serializer(**activity)
|
||||
|
||||
def to_activity(self, **kwargs): # pylint: disable=unused-argument
|
||||
""" convert from a model to a json activity """
|
||||
"""convert from a model to a json activity"""
|
||||
return self.to_activity_dataclass().serialize()
|
||||
|
||||
|
||||
class ObjectMixin(ActivitypubMixin):
|
||||
""" add this mixin for object models that are AP serializable """
|
||||
"""add this mixin for object models that are AP serializable"""
|
||||
|
||||
def save(self, *args, created=None, **kwargs):
|
||||
""" broadcast created/updated/deleted objects as appropriate """
|
||||
"""broadcast created/updated/deleted objects as appropriate"""
|
||||
broadcast = kwargs.get("broadcast", True)
|
||||
# this bonus kwarg would cause an error in the base save method
|
||||
if "broadcast" in kwargs:
|
||||
@ -254,7 +254,7 @@ class ObjectMixin(ActivitypubMixin):
|
||||
self.broadcast(activity, user)
|
||||
|
||||
def to_create_activity(self, user, **kwargs):
|
||||
""" returns the object wrapped in a Create activity """
|
||||
"""returns the object wrapped in a Create activity"""
|
||||
activity_object = self.to_activity_dataclass(**kwargs)
|
||||
|
||||
signature = None
|
||||
@ -280,7 +280,7 @@ class ObjectMixin(ActivitypubMixin):
|
||||
).serialize()
|
||||
|
||||
def to_delete_activity(self, user):
|
||||
""" notice of deletion """
|
||||
"""notice of deletion"""
|
||||
return activitypub.Delete(
|
||||
id=self.remote_id + "/activity",
|
||||
actor=user.remote_id,
|
||||
@ -290,7 +290,7 @@ class ObjectMixin(ActivitypubMixin):
|
||||
).serialize()
|
||||
|
||||
def to_update_activity(self, user):
|
||||
""" wrapper for Updates to an activity """
|
||||
"""wrapper for Updates to an activity"""
|
||||
activity_id = "%s#update/%s" % (self.remote_id, uuid4())
|
||||
return activitypub.Update(
|
||||
id=activity_id,
|
||||
@ -306,13 +306,13 @@ class OrderedCollectionPageMixin(ObjectMixin):
|
||||
|
||||
@property
|
||||
def collection_remote_id(self):
|
||||
""" this can be overriden if there's a special remote id, ie outbox """
|
||||
"""this can be overriden if there's a special remote id, ie outbox"""
|
||||
return self.remote_id
|
||||
|
||||
def to_ordered_collection(
|
||||
self, queryset, remote_id=None, page=False, collection_only=False, **kwargs
|
||||
):
|
||||
""" an ordered collection of whatevers """
|
||||
"""an ordered collection of whatevers"""
|
||||
if not queryset.ordered:
|
||||
raise RuntimeError("queryset must be ordered")
|
||||
|
||||
@ -341,11 +341,11 @@ class OrderedCollectionPageMixin(ObjectMixin):
|
||||
|
||||
|
||||
class OrderedCollectionMixin(OrderedCollectionPageMixin):
|
||||
""" extends activitypub models to work as ordered collections """
|
||||
"""extends activitypub models to work as ordered collections"""
|
||||
|
||||
@property
|
||||
def collection_queryset(self):
|
||||
""" usually an ordered collection model aggregates a different model """
|
||||
"""usually an ordered collection model aggregates a different model"""
|
||||
raise NotImplementedError("Model must define collection_queryset")
|
||||
|
||||
activity_serializer = activitypub.OrderedCollection
|
||||
@ -354,24 +354,24 @@ class OrderedCollectionMixin(OrderedCollectionPageMixin):
|
||||
return self.to_ordered_collection(self.collection_queryset, **kwargs)
|
||||
|
||||
def to_activity(self, **kwargs):
|
||||
""" an ordered collection of the specified model queryset """
|
||||
"""an ordered collection of the specified model queryset"""
|
||||
return self.to_ordered_collection(
|
||||
self.collection_queryset, **kwargs
|
||||
).serialize()
|
||||
|
||||
|
||||
class CollectionItemMixin(ActivitypubMixin):
|
||||
""" for items that are part of an (Ordered)Collection """
|
||||
"""for items that are part of an (Ordered)Collection"""
|
||||
|
||||
activity_serializer = activitypub.CollectionItem
|
||||
|
||||
def broadcast(self, activity, sender, software="bookwyrm"):
|
||||
""" only send book collection updates to other bookwyrm instances """
|
||||
"""only send book collection updates to other bookwyrm instances"""
|
||||
super().broadcast(activity, sender, software=software)
|
||||
|
||||
@property
|
||||
def privacy(self):
|
||||
""" inherit the privacy of the list, or direct if pending """
|
||||
"""inherit the privacy of the list, or direct if pending"""
|
||||
collection_field = getattr(self, self.collection_field)
|
||||
if self.approved:
|
||||
return collection_field.privacy
|
||||
@ -379,7 +379,7 @@ class CollectionItemMixin(ActivitypubMixin):
|
||||
|
||||
@property
|
||||
def recipients(self):
|
||||
""" the owner of the list is a direct recipient """
|
||||
"""the owner of the list is a direct recipient"""
|
||||
collection_field = getattr(self, self.collection_field)
|
||||
if collection_field.user.local:
|
||||
# don't broadcast to yourself
|
||||
@ -387,7 +387,7 @@ class CollectionItemMixin(ActivitypubMixin):
|
||||
return [collection_field.user]
|
||||
|
||||
def save(self, *args, broadcast=True, **kwargs):
|
||||
""" broadcast updated """
|
||||
"""broadcast updated"""
|
||||
# first off, we want to save normally no matter what
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@ -400,14 +400,14 @@ class CollectionItemMixin(ActivitypubMixin):
|
||||
self.broadcast(activity, self.user)
|
||||
|
||||
def delete(self, *args, broadcast=True, **kwargs):
|
||||
""" broadcast a remove activity """
|
||||
"""broadcast a remove activity"""
|
||||
activity = self.to_remove_activity(self.user)
|
||||
super().delete(*args, **kwargs)
|
||||
if self.user.local and broadcast:
|
||||
self.broadcast(activity, self.user)
|
||||
|
||||
def to_add_activity(self, user):
|
||||
""" AP for shelving a book"""
|
||||
"""AP for shelving a book"""
|
||||
collection_field = getattr(self, self.collection_field)
|
||||
return activitypub.Add(
|
||||
id="{:s}#add".format(collection_field.remote_id),
|
||||
@ -417,7 +417,7 @@ class CollectionItemMixin(ActivitypubMixin):
|
||||
).serialize()
|
||||
|
||||
def to_remove_activity(self, user):
|
||||
""" AP for un-shelving a book"""
|
||||
"""AP for un-shelving a book"""
|
||||
collection_field = getattr(self, self.collection_field)
|
||||
return activitypub.Remove(
|
||||
id="{:s}#remove".format(collection_field.remote_id),
|
||||
@ -428,24 +428,24 @@ class CollectionItemMixin(ActivitypubMixin):
|
||||
|
||||
|
||||
class ActivityMixin(ActivitypubMixin):
|
||||
""" add this mixin for models that are AP serializable """
|
||||
"""add this mixin for models that are AP serializable"""
|
||||
|
||||
def save(self, *args, broadcast=True, **kwargs):
|
||||
""" broadcast activity """
|
||||
"""broadcast activity"""
|
||||
super().save(*args, **kwargs)
|
||||
user = self.user if hasattr(self, "user") else self.user_subject
|
||||
if broadcast and user.local:
|
||||
self.broadcast(self.to_activity(), user)
|
||||
|
||||
def delete(self, *args, broadcast=True, **kwargs):
|
||||
""" nevermind, undo that activity """
|
||||
"""nevermind, undo that activity"""
|
||||
user = self.user if hasattr(self, "user") else self.user_subject
|
||||
if broadcast and user.local:
|
||||
self.broadcast(self.to_undo_activity(), user)
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
def to_undo_activity(self):
|
||||
""" undo an action """
|
||||
"""undo an action"""
|
||||
user = self.user if hasattr(self, "user") else self.user_subject
|
||||
return activitypub.Undo(
|
||||
id="%s#undo" % self.remote_id,
|
||||
@ -455,7 +455,7 @@ class ActivityMixin(ActivitypubMixin):
|
||||
|
||||
|
||||
def generate_activity(obj):
|
||||
""" go through the fields on an object """
|
||||
"""go through the fields on an object"""
|
||||
activity = {}
|
||||
for field in obj.activity_fields:
|
||||
field.set_activity_from_field(activity, obj)
|
||||
@ -478,7 +478,7 @@ def generate_activity(obj):
|
||||
|
||||
|
||||
def unfurl_related_field(related_field, sort_field=None):
|
||||
""" load reverse lookups (like public key owner or Status attachment """
|
||||
"""load reverse lookups (like public key owner or Status attachment"""
|
||||
if sort_field and hasattr(related_field, "all"):
|
||||
return [
|
||||
unfurl_related_field(i) for i in related_field.order_by(sort_field).all()
|
||||
@ -494,7 +494,7 @@ def unfurl_related_field(related_field, sort_field=None):
|
||||
|
||||
@app.task
|
||||
def broadcast_task(sender_id, activity, recipients):
|
||||
""" the celery task for broadcast """
|
||||
"""the celery task for broadcast"""
|
||||
user_model = apps.get_model("bookwyrm.User", require_ready=True)
|
||||
sender = user_model.objects.get(id=sender_id)
|
||||
for recipient in recipients:
|
||||
@ -505,7 +505,7 @@ def broadcast_task(sender_id, activity, recipients):
|
||||
|
||||
|
||||
def sign_and_send(sender, data, destination):
|
||||
""" crpyto whatever and http junk """
|
||||
"""crpyto whatever and http junk"""
|
||||
now = http_date()
|
||||
|
||||
if not sender.key_pair.private_key:
|
||||
@ -534,7 +534,7 @@ def sign_and_send(sender, data, destination):
|
||||
def to_ordered_collection_page(
|
||||
queryset, remote_id, id_only=False, page=1, pure=False, **kwargs
|
||||
):
|
||||
""" serialize and pagiante a queryset """
|
||||
"""serialize and pagiante a queryset"""
|
||||
paginated = Paginator(queryset, PAGE_LENGTH)
|
||||
|
||||
activity_page = paginated.get_page(page)
|
||||
|
@ -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/",
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -13,7 +13,7 @@ from . import fields
|
||||
|
||||
|
||||
class BookDataModel(ObjectMixin, BookWyrmModel):
|
||||
""" fields shared between editable book data (books, works, authors) """
|
||||
"""fields shared between editable book data (books, works, authors)"""
|
||||
|
||||
origin_id = models.CharField(max_length=255, null=True, blank=True)
|
||||
openlibrary_key = fields.CharField(
|
||||
@ -33,12 +33,12 @@ class BookDataModel(ObjectMixin, BookWyrmModel):
|
||||
)
|
||||
|
||||
class Meta:
|
||||
""" can't initialize this model, that wouldn't make sense """
|
||||
"""can't initialize this model, that wouldn't make sense"""
|
||||
|
||||
abstract = True
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
""" ensure that the remote_id is within this instance """
|
||||
"""ensure that the remote_id is within this instance"""
|
||||
if self.id:
|
||||
self.remote_id = self.get_remote_id()
|
||||
else:
|
||||
@ -47,12 +47,12 @@ class BookDataModel(ObjectMixin, BookWyrmModel):
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
def broadcast(self, activity, sender, software="bookwyrm"):
|
||||
""" only send book data updates to other bookwyrm instances """
|
||||
"""only send book data updates to other bookwyrm instances"""
|
||||
super().broadcast(activity, sender, software=software)
|
||||
|
||||
|
||||
class Book(BookDataModel):
|
||||
""" a generic book, which can mean either an edition or a work """
|
||||
"""a generic book, which can mean either an edition or a work"""
|
||||
|
||||
connector = models.ForeignKey("Connector", on_delete=models.PROTECT, null=True)
|
||||
|
||||
@ -83,17 +83,17 @@ class Book(BookDataModel):
|
||||
|
||||
@property
|
||||
def author_text(self):
|
||||
""" format a list of authors """
|
||||
"""format a list of authors"""
|
||||
return ", ".join(a.name for a in self.authors.all())
|
||||
|
||||
@property
|
||||
def latest_readthrough(self):
|
||||
""" most recent readthrough activity """
|
||||
"""most recent readthrough activity"""
|
||||
return self.readthrough_set.order_by("-updated_date").first()
|
||||
|
||||
@property
|
||||
def edition_info(self):
|
||||
""" properties of this edition, as a string """
|
||||
"""properties of this edition, as a string"""
|
||||
items = [
|
||||
self.physical_format if hasattr(self, "physical_format") else None,
|
||||
self.languages[0] + " language"
|
||||
@ -106,20 +106,20 @@ class Book(BookDataModel):
|
||||
|
||||
@property
|
||||
def alt_text(self):
|
||||
""" image alt test """
|
||||
"""image alt test"""
|
||||
text = "%s" % self.title
|
||||
if self.edition_info:
|
||||
text += " (%s)" % self.edition_info
|
||||
return text
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
""" can't be abstract for query reasons, but you shouldn't USE it """
|
||||
"""can't be abstract for query reasons, but you shouldn't USE it"""
|
||||
if not isinstance(self, Edition) and not isinstance(self, Work):
|
||||
raise ValueError("Books should be added as Editions or Works")
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
def get_remote_id(self):
|
||||
""" editions and works both use "book" instead of model_name """
|
||||
"""editions and works both use "book" instead of model_name"""
|
||||
return "https://%s/book/%d" % (DOMAIN, self.id)
|
||||
|
||||
def __repr__(self):
|
||||
@ -131,7 +131,7 @@ class Book(BookDataModel):
|
||||
|
||||
|
||||
class Work(OrderedCollectionPageMixin, Book):
|
||||
""" a work (an abstract concept of a book that manifests in an edition) """
|
||||
"""a work (an abstract concept of a book that manifests in an edition)"""
|
||||
|
||||
# library of congress catalog control number
|
||||
lccn = fields.CharField(
|
||||
@ -143,19 +143,19 @@ class Work(OrderedCollectionPageMixin, Book):
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
""" set some fields on the edition object """
|
||||
"""set some fields on the edition object"""
|
||||
# set rank
|
||||
for edition in self.editions.all():
|
||||
edition.save()
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
def get_default_edition(self):
|
||||
""" in case the default edition is not set """
|
||||
"""in case the default edition is not set"""
|
||||
return self.default_edition or self.editions.order_by("-edition_rank").first()
|
||||
|
||||
@transaction.atomic()
|
||||
def reset_default_edition(self):
|
||||
""" sets a new default edition based on computed rank """
|
||||
"""sets a new default edition based on computed rank"""
|
||||
self.default_edition = None
|
||||
# editions are re-ranked implicitly
|
||||
self.save()
|
||||
@ -163,11 +163,11 @@ class Work(OrderedCollectionPageMixin, Book):
|
||||
self.save()
|
||||
|
||||
def to_edition_list(self, **kwargs):
|
||||
""" an ordered collection of editions """
|
||||
"""an ordered collection of editions"""
|
||||
return self.to_ordered_collection(
|
||||
self.editions.order_by("-edition_rank").all(),
|
||||
remote_id="%s/editions" % self.remote_id,
|
||||
**kwargs
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
activity_serializer = activitypub.Work
|
||||
@ -176,7 +176,7 @@ class Work(OrderedCollectionPageMixin, Book):
|
||||
|
||||
|
||||
class Edition(Book):
|
||||
""" an edition of a book """
|
||||
"""an edition of a book"""
|
||||
|
||||
# these identifiers only apply to editions, not works
|
||||
isbn_10 = fields.CharField(
|
||||
@ -215,7 +215,7 @@ class Edition(Book):
|
||||
name_field = "title"
|
||||
|
||||
def get_rank(self, ignore_default=False):
|
||||
""" calculate how complete the data is on this edition """
|
||||
"""calculate how complete the data is on this edition"""
|
||||
if (
|
||||
not ignore_default
|
||||
and self.parent_work
|
||||
@ -235,7 +235,7 @@ class Edition(Book):
|
||||
return rank
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
""" set some fields on the edition object """
|
||||
"""set some fields on the edition object"""
|
||||
# calculate isbn 10/13
|
||||
if self.isbn_13 and self.isbn_13[:3] == "978" and not self.isbn_10:
|
||||
self.isbn_10 = isbn_13_to_10(self.isbn_13)
|
||||
@ -249,7 +249,7 @@ class Edition(Book):
|
||||
|
||||
|
||||
def isbn_10_to_13(isbn_10):
|
||||
""" convert an isbn 10 into an isbn 13 """
|
||||
"""convert an isbn 10 into an isbn 13"""
|
||||
isbn_10 = re.sub(r"[^0-9X]", "", isbn_10)
|
||||
# drop the last character of the isbn 10 number (the original checkdigit)
|
||||
converted = isbn_10[:9]
|
||||
@ -271,7 +271,7 @@ def isbn_10_to_13(isbn_10):
|
||||
|
||||
|
||||
def isbn_13_to_10(isbn_13):
|
||||
""" convert isbn 13 to 10, if possible """
|
||||
"""convert isbn 13 to 10, if possible"""
|
||||
if isbn_13[:3] != "978":
|
||||
return None
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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")
|
||||
|
@ -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()
|
||||
|
@ -18,7 +18,7 @@ from bookwyrm.settings import DOMAIN
|
||||
|
||||
|
||||
def validate_remote_id(value):
|
||||
""" make sure the remote_id looks like a url """
|
||||
"""make sure the remote_id looks like a url"""
|
||||
if not value or not re.match(r"^http.?:\/\/[^\s]+$", value):
|
||||
raise ValidationError(
|
||||
_("%(value)s is not a valid remote_id"),
|
||||
@ -27,7 +27,7 @@ def validate_remote_id(value):
|
||||
|
||||
|
||||
def validate_localname(value):
|
||||
""" make sure localnames look okay """
|
||||
"""make sure localnames look okay"""
|
||||
if not re.match(r"^[A-Za-z\-_\.0-9]+$", value):
|
||||
raise ValidationError(
|
||||
_("%(value)s is not a valid username"),
|
||||
@ -36,7 +36,7 @@ def validate_localname(value):
|
||||
|
||||
|
||||
def validate_username(value):
|
||||
""" make sure usernames look okay """
|
||||
"""make sure usernames look okay"""
|
||||
if not re.match(r"^[A-Za-z\-_\.0-9]+@[A-Za-z\-_\.0-9]+\.[a-z]{2,}$", value):
|
||||
raise ValidationError(
|
||||
_("%(value)s is not a valid username"),
|
||||
@ -45,7 +45,7 @@ def validate_username(value):
|
||||
|
||||
|
||||
class ActivitypubFieldMixin:
|
||||
""" make a database field serializable """
|
||||
"""make a database field serializable"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@ -64,7 +64,7 @@ class ActivitypubFieldMixin:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def set_field_from_activity(self, instance, data):
|
||||
""" helper function for assinging a value to the field """
|
||||
"""helper function for assinging a value to the field"""
|
||||
try:
|
||||
value = getattr(data, self.get_activitypub_field())
|
||||
except AttributeError:
|
||||
@ -78,7 +78,7 @@ class ActivitypubFieldMixin:
|
||||
setattr(instance, self.name, formatted)
|
||||
|
||||
def set_activity_from_field(self, activity, instance):
|
||||
""" update the json object """
|
||||
"""update the json object"""
|
||||
value = getattr(instance, self.name)
|
||||
formatted = self.field_to_activity(value)
|
||||
if formatted is None:
|
||||
@ -94,19 +94,19 @@ class ActivitypubFieldMixin:
|
||||
activity[key] = formatted
|
||||
|
||||
def field_to_activity(self, value):
|
||||
""" formatter to convert a model value into activitypub """
|
||||
"""formatter to convert a model value into activitypub"""
|
||||
if hasattr(self, "activitypub_wrapper"):
|
||||
return {self.activitypub_wrapper: value}
|
||||
return value
|
||||
|
||||
def field_from_activity(self, value):
|
||||
""" formatter to convert activitypub into a model value """
|
||||
"""formatter to convert activitypub into a model value"""
|
||||
if value and hasattr(self, "activitypub_wrapper"):
|
||||
value = value.get(self.activitypub_wrapper)
|
||||
return value
|
||||
|
||||
def get_activitypub_field(self):
|
||||
""" model_field_name to activitypubFieldName """
|
||||
"""model_field_name to activitypubFieldName"""
|
||||
if self.activitypub_field:
|
||||
return self.activitypub_field
|
||||
name = self.name.split(".")[-1]
|
||||
@ -115,7 +115,7 @@ class ActivitypubFieldMixin:
|
||||
|
||||
|
||||
class ActivitypubRelatedFieldMixin(ActivitypubFieldMixin):
|
||||
""" default (de)serialization for foreign key and one to one """
|
||||
"""default (de)serialization for foreign key and one to one"""
|
||||
|
||||
def __init__(self, *args, load_remote=True, **kwargs):
|
||||
self.load_remote = load_remote
|
||||
@ -146,7 +146,7 @@ class ActivitypubRelatedFieldMixin(ActivitypubFieldMixin):
|
||||
|
||||
|
||||
class RemoteIdField(ActivitypubFieldMixin, models.CharField):
|
||||
""" a url that serves as a unique identifier """
|
||||
"""a url that serves as a unique identifier"""
|
||||
|
||||
def __init__(self, *args, max_length=255, validators=None, **kwargs):
|
||||
validators = validators or [validate_remote_id]
|
||||
@ -156,7 +156,7 @@ class RemoteIdField(ActivitypubFieldMixin, models.CharField):
|
||||
|
||||
|
||||
class UsernameField(ActivitypubFieldMixin, models.CharField):
|
||||
""" activitypub-aware username field """
|
||||
"""activitypub-aware username field"""
|
||||
|
||||
def __init__(self, activitypub_field="preferredUsername", **kwargs):
|
||||
self.activitypub_field = activitypub_field
|
||||
@ -172,7 +172,7 @@ class UsernameField(ActivitypubFieldMixin, models.CharField):
|
||||
)
|
||||
|
||||
def deconstruct(self):
|
||||
""" implementation of models.Field deconstruct """
|
||||
"""implementation of models.Field deconstruct"""
|
||||
name, path, args, kwargs = super().deconstruct()
|
||||
del kwargs["verbose_name"]
|
||||
del kwargs["max_length"]
|
||||
@ -191,7 +191,7 @@ PrivacyLevels = models.TextChoices(
|
||||
|
||||
|
||||
class PrivacyField(ActivitypubFieldMixin, models.CharField):
|
||||
""" this maps to two differente activitypub fields """
|
||||
"""this maps to two differente activitypub fields"""
|
||||
|
||||
public = "https://www.w3.org/ns/activitystreams#Public"
|
||||
|
||||
@ -236,7 +236,7 @@ class PrivacyField(ActivitypubFieldMixin, models.CharField):
|
||||
|
||||
|
||||
class ForeignKey(ActivitypubRelatedFieldMixin, models.ForeignKey):
|
||||
""" activitypub-aware foreign key field """
|
||||
"""activitypub-aware foreign key field"""
|
||||
|
||||
def field_to_activity(self, value):
|
||||
if not value:
|
||||
@ -245,7 +245,7 @@ class ForeignKey(ActivitypubRelatedFieldMixin, models.ForeignKey):
|
||||
|
||||
|
||||
class OneToOneField(ActivitypubRelatedFieldMixin, models.OneToOneField):
|
||||
""" activitypub-aware foreign key field """
|
||||
"""activitypub-aware foreign key field"""
|
||||
|
||||
def field_to_activity(self, value):
|
||||
if not value:
|
||||
@ -254,14 +254,14 @@ class OneToOneField(ActivitypubRelatedFieldMixin, models.OneToOneField):
|
||||
|
||||
|
||||
class ManyToManyField(ActivitypubFieldMixin, models.ManyToManyField):
|
||||
""" activitypub-aware many to many field """
|
||||
"""activitypub-aware many to many field"""
|
||||
|
||||
def __init__(self, *args, link_only=False, **kwargs):
|
||||
self.link_only = link_only
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def set_field_from_activity(self, instance, data):
|
||||
""" helper function for assinging a value to the field """
|
||||
"""helper function for assinging a value to the field"""
|
||||
value = getattr(data, self.get_activitypub_field())
|
||||
formatted = self.field_from_activity(value)
|
||||
if formatted is None or formatted is MISSING:
|
||||
@ -293,7 +293,7 @@ class ManyToManyField(ActivitypubFieldMixin, models.ManyToManyField):
|
||||
|
||||
|
||||
class TagField(ManyToManyField):
|
||||
""" special case of many to many that uses Tags """
|
||||
"""special case of many to many that uses Tags"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@ -333,7 +333,7 @@ class TagField(ManyToManyField):
|
||||
|
||||
|
||||
def image_serializer(value, alt):
|
||||
""" helper for serializing images """
|
||||
"""helper for serializing images"""
|
||||
if value and hasattr(value, "url"):
|
||||
url = value.url
|
||||
else:
|
||||
@ -343,7 +343,7 @@ def image_serializer(value, alt):
|
||||
|
||||
|
||||
class ImageField(ActivitypubFieldMixin, models.ImageField):
|
||||
""" activitypub-aware image field """
|
||||
"""activitypub-aware image field"""
|
||||
|
||||
def __init__(self, *args, alt_field=None, **kwargs):
|
||||
self.alt_field = alt_field
|
||||
@ -351,7 +351,7 @@ class ImageField(ActivitypubFieldMixin, models.ImageField):
|
||||
|
||||
# pylint: disable=arguments-differ
|
||||
def set_field_from_activity(self, instance, data, save=True):
|
||||
""" helper function for assinging a value to the field """
|
||||
"""helper function for assinging a value to the field"""
|
||||
value = getattr(data, self.get_activitypub_field())
|
||||
formatted = self.field_from_activity(value)
|
||||
if formatted is None or formatted is MISSING:
|
||||
@ -397,7 +397,7 @@ class ImageField(ActivitypubFieldMixin, models.ImageField):
|
||||
|
||||
|
||||
class DateTimeField(ActivitypubFieldMixin, models.DateTimeField):
|
||||
""" activitypub-aware datetime field """
|
||||
"""activitypub-aware datetime field"""
|
||||
|
||||
def field_to_activity(self, value):
|
||||
if not value:
|
||||
@ -416,7 +416,7 @@ class DateTimeField(ActivitypubFieldMixin, models.DateTimeField):
|
||||
|
||||
|
||||
class HtmlField(ActivitypubFieldMixin, models.TextField):
|
||||
""" a text field for storing html """
|
||||
"""a text field for storing html"""
|
||||
|
||||
def field_from_activity(self, value):
|
||||
if not value or value == MISSING:
|
||||
@ -427,30 +427,30 @@ class HtmlField(ActivitypubFieldMixin, models.TextField):
|
||||
|
||||
|
||||
class ArrayField(ActivitypubFieldMixin, DjangoArrayField):
|
||||
""" activitypub-aware array field """
|
||||
"""activitypub-aware array field"""
|
||||
|
||||
def field_to_activity(self, value):
|
||||
return [str(i) for i in value]
|
||||
|
||||
|
||||
class CharField(ActivitypubFieldMixin, models.CharField):
|
||||
""" activitypub-aware char field """
|
||||
"""activitypub-aware char field"""
|
||||
|
||||
|
||||
class TextField(ActivitypubFieldMixin, models.TextField):
|
||||
""" activitypub-aware text field """
|
||||
"""activitypub-aware text field"""
|
||||
|
||||
|
||||
class BooleanField(ActivitypubFieldMixin, models.BooleanField):
|
||||
""" activitypub-aware boolean field """
|
||||
"""activitypub-aware boolean field"""
|
||||
|
||||
|
||||
class IntegerField(ActivitypubFieldMixin, models.IntegerField):
|
||||
""" activitypub-aware boolean field """
|
||||
"""activitypub-aware boolean field"""
|
||||
|
||||
|
||||
class DecimalField(ActivitypubFieldMixin, models.DecimalField):
|
||||
""" activitypub-aware boolean field """
|
||||
"""activitypub-aware boolean field"""
|
||||
|
||||
def field_to_activity(self, value):
|
||||
if not value:
|
||||
|
@ -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)
|
||||
|
@ -21,7 +21,7 @@ CurationType = models.TextChoices(
|
||||
|
||||
|
||||
class List(OrderedCollectionMixin, BookWyrmModel):
|
||||
""" a list of books """
|
||||
"""a list of books"""
|
||||
|
||||
name = fields.CharField(max_length=100)
|
||||
user = fields.ForeignKey(
|
||||
@ -41,22 +41,22 @@ class List(OrderedCollectionMixin, BookWyrmModel):
|
||||
activity_serializer = activitypub.BookList
|
||||
|
||||
def get_remote_id(self):
|
||||
""" don't want the user to be in there in this case """
|
||||
"""don't want the user to be in there in this case"""
|
||||
return "https://%s/list/%d" % (DOMAIN, self.id)
|
||||
|
||||
@property
|
||||
def collection_queryset(self):
|
||||
""" list of books for this shelf, overrides OrderedCollectionMixin """
|
||||
"""list of books for this shelf, overrides OrderedCollectionMixin"""
|
||||
return self.books.filter(listitem__approved=True).order_by("listitem")
|
||||
|
||||
class Meta:
|
||||
""" default sorting """
|
||||
"""default sorting"""
|
||||
|
||||
ordering = ("-updated_date",)
|
||||
|
||||
|
||||
class ListItem(CollectionItemMixin, BookWyrmModel):
|
||||
""" ok """
|
||||
"""ok"""
|
||||
|
||||
book = fields.ForeignKey(
|
||||
"Edition", on_delete=models.PROTECT, activitypub_field="book"
|
||||
@ -74,7 +74,7 @@ class ListItem(CollectionItemMixin, BookWyrmModel):
|
||||
collection_field = "book_list"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
""" create a notification too """
|
||||
"""create a notification too"""
|
||||
created = not bool(self.id)
|
||||
super().save(*args, **kwargs)
|
||||
# tick the updated date on the parent list
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
|
@ -11,7 +11,7 @@ from . import fields
|
||||
|
||||
|
||||
class UserRelationship(BookWyrmModel):
|
||||
""" many-to-many through table for followers """
|
||||
"""many-to-many through table for followers"""
|
||||
|
||||
user_subject = fields.ForeignKey(
|
||||
"User",
|
||||
@ -28,16 +28,16 @@ class UserRelationship(BookWyrmModel):
|
||||
|
||||
@property
|
||||
def privacy(self):
|
||||
""" all relationships are handled directly with the participants """
|
||||
"""all relationships are handled directly with the participants"""
|
||||
return "direct"
|
||||
|
||||
@property
|
||||
def recipients(self):
|
||||
""" the remote user needs to recieve direct broadcasts """
|
||||
"""the remote user needs to recieve direct broadcasts"""
|
||||
return [u for u in [self.user_subject, self.user_object] if not u.local]
|
||||
|
||||
class Meta:
|
||||
""" relationships should be unique """
|
||||
"""relationships should be unique"""
|
||||
|
||||
abstract = True
|
||||
constraints = [
|
||||
@ -51,22 +51,22 @@ class UserRelationship(BookWyrmModel):
|
||||
]
|
||||
|
||||
def get_remote_id(self):
|
||||
""" use shelf identifier in remote_id """
|
||||
"""use shelf identifier in remote_id"""
|
||||
base_path = self.user_subject.remote_id
|
||||
return "%s#follows/%d" % (base_path, self.id)
|
||||
|
||||
|
||||
class UserFollows(ActivityMixin, UserRelationship):
|
||||
""" Following a user """
|
||||
"""Following a user"""
|
||||
|
||||
status = "follows"
|
||||
|
||||
def to_activity(self): # pylint: disable=arguments-differ
|
||||
""" overrides default to manually set serializer """
|
||||
"""overrides default to manually set serializer"""
|
||||
return activitypub.Follow(**generate_activity(self))
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
""" really really don't let a user follow someone who blocked them """
|
||||
"""really really don't let a user follow someone who blocked them"""
|
||||
# blocking in either direction is a no-go
|
||||
if UserBlocks.objects.filter(
|
||||
Q(
|
||||
@ -85,7 +85,7 @@ class UserFollows(ActivityMixin, UserRelationship):
|
||||
|
||||
@classmethod
|
||||
def from_request(cls, follow_request):
|
||||
""" converts a follow request into a follow relationship """
|
||||
"""converts a follow request into a follow relationship"""
|
||||
return cls.objects.create(
|
||||
user_subject=follow_request.user_subject,
|
||||
user_object=follow_request.user_object,
|
||||
@ -94,13 +94,13 @@ class UserFollows(ActivityMixin, UserRelationship):
|
||||
|
||||
|
||||
class UserFollowRequest(ActivitypubMixin, UserRelationship):
|
||||
""" following a user requires manual or automatic confirmation """
|
||||
"""following a user requires manual or automatic confirmation"""
|
||||
|
||||
status = "follow_request"
|
||||
activity_serializer = activitypub.Follow
|
||||
|
||||
def save(self, *args, broadcast=True, **kwargs):
|
||||
""" make sure the follow or block relationship doesn't already exist """
|
||||
"""make sure the follow or block relationship doesn't already exist"""
|
||||
# if there's a request for a follow that already exists, accept it
|
||||
# without changing the local database state
|
||||
if UserFollows.objects.filter(
|
||||
@ -141,13 +141,13 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship):
|
||||
)
|
||||
|
||||
def get_accept_reject_id(self, status):
|
||||
""" get id for sending an accept or reject of a local user """
|
||||
"""get id for sending an accept or reject of a local user"""
|
||||
|
||||
base_path = self.user_object.remote_id
|
||||
return "%s#%s/%d" % (base_path, status, self.id or 0)
|
||||
|
||||
def accept(self, broadcast_only=False):
|
||||
""" turn this request into the real deal"""
|
||||
"""turn this request into the real deal"""
|
||||
user = self.user_object
|
||||
if not self.user_subject.local:
|
||||
activity = activitypub.Accept(
|
||||
@ -164,7 +164,7 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship):
|
||||
self.delete()
|
||||
|
||||
def reject(self):
|
||||
""" generate a Reject for this follow request """
|
||||
"""generate a Reject for this follow request"""
|
||||
if self.user_object.local:
|
||||
activity = activitypub.Reject(
|
||||
id=self.get_accept_reject_id(status="rejects"),
|
||||
@ -177,13 +177,13 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship):
|
||||
|
||||
|
||||
class UserBlocks(ActivityMixin, UserRelationship):
|
||||
""" prevent another user from following you and seeing your posts """
|
||||
"""prevent another user from following you and seeing your posts"""
|
||||
|
||||
status = "blocks"
|
||||
activity_serializer = activitypub.Block
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
""" remove follow or follow request rels after a block is created """
|
||||
"""remove follow or follow request rels after a block is created"""
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
UserFollows.objects.filter(
|
||||
|
@ -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",)
|
||||
|
@ -9,7 +9,7 @@ from . import fields
|
||||
|
||||
|
||||
class Shelf(OrderedCollectionMixin, BookWyrmModel):
|
||||
""" a list of books owned by a user """
|
||||
"""a list of books owned by a user"""
|
||||
|
||||
TO_READ = "to-read"
|
||||
READING = "reading"
|
||||
@ -34,36 +34,36 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel):
|
||||
activity_serializer = activitypub.Shelf
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
""" set the identifier """
|
||||
"""set the identifier"""
|
||||
super().save(*args, **kwargs)
|
||||
if not self.identifier:
|
||||
self.identifier = self.get_identifier()
|
||||
super().save(*args, **kwargs, broadcast=False)
|
||||
|
||||
def get_identifier(self):
|
||||
""" custom-shelf-123 for the url """
|
||||
"""custom-shelf-123 for the url"""
|
||||
slug = re.sub(r"[^\w]", "", self.name).lower()
|
||||
return "{:s}-{:d}".format(slug, self.id)
|
||||
|
||||
@property
|
||||
def collection_queryset(self):
|
||||
""" list of books for this shelf, overrides OrderedCollectionMixin """
|
||||
"""list of books for this shelf, overrides OrderedCollectionMixin"""
|
||||
return self.books.order_by("shelfbook")
|
||||
|
||||
def get_remote_id(self):
|
||||
""" shelf identifier instead of id """
|
||||
"""shelf identifier instead of id"""
|
||||
base_path = self.user.remote_id
|
||||
identifier = self.identifier or self.get_identifier()
|
||||
return "%s/books/%s" % (base_path, identifier)
|
||||
|
||||
class Meta:
|
||||
""" user/shelf unqiueness """
|
||||
"""user/shelf unqiueness"""
|
||||
|
||||
unique_together = ("user", "identifier")
|
||||
|
||||
|
||||
class ShelfBook(CollectionItemMixin, BookWyrmModel):
|
||||
""" many to many join table for books and shelves """
|
||||
"""many to many join table for books and shelves"""
|
||||
|
||||
book = fields.ForeignKey(
|
||||
"Edition", on_delete=models.PROTECT, activitypub_field="book"
|
||||
|
@ -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)
|
||||
|
@ -19,7 +19,7 @@ from . import fields
|
||||
|
||||
|
||||
class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
||||
""" any post, like a reply to a review, etc """
|
||||
"""any post, like a reply to a review, etc"""
|
||||
|
||||
user = fields.ForeignKey(
|
||||
"User", on_delete=models.PROTECT, activitypub_field="attributedTo"
|
||||
@ -59,12 +59,12 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
||||
deserialize_reverse_fields = [("attachments", "attachment")]
|
||||
|
||||
class Meta:
|
||||
""" default sorting """
|
||||
"""default sorting"""
|
||||
|
||||
ordering = ("-published_date",)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
""" save and notify """
|
||||
"""save and notify"""
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
notification_model = apps.get_model("bookwyrm.Notification", require_ready=True)
|
||||
@ -98,7 +98,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
||||
)
|
||||
|
||||
def delete(self, *args, **kwargs): # pylint: disable=unused-argument
|
||||
""" "delete" a status """
|
||||
""" "delete" a status"""
|
||||
if hasattr(self, "boosted_status"):
|
||||
# okay but if it's a boost really delete it
|
||||
super().delete(*args, **kwargs)
|
||||
@ -109,7 +109,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
||||
|
||||
@property
|
||||
def recipients(self):
|
||||
""" tagged users who definitely need to get this status in broadcast """
|
||||
"""tagged users who definitely need to get this status in broadcast"""
|
||||
mentions = [u for u in self.mention_users.all() if not u.local]
|
||||
if (
|
||||
hasattr(self, "reply_parent")
|
||||
@ -121,7 +121,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
||||
|
||||
@classmethod
|
||||
def ignore_activity(cls, activity): # pylint: disable=too-many-return-statements
|
||||
""" keep notes if they are replies to existing statuses """
|
||||
"""keep notes if they are replies to existing statuses"""
|
||||
if activity.type == "Announce":
|
||||
try:
|
||||
boosted = activitypub.resolve_remote_id(
|
||||
@ -163,16 +163,16 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
||||
|
||||
@property
|
||||
def status_type(self):
|
||||
""" expose the type of status for the ui using activity type """
|
||||
"""expose the type of status for the ui using activity type"""
|
||||
return self.activity_serializer.__name__
|
||||
|
||||
@property
|
||||
def boostable(self):
|
||||
""" you can't boost dms """
|
||||
"""you can't boost dms"""
|
||||
return self.privacy in ["unlisted", "public"]
|
||||
|
||||
def to_replies(self, **kwargs):
|
||||
""" helper function for loading AP serialized replies to a status """
|
||||
"""helper function for loading AP serialized replies to a status"""
|
||||
return self.to_ordered_collection(
|
||||
self.replies(self),
|
||||
remote_id="%s/replies" % self.remote_id,
|
||||
@ -181,7 +181,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
||||
).serialize()
|
||||
|
||||
def to_activity_dataclass(self, pure=False): # pylint: disable=arguments-differ
|
||||
""" return tombstone if the status is deleted """
|
||||
"""return tombstone if the status is deleted"""
|
||||
if self.deleted:
|
||||
return activitypub.Tombstone(
|
||||
id=self.remote_id,
|
||||
@ -210,16 +210,16 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
||||
return activity
|
||||
|
||||
def to_activity(self, pure=False): # pylint: disable=arguments-differ
|
||||
""" json serialized activitypub class """
|
||||
"""json serialized activitypub class"""
|
||||
return self.to_activity_dataclass(pure=pure).serialize()
|
||||
|
||||
|
||||
class GeneratedNote(Status):
|
||||
""" these are app-generated messages about user activity """
|
||||
"""these are app-generated messages about user activity"""
|
||||
|
||||
@property
|
||||
def pure_content(self):
|
||||
""" indicate the book in question for mastodon (or w/e) users """
|
||||
"""indicate the book in question for mastodon (or w/e) users"""
|
||||
message = self.content
|
||||
books = ", ".join(
|
||||
'<a href="%s">"%s"</a>' % (book.remote_id, book.title)
|
||||
@ -232,7 +232,7 @@ class GeneratedNote(Status):
|
||||
|
||||
|
||||
class Comment(Status):
|
||||
""" like a review but without a rating and transient """
|
||||
"""like a review but without a rating and transient"""
|
||||
|
||||
book = fields.ForeignKey(
|
||||
"Edition", on_delete=models.PROTECT, activitypub_field="inReplyToBook"
|
||||
@ -253,7 +253,7 @@ class Comment(Status):
|
||||
|
||||
@property
|
||||
def pure_content(self):
|
||||
""" indicate the book in question for mastodon (or w/e) users """
|
||||
"""indicate the book in question for mastodon (or w/e) users"""
|
||||
return '%s<p>(comment on <a href="%s">"%s"</a>)</p>' % (
|
||||
self.content,
|
||||
self.book.remote_id,
|
||||
@ -265,7 +265,7 @@ class Comment(Status):
|
||||
|
||||
|
||||
class Quotation(Status):
|
||||
""" like a review but without a rating and transient """
|
||||
"""like a review but without a rating and transient"""
|
||||
|
||||
quote = fields.HtmlField()
|
||||
book = fields.ForeignKey(
|
||||
@ -274,7 +274,7 @@ class Quotation(Status):
|
||||
|
||||
@property
|
||||
def pure_content(self):
|
||||
""" indicate the book in question for mastodon (or w/e) users """
|
||||
"""indicate the book in question for mastodon (or w/e) users"""
|
||||
quote = re.sub(r"^<p>", '<p>"', self.quote)
|
||||
quote = re.sub(r"</p>$", '"</p>', quote)
|
||||
return '%s <p>-- <a href="%s">"%s"</a></p>%s' % (
|
||||
@ -289,7 +289,7 @@ class Quotation(Status):
|
||||
|
||||
|
||||
class Review(Status):
|
||||
""" a book review """
|
||||
"""a book review"""
|
||||
|
||||
name = fields.CharField(max_length=255, null=True)
|
||||
book = fields.ForeignKey(
|
||||
@ -306,7 +306,7 @@ class Review(Status):
|
||||
|
||||
@property
|
||||
def pure_name(self):
|
||||
""" clarify review names for mastodon serialization """
|
||||
"""clarify review names for mastodon serialization"""
|
||||
template = get_template("snippets/generated_status/review_pure_name.html")
|
||||
return template.render(
|
||||
{"book": self.book, "rating": self.rating, "name": self.name}
|
||||
@ -314,7 +314,7 @@ class Review(Status):
|
||||
|
||||
@property
|
||||
def pure_content(self):
|
||||
""" indicate the book in question for mastodon (or w/e) users """
|
||||
"""indicate the book in question for mastodon (or w/e) users"""
|
||||
return self.content
|
||||
|
||||
activity_serializer = activitypub.Review
|
||||
@ -322,7 +322,7 @@ class Review(Status):
|
||||
|
||||
|
||||
class ReviewRating(Review):
|
||||
""" a subtype of review that only contains a rating """
|
||||
"""a subtype of review that only contains a rating"""
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.rating:
|
||||
@ -339,7 +339,7 @@ class ReviewRating(Review):
|
||||
|
||||
|
||||
class Boost(ActivityMixin, Status):
|
||||
""" boost'ing a post """
|
||||
"""boost'ing a post"""
|
||||
|
||||
boosted_status = fields.ForeignKey(
|
||||
"Status",
|
||||
@ -350,7 +350,7 @@ class Boost(ActivityMixin, Status):
|
||||
activity_serializer = activitypub.Announce
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
""" save and notify """
|
||||
"""save and notify"""
|
||||
# This constraint can't work as it would cross tables.
|
||||
# class Meta:
|
||||
# unique_together = ('user', 'boosted_status')
|
||||
@ -374,7 +374,7 @@ class Boost(ActivityMixin, Status):
|
||||
)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
""" delete and un-notify """
|
||||
"""delete and un-notify"""
|
||||
notification_model = apps.get_model("bookwyrm.Notification", require_ready=True)
|
||||
notification_model.objects.filter(
|
||||
user=self.boosted_status.user,
|
||||
@ -385,7 +385,7 @@ class Boost(ActivityMixin, Status):
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
""" the user field is "actor" here instead of "attributedTo" """
|
||||
"""the user field is "actor" here instead of "attributedTo" """
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
reserve_fields = ["user", "boosted_status", "published_date", "privacy"]
|
||||
|
@ -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)
|
||||
|
||||
|
Reference in New Issue
Block a user