mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-10-31 22:19:00 +00:00
Merge branch 'main' into production
This commit is contained in:
commit
12dbd47207
22 changed files with 529 additions and 63 deletions
|
@ -41,6 +41,7 @@ class Edition(Book):
|
|||
pages: int = None
|
||||
physicalFormat: str = ''
|
||||
publishers: List[str] = field(default_factory=lambda: [])
|
||||
editionRank: int = 0
|
||||
|
||||
type: str = 'Edition'
|
||||
|
||||
|
|
|
@ -7,7 +7,11 @@ class Connector(AbstractMinimalConnector):
|
|||
''' this is basically just for search '''
|
||||
|
||||
def get_or_create_book(self, remote_id):
|
||||
return activitypub.resolve_remote_id(models.Edition, remote_id)
|
||||
edition = activitypub.resolve_remote_id(models.Edition, remote_id)
|
||||
work = edition.parent_work
|
||||
work.default_edition = work.get_default_edition()
|
||||
work.save()
|
||||
return edition
|
||||
|
||||
def parse_search_data(self, data):
|
||||
return data
|
||||
|
|
27
bookwyrm/migrations/0035_edition_edition_rank.py
Normal file
27
bookwyrm/migrations/0035_edition_edition_rank.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
# Generated by Django 3.0.7 on 2021-01-11 17:18
|
||||
|
||||
import bookwyrm.models.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def set_rank(app_registry, schema_editor):
|
||||
db_alias = schema_editor.connection.alias
|
||||
books = app_registry.get_model('bookwyrm', 'Edition')
|
||||
for book in books.objects.using(db_alias):
|
||||
book.edition_rank = book.get_rank
|
||||
book.save()
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bookwyrm', '0034_importjob_complete'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='edition',
|
||||
name='edition_rank',
|
||||
field=bookwyrm.models.fields.IntegerField(default=0),
|
||||
),
|
||||
migrations.RunPython(set_rank),
|
||||
]
|
|
@ -52,10 +52,11 @@ def execute_after_save(sender, instance, created, *args, **kwargs):
|
|||
instance.save()
|
||||
|
||||
|
||||
def unfurl_related_field(related_field):
|
||||
def unfurl_related_field(related_field, sort_field=None):
|
||||
''' load reverse lookups (like public key owner or Status attachment '''
|
||||
if hasattr(related_field, 'all'):
|
||||
return [unfurl_related_field(i) for i in related_field.all()]
|
||||
return [unfurl_related_field(i) for i in related_field.order_by(
|
||||
sort_field).all()]
|
||||
if related_field.reverse_unfurl:
|
||||
return related_field.field_to_activity()
|
||||
return related_field.remote_id
|
||||
|
@ -145,11 +146,11 @@ class ActivitypubMixin:
|
|||
|
||||
if hasattr(self, 'serialize_reverse_fields'):
|
||||
# for example, editions of a work
|
||||
for model_field_name, activity_field_name in \
|
||||
for model_field_name, activity_field_name, sort_field in \
|
||||
self.serialize_reverse_fields:
|
||||
related_field = getattr(self, model_field_name)
|
||||
activity[activity_field_name] = \
|
||||
unfurl_related_field(related_field)
|
||||
unfurl_related_field(related_field, sort_field)
|
||||
|
||||
if not activity.get('id'):
|
||||
activity['id'] = self.get_remote_id()
|
||||
|
|
|
@ -122,20 +122,29 @@ class Work(OrderedCollectionPageMixin, Book):
|
|||
load_remote=False
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
''' 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 '''
|
||||
return self.default_edition or self.editions.first()
|
||||
return self.default_edition or self.editions.order_by(
|
||||
'-edition_rank'
|
||||
).first()
|
||||
|
||||
def to_edition_list(self, **kwargs):
|
||||
''' an ordered collection of editions '''
|
||||
return self.to_ordered_collection(
|
||||
self.editions.order_by('-updated_date').all(),
|
||||
self.editions.order_by('-edition_rank').all(),
|
||||
remote_id='%s/editions' % self.remote_id,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
activity_serializer = activitypub.Work
|
||||
serialize_reverse_fields = [('editions', 'editions')]
|
||||
serialize_reverse_fields = [('editions', 'editions', '-edition_rank')]
|
||||
deserialize_reverse_fields = [('editions', 'editions')]
|
||||
|
||||
|
||||
|
@ -164,17 +173,39 @@ class Edition(Book):
|
|||
parent_work = fields.ForeignKey(
|
||||
'Work', on_delete=models.PROTECT, null=True,
|
||||
related_name='editions', activitypub_field='work')
|
||||
edition_rank = fields.IntegerField(default=0)
|
||||
|
||||
activity_serializer = activitypub.Edition
|
||||
name_field = 'title'
|
||||
|
||||
@property
|
||||
def get_rank(self):
|
||||
''' calculate how complete the data is on this edition '''
|
||||
if self.parent_work and self.parent_work.default_edition == self:
|
||||
# default edition has the highest rank
|
||||
return 20
|
||||
rank = 0
|
||||
rank += int(bool(self.cover)) * 3
|
||||
rank += int(bool(self.isbn_13))
|
||||
rank += int(bool(self.isbn_10))
|
||||
rank += int(bool(self.oclc_number))
|
||||
rank += int(bool(self.pages))
|
||||
rank += int(bool(self.physical_format))
|
||||
rank += int(bool(self.description))
|
||||
# max rank is 9
|
||||
return rank
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
''' calculate isbn 10/13 '''
|
||||
''' 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)
|
||||
if self.isbn_10 and not self.isbn_13:
|
||||
self.isbn_13 = isbn_10_to_13(self.isbn_10)
|
||||
|
||||
# set rank
|
||||
self.edition_rank = self.get_rank
|
||||
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel):
|
|||
@property
|
||||
def collection_queryset(self):
|
||||
''' list of books for this shelf, overrides OrderedCollectionMixin '''
|
||||
return self.books
|
||||
return self.books.all()
|
||||
|
||||
def get_remote_id(self):
|
||||
''' shelf identifier instead of id '''
|
||||
|
|
|
@ -47,7 +47,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
|||
objects = InheritanceManager()
|
||||
|
||||
activity_serializer = activitypub.Note
|
||||
serialize_reverse_fields = [('attachments', 'attachment')]
|
||||
serialize_reverse_fields = [('attachments', 'attachment', 'id')]
|
||||
deserialize_reverse_fields = [('attachments', 'attachment')]
|
||||
|
||||
@classmethod
|
||||
|
|
|
@ -200,7 +200,7 @@ class KeyPair(ActivitypubMixin, BookWyrmModel):
|
|||
blank=True, null=True, activitypub_field='publicKeyPem')
|
||||
|
||||
activity_serializer = activitypub.PublicKey
|
||||
serialize_reverse_fields = [('owner', 'owner')]
|
||||
serialize_reverse_fields = [('owner', 'owner', 'id')]
|
||||
|
||||
def get_remote_id(self):
|
||||
# self.owner is set by the OneToOneField on User
|
||||
|
|
|
@ -180,6 +180,13 @@ def handle_imported_book(user, item, include_reviews, privacy):
|
|||
broadcast(user, shelf_book.to_add_activity(user), privacy=privacy)
|
||||
|
||||
for read in item.reads:
|
||||
# check for an existing readthrough with the same dates
|
||||
if models.ReadThrough.objects.filter(
|
||||
user=user, book=item.book,
|
||||
start_date=read.start_date,
|
||||
finish_date=read.finish_date
|
||||
).exists():
|
||||
continue
|
||||
read.book = item.book
|
||||
read.user = user
|
||||
read.save()
|
||||
|
@ -296,7 +303,7 @@ def find_mentions(content):
|
|||
def format_links(content):
|
||||
''' detect and format links '''
|
||||
return re.sub(
|
||||
r'([^(href=")]|^)(https?:\/\/(%s([\w\.\-_\/+&\?=:;,])*))' % \
|
||||
r'([^(href=")]|^|\()(https?:\/\/(%s([\w\.\-_\/+&\?=:;,])*))' % \
|
||||
regex.domain,
|
||||
r'\g<1><a href="\g<2>">\g<3></a>',
|
||||
content)
|
||||
|
@ -325,12 +332,13 @@ def handle_favorite(user, status):
|
|||
fav_activity = favorite.to_activity()
|
||||
broadcast(
|
||||
user, fav_activity, privacy='direct', direct_recipients=[status.user])
|
||||
create_notification(
|
||||
status.user,
|
||||
'FAVORITE',
|
||||
related_user=user,
|
||||
related_status=status
|
||||
)
|
||||
if status.user.local:
|
||||
create_notification(
|
||||
status.user,
|
||||
'FAVORITE',
|
||||
related_user=user,
|
||||
related_status=status
|
||||
)
|
||||
|
||||
|
||||
def handle_unfavorite(user, status):
|
||||
|
@ -348,6 +356,15 @@ def handle_unfavorite(user, status):
|
|||
favorite.delete()
|
||||
broadcast(user, fav_activity, direct_recipients=[status.user])
|
||||
|
||||
# check for notification
|
||||
if status.user.local:
|
||||
notification = models.Notification.objects.filter(
|
||||
user=status.user, related_user=user,
|
||||
related_status=status, notification_type='FAVORITE'
|
||||
).first()
|
||||
if notification:
|
||||
notification.delete()
|
||||
|
||||
|
||||
def handle_boost(user, status):
|
||||
''' a user wishes to boost a status '''
|
||||
|
@ -368,12 +385,13 @@ def handle_boost(user, status):
|
|||
boost_activity = boost.to_activity()
|
||||
broadcast(user, boost_activity)
|
||||
|
||||
create_notification(
|
||||
status.user,
|
||||
'BOOST',
|
||||
related_user=user,
|
||||
related_status=status
|
||||
)
|
||||
if status.user.local:
|
||||
create_notification(
|
||||
status.user,
|
||||
'BOOST',
|
||||
related_user=user,
|
||||
related_status=status
|
||||
)
|
||||
|
||||
|
||||
def handle_unboost(user, status):
|
||||
|
@ -386,12 +404,11 @@ def handle_unboost(user, status):
|
|||
boost.delete()
|
||||
broadcast(user, activity)
|
||||
|
||||
|
||||
def handle_update_book_data(user, item):
|
||||
''' broadcast the news about our book '''
|
||||
broadcast(user, item.to_update_activity(user))
|
||||
|
||||
|
||||
def handle_update_user(user):
|
||||
''' broadcast editing a user's profile '''
|
||||
broadcast(user, user.to_update_activity(user))
|
||||
# delete related notification
|
||||
if status.user.local:
|
||||
notification = models.Notification.objects.filter(
|
||||
user=status.user, related_user=user,
|
||||
related_status=status, notification_type='BOOST'
|
||||
).first()
|
||||
if notification:
|
||||
notification.delete()
|
||||
|
|
|
@ -74,10 +74,11 @@ input.toggle-control:checked ~ .modal.toggle-content {
|
|||
}
|
||||
.cover-container.is-large {
|
||||
height: max-content;
|
||||
max-width: 500px;
|
||||
max-width: 330px;
|
||||
}
|
||||
.cover-container.is-large img {
|
||||
max-height: 500px;
|
||||
height: auto;
|
||||
}
|
||||
.cover-container.is-medium {
|
||||
height: 150px;
|
||||
|
|
|
@ -151,6 +151,26 @@
|
|||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="block">
|
||||
<div>
|
||||
<input type="radio" class="toggle-control" name="add-readthrough-form" id="hide-create-readthrough" checked>
|
||||
<div class="toggle-content hidden">
|
||||
<label class="button" for="add-readthrough" class="button" role="button" tabindex="0">Add read dates</label>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<input type="radio" class="toggle-control" id="add-readthrough" name="add-readthrough-form">
|
||||
<div class="toggle-content hidden box">
|
||||
<form name="add-readthrough" action="/create-readthrough" method="post">
|
||||
{% include 'snippets/readthrough_form.html' with readthrough=None %}
|
||||
<div class="field is-grouped">
|
||||
<button class="button is-primary" type="submit">Create</button>
|
||||
<label class="button" for="hide-create-readthrough" role="button" tabindex="0">Cancel</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if request.user.is_authenticated %}
|
||||
<div class="box">
|
||||
|
|
|
@ -106,6 +106,8 @@
|
|||
<label class="label" for="id_name">Name:</label>
|
||||
<input type="text" name="name" maxlength="100" class="input" required="true" value="{{ shelf.name }}" id="id_name">
|
||||
</div>
|
||||
{% else %}
|
||||
<input type="hidden" name="name" required="true" value="{{ shelf.name }}">
|
||||
{% endif %}
|
||||
|
||||
<label class="label">
|
||||
|
|
|
@ -11,9 +11,9 @@ Follow request already sent.
|
|||
{% csrf_token %}
|
||||
<input type="hidden" name="user" value="{{ user.username }}">
|
||||
{% if user.manually_approves_followers %}
|
||||
<button class="button is-small is-primary" type="submit">Send follow request</button>
|
||||
<button class="button is-small is-link" type="submit">Send follow request</button>
|
||||
{% else %}
|
||||
<button class="button is-small is-primary" type="submit">Follow</button>
|
||||
<button class="button is-small is-link" type="submit">Follow</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
<form action="/unfollow/" method="POST" onsubmit="interact(event)" class="follow-{{ user.id }} {% if not request.user in user.followers.all %}hidden{%endif %}" data-id="follow-{{ user.id }}">
|
||||
|
|
|
@ -36,20 +36,7 @@
|
|||
<div class="toggle-content hidden">
|
||||
<div class="box">
|
||||
<form name="edit-readthrough" action="/edit-readthrough" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="id" value="{{ readthrough.id }}">
|
||||
<div class="field">
|
||||
<label class="label">
|
||||
Started reading
|
||||
<input type="date" name="start_date" class="input" id="id_start_date-{{ readthrough.id }}" value="{{ readthrough.start_date | date:"Y-m-d" }}">
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">
|
||||
Finished reading
|
||||
<input type="date" name="finish_date" class="input" id="id_finish_date-{{ readthrough.id }}" value="{{ readthrough.finish_date | date:"Y-m-d" }}">
|
||||
</label>
|
||||
</div>
|
||||
{% include 'snippets/readthrough_form.html' with readthrough=readthrough %}
|
||||
<div class="field is-grouped">
|
||||
<button class="button is-primary" type="submit">Save</button>
|
||||
<label class="button" for="show-readthrough-{{ readthrough.id }}" role="button" tabindex="0">Cancel</label>
|
||||
|
|
15
bookwyrm/templates/snippets/readthrough_form.html
Normal file
15
bookwyrm/templates/snippets/readthrough_form.html
Normal file
|
@ -0,0 +1,15 @@
|
|||
{% csrf_token %}
|
||||
<input type="hidden" name="id" value="{{ readthrough.id }}">
|
||||
<input type="hidden" name="book" value="{{ book.id }}">
|
||||
<div class="field">
|
||||
<label class="label">
|
||||
Started reading
|
||||
<input type="date" name="start_date" class="input" id="id_start_date-{{ readthrough.id }}" value="{{ readthrough.start_date | date:"Y-m-d" }}">
|
||||
</label>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">
|
||||
Finished reading
|
||||
<input type="date" name="finish_date" class="input" id="id_finish_date-{{ readthrough.id }}" value="{{ readthrough.finish_date | date:"Y-m-d" }}">
|
||||
</label>
|
||||
</div>
|
|
@ -82,3 +82,19 @@ class Book(TestCase):
|
|||
self.assertEqual(book.edition_info, 'worm, Glorbish language, 2020')
|
||||
self.assertEqual(
|
||||
book.alt_text, 'Test Edition cover (worm, Glorbish language, 2020)')
|
||||
|
||||
|
||||
def test_get_rank(self):
|
||||
''' sets the data quality index for the book '''
|
||||
# basic rank
|
||||
self.assertEqual(self.first_edition.edition_rank, 0)
|
||||
|
||||
self.first_edition.description = 'hi'
|
||||
self.first_edition.save()
|
||||
self.assertEqual(self.first_edition.edition_rank, 1)
|
||||
|
||||
# default edition
|
||||
self.work.default_edition = self.first_edition
|
||||
self.work.save()
|
||||
self.first_edition.refresh_from_db()
|
||||
self.assertEqual(self.first_edition.edition_rank, 20)
|
||||
|
|
|
@ -260,6 +260,47 @@ class Outgoing(TestCase):
|
|||
self.assertEqual(self.shelf.books.count(), 0)
|
||||
|
||||
|
||||
def test_handle_reading_status_to_read(self):
|
||||
''' posts shelve activities '''
|
||||
shelf = self.local_user.shelf_set.get(identifier='to-read')
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
outgoing.handle_reading_status(
|
||||
self.local_user, shelf, self.book, 'public')
|
||||
status = models.GeneratedNote.objects.get()
|
||||
self.assertEqual(status.user, self.local_user)
|
||||
self.assertEqual(status.mention_books.first(), self.book)
|
||||
self.assertEqual(status.content, 'wants to read')
|
||||
|
||||
def test_handle_reading_status_reading(self):
|
||||
''' posts shelve activities '''
|
||||
shelf = self.local_user.shelf_set.get(identifier='reading')
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
outgoing.handle_reading_status(
|
||||
self.local_user, shelf, self.book, 'public')
|
||||
status = models.GeneratedNote.objects.get()
|
||||
self.assertEqual(status.user, self.local_user)
|
||||
self.assertEqual(status.mention_books.first(), self.book)
|
||||
self.assertEqual(status.content, 'started reading')
|
||||
|
||||
def test_handle_reading_status_read(self):
|
||||
''' posts shelve activities '''
|
||||
shelf = self.local_user.shelf_set.get(identifier='read')
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
outgoing.handle_reading_status(
|
||||
self.local_user, shelf, self.book, 'public')
|
||||
status = models.GeneratedNote.objects.get()
|
||||
self.assertEqual(status.user, self.local_user)
|
||||
self.assertEqual(status.mention_books.first(), self.book)
|
||||
self.assertEqual(status.content, 'finished reading')
|
||||
|
||||
def test_handle_reading_status_other(self):
|
||||
''' posts shelve activities '''
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
outgoing.handle_reading_status(
|
||||
self.local_user, self.shelf, self.book, 'public')
|
||||
self.assertFalse(models.GeneratedNote.objects.exists())
|
||||
|
||||
|
||||
def test_handle_imported_book(self):
|
||||
''' goodreads import added a book, this adds related connections '''
|
||||
shelf = self.local_user.shelf_set.filter(identifier='read').first()
|
||||
|
@ -323,6 +364,37 @@ class Outgoing(TestCase):
|
|||
self.assertEqual(readthrough.finish_date.day, 25)
|
||||
|
||||
|
||||
def test_handle_import_twice(self):
|
||||
''' re-importing books '''
|
||||
shelf = self.local_user.shelf_set.filter(identifier='read').first()
|
||||
import_job = models.ImportJob.objects.create(user=self.local_user)
|
||||
datafile = pathlib.Path(__file__).parent.joinpath('data/goodreads.csv')
|
||||
csv_file = open(datafile, 'r')
|
||||
for index, entry in enumerate(list(csv.DictReader(csv_file))):
|
||||
import_item = models.ImportItem.objects.create(
|
||||
job_id=import_job.id, index=index, data=entry, book=self.book)
|
||||
break
|
||||
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
outgoing.handle_imported_book(
|
||||
self.local_user, import_item, False, 'public')
|
||||
outgoing.handle_imported_book(
|
||||
self.local_user, import_item, False, 'public')
|
||||
|
||||
shelf.refresh_from_db()
|
||||
self.assertEqual(shelf.books.first(), self.book)
|
||||
|
||||
readthrough = models.ReadThrough.objects.get(user=self.local_user)
|
||||
self.assertEqual(readthrough.book, self.book)
|
||||
# I can't remember how to create dates and I don't want to look it up.
|
||||
self.assertEqual(readthrough.start_date.year, 2020)
|
||||
self.assertEqual(readthrough.start_date.month, 10)
|
||||
self.assertEqual(readthrough.start_date.day, 21)
|
||||
self.assertEqual(readthrough.finish_date.year, 2020)
|
||||
self.assertEqual(readthrough.finish_date.month, 10)
|
||||
self.assertEqual(readthrough.finish_date.day, 25)
|
||||
|
||||
|
||||
def test_handle_imported_book_review(self):
|
||||
''' goodreads review import '''
|
||||
import_job = models.ImportJob.objects.create(user=self.local_user)
|
||||
|
@ -361,6 +433,17 @@ class Outgoing(TestCase):
|
|||
).exists())
|
||||
|
||||
|
||||
def test_handle_delete_status(self):
|
||||
''' marks a status as deleted '''
|
||||
status = models.Status.objects.create(
|
||||
user=self.local_user, content='hi')
|
||||
self.assertFalse(status.deleted)
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
outgoing.handle_delete_status(self.local_user, status)
|
||||
status.refresh_from_db()
|
||||
self.assertTrue(status.deleted)
|
||||
|
||||
|
||||
def test_handle_status(self):
|
||||
''' create a status '''
|
||||
form = forms.CommentForm({
|
||||
|
@ -497,6 +580,9 @@ class Outgoing(TestCase):
|
|||
self.assertEqual(
|
||||
outgoing.format_links(url),
|
||||
'<a href="%s">www.fish.com/</a>' % url)
|
||||
self.assertEqual(
|
||||
outgoing.format_links('(%s)' % url),
|
||||
'(<a href="%s">www.fish.com/</a>)' % url)
|
||||
url = 'https://archive.org/details/dli.granth.72113/page/n25/mode/2up'
|
||||
self.assertEqual(
|
||||
outgoing.format_links(url),
|
||||
|
@ -509,3 +595,111 @@ class Outgoing(TestCase):
|
|||
outgoing.format_links(url),
|
||||
'<a href="%s">openlibrary.org/search' \
|
||||
'?q=arkady+strugatsky&mode=everything</a>' % url)
|
||||
|
||||
|
||||
def test_to_markdown(self):
|
||||
''' this is mostly handled in other places, but nonetheless '''
|
||||
text = '_hi_ and http://fish.com is <marquee>rad</marquee>'
|
||||
result = outgoing.to_markdown(text)
|
||||
self.assertEqual(
|
||||
result,
|
||||
'<p><em>hi</em> and <a href="http://fish.com">fish.com</a> ' \
|
||||
'is rad</p>')
|
||||
|
||||
|
||||
def test_handle_favorite(self):
|
||||
''' create and broadcast faving a status '''
|
||||
status = models.Status.objects.create(
|
||||
user=self.local_user, content='hi')
|
||||
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
outgoing.handle_favorite(self.remote_user, status)
|
||||
fav = models.Favorite.objects.get()
|
||||
self.assertEqual(fav.status, status)
|
||||
self.assertEqual(fav.user, self.remote_user)
|
||||
|
||||
notification = models.Notification.objects.get()
|
||||
self.assertEqual(notification.notification_type, 'FAVORITE')
|
||||
self.assertEqual(notification.user, self.local_user)
|
||||
self.assertEqual(notification.related_user, self.remote_user)
|
||||
|
||||
|
||||
def test_handle_unfavorite(self):
|
||||
''' unfav a status '''
|
||||
status = models.Status.objects.create(
|
||||
user=self.local_user, content='hi')
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
outgoing.handle_favorite(self.remote_user, status)
|
||||
|
||||
self.assertEqual(models.Favorite.objects.count(), 1)
|
||||
self.assertEqual(models.Notification.objects.count(), 1)
|
||||
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
outgoing.handle_unfavorite(self.remote_user, status)
|
||||
self.assertEqual(models.Favorite.objects.count(), 0)
|
||||
self.assertEqual(models.Notification.objects.count(), 0)
|
||||
|
||||
|
||||
def test_handle_boost(self):
|
||||
''' boost a status '''
|
||||
status = models.Status.objects.create(
|
||||
user=self.local_user, content='hi')
|
||||
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
outgoing.handle_boost(self.remote_user, status)
|
||||
|
||||
boost = models.Boost.objects.get()
|
||||
self.assertEqual(boost.boosted_status, status)
|
||||
self.assertEqual(boost.user, self.remote_user)
|
||||
self.assertEqual(boost.privacy, 'public')
|
||||
|
||||
notification = models.Notification.objects.get()
|
||||
self.assertEqual(notification.notification_type, 'BOOST')
|
||||
self.assertEqual(notification.user, self.local_user)
|
||||
self.assertEqual(notification.related_user, self.remote_user)
|
||||
self.assertEqual(notification.related_status, status)
|
||||
|
||||
def test_handle_boost_unlisted(self):
|
||||
''' boost a status '''
|
||||
status = models.Status.objects.create(
|
||||
user=self.local_user, content='hi', privacy='unlisted')
|
||||
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
outgoing.handle_boost(self.remote_user, status)
|
||||
|
||||
boost = models.Boost.objects.get()
|
||||
self.assertEqual(boost.privacy, 'unlisted')
|
||||
|
||||
def test_handle_boost_private(self):
|
||||
''' boost a status '''
|
||||
status = models.Status.objects.create(
|
||||
user=self.local_user, content='hi', privacy='followers')
|
||||
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
outgoing.handle_boost(self.remote_user, status)
|
||||
self.assertFalse(models.Boost.objects.exists())
|
||||
|
||||
def test_handle_boost_twice(self):
|
||||
''' boost a status '''
|
||||
status = models.Status.objects.create(
|
||||
user=self.local_user, content='hi')
|
||||
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
outgoing.handle_boost(self.remote_user, status)
|
||||
outgoing.handle_boost(self.remote_user, status)
|
||||
self.assertEqual(models.Boost.objects.count(), 1)
|
||||
|
||||
|
||||
def test_handle_unboost(self):
|
||||
''' undo a boost '''
|
||||
status = models.Status.objects.create(
|
||||
user=self.local_user, content='hi')
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
outgoing.handle_boost(self.remote_user, status)
|
||||
|
||||
self.assertEqual(models.Boost.objects.count(), 1)
|
||||
self.assertEqual(models.Notification.objects.count(), 1)
|
||||
with patch('bookwyrm.broadcast.broadcast_task.delay'):
|
||||
outgoing.handle_unboost(self.remote_user, status)
|
||||
self.assertEqual(models.Boost.objects.count(), 0)
|
||||
self.assertEqual(models.Notification.objects.count(), 0)
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
''' test for app action functionality '''
|
||||
from unittest.mock import patch
|
||||
|
||||
import dateutil
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.http.response import Http404
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from django.utils import timezone
|
||||
|
||||
from bookwyrm import forms, models, view_actions as actions
|
||||
from bookwyrm.settings import DOMAIN
|
||||
|
@ -358,6 +360,126 @@ class ViewActions(TestCase):
|
|||
self.assertEqual(resp.template_name, 'edit_author.html')
|
||||
|
||||
|
||||
def test_edit_shelf_privacy(self):
|
||||
''' set name or privacy on shelf '''
|
||||
shelf = self.local_user.shelf_set.get(identifier='to-read')
|
||||
self.assertEqual(shelf.privacy, 'public')
|
||||
|
||||
request = self.factory.post(
|
||||
'', {
|
||||
'privacy': 'unlisted',
|
||||
'user': self.local_user.id,
|
||||
'name': 'To Read',
|
||||
})
|
||||
request.user = self.local_user
|
||||
actions.edit_shelf(request, shelf.id)
|
||||
shelf.refresh_from_db()
|
||||
|
||||
self.assertEqual(shelf.privacy, 'unlisted')
|
||||
|
||||
|
||||
def test_edit_shelf_name(self):
|
||||
''' change the name of an editable shelf '''
|
||||
shelf = models.Shelf.objects.create(
|
||||
name='Test Shelf', user=self.local_user)
|
||||
self.assertEqual(shelf.privacy, 'public')
|
||||
|
||||
request = self.factory.post(
|
||||
'', {
|
||||
'privacy': 'public',
|
||||
'user': self.local_user.id,
|
||||
'name': 'cool name'
|
||||
})
|
||||
request.user = self.local_user
|
||||
actions.edit_shelf(request, shelf.id)
|
||||
shelf.refresh_from_db()
|
||||
|
||||
self.assertEqual(shelf.name, 'cool name')
|
||||
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 '''
|
||||
shelf = self.local_user.shelf_set.get(identifier='to-read')
|
||||
self.assertEqual(shelf.privacy, 'public')
|
||||
|
||||
request = self.factory.post(
|
||||
'', {
|
||||
'privacy': 'public',
|
||||
'user': self.local_user.id,
|
||||
'name': 'cool name'
|
||||
})
|
||||
request.user = self.local_user
|
||||
actions.edit_shelf(request, shelf.id)
|
||||
|
||||
self.assertEqual(shelf.name, 'To Read')
|
||||
|
||||
|
||||
def test_edit_readthrough(self):
|
||||
''' 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)
|
||||
request = self.factory.post(
|
||||
'', {
|
||||
'start_date': '2017-01-01',
|
||||
'finish_date': '2018-03-07',
|
||||
'book': '',
|
||||
'id': readthrough.id,
|
||||
})
|
||||
request.user = self.local_user
|
||||
|
||||
actions.edit_readthrough(request)
|
||||
readthrough.refresh_from_db()
|
||||
self.assertEqual(readthrough.start_date.year, 2017)
|
||||
self.assertEqual(readthrough.start_date.month, 1)
|
||||
self.assertEqual(readthrough.start_date.day, 1)
|
||||
self.assertEqual(readthrough.finish_date.year, 2018)
|
||||
self.assertEqual(readthrough.finish_date.month, 3)
|
||||
self.assertEqual(readthrough.finish_date.day, 7)
|
||||
self.assertEqual(readthrough.book, self.book)
|
||||
|
||||
|
||||
def test_delete_readthrough(self):
|
||||
''' remove a readthrough '''
|
||||
readthrough = models.ReadThrough.objects.create(
|
||||
book=self.book, user=self.local_user)
|
||||
models.ReadThrough.objects.create(
|
||||
book=self.book, user=self.local_user)
|
||||
request = self.factory.post(
|
||||
'', {
|
||||
'id': readthrough.id,
|
||||
})
|
||||
request.user = self.local_user
|
||||
|
||||
actions.delete_readthrough(request)
|
||||
self.assertFalse(
|
||||
models.ReadThrough.objects.filter(id=readthrough.id).exists())
|
||||
|
||||
|
||||
def test_create_readthrough(self):
|
||||
''' adding new read dates '''
|
||||
request = self.factory.post(
|
||||
'', {
|
||||
'start_date': '2017-01-01',
|
||||
'finish_date': '2018-03-07',
|
||||
'book': self.book.id,
|
||||
'id': '',
|
||||
})
|
||||
request.user = self.local_user
|
||||
|
||||
actions.create_readthrough(request)
|
||||
readthrough = models.ReadThrough.objects.get()
|
||||
self.assertEqual(readthrough.start_date.year, 2017)
|
||||
self.assertEqual(readthrough.start_date.month, 1)
|
||||
self.assertEqual(readthrough.start_date.day, 1)
|
||||
self.assertEqual(readthrough.finish_date.year, 2018)
|
||||
self.assertEqual(readthrough.finish_date.month, 3)
|
||||
self.assertEqual(readthrough.finish_date.day, 7)
|
||||
self.assertEqual(readthrough.book, self.book)
|
||||
self.assertEqual(readthrough.user, self.local_user)
|
||||
|
||||
|
||||
def test_tag(self):
|
||||
''' add a tag to a book '''
|
||||
request = self.factory.post(
|
||||
|
|
|
@ -570,6 +570,16 @@ class Views(TestCase):
|
|||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
|
||||
request = self.factory.get('/?page=1')
|
||||
request.user = self.local_user
|
||||
with patch('bookwyrm.views.is_api_request') as is_api:
|
||||
is_api.return_value = True
|
||||
result = views.shelf_page(
|
||||
request, self.local_user.username, shelf.identifier)
|
||||
self.assertIsInstance(result, ActivitypubResponse)
|
||||
self.assertEqual(result.status_code, 200)
|
||||
|
||||
|
||||
def test_is_bookwyrm_request(self):
|
||||
''' checks if a request came from a bookwyrm instance '''
|
||||
request = self.factory.get('', {'q': 'Test Book'})
|
||||
|
|
|
@ -110,6 +110,7 @@ urlpatterns = [
|
|||
re_path(r'^switch-edition/?$', actions.switch_edition),
|
||||
re_path(r'^edit-readthrough/?$', actions.edit_readthrough),
|
||||
re_path(r'^delete-readthrough/?$', actions.delete_readthrough),
|
||||
re_path(r'^create-readthrough/?$', actions.create_readthrough),
|
||||
|
||||
re_path(r'^rate/?$', actions.rate),
|
||||
re_path(r'^review/?$', actions.review),
|
||||
|
|
|
@ -210,7 +210,7 @@ def edit_profile(request):
|
|||
user.avatar.save(filename, ContentFile(output.getvalue()))
|
||||
user.save()
|
||||
|
||||
outgoing.handle_update_user(user)
|
||||
broadcast(user, user.to_update_activity(user))
|
||||
return redirect('/user/%s' % request.user.localname)
|
||||
|
||||
|
||||
|
@ -241,7 +241,7 @@ def edit_book(request, book_id):
|
|||
return TemplateResponse(request, 'edit_book.html', data)
|
||||
book = form.save()
|
||||
|
||||
outgoing.handle_update_book_data(request.user, book)
|
||||
broadcast(request.user, book.to_update_activity(request.user))
|
||||
return redirect('/book/%s' % book.id)
|
||||
|
||||
|
||||
|
@ -288,7 +288,7 @@ def upload_cover(request, book_id):
|
|||
book.cover = form.files['cover']
|
||||
book.save()
|
||||
|
||||
outgoing.handle_update_book_data(request.user, book)
|
||||
broadcast(request.user, book.to_update_activity(request.user))
|
||||
return redirect('/book/%s' % book.id)
|
||||
|
||||
|
||||
|
@ -307,7 +307,7 @@ def add_description(request, book_id):
|
|||
book.description = description
|
||||
book.save()
|
||||
|
||||
outgoing.handle_update_book_data(request.user, book)
|
||||
broadcast(request.user, book.to_update_activity(request.user))
|
||||
return redirect('/book/%s' % book.id)
|
||||
|
||||
|
||||
|
@ -328,7 +328,7 @@ def edit_author(request, author_id):
|
|||
return TemplateResponse(request, 'edit_author.html', data)
|
||||
author = form.save()
|
||||
|
||||
outgoing.handle_update_book_data(request.user, author)
|
||||
broadcast(request.user, author.to_update_activity(request.user))
|
||||
return redirect('/author/%s' % author.id)
|
||||
|
||||
|
||||
|
@ -352,13 +352,14 @@ def edit_shelf(request, shelf_id):
|
|||
shelf = get_object_or_404(models.Shelf, id=shelf_id)
|
||||
if request.user != shelf.user:
|
||||
return HttpResponseBadRequest()
|
||||
if not shelf.editable and request.POST.get('name') != shelf.name:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
form = forms.ShelfForm(request.POST, instance=shelf)
|
||||
if not form.is_valid():
|
||||
return redirect(request.headers.get('Referer', '/'))
|
||||
return redirect(shelf.local_path)
|
||||
shelf = form.save()
|
||||
return redirect('/user/%s/shelf/%s' % \
|
||||
(request.user.localname, shelf.identifier))
|
||||
return redirect(shelf.local_path)
|
||||
|
||||
|
||||
@login_required
|
||||
|
@ -402,7 +403,7 @@ def shelve(request):
|
|||
request.user,
|
||||
desired_shelf,
|
||||
book,
|
||||
privacy='public'
|
||||
privacy=desired_shelf.privacy
|
||||
)
|
||||
|
||||
return redirect('/')
|
||||
|
@ -522,6 +523,18 @@ def delete_readthrough(request):
|
|||
return redirect(request.headers.get('Referer', '/'))
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
def create_readthrough(request):
|
||||
''' 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:
|
||||
return redirect(book.local_path)
|
||||
readthrough.save()
|
||||
return redirect(request.headers.get('Referer', '/'))
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
def rate(request):
|
||||
|
@ -824,7 +837,7 @@ def update_readthrough(request, book=None, create=True):
|
|||
start_date = request.POST.get('start_date')
|
||||
if start_date:
|
||||
try:
|
||||
start_date = dateutil.parser.parse(start_date)
|
||||
start_date = timezone.make_aware(dateutil.parser.parse(start_date))
|
||||
readthrough.start_date = start_date
|
||||
except ParserError:
|
||||
pass
|
||||
|
@ -832,9 +845,13 @@ def update_readthrough(request, book=None, create=True):
|
|||
finish_date = request.POST.get('finish_date')
|
||||
if finish_date:
|
||||
try:
|
||||
finish_date = dateutil.parser.parse(finish_date)
|
||||
finish_date = timezone.make_aware(
|
||||
dateutil.parser.parse(finish_date))
|
||||
readthrough.finish_date = finish_date
|
||||
except ParserError:
|
||||
pass
|
||||
|
||||
if not readthrough.start_date and not readthrough.finish_date:
|
||||
return None
|
||||
|
||||
return readthrough
|
||||
|
|
|
@ -736,7 +736,7 @@ def editions_page(request, book_id):
|
|||
|
||||
data = {
|
||||
'title': 'Editions of %s' % work.title,
|
||||
'editions': work.editions.all(),
|
||||
'editions': work.editions.order_by('-edition_rank').all(),
|
||||
'work': work,
|
||||
}
|
||||
return TemplateResponse(request, 'editions.html', data)
|
||||
|
|
Loading…
Reference in a new issue