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
0eacee02ac
35 changed files with 505 additions and 225 deletions
|
@ -159,3 +159,8 @@ class CreateInviteForm(CustomForm):
|
|||
choices=[(i, "%d uses" % (i,)) for i in [1, 5, 10, 25, 50, 100]]
|
||||
+ [(None, 'Unlimited')])
|
||||
}
|
||||
|
||||
class ShelfForm(CustomForm):
|
||||
class Meta:
|
||||
model = models.Shelf
|
||||
fields = ['user', 'name', 'privacy']
|
||||
|
|
18
bookwyrm/migrations/0009_shelf_privacy.py
Normal file
18
bookwyrm/migrations/0009_shelf_privacy.py
Normal 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),
|
||||
),
|
||||
]
|
|
@ -1,8 +1,9 @@
|
|||
''' puttin' books on shelves '''
|
||||
import re
|
||||
from django.db import models
|
||||
|
||||
from bookwyrm import activitypub
|
||||
from .base_model import BookWyrmModel, OrderedCollectionMixin
|
||||
from .base_model import BookWyrmModel, OrderedCollectionMixin, PrivacyLevels
|
||||
|
||||
|
||||
class Shelf(OrderedCollectionMixin, BookWyrmModel):
|
||||
|
@ -11,6 +12,11 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel):
|
|||
identifier = models.CharField(max_length=100)
|
||||
user = models.ForeignKey('User', on_delete=models.PROTECT)
|
||||
editable = models.BooleanField(default=True)
|
||||
privacy = models.CharField(
|
||||
max_length=255,
|
||||
default='public',
|
||||
choices=PrivacyLevels.choices
|
||||
)
|
||||
books = models.ManyToManyField(
|
||||
'Edition',
|
||||
symmetrical=False,
|
||||
|
@ -18,6 +24,15 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel):
|
|||
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
|
||||
def collection_queryset(self):
|
||||
''' list of books for this shelf, overrides OrderedCollectionMixin '''
|
||||
|
|
|
@ -6,6 +6,8 @@ from environs import Env
|
|||
env = Env()
|
||||
DOMAIN = env('DOMAIN')
|
||||
|
||||
PAGE_LENGTH = env('PAGE_LENGTH', 15)
|
||||
|
||||
# celery
|
||||
CELERY_BROKER = env('CELERY_BROKER')
|
||||
CELERY_RESULT_BACKEND = env('CELERY_RESULT_BACKEND')
|
||||
|
|
Binary file not shown.
|
@ -37,4 +37,5 @@
|
|||
<glyph unicode="" 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="" 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="" 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="" 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>
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
Binary file not shown.
Binary file not shown.
|
@ -1,10 +1,10 @@
|
|||
@font-face {
|
||||
font-family: 'icomoon';
|
||||
src: url('fonts/icomoon.eot?jhaogg');
|
||||
src: url('fonts/icomoon.eot?jhaogg#iefix') format('embedded-opentype'),
|
||||
url('fonts/icomoon.ttf?jhaogg') format('truetype'),
|
||||
url('fonts/icomoon.woff?jhaogg') format('woff'),
|
||||
url('fonts/icomoon.svg?jhaogg#icomoon') format('svg');
|
||||
src: url('fonts/icomoon.eot?rd4abb');
|
||||
src: url('fonts/icomoon.eot?rd4abb#iefix') format('embedded-opentype'),
|
||||
url('fonts/icomoon.ttf?rd4abb') format('truetype'),
|
||||
url('fonts/icomoon.woff?rd4abb') format('woff'),
|
||||
url('fonts/icomoon.svg?rd4abb#icomoon') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
|
@ -115,3 +115,6 @@
|
|||
.icon-heart:before {
|
||||
content: "\e9da";
|
||||
}
|
||||
.icon-plus:before {
|
||||
content: "\ea0a";
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
|
||||
{% if request.user.is_authenticated and not book.cover %}
|
||||
<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 %}
|
||||
<div class="field">
|
||||
<label class="label" for="id_cover">Cover:</label>
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
</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 %}
|
||||
<div class="block">
|
||||
<h2 class="title is-4">Data sync
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
{% if form.non_field_errors %}
|
||||
<p class="notification is-danger">{{ form.non_field_errors }}</p>
|
||||
{% 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 %}
|
||||
<p class="block">
|
||||
<label class="label" for="id_avatar">Avatar:</label>
|
||||
|
|
|
@ -1,6 +1,16 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% load fr_display %}
|
||||
{% 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 %}
|
||||
|
||||
<div class="block">
|
||||
|
|
|
@ -1,6 +1,16 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% load fr_display %}
|
||||
{% 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 %}
|
||||
|
||||
<div class="block">
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
{% block content %}
|
||||
<div class="block">
|
||||
<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 %}
|
||||
<div class="field">
|
||||
{{ import_form.as_p }}
|
||||
|
@ -31,7 +31,7 @@
|
|||
{% endif %}
|
||||
<ul>
|
||||
{% 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 %}
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
<div class="block">
|
||||
<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 %}
|
||||
<div class="field is-grouped">
|
||||
<div class="control">
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
boosted your <a href="{{ notification.related_status.remote_id}}">status</a>
|
||||
{% endif %}
|
||||
{% 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 %}
|
||||
</p>
|
||||
|
|
|
@ -26,7 +26,9 @@
|
|||
|
||||
<input class="toggle-control" type="radio" name="more-results" id="fewer-results" checked>
|
||||
<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>
|
||||
{% endif %}
|
||||
|
@ -45,7 +47,7 @@
|
|||
<ul>
|
||||
{% for result in result_set.results %}
|
||||
<li class="pb-4">
|
||||
<form action="/resolve_book" method="POST">
|
||||
<form action="/resolve-book" method="POST">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="remote_id" value="{{ result.key }}">
|
||||
<div>{% include 'snippets/search_result_text.html' with result=result link=False %}</div>
|
||||
|
@ -58,7 +60,9 @@
|
|||
{% endif %}
|
||||
{% endfor %}
|
||||
{% 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 %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,17 +1,122 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% load fr_display %}
|
||||
{% 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 %}
|
||||
|
||||
<div class="block">
|
||||
<div class="tabs">
|
||||
<ul>
|
||||
{% for shelf_tab in shelves %}
|
||||
<li class="{% if shelf_tab.identifier == shelf.identifier %}is-active{% endif %}">
|
||||
<a href="/user/{{ user | username }}/shelf/{{ shelf_tab.identifier }}">{{ shelf_tab.name }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="block columns">
|
||||
<div class="column">
|
||||
<div class="tabs" role="tablist">
|
||||
<ul>
|
||||
{% for shelf_tab in shelves %}
|
||||
<li class="{% if shelf_tab.identifier == shelf.identifier %}is-active{% endif %}">
|
||||
<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>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</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>
|
||||
|
||||
|
|
|
@ -15,14 +15,13 @@
|
|||
<fieldset>
|
||||
<legend class="is-sr-only">Rating</legend>
|
||||
<div class="field is-grouped stars form-rate-stars">
|
||||
<label class="is-sr-only">No rating
|
||||
<input class="is-sr-only" type="radio" name="rating" value="" checked>
|
||||
</label>
|
||||
<label class="is-sr-only" for="no-rating-{{ book.id }}">No rating</label>
|
||||
<input class="is-sr-only" type="radio" name="rating" value="" id="no-rating-{{ book.id }}" checked>
|
||||
{% 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 }}">
|
||||
<label class="icon icon-star-empty" for="book{{book.id}}-star-{{ forloop.counter }}">
|
||||
<span class="is-sr-only">{{ forloop.counter }} star{{ forloop.counter | pluralize }}</span>
|
||||
</label>
|
||||
<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 }}">
|
||||
<span class="is-sr-only">{{ forloop.counter }} star{{ forloop.counter | pluralize }}</span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
|
|
@ -9,10 +9,9 @@
|
|||
<label class="delete" for="finish-reading-{{ uuid }}" aria-label="close" role="button"></label>
|
||||
</header>
|
||||
{% 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">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="book" value="{{ book.id }}">
|
||||
<input type="hidden" name="id" value="{{ readthrough.id }}">
|
||||
<div class="field">
|
||||
<label class="label">
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
{% load fr_display %}
|
||||
{% if request.user|follow_request_exists:user %}
|
||||
<form action="/accept_follow_request/" method="POST">
|
||||
<form action="/accept-follow-request/" method="POST">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="user" value="{{ user.username }}">
|
||||
<button class="button is-primary is-small" type="submit">Accept</button>
|
||||
</form>
|
||||
<form action="/delete_follow_request/" method="POST">
|
||||
<form action="/delete-follow-request/" method="POST">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="user" value="{{ user.username }}">
|
||||
<button class="button is-danger is-light is-small" type="submit" class="warning">Delete</button>
|
||||
|
|
18
bookwyrm/templates/snippets/privacy-icons.html
Normal file
18
bookwyrm/templates/snippets/privacy-icons.html
Normal 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 %}
|
||||
|
|
@ -5,10 +5,18 @@
|
|||
<label class="is-sr-only" for="privacy-{{ uuid }}">Post privacy</label>
|
||||
{% endif %}
|
||||
<select name="privacy" id="privacy-{{ uuid }}">
|
||||
<option value="public" selected>Public</option>
|
||||
<option value="unlisted">Unlisted</option>
|
||||
<option value="followers">Followers only</option>
|
||||
<option value="direct">Private</option>
|
||||
<option value="public" {% if not current or current == 'public' %}selected{% endif %}>
|
||||
Public
|
||||
</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>
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
|
|
@ -76,5 +76,15 @@
|
|||
</table>
|
||||
{% else %}
|
||||
<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 %}
|
||||
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
<div class="dropdown is-hoverable">
|
||||
<div class="dropdown-trigger button">
|
||||
<p>Change shelf</p>
|
||||
<span class="icon icon-arrow-down"></span>
|
||||
<div class="dropdown">
|
||||
<div class="dropdown-trigger">
|
||||
<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 }}">
|
||||
<div class="button">
|
||||
<span>Change shelf</span>
|
||||
<span class="icon icon-arrow-down" aria-hidden="true"></span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="dropdown-menu">
|
||||
<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">
|
||||
{% for shelf in request.user.shelf_set.all %}
|
||||
{% if shelf.identifier != current.identifier %}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<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 %}
|
||||
<span class="icon icon-star-{% if rating >= forloop.counter %}full{% elif rating|floatformat:0 >= forloop.counter|floatformat:0 %}half{% else %}empty{% endif %}">
|
||||
</span>
|
||||
|
|
|
@ -7,10 +7,9 @@
|
|||
<p class="modal-card-title">Start "{{ book.title }}"</p>
|
||||
<label class="delete" for="start-reading-{{ uuid }}" aria-label="close" role="button" tabindex="0"></label>
|
||||
</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">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="book" value="{{ book.id }}">
|
||||
<div class="field">
|
||||
<label class="label">
|
||||
Started reading
|
||||
|
|
|
@ -33,7 +33,11 @@
|
|||
{% if request.user.is_authenticated %}
|
||||
|
||||
<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>
|
||||
{% include 'snippets/boost_button.html' with status=status %}
|
||||
{% include 'snippets/fav_button.html' with status=status %}
|
||||
|
@ -56,23 +60,7 @@
|
|||
</div>
|
||||
|
||||
<div class="card-footer-item">
|
||||
{% if status.privacy == 'public' %}
|
||||
<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 %}
|
||||
{% include 'snippets/privacy-icons.html' with item=status %}
|
||||
</div>
|
||||
|
||||
<div class="card-footer-item">
|
||||
|
@ -94,9 +82,8 @@
|
|||
<div class="toggle-content hidden card-footer">
|
||||
{% if status.user == request.user %}
|
||||
<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 %}
|
||||
<input type="hidden" name="status" value="{{ status.id }}">
|
||||
<button class="button is-danger is-light" type="submit">
|
||||
Delete post
|
||||
</button>
|
||||
|
|
|
@ -1,19 +1,6 @@
|
|||
{% load humanize %}
|
||||
{% load fr_display %}
|
||||
<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="column is-narrow">
|
||||
<div class="media">
|
||||
|
|
|
@ -1,6 +1,21 @@
|
|||
{% extends 'layout.html' %}
|
||||
{% 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 %}
|
||||
|
||||
<div class="block">
|
||||
|
@ -39,6 +54,26 @@
|
|||
<p>No activities yet!</a>
|
||||
</div>
|
||||
{% 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>
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
@ -55,7 +55,7 @@ urlpatterns = [
|
|||
re_path(r'^(?P<tab>home|local|federated)/?$', views.home_tab),
|
||||
re_path(r'^notifications/?', views.notifications_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),
|
||||
|
||||
# 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'^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'^%s/shelf/(?P<shelf_identifier>[\w-]+)(.json)?/?$' % \
|
||||
user_path, views.shelf_page),
|
||||
|
@ -95,12 +95,12 @@ urlpatterns = [
|
|||
re_path(r'^reset-password/?$', actions.password_reset),
|
||||
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'^resolve_book/?', actions.resolve_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'^import-data/?', actions.import_data),
|
||||
re_path(r'^resolve-book/?', actions.resolve_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'^edit-readthrough/?', actions.edit_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'^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'^unshelve/?$', actions.unshelve),
|
||||
re_path(r'^start-reading/?$', actions.start_reading),
|
||||
re_path(r'^finish-reading/?$', actions.finish_reading),
|
||||
re_path(r'^start-reading/(?P<book_id>\d+)/?$', actions.start_reading),
|
||||
re_path(r'^finish-reading/(?P<book_id>\d+)/?$', actions.finish_reading),
|
||||
|
||||
re_path(r'^follow/?$', actions.follow),
|
||||
re_path(r'^unfollow/?$', actions.unfollow),
|
||||
re_path(r'^accept_follow_request/?$', actions.accept_follow_request),
|
||||
re_path(r'^delete_follow_request/?$', actions.delete_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'^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)
|
||||
|
|
|
@ -11,7 +11,7 @@ from django.contrib.auth.decorators import login_required, permission_required
|
|||
from django.core.exceptions import PermissionDenied
|
||||
from django.core.files.base import ContentFile
|
||||
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.utils import timezone
|
||||
|
||||
|
@ -61,10 +61,7 @@ def register(request):
|
|||
if not invite_code:
|
||||
raise PermissionDenied
|
||||
|
||||
try:
|
||||
invite = models.SiteInvite.objects.get(code=invite_code)
|
||||
except models.SiteInvite.DoesNotExist:
|
||||
raise PermissionDenied
|
||||
invite = get_object_or_404(models.SiteInvite, code=invite_code)
|
||||
else:
|
||||
invite = None
|
||||
|
||||
|
@ -234,10 +231,7 @@ def edit_book(request, book_id):
|
|||
if not request.method == 'POST':
|
||||
return redirect('/book/%s' % book_id)
|
||||
|
||||
try:
|
||||
book = models.Edition.objects.get(id=book_id)
|
||||
except models.Edition.DoesNotExist:
|
||||
return HttpResponseNotFound()
|
||||
book = get_object_or_404(models.Edition, id=book_id)
|
||||
|
||||
form = forms.EditionForm(request.POST, request.FILES, instance=book)
|
||||
if not form.is_valid():
|
||||
|
@ -251,14 +245,10 @@ def edit_book(request, book_id):
|
|||
@login_required
|
||||
def upload_cover(request, book_id):
|
||||
''' upload a new cover '''
|
||||
# TODO: alternate covers?
|
||||
if not request.method == 'POST':
|
||||
return redirect('/book/%s' % request.user.localname)
|
||||
|
||||
try:
|
||||
book = models.Edition.objects.get(id=book_id)
|
||||
except models.Edition.DoesNotExist:
|
||||
return HttpResponseNotFound()
|
||||
book = get_object_or_404(models.Edition, id=book_id)
|
||||
|
||||
form = forms.CoverForm(request.POST, request.FILES, instance=book)
|
||||
if not form.is_valid():
|
||||
|
@ -272,6 +262,44 @@ def upload_cover(request, 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
|
||||
def shelve(request):
|
||||
''' put a on a user's shelf '''
|
||||
|
@ -317,9 +345,9 @@ def unshelve(request):
|
|||
|
||||
|
||||
@login_required
|
||||
def start_reading(request):
|
||||
def start_reading(request, book_id):
|
||||
''' begin reading a book '''
|
||||
book = books_manager.get_edition(request.POST['book'])
|
||||
book = books_manager.get_edition(book_id)
|
||||
shelf = models.Shelf.objects.filter(
|
||||
identifier='reading',
|
||||
user=request.user
|
||||
|
@ -352,9 +380,9 @@ def start_reading(request):
|
|||
|
||||
|
||||
@login_required
|
||||
def finish_reading(request):
|
||||
def finish_reading(request, book_id):
|
||||
''' 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(
|
||||
identifier='read',
|
||||
user=request.user
|
||||
|
@ -404,10 +432,8 @@ def edit_readthrough(request):
|
|||
@login_required
|
||||
def delete_readthrough(request):
|
||||
''' remove a readthrough '''
|
||||
try:
|
||||
readthrough = models.ReadThrough.objects.get(id=request.POST.get('id'))
|
||||
except models.ReadThrough.DoesNotExist:
|
||||
return HttpResponseNotFound()
|
||||
readthrough = get_object_or_404(
|
||||
models.ReadThrough, id=request.POST.get('id'))
|
||||
|
||||
# don't let people edit other people's data
|
||||
if request.user != readthrough.user:
|
||||
|
@ -468,10 +494,7 @@ def tag(request):
|
|||
# field which doesn't validate
|
||||
name = request.POST.get('name')
|
||||
book_id = request.POST.get('book')
|
||||
try:
|
||||
book = models.Edition.objects.get(id=book_id)
|
||||
except models.Edition.DoesNotExist:
|
||||
return HttpResponseNotFound()
|
||||
book = get_object_or_404(models.Edition, id=book_id)
|
||||
tag_obj, created = models.Tag.objects.get_or_create(
|
||||
name=name,
|
||||
book=book,
|
||||
|
@ -526,15 +549,9 @@ def unboost(request, status_id):
|
|||
|
||||
|
||||
@login_required
|
||||
def delete_status(request):
|
||||
def delete_status(request, status_id):
|
||||
''' delete and tombstone a status '''
|
||||
status_id = request.POST.get('status')
|
||||
if not status_id:
|
||||
return HttpResponseBadRequest()
|
||||
try:
|
||||
status = models.Status.objects.get(id=status_id)
|
||||
except models.Status.DoesNotExist:
|
||||
return HttpResponseBadRequest()
|
||||
status = get_object_or_404(models.Status, id=status_id)
|
||||
|
||||
# don't let people delete other people's statuses
|
||||
if status.user != request.user:
|
||||
|
@ -645,7 +662,7 @@ def import_data(request):
|
|||
except (UnicodeDecodeError, ValueError):
|
||||
return HttpResponseBadRequest('Not a valid csv file')
|
||||
goodreads_import.start_import(job)
|
||||
return redirect('/import_status/%d' % (job.id,))
|
||||
return redirect('/import-status/%d' % (job.id,))
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
|
||||
|
|
|
@ -3,11 +3,12 @@ import re
|
|||
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from django.contrib.postgres.search import TrigramSimilarity
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import Avg, Q
|
||||
from django.http import HttpResponseBadRequest, HttpResponseNotFound,\
|
||||
JsonResponse
|
||||
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.views.decorators.csrf import csrf_exempt
|
||||
|
||||
|
@ -15,6 +16,7 @@ from bookwyrm import outgoing
|
|||
from bookwyrm.activitypub import ActivityEncoder
|
||||
from bookwyrm import forms, models, books_manager
|
||||
from bookwyrm import goodreads_import
|
||||
from bookwyrm.settings import PAGE_LENGTH
|
||||
from bookwyrm.tasks import app
|
||||
from bookwyrm.utils import regex
|
||||
|
||||
|
@ -53,8 +55,6 @@ def home(request):
|
|||
@login_required
|
||||
def home_tab(request, tab):
|
||||
''' user's homepage with activity feed '''
|
||||
# TODO: why on earth would this be where the pagination is set
|
||||
page_size = 15
|
||||
try:
|
||||
page = int(request.GET.get('page', 1))
|
||||
except ValueError:
|
||||
|
@ -63,23 +63,24 @@ def home_tab(request, tab):
|
|||
suggested_books = get_suggested_books(request.user)
|
||||
|
||||
activities = get_activity_feed(request.user, tab)
|
||||
paginated = Paginator(activities, PAGE_LENGTH)
|
||||
activity_page = paginated.page(page)
|
||||
|
||||
activity_count = activities.count()
|
||||
activities = activities[(page - 1) * page_size:page * page_size]
|
||||
|
||||
next_page = '/?page=%d#feed' % (page + 1)
|
||||
prev_page = '/?page=%d#feed' % (page - 1)
|
||||
prev_page = next_page = None
|
||||
if activity_page.has_next():
|
||||
next_page = '/%s/?page=%d#feed' % \
|
||||
(tab, activity_page.next_page_number())
|
||||
if activity_page.has_previous():
|
||||
prev_page = '/%s/?page=%d#feed' % \
|
||||
(tab, activity_page.previous_page_number())
|
||||
data = {
|
||||
'title': 'Updates Feed',
|
||||
'user': request.user,
|
||||
'suggested_books': suggested_books,
|
||||
'activities': activities,
|
||||
'review_form': forms.ReviewForm(),
|
||||
'quotation_form': forms.QuotationForm(),
|
||||
'activities': activity_page.object_list,
|
||||
'tab': tab,
|
||||
'comment_form': forms.CommentForm(),
|
||||
'next': next_page if activity_count > (page_size * page) else None,
|
||||
'prev': prev_page if page > 1 else None,
|
||||
'next': next_page,
|
||||
'prev': prev_page,
|
||||
}
|
||||
return TemplateResponse(request, 'feed.html', data)
|
||||
|
||||
|
@ -168,7 +169,7 @@ def search(request):
|
|||
book_results = books_manager.local_search(query)
|
||||
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):
|
||||
outgoing.handle_remote_webfinger(query)
|
||||
|
||||
|
@ -176,7 +177,7 @@ def search(request):
|
|||
user_results = models.User.objects.annotate(
|
||||
similarity=TrigramSimilarity('username', query),
|
||||
).filter(
|
||||
similarity__gt=0.1,
|
||||
similarity__gt=0.5,
|
||||
).order_by('-similarity')[:10]
|
||||
|
||||
book_results = books_manager.search(query)
|
||||
|
@ -285,6 +286,7 @@ def invite_page(request, code):
|
|||
}
|
||||
return TemplateResponse(request, 'invite.html', data)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('bookwyrm.create_invites', raise_exception=True)
|
||||
def manage_invites(request):
|
||||
|
@ -311,8 +313,9 @@ def notifications_page(request):
|
|||
notifications.update(read=True)
|
||||
return TemplateResponse(request, 'notifications.html', data)
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
def user_page(request, username, subpage=None, shelf=None):
|
||||
def user_page(request, username):
|
||||
''' profile page for a user '''
|
||||
try:
|
||||
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)
|
||||
# otherwise we're at a UI view
|
||||
|
||||
data = {
|
||||
'title': user.name,
|
||||
'user': user,
|
||||
'is_self': request.user.id == user.id,
|
||||
}
|
||||
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)
|
||||
try:
|
||||
page = int(request.GET.get('page', 1))
|
||||
except ValueError:
|
||||
page = 1
|
||||
|
||||
data['shelf_count'] = user.shelf_set.count()
|
||||
shelves = []
|
||||
for user_shelf in user.shelf_set.all():
|
||||
shelf_preview = []
|
||||
|
||||
# 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():
|
||||
continue
|
||||
shelves.append({
|
||||
shelf_preview.append({
|
||||
'name': user_shelf.name,
|
||||
'remote_id': user_shelf.remote_id,
|
||||
'books': user_shelf.books.all()[:3],
|
||||
'size': user_shelf.books.count(),
|
||||
})
|
||||
if len(shelves) > 2:
|
||||
if len(shelf_preview) > 2:
|
||||
break
|
||||
|
||||
data['shelves'] = shelves
|
||||
data['activities'] = get_activity_feed(user, 'self')[:15]
|
||||
# user's posts
|
||||
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)
|
||||
|
||||
|
||||
|
@ -376,7 +396,13 @@ def followers_page(request, username):
|
|||
if is_api_request(request):
|
||||
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
|
||||
|
@ -393,16 +419,13 @@ def following_page(request, username):
|
|||
if is_api_request(request):
|
||||
return JsonResponse(user.to_following_activity(**request.GET))
|
||||
|
||||
return user_page(request, username, subpage='following')
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
def user_shelves_page(request, username):
|
||||
''' list of followers '''
|
||||
if request.method != 'GET':
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
return user_page(request, username, subpage='shelves')
|
||||
data = {
|
||||
'title': '%s: following' % user.name,
|
||||
'user': user,
|
||||
'is_self': request.user.id == user.id,
|
||||
'following': user.following.all(),
|
||||
}
|
||||
return TemplateResponse(request, 'following.html', data)
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
|
@ -434,6 +457,7 @@ def status_page(request, username, status_id):
|
|||
}
|
||||
return TemplateResponse(request, 'status.html', data)
|
||||
|
||||
|
||||
def status_visible_to_user(viewer, status):
|
||||
''' is a user authorized to view a status? '''
|
||||
if viewer == status.user or status.privacy in ['public', 'unlisted']:
|
||||
|
@ -447,7 +471,6 @@ def status_visible_to_user(viewer, status):
|
|||
return False
|
||||
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
def replies_page(request, username, status_id):
|
||||
''' ordered collection of replies to a status '''
|
||||
|
@ -483,6 +506,11 @@ def edit_profile_page(request):
|
|||
|
||||
def book_page(request, book_id):
|
||||
''' 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)
|
||||
if is_api_request(request):
|
||||
return JsonResponse(book.to_activity(), encoder=ActivityEncoder)
|
||||
|
@ -499,8 +527,21 @@ def book_page(request, book_id):
|
|||
reviews = models.Review.objects.filter(
|
||||
book__in=work.edition_set.all(),
|
||||
)
|
||||
# all reviews for the book
|
||||
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 = []
|
||||
readthroughs = []
|
||||
if request.user.is_authenticated:
|
||||
|
@ -523,18 +564,13 @@ def book_page(request, book_id):
|
|||
data = {
|
||||
'title': book.title,
|
||||
'book': book,
|
||||
'reviews': reviews.filter(content__isnull=False),
|
||||
'reviews': reviews_page,
|
||||
'ratings': reviews.filter(content__isnull=True),
|
||||
'rating': rating['rating__avg'],
|
||||
'tags': tags,
|
||||
'user_tags': user_tags,
|
||||
'review_form': forms.ReviewForm(),
|
||||
'quotation_form': forms.QuotationForm(),
|
||||
'comment_form': forms.CommentForm(),
|
||||
'readthroughs': readthroughs,
|
||||
'tag_form': forms.TagForm(),
|
||||
'path': '/book/%s' % book_id,
|
||||
'cover_form': forms.CoverForm(instance=book),
|
||||
'info_fields': [
|
||||
{'name': 'ISBN', 'value': book.isbn_13},
|
||||
{'name': 'OCLC number', 'value': book.oclc_number},
|
||||
|
@ -543,6 +579,8 @@ def book_page(request, book_id):
|
|||
{'name': 'Format', 'value': book.physical_format},
|
||||
{'name': 'Pages', 'value': book.pages},
|
||||
],
|
||||
'next': next_page,
|
||||
'prev': prev_page,
|
||||
}
|
||||
return TemplateResponse(request, 'book.html', data)
|
||||
|
||||
|
@ -564,10 +602,7 @@ def edit_book_page(request, book_id):
|
|||
|
||||
def editions_page(request, book_id):
|
||||
''' list of editions of a book '''
|
||||
try:
|
||||
work = models.Work.objects.get(id=book_id)
|
||||
except models.Work.DoesNotExist:
|
||||
return HttpResponseNotFound()
|
||||
work = get_object_or_404(models.Work, id=book_id)
|
||||
|
||||
if is_api_request(request):
|
||||
return JsonResponse(
|
||||
|
@ -586,10 +621,7 @@ def editions_page(request, book_id):
|
|||
|
||||
def author_page(request, author_id):
|
||||
''' landing page for an author '''
|
||||
try:
|
||||
author = models.Author.objects.get(id=author_id)
|
||||
except ValueError:
|
||||
return HttpResponseNotFound()
|
||||
author = get_object_or_404(models.Author, id=author_id)
|
||||
|
||||
if is_api_request(request):
|
||||
return JsonResponse(author.to_activity(), encoder=ActivityEncoder)
|
||||
|
@ -622,6 +654,12 @@ def tag_page(request, tag_id):
|
|||
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):
|
||||
''' display a shelf '''
|
||||
try:
|
||||
|
@ -629,10 +667,37 @@ def shelf_page(request, username, shelf_identifier):
|
|||
except models.User.DoesNotExist:
|
||||
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):
|
||||
return JsonResponse(shelf.to_activity(**request.GET))
|
||||
|
||||
return user_page(
|
||||
request, username, subpage='shelves', shelf=shelf_identifier)
|
||||
data = {
|
||||
'title': user.name,
|
||||
'user': user,
|
||||
'is_self': is_self,
|
||||
'shelves': shelves.all(),
|
||||
'shelf': shelf,
|
||||
}
|
||||
|
||||
return TemplateResponse(request, 'shelf.html', data)
|
||||
|
|
Loading…
Reference in a new issue