Merge branch 'main' into production

This commit is contained in:
Mouse Reeve 2020-11-12 14:40:49 -08:00
commit e9ed457012
16 changed files with 149 additions and 91 deletions

View file

@ -8,6 +8,10 @@ from django.db.models.fields.related_descriptors \
import ForwardManyToOneDescriptor import ForwardManyToOneDescriptor
class ActivitySerializerError(ValueError):
''' routine problems serializing activitypub json '''
class ActivityEncoder(JSONEncoder): class ActivityEncoder(JSONEncoder):
''' used to convert an Activity object into json ''' ''' used to convert an Activity object into json '''
def default(self, o): def default(self, o):
@ -66,7 +70,8 @@ class ActivityObject:
value = kwargs[field.name] value = kwargs[field.name]
except KeyError: except KeyError:
if field.default == MISSING: if field.default == MISSING:
raise TypeError('Missing required field: %s' % field.name) raise ActivitySerializerError(\
'Missing required field: %s' % field.name)
value = field.default value = field.default
setattr(self, field.name, value) setattr(self, field.name, value)
@ -74,7 +79,7 @@ class ActivityObject:
def to_model(self, model, instance=None): def to_model(self, model, instance=None):
''' convert from an activity to a model instance ''' ''' convert from an activity to a model instance '''
if not isinstance(self, model.activity_serializer): if not isinstance(self, model.activity_serializer):
raise TypeError('Wrong activity type for model') raise ActivitySerializerError('Wrong activity type for model')
# check for an existing instance, if we're not updating a known obj # check for an existing instance, if we're not updating a known obj
if not instance: if not instance:
@ -136,6 +141,7 @@ def resolve_foreign_key(model, remote_id):
).first() ).first()
if not result: if not result:
raise ValueError('Could not resolve remote_id in %s model: %s' % \ raise ActivitySerializerError(
'Could not resolve remote_id in %s model: %s' % \
(model.__name__, remote_id)) (model.__name__, remote_id))
return result return result

View file

@ -79,10 +79,17 @@ class Connector(AbstractConnector):
cover_data = data.get('attachment') cover_data = data.get('attachment')
if not cover_data: if not cover_data:
return None return None
cover_url = cover_data[0].get('url') try:
response = requests.get(cover_url) cover_url = cover_data[0].get('url')
except IndexError:
return None
try:
response = requests.get(cover_url)
except ConnectionError:
return None
if not response.ok: if not response.ok:
response.raise_for_status() return None
image_name = str(uuid4()) + '.' + cover_url.split('.')[-1] image_name = str(uuid4()) + '.' + cover_url.split('.')[-1]
image_content = ContentFile(response.content) image_content = ContentFile(response.content)

View file

@ -177,10 +177,9 @@ class Connector(AbstractConnector):
''' load that author ''' ''' load that author '''
if not re.match(r'^OL\d+A$', olkey): if not re.match(r'^OL\d+A$', olkey):
raise ValueError('Invalid OpenLibrary author ID') raise ValueError('Invalid OpenLibrary author ID')
try: author = models.Author.objects.filter(openlibrary_key=olkey).first()
return models.Author.objects.get(openlibrary_key=olkey) if author:
except models.Author.DoesNotExist: return author
pass
url = '%s/authors/%s.json' % (self.base_url, olkey) url = '%s/authors/%s.json' % (self.base_url, olkey)
data = get_data(url) data = get_data(url)

View file

@ -20,7 +20,7 @@ def create_job(user, csv_file, include_reviews, privacy):
) )
for index, entry in enumerate(list(csv.DictReader(csv_file))[:MAX_ENTRIES]): for index, entry in enumerate(list(csv.DictReader(csv_file))[:MAX_ENTRIES]):
if not all(x in entry for x in ('ISBN13', 'Title', 'Author')): if not all(x in entry for x in ('ISBN13', 'Title', 'Author')):
raise ValueError("Author, title, and isbn must be in data.") raise ValueError('Author, title, and isbn must be in data.')
ImportItem(job=job, index=index, data=entry).save() ImportItem(job=job, index=index, data=entry).save()
return job return job
@ -41,8 +41,11 @@ def import_data(job_id):
for item in job.items.all(): for item in job.items.all():
try: try:
item.resolve() item.resolve()
except HTTPError: except:
pass item.fail_reason = 'Error loading book'
item.save()
continue
if item.book: if item.book:
item.save() item.save()
results.append(item) results.append(item)
@ -51,7 +54,7 @@ def import_data(job_id):
outgoing.handle_imported_book( outgoing.handle_imported_book(
job.user, item, job.include_reviews, job.privacy) job.user, item, job.include_reviews, job.privacy)
else: else:
item.fail_reason = "Could not find a match for book" item.fail_reason = 'Could not find a match for book'
item.save() item.save()
finally: finally:
create_notification(job.user, 'IMPORT', related_import=job) create_notification(job.user, 'IMPORT', related_import=job)

View file

@ -238,7 +238,12 @@ def handle_create(activity):
@app.task @app.task
def handle_delete_status(activity): def handle_delete_status(activity):
''' remove a status ''' ''' remove a status '''
status_id = activity['object']['id'] try:
status_id = activity['object']['id']
except TypeError:
# this isn't a great fix, because you hit this when mastadon
# is trying to delete a user.
return
try: try:
status = models.Status.objects.select_subclasses().get( status = models.Status.objects.select_subclasses().get(
remote_id=status_id remote_id=status_id
@ -282,7 +287,11 @@ def handle_unfavorite(activity):
@app.task @app.task
def handle_boost(activity): def handle_boost(activity):
''' someone gave us a boost! ''' ''' someone gave us a boost! '''
boost = activitypub.Boost(**activity).to_model(models.Boost) try:
boost = activitypub.Boost(**activity).to_model(models.Boost)
except activitypub.ActivitySerializerError:
# this probably just means we tried to boost an unknown status
return
if not boost.user.local: if not boost.user.local:
status_builder.create_notification( status_builder.create_notification(

View file

@ -37,10 +37,14 @@ def get_or_create_remote_user(actor):
def fetch_user_data(actor): def fetch_user_data(actor):
''' load the user's info from the actor url ''' ''' load the user's info from the actor url '''
response = requests.get( try:
actor, response = requests.get(
headers={'Accept': 'application/activity+json'} actor,
) headers={'Accept': 'application/activity+json'}
)
except ConnectionError:
return None
if not response.ok: if not response.ok:
response.raise_for_status() response.raise_for_status()
data = response.json() data = response.json()
@ -83,7 +87,10 @@ def get_avatar(data):
@app.task @app.task
def get_remote_reviews(user_id): def get_remote_reviews(user_id):
''' ingest reviews by a new remote bookwyrm user ''' ''' ingest reviews by a new remote bookwyrm user '''
user = models.User.objects.get(id=user_id) try:
user = models.User.objects.get(id=user_id)
except models.User.DoesNotExist:
return
outbox_page = user.outbox + '?page=true' outbox_page = user.outbox + '?page=true'
response = requests.get( response = requests.get(
outbox_page, outbox_page,

View file

@ -20,6 +20,7 @@ function reply(e) {
return true; return true;
} }
function rate_stars(e) { function rate_stars(e) {
e.preventDefault(); e.preventDefault();
ajaxPost(e.target); ajaxPost(e.target);

View file

@ -55,7 +55,7 @@
<div class="block"> <div class="block">
<h3 class="field is-grouped">{% include 'snippets/stars.html' with rating=rating %} ({{ reviews|length }} review{{ reviews|length|pluralize }})</h3> <h3 class="field is-grouped">{% include 'snippets/stars.html' with rating=rating %} ({{ reviews|length }} review{{ reviews|length|pluralize }})</h3>
{% include 'snippets/book_description.html' %} {% include 'snippets/trimmed_text.html' with full=book|book_description %}
{% if book.parent_work.edition_set.count > 1 %} {% if book.parent_work.edition_set.count > 1 %}
<p><a href="/book/{{ book.parent_work.id }}/editions">{{ book.parent_work.edition_set.count }} editions</a></p> <p><a href="/book/{{ book.parent_work.id }}/editions">{{ book.parent_work.edition_set.count }} editions</a></p>
@ -77,12 +77,12 @@
{% endif %} {% endif %}
</dl> </dl>
<div class="field is-grouped"> <div class="field is-grouped">
<label class="button is-small" for="edit-readthrough-{{ readthrough.id }}"> <label class="button is-small" for="edit-readthrough-{{ readthrough.id }}" role="button" tabindex="0">
<span class="icon icon-pencil"> <span class="icon icon-pencil">
<span class="is-sr-only">Edit read-through dates</span> <span class="is-sr-only">Edit read-through dates</span>
</span> </span>
</label> </label>
<label class="button is-small" for="delete-readthrough-{{ readthrough.id }}"> <label class="button is-small" for="delete-readthrough-{{ readthrough.id }}" role="button" tabindex="0">
<span class="icon icon-x"> <span class="icon icon-x">
<span class="is-sr-only">Delete this read-through</span> <span class="is-sr-only">Delete this read-through</span>
</span> </span>

View file

@ -14,7 +14,8 @@
</label> </label>
</div> </div>
<div class="field"> <div class="field">
<label class="label">Privacy setting for imported reviews <label class="label">
<p>Privacy setting for imported reviews:</p>
{% include 'snippets/privacy_select.html' with no_label=True %} {% include 'snippets/privacy_select.html' with no_label=True %}
</label> </label>
</div> </div>

View file

@ -6,29 +6,44 @@
<h1 class="title">Import Status</h1> <h1 class="title">Import Status</h1>
<p> <p>
Import started: {{ job.created_date | naturaltime }} Import started: {{ job.created_date | naturaltime }}
</p>
{% if task.successful %}
<p> <p>
{% if task.ready %} Import completed: {{ task.date_done | naturaltime }}
Import completed: {{ task.date_done | naturaltime }} </p>
{% if task.failed %} {% elif task.failed %}
<h3><span style="background-color: #ffaaaa;">TASK FAILED</span></h3> <div class="notification is-danger">TASK FAILED</div>
<p>
{{ task.info }}
{% endif %} {% endif %}
</div> </div>
<div class="block"> <div class="block">
{% if job.import_status %} {% if not task.ready %}
{% include 'snippets/status.html' with status=job.import_status %}
{% endif %}
{% else %}
Import still in progress. Import still in progress.
<p> <p>
(Hit reload to update!) (Hit reload to update!)
</p>
{% endif %} {% endif %}
</div> </div>
{% if failed_items %}
<div class="block"> <div class="block">
<h2 class="title is-4">Failed to load</h2>
<ul>
{% for item in failed_items %}
<li>
Line {{ item.index }}:
<strong>{{ item.data|dict_key:'Title' }}</strong> by
{{ item.data|dict_key:'Author' }}
({{ item.fail_reason }})
</li>
{% endfor %}
</ul>
</div>
{% endif %}
<div class="block">
<h2 class="title is-4">Successfully imported</h2>
<table class="table"> <table class="table">
<tr> <tr>
<th> <th>
@ -59,9 +74,10 @@
{{ item.data|dict_key:'Author' }} {{ item.data|dict_key:'Author' }}
</td> </td>
<td> <td>
{% if item.book %}✓ {% if item.book %}
{% elif item.fail_reason %} <span class="icon icon-check">
{{ item.fail_reason }} <span class="is-sr-only">Imported</span>
</span>
{% endif %} {% endif %}
</td> </td>
</tr> </tr>

View file

@ -1,26 +0,0 @@
{% load fr_display %}
{% with book.id|uuid as uuid %}
{% with book|book_description as full %}
{% if full %}
{% with full|text_overflow as trimmed %}
{% if trimmed != full %}
<div>
<input type="radio" name="show-hide-{{ book.id }}-{{ uuid }}" id="show-{{ book.id }}-{{ uuid }}" class="toggle-control" checked>
<blockquote class="content toggle-content hidden">{{ trimmed }}
<label class="button is-small" for="hide-{{ book.id }}-{{ uuid }}"><div role="button" tabindex="0">show more</div></label>
</blockquote>
</div>
<div>
<input type="radio" name="show-hide-{{ book.id }}-{{ uuid }}" id="hide-{{ book.id }}-{{ uuid }}" class="toggle-control">
<blockquote class="content toggle-content hidden">{{ full }}
<label class="button is-small" for="show-{{ book.id }}-{{ uuid }}"><div role="button" tabindex="0">show less</div></label>
</blockquote>
</div>
{% else %}
<blockquote class="content">{{ full }}
</blockquote>
{% endif %}
{% endwith %}
{% endif %}
{% endwith %}
{% endwith %}

View file

@ -1,3 +1,4 @@
{% load fr_display %}
<div class="columns"> <div class="columns">
<div class="column is-narrow"> <div class="column is-narrow">
<div> <div>
@ -7,6 +8,6 @@
</div> </div>
<div class="column"> <div class="column">
<h3 class="title is-6">{% include 'snippets/book_titleby.html' with book=book %}</h3> <h3 class="title is-6">{% include 'snippets/book_titleby.html' with book=book %}</h3>
{% include 'snippets/book_description.html' with book=book %} {% include 'snippets/trimmed_text.html' with full=book|book_description %}
</div> </div>
</div> </div>

View file

@ -18,24 +18,13 @@
</div> </div>
<footer> <footer>
{% if request.user.is_authenticated %}
<input class="toggle-control" type="checkbox" name="show-comment-{{ status.id }}" id="show-comment-{{ status.id }}">
<div class="toggle-content hidden">
<div class="card-footer">
<div class="card-footer-item">
{% include 'snippets/reply_form.html' with status=status %}
</div>
</div>
</div>
{% endif %}
<div class="card-footer"> <div class="card-footer">
<div class="card-footer-item"> <div class="card-footer-item">
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
<label class="button is-small" for="show-comment-{{ status.id }}"> <label class="button is-small" for="show-comment-{{ status.id }}">
<div role="button" tabindex="0"> <div role="button" tabindex="0">
<span class="icon icon-comment"> <span class="icon icon-comment">
<span class="is-sr-only">Comment</span> <span class="is-sr-only">Reply</span>
</span> </span>
</div> </div>
</label> </label>
@ -44,17 +33,17 @@
{% else %} {% else %}
<a href="/login"> <a href="/login">
<span class="icon icon-comment"> <span class="icon icon-comment">
<span class="is-sr-only">Comment</span> <span class="is-sr-only">Reply</span>
</span> </span>
<span class="icon icon-boost"> <span class="icon icon-boost">
<span class="is-sr-only">Boost status</span> <span class="is-sr-only">Boost status</span>
</span> </span>
<span class="icon icon-heart"> <span class="icon icon-heart">
<span class="is-sr-only">Like status</span> <span class="is-sr-only">Like status</span>
</span> </span>
</a> </a>
{% endif %} {% endif %}
</div> </div>
@ -77,6 +66,17 @@
{% endif %} {% endif %}
</div> </div>
{% if request.user.is_authenticated %}
<input class="toggle-control" type="checkbox" name="show-comment-{{ status.id }}" id="show-comment-{{ status.id }}">
<div class="toggle-content hidden">
<div class="card-footer">
<div class="card-footer-item">
{% include 'snippets/reply_form.html' with status=status %}
</div>
</div>
</div>
{% endif %}
{% if status.user == request.user %} {% if status.user == request.user %}
<div> <div>
<input class="toggle-control" type="checkbox" name="more-info-{{ status.id }}" id="more-info-{{ status.id }}"> <input class="toggle-control" type="checkbox" name="more-info-{{ status.id }}" id="more-info-{{ status.id }}">

View file

@ -16,7 +16,7 @@
{% endif %} {% endif %}
{% if status.content and status.status_type != 'GeneratedNote' and status.status_type != 'Boost' %} {% if status.content and status.status_type != 'GeneratedNote' and status.status_type != 'Boost' %}
<blockquote>{{ status.content | safe }}</blockquote> {% include 'snippets/trimmed_text.html' with full=status.content|safe %}
{% endif %} {% endif %}
</div> </div>

View file

@ -0,0 +1,26 @@
{% load fr_display %}
{% with 0|uuid as uuid %}
{% if full %}
{% with full|text_overflow as trimmed %}
{% if trimmed != full %}
<div>
<input type="radio" name="show-hide-{{ uuid }}" id="show-{{ uuid }}" class="toggle-control" checked>
<blockquote class="content toggle-content hidden">{{ trimmed }}
<label class="button is-small" for="hide-{{ uuid }}"><div role="button" tabindex="0">show more</div></label>
</blockquote>
</div>
<div>
<input type="radio" name="show-hide-{{ uuid }}" id="hide-{{ uuid }}" class="toggle-control">
<blockquote class="content toggle-content hidden">{{ full }}
<label class="button is-small" for="show-{{ uuid }}"><div role="button" tabindex="0">show less</div></label>
</blockquote>
</div>
{% else %}
<blockquote class="content">{{ full }}</blockquote>
{% endif %}
{% endwith %}
{% endif %}
{% endwith %}

View file

@ -209,10 +209,14 @@ def import_status(request, job_id):
if job.user != request.user: if job.user != request.user:
raise PermissionDenied raise PermissionDenied
task = app.AsyncResult(job.task_id) task = app.AsyncResult(job.task_id)
items = job.items.order_by('index').all()
failed_items = [i for i in items if i.fail_reason]
items = [i for i in items if not i.fail_reason]
return TemplateResponse(request, 'import_status.html', { return TemplateResponse(request, 'import_status.html', {
'title': 'Import Status', 'title': 'Import Status',
'job': job, 'job': job,
'items': job.items.order_by('index').all(), 'items': items,
'failed_items': failed_items,
'task': task 'task': task
}) })
@ -511,7 +515,11 @@ def book_page(request, book_id):
except ValueError: except ValueError:
page = 1 page = 1
book = models.Book.objects.select_subclasses().get(id=book_id) try:
book = models.Book.objects.select_subclasses().get(id=book_id)
except models.Book.DoesNotExist:
return HttpResponseNotFound()
if is_api_request(request): if is_api_request(request):
return JsonResponse(book.to_activity(), encoder=ActivityEncoder) return JsonResponse(book.to_activity(), encoder=ActivityEncoder)