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
e9ed457012
16 changed files with 149 additions and 91 deletions
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 %}
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 }}">
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
26
bookwyrm/templates/snippets/trimmed_text.html
Normal file
26
bookwyrm/templates/snippets/trimmed_text.html
Normal 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 %}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue