Merge branch 'main' into production

This commit is contained in:
Mouse Reeve 2020-11-11 11:10:15 -08:00
commit 0eacee02ac
35 changed files with 505 additions and 225 deletions

View file

@ -159,3 +159,8 @@ class CreateInviteForm(CustomForm):
choices=[(i, "%d uses" % (i,)) for i in [1, 5, 10, 25, 50, 100]] choices=[(i, "%d uses" % (i,)) for i in [1, 5, 10, 25, 50, 100]]
+ [(None, 'Unlimited')]) + [(None, 'Unlimited')])
} }
class ShelfForm(CustomForm):
class Meta:
model = models.Shelf
fields = ['user', 'name', 'privacy']

View file

@ -0,0 +1,18 @@
# Generated by Django 3.0.7 on 2020-11-10 20:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('bookwyrm', '0008_work_default_edition'),
]
operations = [
migrations.AddField(
model_name='shelf',
name='privacy',
field=models.CharField(choices=[('public', 'Public'), ('unlisted', 'Unlisted'), ('followers', 'Followers'), ('direct', 'Direct')], default='public', max_length=255),
),
]

View file

@ -1,8 +1,9 @@
''' puttin' books on shelves ''' ''' puttin' books on shelves '''
import re
from django.db import models from django.db import models
from bookwyrm import activitypub from bookwyrm import activitypub
from .base_model import BookWyrmModel, OrderedCollectionMixin from .base_model import BookWyrmModel, OrderedCollectionMixin, PrivacyLevels
class Shelf(OrderedCollectionMixin, BookWyrmModel): class Shelf(OrderedCollectionMixin, BookWyrmModel):
@ -11,6 +12,11 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel):
identifier = models.CharField(max_length=100) identifier = models.CharField(max_length=100)
user = models.ForeignKey('User', on_delete=models.PROTECT) user = models.ForeignKey('User', on_delete=models.PROTECT)
editable = models.BooleanField(default=True) editable = models.BooleanField(default=True)
privacy = models.CharField(
max_length=255,
default='public',
choices=PrivacyLevels.choices
)
books = models.ManyToManyField( books = models.ManyToManyField(
'Edition', 'Edition',
symmetrical=False, symmetrical=False,
@ -18,6 +24,15 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel):
through_fields=('shelf', 'book') through_fields=('shelf', 'book')
) )
def save(self, *args, **kwargs):
''' set the identifier '''
saved = super().save(*args, **kwargs)
if not self.identifier:
slug = re.sub(r'[^\w]', '', self.name).lower()
self.identifier = '%s-%d' % (slug, self.id)
return super().save(*args, **kwargs)
return saved
@property @property
def collection_queryset(self): def collection_queryset(self):
''' list of books for this shelf, overrides OrderedCollectionMixin ''' ''' list of books for this shelf, overrides OrderedCollectionMixin '''

View file

@ -6,6 +6,8 @@ from environs import Env
env = Env() env = Env()
DOMAIN = env('DOMAIN') DOMAIN = env('DOMAIN')
PAGE_LENGTH = env('PAGE_LENGTH', 15)
# celery # celery
CELERY_BROKER = env('CELERY_BROKER') CELERY_BROKER = env('CELERY_BROKER')
CELERY_RESULT_BACKEND = env('CELERY_RESULT_BACKEND') CELERY_RESULT_BACKEND = env('CELERY_RESULT_BACKEND')

View file

@ -37,4 +37,5 @@
<glyph unicode="&#xe9d8;" glyph-name="star-half" d="M1024 562.95l-353.78 51.408-158.22 320.582-158.216-320.582-353.784-51.408 256-249.538-60.432-352.352 316.432 166.358 316.432-166.358-60.434 352.352 256.002 249.538zM512 206.502l-0.942-0.496 0.942 570.768 111.736-226.396 249.836-36.304-180.788-176.222 42.678-248.83-223.462 117.48z" /> <glyph unicode="&#xe9d8;" glyph-name="star-half" d="M1024 562.95l-353.78 51.408-158.22 320.582-158.216-320.582-353.784-51.408 256-249.538-60.432-352.352 316.432 166.358 316.432-166.358-60.434 352.352 256.002 249.538zM512 206.502l-0.942-0.496 0.942 570.768 111.736-226.396 249.836-36.304-180.788-176.222 42.678-248.83-223.462 117.48z" />
<glyph unicode="&#xe9d9;" glyph-name="star-full" d="M1024 562.95l-353.78 51.408-158.22 320.582-158.216-320.582-353.784-51.408 256-249.538-60.432-352.352 316.432 166.358 316.432-166.358-60.434 352.352 256.002 249.538z" /> <glyph unicode="&#xe9d9;" glyph-name="star-full" d="M1024 562.95l-353.78 51.408-158.22 320.582-158.216-320.582-353.784-51.408 256-249.538-60.432-352.352 316.432 166.358 316.432-166.358-60.434 352.352 256.002 249.538z" />
<glyph unicode="&#xe9da;" glyph-name="heart" d="M755.188 896c-107.63 0-200.258-87.554-243.164-179-42.938 91.444-135.578 179-243.216 179-148.382 0-268.808-120.44-268.808-268.832 0-301.846 304.5-380.994 512.022-679.418 196.154 296.576 511.978 387.206 511.978 679.418 0 148.392-120.43 268.832-268.812 268.832z" /> <glyph unicode="&#xe9da;" glyph-name="heart" d="M755.188 896c-107.63 0-200.258-87.554-243.164-179-42.938 91.444-135.578 179-243.216 179-148.382 0-268.808-120.44-268.808-268.832 0-301.846 304.5-380.994 512.022-679.418 196.154 296.576 511.978 387.206 511.978 679.418 0 148.392-120.43 268.832-268.812 268.832z" />
<glyph unicode="&#xea0a;" glyph-name="plus" d="M992 576h-352v352c0 17.672-14.328 32-32 32h-192c-17.672 0-32-14.328-32-32v-352h-352c-17.672 0-32-14.328-32-32v-192c0-17.672 14.328-32 32-32h352v-352c0-17.672 14.328-32 32-32h192c17.672 0 32 14.328 32 32v352h352c17.672 0 32 14.328 32 32v192c0 17.672-14.328 32-32 32z" />
</font></defs></svg> </font></defs></svg>

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -1,10 +1,10 @@
@font-face { @font-face {
font-family: 'icomoon'; font-family: 'icomoon';
src: url('fonts/icomoon.eot?jhaogg'); src: url('fonts/icomoon.eot?rd4abb');
src: url('fonts/icomoon.eot?jhaogg#iefix') format('embedded-opentype'), src: url('fonts/icomoon.eot?rd4abb#iefix') format('embedded-opentype'),
url('fonts/icomoon.ttf?jhaogg') format('truetype'), url('fonts/icomoon.ttf?rd4abb') format('truetype'),
url('fonts/icomoon.woff?jhaogg') format('woff'), url('fonts/icomoon.woff?rd4abb') format('woff'),
url('fonts/icomoon.svg?jhaogg#icomoon') format('svg'); url('fonts/icomoon.svg?rd4abb#icomoon') format('svg');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
font-display: block; font-display: block;
@ -115,3 +115,6 @@
.icon-heart:before { .icon-heart:before {
content: "\e9da"; content: "\e9da";
} }
.icon-plus:before {
content: "\ea0a";
}

View file

@ -28,7 +28,7 @@
{% if request.user.is_authenticated and not book.cover %} {% if request.user.is_authenticated and not book.cover %}
<div class="box p-2"> <div class="box p-2">
<form name="add-cover" method="POST" action="/upload_cover/{{ book.id }}" enctype="multipart/form-data"> <form name="add-cover" method="POST" action="/upload-cover/{{ book.id }}" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
<div class="field"> <div class="field">
<label class="label" for="id_cover">Cover:</label> <label class="label" for="id_cover">Cover:</label>

View file

@ -20,7 +20,7 @@
</div> </div>
</div> </div>
<form class="block" name="edit-book" action="/edit_book/{{ book.id }}" method="post" enctype="multipart/form-data"> <form class="block" name="edit-book" action="/edit-book/{{ book.id }}" method="post" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
<div class="block"> <div class="block">
<h2 class="title is-4">Data sync <h2 class="title is-4">Data sync

View file

@ -6,7 +6,7 @@
{% if form.non_field_errors %} {% if form.non_field_errors %}
<p class="notification is-danger">{{ form.non_field_errors }}</p> <p class="notification is-danger">{{ form.non_field_errors }}</p>
{% endif %} {% endif %}
<form name="edit-profile" action="/edit_profile/" method="post" enctype="multipart/form-data"> <form name="edit-profile" action="/edit-profile/" method="post" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
<p class="block"> <p class="block">
<label class="label" for="id_avatar">Avatar:</label> <label class="label" for="id_avatar">Avatar:</label>

View file

@ -1,6 +1,16 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% load fr_display %} {% load fr_display %}
{% block content %} {% block content %}
<div class="block">
<h1 class="title">
{% if is_self %}Your
{% else %}
{% include 'snippets/username.html' with user=user possessive=True %}
{% endif %}
followers
</h1>
</div>
{% include 'snippets/user_header.html' with user=user %} {% include 'snippets/user_header.html' with user=user %}
<div class="block"> <div class="block">

View file

@ -1,6 +1,16 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% load fr_display %} {% load fr_display %}
{% block content %} {% block content %}
<div class="block">
<h1 class="title">
Users following
{% if is_self %}you
{% else %}
{% include 'snippets/username.html' with user=user %}
{% endif %}
</h1>
</div>
{% include 'snippets/user_header.html' with user=user %} {% include 'snippets/user_header.html' with user=user %}
<div class="block"> <div class="block">

View file

@ -3,7 +3,7 @@
{% block content %} {% block content %}
<div class="block"> <div class="block">
<h1 class="title">Import Books from GoodReads</h1> <h1 class="title">Import Books from GoodReads</h1>
<form name="import" action="/import_data/" method="post" enctype="multipart/form-data"> <form name="import" action="/import-data/" method="post" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
<div class="field"> <div class="field">
{{ import_form.as_p }} {{ import_form.as_p }}
@ -31,7 +31,7 @@
{% endif %} {% endif %}
<ul> <ul>
{% for job in jobs %} {% for job in jobs %}
<li><a href="/import_status/{{ job.id }}">{{ job.created_date | naturaltime }}</a></li> <li><a href="/import-status/{{ job.id }}">{{ job.created_date | naturaltime }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>

View file

@ -27,7 +27,7 @@
<div class="block"> <div class="block">
<h2 class="title is-4">Generate New Invite</h2> <h2 class="title is-4">Generate New Invite</h2>
<form name="invite" action="/create_invite/" method="post"> <form name="invite" action="/create-invite/" method="post">
{% csrf_token %} {% csrf_token %}
<div class="field is-grouped"> <div class="field is-grouped">
<div class="control"> <div class="control">

View file

@ -44,7 +44,7 @@
boosted your <a href="{{ notification.related_status.remote_id}}">status</a> boosted your <a href="{{ notification.related_status.remote_id}}">status</a>
{% endif %} {% endif %}
{% else %} {% else %}
your <a href="/import_status/{{ notification.related_import.id }}">import</a> completed. your <a href="/import-status/{{ notification.related_import.id }}">import</a> completed.
{% endif %} {% endif %}
</p> </p>

View file

@ -26,7 +26,9 @@
<input class="toggle-control" type="radio" name="more-results" id="fewer-results" checked> <input class="toggle-control" type="radio" name="more-results" id="fewer-results" checked>
<div class="toggle-content hidden"> <div class="toggle-content hidden">
<label class="button is-small" for="more-results">Show results from other catalogues</label> <label class="button is-small" for="more-results">
<div role="button" tabindex="0">Show results from other catalogues</div>
</label>
</div> </div>
</div> </div>
{% endif %} {% endif %}
@ -45,7 +47,7 @@
<ul> <ul>
{% for result in result_set.results %} {% for result in result_set.results %}
<li class="pb-4"> <li class="pb-4">
<form action="/resolve_book" method="POST"> <form action="/resolve-book" method="POST">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="remote_id" value="{{ result.key }}"> <input type="hidden" name="remote_id" value="{{ result.key }}">
<div>{% include 'snippets/search_result_text.html' with result=result link=False %}</div> <div>{% include 'snippets/search_result_text.html' with result=result link=False %}</div>
@ -58,7 +60,9 @@
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if local_results.results %} {% if local_results.results %}
<label class="button is-small" for="fewer-results">Hide results from other catalogues</label> <label class="button is-small" for="fewer-results">
<div role="button" tabindex="0">Hide results from other catalogues</div>
</label>
{% endif %} {% endif %}
</div> </div>
</div> </div>

View file

@ -1,20 +1,125 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% load fr_display %} {% load fr_display %}
{% block content %} {% block content %}
<div class="columns">
<div class="column">
<h1 class="title">
{% if is_self %}Your
{% else %}
{% include 'snippets/username.html' with user=user possessive=True %}
{% endif %}
shelves
</h1>
</div>
</div>
{% include 'snippets/user_header.html' with user=user %} {% include 'snippets/user_header.html' with user=user %}
<div class="block"> <div class="block columns">
<div class="tabs"> <div class="column">
<div class="tabs" role="tablist">
<ul> <ul>
{% for shelf_tab in shelves %} {% for shelf_tab in shelves %}
<li class="{% if shelf_tab.identifier == shelf.identifier %}is-active{% endif %}"> <li class="{% if shelf_tab.identifier == shelf.identifier %}is-active{% endif %}">
<a href="/user/{{ user | username }}/shelf/{{ shelf_tab.identifier }}">{{ shelf_tab.name }}</a> <a href="/user/{{ user | username }}/shelf/{{ shelf_tab.identifier }}" role="tab" aria-selected="{% if shelf_tab.identifier == shelf.identifier %}true{% else %}false{% endif %}">{{ shelf_tab.name }}</a>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
</div> </div>
{% if is_self %}
<div class="column is-narrow">
<input type="radio" id="create-shelf-form-hide" name="create-shelf-form" class="toggle-control" checked>
<div class="toggle-content hidden">
<label for="create-shelf-form-show">
<div role="button" tabindex="0">
<span class="icon icon-plus">
<span class="is-sr-only">Create new shelf</span>
</span>
</div>
</label>
</div>
</div>
{% endif %}
</div>
<input type="radio" id="create-shelf-form-show" name="create-shelf-form" class="toggle-control">
<div class="toggle-content hidden">
<div class="box mb-5">
<h2 class="title is-4">Create new shelf</h2>
<form name="create-shelf" action="/create-shelf/" method="post">
{% csrf_token %}
<input type="hidden" name="user" value="{{ request.user.id }}">
<div class="field">
<label class="label" for="id_name_create">Name:</label>
<input type="text" name="name" maxlength="100" class="input" required="true" id="id_name_create">
</div>
<label class="label">
<p>Shelf privacy:</p>
{% include 'snippets/privacy_select.html' with no_label=True %}
</label>
<div class="field is-grouped">
<button class="button is-primary" type="submit">Create shelf</button>
<label role="button" class="button" for="create-shelf-form-hide" tabindex="0">Cancel<label>
</div>
</form>
</div>
</div>
<div class="block columns">
<div class="column">
<h2 class="title is-3">
{{ shelf.name }}
<span class="subtitle">
{% include 'snippets/privacy-icons.html' with item=shelf %}
</span>
</h2>
</div>
{% if is_self %}
<div class="column is-narrow">
<input type="radio" id="edit-shelf-form-hide" name="edit-shelf-form" class="toggle-control" checked>
<div class="toggle-content hidden">
<label for="edit-shelf-form-show">
<div role="button" tabindex="0">
<span class="icon icon-pencil">
<span class="is-sr-only">Edit shelf</span>
</span>
</div>
</label>
</div>
</div>
{% endif %}
</div>
<input type="radio" id="edit-shelf-form-show" name="edit-shelf-form" class="toggle-control">
<div class="toggle-content hidden">
<div class="box mb-5">
<h2 class="title is-4">Edit shelf</h2>
<form name="create-shelf" action="/edit-shelf/{{ shelf.id }}" method="post">
{% csrf_token %}
<input type="hidden" name="user" value="{{ request.user.id }}">
{% if shelf.editable %}
<div class="field">
<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>
{% endif %}
<label class="label">
<p>Shelf privacy:</p>
{% include 'snippets/privacy_select.html' with no_label=True current=shelf.privacy %}
</label>
<div class="field is-grouped">
<button class="button is-primary" type="submit">Update shelf</button>
<label role="button" class="button" for="edit-shelf-form-hide" tabindex="0">Cancel<label>
</div>
</form>
</div>
</div>
<div class="block"> <div class="block">
<div> <div>
{% include 'snippets/shelf.html' with shelf=shelf ratings=ratings %} {% include 'snippets/shelf.html' with shelf=shelf ratings=ratings %}

View file

@ -15,9 +15,8 @@
<fieldset> <fieldset>
<legend class="is-sr-only">Rating</legend> <legend class="is-sr-only">Rating</legend>
<div class="field is-grouped stars form-rate-stars"> <div class="field is-grouped stars form-rate-stars">
<label class="is-sr-only">No rating <label class="is-sr-only" for="no-rating-{{ book.id }}">No rating</label>
<input class="is-sr-only" type="radio" name="rating" value="" checked> <input class="is-sr-only" type="radio" name="rating" value="" id="no-rating-{{ book.id }}" checked>
</label>
{% for i in '12345'|make_list %} {% for i in '12345'|make_list %}
<input class="is-sr-only" id="book{{book.id}}-star-{{ forloop.counter }}" type="radio" name="rating" value="{{ forloop.counter }}"> <input class="is-sr-only" id="book{{book.id}}-star-{{ forloop.counter }}" type="radio" name="rating" value="{{ forloop.counter }}">
<label class="icon icon-star-empty" for="book{{book.id}}-star-{{ forloop.counter }}"> <label class="icon icon-star-empty" for="book{{book.id}}-star-{{ forloop.counter }}">

View file

@ -9,10 +9,9 @@
<label class="delete" for="finish-reading-{{ uuid }}" aria-label="close" role="button"></label> <label class="delete" for="finish-reading-{{ uuid }}" aria-label="close" role="button"></label>
</header> </header>
{% active_read_through book user as readthrough %} {% active_read_through book user as readthrough %}
<form name="finish-reading" action="/finish-reading" method="post"> <form name="finish-reading" action="/finish-reading/{{ book.id }}" method="post">
<section class="modal-card-body"> <section class="modal-card-body">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}">
<input type="hidden" name="id" value="{{ readthrough.id }}"> <input type="hidden" name="id" value="{{ readthrough.id }}">
<div class="field"> <div class="field">
<label class="label"> <label class="label">

View file

@ -1,11 +1,11 @@
{% load fr_display %} {% load fr_display %}
{% if request.user|follow_request_exists:user %} {% if request.user|follow_request_exists:user %}
<form action="/accept_follow_request/" method="POST"> <form action="/accept-follow-request/" method="POST">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="user" value="{{ user.username }}"> <input type="hidden" name="user" value="{{ user.username }}">
<button class="button is-primary is-small" type="submit">Accept</button> <button class="button is-primary is-small" type="submit">Accept</button>
</form> </form>
<form action="/delete_follow_request/" method="POST"> <form action="/delete-follow-request/" method="POST">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="user" value="{{ user.username }}"> <input type="hidden" name="user" value="{{ user.username }}">
<button class="button is-danger is-light is-small" type="submit" class="warning">Delete</button> <button class="button is-danger is-light is-small" type="submit" class="warning">Delete</button>

View file

@ -0,0 +1,18 @@
{% if item.privacy == 'public' %}
<span class="icon icon-globe">
<span class="is-sr-only">Public post</span>
</span>
{% elif item.privacy == 'unlisted' %}
<span class="icon icon-unlock">
<span class="is-sr-only">Unlisted post</span>
</span>
{% elif item.privacy == 'followers' %}
<span class="icon icon-lock">
<span class="is-sr-only">Followers-only post</span>
</span>
{% else %}
<span class="icon icon-envelope">
<span class="is-sr-only">Private post</span>
</span>
{% endif %}

View file

@ -5,10 +5,18 @@
<label class="is-sr-only" for="privacy-{{ uuid }}">Post privacy</label> <label class="is-sr-only" for="privacy-{{ uuid }}">Post privacy</label>
{% endif %} {% endif %}
<select name="privacy" id="privacy-{{ uuid }}"> <select name="privacy" id="privacy-{{ uuid }}">
<option value="public" selected>Public</option> <option value="public" {% if not current or current == 'public' %}selected{% endif %}>
<option value="unlisted">Unlisted</option> Public
<option value="followers">Followers only</option> </option>
<option value="direct">Private</option> <option value="unlisted" {% if current == 'unlisted' %}selected{% endif %}>
Unlisted
</option>
<option value="followers" {% if current == 'followers' %}selected{% endif %}>
Followers only
</option>
<option value="direct" {% if current == 'direct' %}selected{% endif %}>
Private
</option>
</select> </select>
{% endwith %} {% endwith %}
</div> </div>

View file

@ -76,5 +76,15 @@
</table> </table>
{% else %} {% else %}
<p>This shelf is empty.</p> <p>This shelf is empty.</p>
{% if shelf.editable %}
<form name="delete-shelf" action="/delete-shelf/{{ shelf.id }}" method="post">
{% csrf_token %}
<input type="hidden" name="user" value="{{ request.user.id }}">
<button class="button is-danger is-light" type="submit">
Delete shelf
</button>
</form>
{% endif %}
{% endif %} {% endif %}

View file

@ -1,9 +1,14 @@
<div class="dropdown is-hoverable"> <div class="dropdown">
<div class="dropdown-trigger button"> <div class="dropdown-trigger">
<p>Change shelf</p> <label for="shelf-select-dropdown-{{ book.id }}-toggle" role="button" aria-expanded="false" onclick="toggleMenu(this)" tabindex="0" aria-haspopup="true" aria-controls="shelf-select-{{ book.id }}">
<span class="icon icon-arrow-down"></span> <div class="button">
<span>Change shelf</span>
<span class="icon icon-arrow-down" aria-hidden="true"></span>
</div> </div>
<div class="dropdown-menu"> </label>
</div>
<input type="checkbox" class="toggle-control" id="shelf-select-dropdown-{{ book.id }}-toggle">
<div class="dropdown-menu toggle-content hidden" id="shelf-select-{{ book.id }}" role="menu">
<ul class="dropdown-content"> <ul class="dropdown-content">
{% for shelf in request.user.shelf_set.all %} {% for shelf in request.user.shelf_set.all %}
{% if shelf.identifier != current.identifier %} {% if shelf.identifier != current.identifier %}

View file

@ -1,5 +1,5 @@
<div class="stars"> <div class="stars">
<span class="is-sr-only">{{ rating|floatformat }} star{{ rating|floatformat | pluralize }}</span> <span class="is-sr-only">{% if rating %}{{ rating|floatformat }} star{{ rating|floatformat | pluralize }}{% else %}No rating{% endif %}</span>
{% for i in '12345'|make_list %} {% for i in '12345'|make_list %}
<span class="icon icon-star-{% if rating >= forloop.counter %}full{% elif rating|floatformat:0 >= forloop.counter|floatformat:0 %}half{% else %}empty{% endif %}"> <span class="icon icon-star-{% if rating >= forloop.counter %}full{% elif rating|floatformat:0 >= forloop.counter|floatformat:0 %}half{% else %}empty{% endif %}">
</span> </span>

View file

@ -7,10 +7,9 @@
<p class="modal-card-title">Start "{{ book.title }}"</p> <p class="modal-card-title">Start "{{ book.title }}"</p>
<label class="delete" for="start-reading-{{ uuid }}" aria-label="close" role="button" tabindex="0"></label> <label class="delete" for="start-reading-{{ uuid }}" aria-label="close" role="button" tabindex="0"></label>
</header> </header>
<form name="start-reading" action="/start-reading" method="post"> <form name="start-reading" action="/start-reading/{{ book.id }}" method="post">
<section class="modal-card-body"> <section class="modal-card-body">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}">
<div class="field"> <div class="field">
<label class="label"> <label class="label">
Started reading Started reading

View file

@ -33,7 +33,11 @@
{% 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 }}">
<span class="icon icon-comment"><span class="is-sr-only">Comment</span></span> <div role="button" tabindex="0">
<span class="icon icon-comment">
<span class="is-sr-only">Comment</span>
</span>
</div>
</label> </label>
{% include 'snippets/boost_button.html' with status=status %} {% include 'snippets/boost_button.html' with status=status %}
{% include 'snippets/fav_button.html' with status=status %} {% include 'snippets/fav_button.html' with status=status %}
@ -56,23 +60,7 @@
</div> </div>
<div class="card-footer-item"> <div class="card-footer-item">
{% if status.privacy == 'public' %} {% include 'snippets/privacy-icons.html' with item=status %}
<span class="icon icon-globe">
<span class="is-sr-only">Public post</span>
</span>
{% elif status.privacy == 'unlisted' %}
<span class="icon icon-unlock">
<span class="is-sr-only">Unlisted post</span>
</span>
{% elif status.privacy == 'followers' %}
<span class="icon icon-lock">
<span class="is-sr-only">Followers-only post</span>
</span>
{% else %}
<span class="icon icon-envelope">
<span class="is-sr-only">Private post</span>
</span>
{% endif %}
</div> </div>
<div class="card-footer-item"> <div class="card-footer-item">
@ -94,9 +82,8 @@
<div class="toggle-content hidden card-footer"> <div class="toggle-content hidden card-footer">
{% if status.user == request.user %} {% if status.user == request.user %}
<div class="card-footer-item"> <div class="card-footer-item">
<form name="delete-{{status.id}}" action="/delete-status" method="post"> <form name="delete-{{status.id}}" action="/delete-status/{{ status.id }}" method="post">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="status" value="{{ status.id }}">
<button class="button is-danger is-light" type="submit"> <button class="button is-danger is-light" type="submit">
Delete post Delete post
</button> </button>

View file

@ -1,19 +1,6 @@
{% load humanize %} {% load humanize %}
{% load fr_display %} {% load fr_display %}
<div class="block"> <div class="block">
<div class="level">
<h2 class="title">User Profile</h2>
{% if is_self %}
<div class="level-right">
<a href="/user-edit/" class="edit-link">edit
<span class="icon icon-pencil">
<span class="is-sr-only">Edit profile</span>
</span>
</a>
</div>
{% endif %}
</div>
<div class="columns"> <div class="columns">
<div class="column is-narrow"> <div class="column is-narrow">
<div class="media"> <div class="media">

View file

@ -1,6 +1,21 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% block content %} {% block content %}
<div class="columns">
<div class="column">
<h1 class="title">User profile</h1>
</div>
{% if is_self %}
<div class="column is-narrow">
<a href="/user-edit/">
<span class="icon icon-pencil">
<span class="is-sr-only">Edit profile</span>
</span>
</a>
</div>
{% endif %}
</div>
{% include 'snippets/user_header.html' with user=user %} {% include 'snippets/user_header.html' with user=user %}
<div class="block"> <div class="block">
@ -39,6 +54,26 @@
<p>No activities yet!</a> <p>No activities yet!</a>
</div> </div>
{% endif %} {% endif %}
<nav class="pagination" role="navigation" aria-label="pagination">
{% if prev %}
<p class="pagination-previous">
<a href="{{ prev }}">
<span class="icon icon-arrow-left"></span>
Previous
</a>
</p>
{% endif %}
{% if next %}
<p class="pagination-next">
<a href="{{ next }}">
Next
<span class="icon icon-arrow-right"></span>
</a>
</p>
{% endif %}
</nav>
</div> </div>
{% endblock %} {% endblock %}

View file

@ -1,25 +0,0 @@
{% extends 'layout.html' %}
{% load fr_display %}
{% block content %}
{% include 'snippets/user_header.html' with user=user %}
<div class="block">
<div class="tabs">
<ul>
{% for shelf in shelves %}
<li class="{% if true %}is-active{% endif %}">
<a href="/user/{{ user | username }}/shelves/{{ shelf.identifier }}">{{ shelf.name }}</a>
</li>
{% endfor %}
</ul>
<h2 class="title">{{ shelf.name }}</h2>
</div>
{% for shelf in shelves %}
<div class="block">
{% include 'snippets/shelf.html' with shelf=shelf ratings=ratings %}
</div>
{% endfor %}
{% endblock %}

View file

@ -55,7 +55,7 @@ urlpatterns = [
re_path(r'^(?P<tab>home|local|federated)/?$', views.home_tab), re_path(r'^(?P<tab>home|local|federated)/?$', views.home_tab),
re_path(r'^notifications/?', views.notifications_page), re_path(r'^notifications/?', views.notifications_page),
re_path(r'import/?$', views.import_page), re_path(r'import/?$', views.import_page),
re_path(r'import_status/(\d+)/?$', views.import_status), re_path(r'import-status/(\d+)/?$', views.import_status),
re_path(r'user-edit/?$', views.edit_profile_page), re_path(r'user-edit/?$', views.edit_profile_page),
# should return a ui view or activitypub json blob as requested # should return a ui view or activitypub json blob as requested
@ -78,7 +78,7 @@ urlpatterns = [
re_path(r'%s/editions(.json)?/?$' % book_path, views.editions_page), re_path(r'%s/editions(.json)?/?$' % book_path, views.editions_page),
re_path(r'^author/(?P<author_id>[\w\-]+)(.json)?/?$', views.author_page), re_path(r'^author/(?P<author_id>[\w\-]+)(.json)?/?$', views.author_page),
# TODO: tag needs a .json path re_path(r'^tag/(?P<tag_id>.+)\.json/?$', views.tag_page),
re_path(r'^tag/(?P<tag_id>.+)/?$', views.tag_page), re_path(r'^tag/(?P<tag_id>.+)/?$', views.tag_page),
re_path(r'^%s/shelf/(?P<shelf_identifier>[\w-]+)(.json)?/?$' % \ re_path(r'^%s/shelf/(?P<shelf_identifier>[\w-]+)(.json)?/?$' % \
user_path, views.shelf_page), user_path, views.shelf_page),
@ -95,12 +95,12 @@ urlpatterns = [
re_path(r'^reset-password/?$', actions.password_reset), re_path(r'^reset-password/?$', actions.password_reset),
re_path(r'^change-password/?$', actions.password_change), re_path(r'^change-password/?$', actions.password_change),
re_path(r'^edit_profile/?$', actions.edit_profile), re_path(r'^edit-profile/?$', actions.edit_profile),
re_path(r'^import_data/?', actions.import_data), re_path(r'^import-data/?', actions.import_data),
re_path(r'^resolve_book/?', actions.resolve_book), re_path(r'^resolve-book/?', actions.resolve_book),
re_path(r'^edit_book/(?P<book_id>\d+)/?', actions.edit_book), re_path(r'^edit-book/(?P<book_id>\d+)/?', actions.edit_book),
re_path(r'^upload_cover/(?P<book_id>\d+)/?', actions.upload_cover), re_path(r'^upload-cover/(?P<book_id>\d+)/?', actions.upload_cover),
re_path(r'^edit-readthrough/?', actions.edit_readthrough), re_path(r'^edit-readthrough/?', actions.edit_readthrough),
re_path(r'^delete-readthrough/?', actions.delete_readthrough), re_path(r'^delete-readthrough/?', actions.delete_readthrough),
@ -118,20 +118,23 @@ urlpatterns = [
re_path(r'^boost/(?P<status_id>\d+)/?$', actions.boost), re_path(r'^boost/(?P<status_id>\d+)/?$', actions.boost),
re_path(r'^unboost/(?P<status_id>\d+)/?$', actions.unboost), re_path(r'^unboost/(?P<status_id>\d+)/?$', actions.unboost),
re_path(r'^delete-status/?$', actions.delete_status), re_path(r'^delete-status/(?P<status_id>\d+)/?$', actions.delete_status),
re_path(r'^create-shelf/?$', actions.create_shelf),
re_path(r'^edit-shelf/(?P<shelf_id>\d+)?$', actions.edit_shelf),
re_path(r'^delete-shelf/(?P<shelf_id>\d+)?$', actions.delete_shelf),
re_path(r'^shelve/?$', actions.shelve), re_path(r'^shelve/?$', actions.shelve),
re_path(r'^unshelve/?$', actions.unshelve), re_path(r'^unshelve/?$', actions.unshelve),
re_path(r'^start-reading/?$', actions.start_reading), re_path(r'^start-reading/(?P<book_id>\d+)/?$', actions.start_reading),
re_path(r'^finish-reading/?$', actions.finish_reading), re_path(r'^finish-reading/(?P<book_id>\d+)/?$', actions.finish_reading),
re_path(r'^follow/?$', actions.follow), re_path(r'^follow/?$', actions.follow),
re_path(r'^unfollow/?$', actions.unfollow), re_path(r'^unfollow/?$', actions.unfollow),
re_path(r'^accept_follow_request/?$', actions.accept_follow_request), re_path(r'^accept-follow-request/?$', actions.accept_follow_request),
re_path(r'^delete_follow_request/?$', actions.delete_follow_request), re_path(r'^delete-follow-request/?$', actions.delete_follow_request),
re_path(r'^clear-notifications/?$', actions.clear_notifications), re_path(r'^clear-notifications/?$', actions.clear_notifications),
re_path(r'^create_invite/?$', actions.create_invite), re_path(r'^create-invite/?$', actions.create_invite),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View file

@ -11,7 +11,7 @@ from django.contrib.auth.decorators import login_required, permission_required
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.http import HttpResponseBadRequest, HttpResponseNotFound from django.http import HttpResponseBadRequest, HttpResponseNotFound
from django.shortcuts import redirect from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.utils import timezone from django.utils import timezone
@ -61,10 +61,7 @@ def register(request):
if not invite_code: if not invite_code:
raise PermissionDenied raise PermissionDenied
try: invite = get_object_or_404(models.SiteInvite, code=invite_code)
invite = models.SiteInvite.objects.get(code=invite_code)
except models.SiteInvite.DoesNotExist:
raise PermissionDenied
else: else:
invite = None invite = None
@ -234,10 +231,7 @@ def edit_book(request, book_id):
if not request.method == 'POST': if not request.method == 'POST':
return redirect('/book/%s' % book_id) return redirect('/book/%s' % book_id)
try: book = get_object_or_404(models.Edition, id=book_id)
book = models.Edition.objects.get(id=book_id)
except models.Edition.DoesNotExist:
return HttpResponseNotFound()
form = forms.EditionForm(request.POST, request.FILES, instance=book) form = forms.EditionForm(request.POST, request.FILES, instance=book)
if not form.is_valid(): if not form.is_valid():
@ -251,14 +245,10 @@ def edit_book(request, book_id):
@login_required @login_required
def upload_cover(request, book_id): def upload_cover(request, book_id):
''' upload a new cover ''' ''' upload a new cover '''
# TODO: alternate covers?
if not request.method == 'POST': if not request.method == 'POST':
return redirect('/book/%s' % request.user.localname) return redirect('/book/%s' % request.user.localname)
try: book = get_object_or_404(models.Edition, id=book_id)
book = models.Edition.objects.get(id=book_id)
except models.Edition.DoesNotExist:
return HttpResponseNotFound()
form = forms.CoverForm(request.POST, request.FILES, instance=book) form = forms.CoverForm(request.POST, request.FILES, instance=book)
if not form.is_valid(): if not form.is_valid():
@ -272,6 +262,44 @@ def upload_cover(request, book_id):
return redirect('/book/%s' % book.id) return redirect('/book/%s' % book.id)
@login_required
def create_shelf(request):
''' user generated shelves '''
form = forms.ShelfForm(request.POST)
if not form.is_valid():
return redirect(request.headers.get('Referer', '/'))
shelf = form.save()
return redirect('/user/%s/shelf/%s' % \
(request.user.localname, shelf.identifier))
@login_required
def edit_shelf(request, shelf_id):
''' user generated shelves '''
shelf = get_object_or_404(models.Shelf, id=shelf_id)
if request.user != shelf.user:
return HttpResponseBadRequest()
form = forms.ShelfForm(request.POST, instance=shelf)
if not form.is_valid():
return redirect(request.headers.get('Referer', '/'))
shelf = form.save()
return redirect('/user/%s/shelf/%s' % \
(request.user.localname, shelf.identifier))
@login_required
def delete_shelf(request, shelf_id):
''' user generated shelves '''
shelf = get_object_or_404(models.Shelf, id=shelf_id)
if request.user != shelf.user or not shelf.editable:
return HttpResponseBadRequest()
shelf.delete()
return redirect('/user/%s/shelves' % request.user.localname)
@login_required @login_required
def shelve(request): def shelve(request):
''' put a on a user's shelf ''' ''' put a on a user's shelf '''
@ -317,9 +345,9 @@ def unshelve(request):
@login_required @login_required
def start_reading(request): def start_reading(request, book_id):
''' begin reading a book ''' ''' begin reading a book '''
book = books_manager.get_edition(request.POST['book']) book = books_manager.get_edition(book_id)
shelf = models.Shelf.objects.filter( shelf = models.Shelf.objects.filter(
identifier='reading', identifier='reading',
user=request.user user=request.user
@ -352,9 +380,9 @@ def start_reading(request):
@login_required @login_required
def finish_reading(request): def finish_reading(request, book_id):
''' a user completed a book, yay ''' ''' a user completed a book, yay '''
book = books_manager.get_edition(request.POST['book']) book = books_manager.get_edition(book_id)
shelf = models.Shelf.objects.filter( shelf = models.Shelf.objects.filter(
identifier='read', identifier='read',
user=request.user user=request.user
@ -404,10 +432,8 @@ def edit_readthrough(request):
@login_required @login_required
def delete_readthrough(request): def delete_readthrough(request):
''' remove a readthrough ''' ''' remove a readthrough '''
try: readthrough = get_object_or_404(
readthrough = models.ReadThrough.objects.get(id=request.POST.get('id')) models.ReadThrough, id=request.POST.get('id'))
except models.ReadThrough.DoesNotExist:
return HttpResponseNotFound()
# don't let people edit other people's data # don't let people edit other people's data
if request.user != readthrough.user: if request.user != readthrough.user:
@ -468,10 +494,7 @@ def tag(request):
# field which doesn't validate # field which doesn't validate
name = request.POST.get('name') name = request.POST.get('name')
book_id = request.POST.get('book') book_id = request.POST.get('book')
try: book = get_object_or_404(models.Edition, id=book_id)
book = models.Edition.objects.get(id=book_id)
except models.Edition.DoesNotExist:
return HttpResponseNotFound()
tag_obj, created = models.Tag.objects.get_or_create( tag_obj, created = models.Tag.objects.get_or_create(
name=name, name=name,
book=book, book=book,
@ -526,15 +549,9 @@ def unboost(request, status_id):
@login_required @login_required
def delete_status(request): def delete_status(request, status_id):
''' delete and tombstone a status ''' ''' delete and tombstone a status '''
status_id = request.POST.get('status') status = get_object_or_404(models.Status, id=status_id)
if not status_id:
return HttpResponseBadRequest()
try:
status = models.Status.objects.get(id=status_id)
except models.Status.DoesNotExist:
return HttpResponseBadRequest()
# don't let people delete other people's statuses # don't let people delete other people's statuses
if status.user != request.user: if status.user != request.user:
@ -645,7 +662,7 @@ def import_data(request):
except (UnicodeDecodeError, ValueError): except (UnicodeDecodeError, ValueError):
return HttpResponseBadRequest('Not a valid csv file') return HttpResponseBadRequest('Not a valid csv file')
goodreads_import.start_import(job) goodreads_import.start_import(job)
return redirect('/import_status/%d' % (job.id,)) return redirect('/import-status/%d' % (job.id,))
return HttpResponseBadRequest() return HttpResponseBadRequest()

View file

@ -3,11 +3,12 @@ import re
from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.decorators import login_required, permission_required
from django.contrib.postgres.search import TrigramSimilarity from django.contrib.postgres.search import TrigramSimilarity
from django.core.paginator import Paginator
from django.db.models import Avg, Q from django.db.models import Avg, Q
from django.http import HttpResponseBadRequest, HttpResponseNotFound,\ from django.http import HttpResponseBadRequest, HttpResponseNotFound,\
JsonResponse JsonResponse
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.shortcuts import redirect from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
@ -15,6 +16,7 @@ from bookwyrm import outgoing
from bookwyrm.activitypub import ActivityEncoder from bookwyrm.activitypub import ActivityEncoder
from bookwyrm import forms, models, books_manager from bookwyrm import forms, models, books_manager
from bookwyrm import goodreads_import from bookwyrm import goodreads_import
from bookwyrm.settings import PAGE_LENGTH
from bookwyrm.tasks import app from bookwyrm.tasks import app
from bookwyrm.utils import regex from bookwyrm.utils import regex
@ -53,8 +55,6 @@ def home(request):
@login_required @login_required
def home_tab(request, tab): def home_tab(request, tab):
''' user's homepage with activity feed ''' ''' user's homepage with activity feed '''
# TODO: why on earth would this be where the pagination is set
page_size = 15
try: try:
page = int(request.GET.get('page', 1)) page = int(request.GET.get('page', 1))
except ValueError: except ValueError:
@ -63,23 +63,24 @@ def home_tab(request, tab):
suggested_books = get_suggested_books(request.user) suggested_books = get_suggested_books(request.user)
activities = get_activity_feed(request.user, tab) activities = get_activity_feed(request.user, tab)
paginated = Paginator(activities, PAGE_LENGTH)
activity_page = paginated.page(page)
activity_count = activities.count() prev_page = next_page = None
activities = activities[(page - 1) * page_size:page * page_size] if activity_page.has_next():
next_page = '/%s/?page=%d#feed' % \
next_page = '/?page=%d#feed' % (page + 1) (tab, activity_page.next_page_number())
prev_page = '/?page=%d#feed' % (page - 1) if activity_page.has_previous():
prev_page = '/%s/?page=%d#feed' % \
(tab, activity_page.previous_page_number())
data = { data = {
'title': 'Updates Feed', 'title': 'Updates Feed',
'user': request.user, 'user': request.user,
'suggested_books': suggested_books, 'suggested_books': suggested_books,
'activities': activities, 'activities': activity_page.object_list,
'review_form': forms.ReviewForm(),
'quotation_form': forms.QuotationForm(),
'tab': tab, 'tab': tab,
'comment_form': forms.CommentForm(), 'next': next_page,
'next': next_page if activity_count > (page_size * page) else None, 'prev': prev_page,
'prev': prev_page if page > 1 else None,
} }
return TemplateResponse(request, 'feed.html', data) return TemplateResponse(request, 'feed.html', data)
@ -168,7 +169,7 @@ def search(request):
book_results = books_manager.local_search(query) book_results = books_manager.local_search(query)
return JsonResponse([r.__dict__ for r in book_results], safe=False) return JsonResponse([r.__dict__ for r in book_results], safe=False)
# use webfinger looks like a mastodon style account@domain.com username # use webfinger for mastodon style account@domain.com username
if re.match(regex.full_username, query): if re.match(regex.full_username, query):
outgoing.handle_remote_webfinger(query) outgoing.handle_remote_webfinger(query)
@ -176,7 +177,7 @@ def search(request):
user_results = models.User.objects.annotate( user_results = models.User.objects.annotate(
similarity=TrigramSimilarity('username', query), similarity=TrigramSimilarity('username', query),
).filter( ).filter(
similarity__gt=0.1, similarity__gt=0.5,
).order_by('-similarity')[:10] ).order_by('-similarity')[:10]
book_results = books_manager.search(query) book_results = books_manager.search(query)
@ -285,6 +286,7 @@ def invite_page(request, code):
} }
return TemplateResponse(request, 'invite.html', data) return TemplateResponse(request, 'invite.html', data)
@login_required @login_required
@permission_required('bookwyrm.create_invites', raise_exception=True) @permission_required('bookwyrm.create_invites', raise_exception=True)
def manage_invites(request): def manage_invites(request):
@ -311,8 +313,9 @@ def notifications_page(request):
notifications.update(read=True) notifications.update(read=True)
return TemplateResponse(request, 'notifications.html', data) return TemplateResponse(request, 'notifications.html', data)
@csrf_exempt @csrf_exempt
def user_page(request, username, subpage=None, shelf=None): def user_page(request, username):
''' profile page for a user ''' ''' profile page for a user '''
try: try:
user = get_user_from_username(username) user = get_user_from_username(username)
@ -324,41 +327,58 @@ def user_page(request, username, subpage=None, shelf=None):
return JsonResponse(user.to_activity(), encoder=ActivityEncoder) return JsonResponse(user.to_activity(), encoder=ActivityEncoder)
# otherwise we're at a UI view # otherwise we're at a UI view
data = { try:
'title': user.name, page = int(request.GET.get('page', 1))
'user': user, except ValueError:
'is_self': request.user.id == user.id, page = 1
}
if subpage == 'followers':
data['followers'] = user.followers.all()
return TemplateResponse(request, 'followers.html', data)
if subpage == 'following':
data['following'] = user.following.all()
return TemplateResponse(request, 'following.html', data)
if subpage == 'shelves':
data['shelves'] = user.shelf_set.all()
if shelf:
data['shelf'] = user.shelf_set.get(identifier=shelf)
else:
data['shelf'] = user.shelf_set.first()
return TemplateResponse(request, 'shelf.html', data)
data['shelf_count'] = user.shelf_set.count() shelf_preview = []
shelves = []
for user_shelf in user.shelf_set.all(): # only show other shelves that should be visible
shelves = user.shelf_set
is_self = request.user.id == user.id
if not is_self:
follower = user.followers.filter(id=request.user.id).exists()
if follower:
shelves = shelves.filter(privacy__in=['public', 'followers'])
else:
shelves = shelves.filter(privacy='public')
for user_shelf in shelves.all():
if not user_shelf.books.count(): if not user_shelf.books.count():
continue continue
shelves.append({ shelf_preview.append({
'name': user_shelf.name, 'name': user_shelf.name,
'remote_id': user_shelf.remote_id, 'remote_id': user_shelf.remote_id,
'books': user_shelf.books.all()[:3], 'books': user_shelf.books.all()[:3],
'size': user_shelf.books.count(), 'size': user_shelf.books.count(),
}) })
if len(shelves) > 2: if len(shelf_preview) > 2:
break break
data['shelves'] = shelves # user's posts
data['activities'] = get_activity_feed(user, 'self')[:15] activities = get_activity_feed(user, 'self')
paginated = Paginator(activities, PAGE_LENGTH)
activity_page = paginated.page(page)
prev_page = next_page = None
if activity_page.has_next():
next_page = '/user/%s/?page=%d' % \
(username, activity_page.next_page_number())
if activity_page.has_previous():
prev_page = '/user/%s/?page=%d' % \
(username, activity_page.previous_page_number())
data = {
'title': user.name,
'user': user,
'is_self': is_self,
'shelves': shelf_preview,
'shelf_count': shelves.count(),
'activities': activity_page.object_list,
'next': next_page,
'prev': prev_page,
}
return TemplateResponse(request, 'user.html', data) return TemplateResponse(request, 'user.html', data)
@ -376,7 +396,13 @@ def followers_page(request, username):
if is_api_request(request): if is_api_request(request):
return JsonResponse(user.to_followers_activity(**request.GET)) return JsonResponse(user.to_followers_activity(**request.GET))
return user_page(request, username, subpage='followers') data = {
'title': '%s: followers' % user.name,
'user': user,
'is_self': request.user.id == user.id,
'followers': user.followers.all(),
}
return TemplateResponse(request, 'followers.html', data)
@csrf_exempt @csrf_exempt
@ -393,16 +419,13 @@ def following_page(request, username):
if is_api_request(request): if is_api_request(request):
return JsonResponse(user.to_following_activity(**request.GET)) return JsonResponse(user.to_following_activity(**request.GET))
return user_page(request, username, subpage='following') data = {
'title': '%s: following' % user.name,
'user': user,
@csrf_exempt 'is_self': request.user.id == user.id,
def user_shelves_page(request, username): 'following': user.following.all(),
''' list of followers ''' }
if request.method != 'GET': return TemplateResponse(request, 'following.html', data)
return HttpResponseBadRequest()
return user_page(request, username, subpage='shelves')
@csrf_exempt @csrf_exempt
@ -434,6 +457,7 @@ def status_page(request, username, status_id):
} }
return TemplateResponse(request, 'status.html', data) return TemplateResponse(request, 'status.html', data)
def status_visible_to_user(viewer, status): def status_visible_to_user(viewer, status):
''' is a user authorized to view a status? ''' ''' is a user authorized to view a status? '''
if viewer == status.user or status.privacy in ['public', 'unlisted']: if viewer == status.user or status.privacy in ['public', 'unlisted']:
@ -447,7 +471,6 @@ def status_visible_to_user(viewer, status):
return False return False
@csrf_exempt @csrf_exempt
def replies_page(request, username, status_id): def replies_page(request, username, status_id):
''' ordered collection of replies to a status ''' ''' ordered collection of replies to a status '''
@ -483,6 +506,11 @@ def edit_profile_page(request):
def book_page(request, book_id): def book_page(request, book_id):
''' info about a book ''' ''' info about a book '''
try:
page = int(request.GET.get('page', 1))
except ValueError:
page = 1
book = models.Book.objects.select_subclasses().get(id=book_id) book = models.Book.objects.select_subclasses().get(id=book_id)
if is_api_request(request): if is_api_request(request):
return JsonResponse(book.to_activity(), encoder=ActivityEncoder) return JsonResponse(book.to_activity(), encoder=ActivityEncoder)
@ -499,8 +527,21 @@ def book_page(request, book_id):
reviews = models.Review.objects.filter( reviews = models.Review.objects.filter(
book__in=work.edition_set.all(), book__in=work.edition_set.all(),
) )
# all reviews for the book
reviews = get_activity_feed(request.user, 'federated', model=reviews) reviews = get_activity_feed(request.user, 'federated', model=reviews)
# the reviews to show
paginated = Paginator(reviews.filter(content__isnull=False), PAGE_LENGTH)
reviews_page = paginated.page(page)
prev_page = next_page = None
if reviews_page.has_next():
next_page = '/book/%d/?page=%d' % \
(book_id, reviews_page.next_page_number())
if reviews_page.has_previous():
prev_page = '/book/%s/?page=%d' % \
(book_id, reviews_page.previous_page_number())
user_tags = [] user_tags = []
readthroughs = [] readthroughs = []
if request.user.is_authenticated: if request.user.is_authenticated:
@ -523,18 +564,13 @@ def book_page(request, book_id):
data = { data = {
'title': book.title, 'title': book.title,
'book': book, 'book': book,
'reviews': reviews.filter(content__isnull=False), 'reviews': reviews_page,
'ratings': reviews.filter(content__isnull=True), 'ratings': reviews.filter(content__isnull=True),
'rating': rating['rating__avg'], 'rating': rating['rating__avg'],
'tags': tags, 'tags': tags,
'user_tags': user_tags, 'user_tags': user_tags,
'review_form': forms.ReviewForm(),
'quotation_form': forms.QuotationForm(),
'comment_form': forms.CommentForm(),
'readthroughs': readthroughs, 'readthroughs': readthroughs,
'tag_form': forms.TagForm(),
'path': '/book/%s' % book_id, 'path': '/book/%s' % book_id,
'cover_form': forms.CoverForm(instance=book),
'info_fields': [ 'info_fields': [
{'name': 'ISBN', 'value': book.isbn_13}, {'name': 'ISBN', 'value': book.isbn_13},
{'name': 'OCLC number', 'value': book.oclc_number}, {'name': 'OCLC number', 'value': book.oclc_number},
@ -543,6 +579,8 @@ def book_page(request, book_id):
{'name': 'Format', 'value': book.physical_format}, {'name': 'Format', 'value': book.physical_format},
{'name': 'Pages', 'value': book.pages}, {'name': 'Pages', 'value': book.pages},
], ],
'next': next_page,
'prev': prev_page,
} }
return TemplateResponse(request, 'book.html', data) return TemplateResponse(request, 'book.html', data)
@ -564,10 +602,7 @@ def edit_book_page(request, book_id):
def editions_page(request, book_id): def editions_page(request, book_id):
''' list of editions of a book ''' ''' list of editions of a book '''
try: work = get_object_or_404(models.Work, id=book_id)
work = models.Work.objects.get(id=book_id)
except models.Work.DoesNotExist:
return HttpResponseNotFound()
if is_api_request(request): if is_api_request(request):
return JsonResponse( return JsonResponse(
@ -586,10 +621,7 @@ def editions_page(request, book_id):
def author_page(request, author_id): def author_page(request, author_id):
''' landing page for an author ''' ''' landing page for an author '''
try: author = get_object_or_404(models.Author, id=author_id)
author = models.Author.objects.get(id=author_id)
except ValueError:
return HttpResponseNotFound()
if is_api_request(request): if is_api_request(request):
return JsonResponse(author.to_activity(), encoder=ActivityEncoder) return JsonResponse(author.to_activity(), encoder=ActivityEncoder)
@ -622,6 +654,12 @@ def tag_page(request, tag_id):
return TemplateResponse(request, 'tag.html', data) return TemplateResponse(request, 'tag.html', data)
@csrf_exempt
def user_shelves_page(request, username):
''' list of followers '''
return shelf_page(request, username, None)
def shelf_page(request, username, shelf_identifier): def shelf_page(request, username, shelf_identifier):
''' display a shelf ''' ''' display a shelf '''
try: try:
@ -629,10 +667,37 @@ def shelf_page(request, username, shelf_identifier):
except models.User.DoesNotExist: except models.User.DoesNotExist:
return HttpResponseNotFound() return HttpResponseNotFound()
shelf = models.Shelf.objects.get(user=user, identifier=shelf_identifier) if shelf_identifier:
shelf = user.shelf_set.get(identifier=shelf_identifier)
else:
shelf = user.shelf_set.first()
is_self = request.user == user
shelves = user.shelf_set
if not is_self:
follower = user.followers.filter(id=request.user.id).exists()
# make sure the user has permission to view the shelf
if shelf.privacy == 'direct' or \
(shelf.privacy == 'followers' and not follower):
return HttpResponseNotFound()
# only show other shelves that should be visible
if follower:
shelves = shelves.filter(privacy__in=['public', 'followers'])
else:
shelves = shelves.filter(privacy='public')
if is_api_request(request): if is_api_request(request):
return JsonResponse(shelf.to_activity(**request.GET)) return JsonResponse(shelf.to_activity(**request.GET))
return user_page( data = {
request, username, subpage='shelves', shelf=shelf_identifier) 'title': user.name,
'user': user,
'is_self': is_self,
'shelves': shelves.all(),
'shelf': shelf,
}
return TemplateResponse(request, 'shelf.html', data)