Merge pull request #206 from mouse-reeve/ui-overhaul

Ui overhaul
This commit is contained in:
Mouse Reeve 2020-09-30 17:15:22 -07:00 committed by GitHub
commit 4fda5c8e22
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
68 changed files with 1140 additions and 1755 deletions

View file

@ -57,7 +57,7 @@ def broadcast_task(sender_id, activity, recipients):
except requests.exceptions.HTTPError as e:
# TODO: maybe keep track of users who cause errors
errors.append({
'error': e,
'error': str(e),
'recipient': recipient,
'activity': activity,
})

View file

@ -1,13 +1,33 @@
''' using django model forms '''
import datetime
from collections import defaultdict
from django.forms import ModelForm, PasswordInput, widgets
from django import forms
from django.forms import ModelForm, PasswordInput, widgets
from django.forms.widgets import Textarea
from bookwyrm import models
class LoginForm(ModelForm):
class CustomForm(ModelForm):
''' add css classes to the forms '''
def __init__(self, *args, **kwargs):
css_classes = defaultdict(lambda: '')
css_classes['text'] = 'input'
css_classes['password'] = 'input'
css_classes['email'] = 'input'
css_classes['number'] = 'input'
css_classes['checkbox'] = 'checkbox'
css_classes['textarea'] = 'textarea'
super(CustomForm, self).__init__(*args, **kwargs)
for visible in self.visible_fields():
if hasattr(visible.field.widget, 'input_type'):
input_type = visible.field.widget.input_type
if isinstance(visible.field.widget, Textarea):
input_type = 'textarea'
visible.field.widget.attrs['class'] = css_classes[input_type]
class LoginForm(CustomForm):
class Meta:
model = models.User
fields = ['username', 'password']
@ -17,7 +37,7 @@ class LoginForm(ModelForm):
}
class RegisterForm(ModelForm):
class RegisterForm(CustomForm):
class Meta:
model = models.User
fields = ['username', 'email', 'password']
@ -27,13 +47,13 @@ class RegisterForm(ModelForm):
}
class RatingForm(ModelForm):
class RatingForm(CustomForm):
class Meta:
model = models.Review
fields = ['rating']
class ReviewForm(ModelForm):
class ReviewForm(CustomForm):
class Meta:
model = models.Review
fields = ['name', 'content']
@ -44,7 +64,7 @@ class ReviewForm(ModelForm):
}
class CommentForm(ModelForm):
class CommentForm(CustomForm):
class Meta:
model = models.Comment
fields = ['content']
@ -54,7 +74,7 @@ class CommentForm(ModelForm):
}
class QuotationForm(ModelForm):
class QuotationForm(CustomForm):
class Meta:
model = models.Quotation
fields = ['quote', 'content']
@ -65,7 +85,7 @@ class QuotationForm(ModelForm):
}
class ReplyForm(ModelForm):
class ReplyForm(CustomForm):
class Meta:
model = models.Status
fields = ['content']
@ -73,14 +93,14 @@ class ReplyForm(ModelForm):
labels = {'content': 'Comment'}
class EditUserForm(ModelForm):
class EditUserForm(CustomForm):
class Meta:
model = models.User
fields = ['avatar', 'name', 'summary', 'manually_approves_followers']
help_texts = {f: None for f in fields}
class TagForm(ModelForm):
class TagForm(CustomForm):
class Meta:
model = models.Tag
fields = ['name']
@ -88,17 +108,18 @@ class TagForm(ModelForm):
labels = {'name': 'Add a tag'}
class CoverForm(ModelForm):
class CoverForm(CustomForm):
class Meta:
model = models.Book
fields = ['cover']
help_texts = {f: None for f in fields}
class EditionForm(ModelForm):
class EditionForm(CustomForm):
class Meta:
model = models.Edition
exclude = [
'remote_id',
'created_date',
'updated_date',
'last_sync_date',
@ -135,7 +156,7 @@ class ExpiryWidget(widgets.Select):
return datetime.datetime.now() + interval
class CreateInviteForm(ModelForm):
class CreateInviteForm(CustomForm):
class Meta:
model = models.SiteInvite
exclude = ['code', 'user', 'times_used']

View file

@ -91,6 +91,11 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
to write this so it's just a property '''
return cls.objects.filter(reply_parent=status).select_subclasses()
@property
def status_type(self):
''' expose the type of status for the ui using activity type '''
return self.activity_serializer.__name__
def to_replies(self, **kwargs):
''' helper function for loading AP serialized replies to a status '''
return self.to_ordered_collection(
@ -211,7 +216,7 @@ class Boost(Status):
ActivityMapping('object', 'boosted_status'),
]
activity_serializer = activitypub.Like
activity_serializer = activitypub.Boost
# This constraint can't work as it would cross tables.
# class Meta:

View file

@ -43,17 +43,20 @@ def handle_account_search(query):
except models.User.DoesNotExist:
url = 'https://%s/.well-known/webfinger?resource=acct:%s' % \
(domain, query)
try:
response = requests.get(url)
except requests.exceptions.ConnectionError:
return None
if not response.ok:
response.raise_for_status()
return None
data = response.json()
for link in data['links']:
if link['rel'] == 'self':
try:
user = get_or_create_remote_user(link['href'])
except KeyError:
return HttpResponseNotFound()
return user
return None
return [user]
def handle_follow(user, to_follow):

View file

@ -44,8 +44,7 @@ def make_signature(sender, destination, date, digest):
def make_digest(data):
''' creates a message digest for signing '''
return 'SHA-256=' + b64encode(hashlib.sha256(data.encode('utf-8'))\
.digest()).decode('utf-8')
return 'SHA-256=' + b64encode(hashlib.sha256(data).digest()).decode('utf-8')
def verify_digest(request):

File diff suppressed because one or more lines are too long

1
bookwyrm/static/css/bulma.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View file

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View file

@ -0,0 +1,115 @@
/* --- ICONS --- */
/* --- TOGGLES --- */
input.toggle-control {
display: none;
}
.hidden {
display: none;
}
input.toggle-control:checked ~ .toggle-content {
display: block;
}
/* --- STARS --- */
.rate-stars button.icon {
background: none;
border: none;
padding: 0;
margin: 0;
display: inline;
}
.rate-stars:hover .icon:before {
content: '\e9d9';
}
.rate-stars form:hover ~ form .icon:before{
content: '\e9d7';
}
/* stars in a review form */
.form-rate-stars:hover .icon:before {
content: '\e9d9';
}
.form-rate-stars input + .icon:before {
content: '\e9d9';
}
.form-rate-stars input:checked + .icon:before {
content: '\e9d9';
}
.form-rate-stars input:checked + * ~ .icon:before {
content: '\e9d7';
}
.form-rate-stars:hover label.icon:before {
content: '\e9d9';
}
.form-rate-stars label.icon:hover:before {
content: '\e9d9';
}
.form-rate-stars label.icon:hover ~ label.icon:before{
content: '\e9d7';
}
/* --- BOOK COVERS --- */
.cover-container {
height: 250px;
width: max-content;
}
.cover-container.is-medium {
height: 150px;
}
.cover-container.is-medium .no-cover div {
font-size: 0.9em;
padding: 0.3em;
}
.cover-container.is-small {
height: 100px;
}
.cover-container.is-small .no-cover div {
font-size: 0.7em;
padding: 0.1em;
}
.book-cover {
height: 100%;
object-fit: scale-down;
}
.no-cover {
position: relative;
white-space: normal;
}
.no-cover div {
position: absolute;
padding: 1em;
color: white;
top: 0;
left: 0;
text-align: center;
}
/* --- AVATAR --- */
.avatar {
vertical-align: middle;
display: inline;
}
/* --- QUOTES --- */
.quote blockquote {
position: relative;
padding-left: 2em;
}
.quote blockquote:before, .quote blockquote:after {
font-family: 'icomoon';
position: absolute;
}
.quote blockquote:before {
content: "\e904";
top: 0;
left: 0;
}
.quote blockquote:after {
content: "\e903";
right: 0;
}

View file

@ -1,822 +0,0 @@
/* some colors that are okay: #247BA0 #70C1B2 #B2DBBF #F3FFBD #FF1654 */
/* general override */
* {
margin: 0;
padding: 0;
line-height: 1.3em;
font-family: sans-serif;
}
html {
background-color: #FFF;
color: black;
}
a {
color: #247BA0;
}
h1 {
font-weight: normal;
font-size: 1.5rem;
}
h2 {
font-weight: normal;
font-size: 1rem;
padding: 0.5rem 0.2rem;
margin-bottom: 1rem;
border-bottom: 3px solid #B2DBBF;
}
h2 .edit-link {
text-decoration: none;
font-size: 0.9em;
float: right;
}
h2 .edit-link .icon {
font-size: 1.2em;
}
h3 {
font-size: 1rem;
font-weight: bold;
margin-bottom: 0.5em;
}
h3 small {
font-weight: normal;
}
section {
margin-bottom: 1em;
}
/* fixed display top bar */
body {
padding-top: 90px;
}
#top-bar {
overflow: visible;
padding: 0.5rem;
border-bottom: 3px solid #247BA0;
margin-bottom: 1em;
width: 100%;
background-color: #FFF;
position: fixed;
top: 0;
height: 47px;
z-index: 2;
}
/* --- header bar content */
#branding {
flex-grow: 0;
}
#menu {
list-style: none;
text-align: center;
margin-top: 1.5rem;
flex-grow: 2;
font-size: 0.9em;
}
#menu li {
display: inline-block;
padding: 0 0.5em;
text-transform: uppercase;
}
#menu a {
color: #555;
text-decoration: none;
font-size: 0.9em;
}
#actions {
margin-top: 1em;
}
#actions > * {
display: inline-block;
}
#actions > *:last-child {
margin-left: 0.5em;
}
#notifications .icon {
font-size: 1.1rem;
}
#notifications a {
color: black;
text-decoration: none;
position: relative;
top: 0.2rem;
}
#notifications .count {
background-color: #FF1654;
color: white;
font-size: 0.85rem;
border-radius: 50%;
display: block;
position: absolute;
text-align: center;
top: -0.65rem;
right: -0.5rem;
height: 1rem;
width: 1rem;
}
.notification {
margin-bottom: 1em;
padding: 1em 0;
background-color: #EEE;
}
.notification.unread {
background-color: #DDD;
}
#search button {
border: none;
background: none;
}
#main, header {
margin: 0 auto;
max-width: 55rem;
padding-right: 1em;
}
/* pulldown */
.pulldown-container {
position: relative;
display: inline;
}
.pulldown {
display: none;
position: absolute;
list-style: none;
background: white;
padding: 1em;
right: 0;
font-size: 0.9rem;
box-shadow: 0 5px 10px rgba(0,0,0,0.15);
width: max-content;
text-align: left;
z-index: 1;
}
.pulldown-container:hover .pulldown {
display: block;
}
.pulldown li a {
display: block;
margin-bottom: 0.5em;
text-decoration: none;
padding: 0.3em 0.8em
}
div.pulldown-button {
background-color: #eee;
border-radius: 0.3em;
color: #247BA0;
width: max-content;
margin: 0 auto;
white-space: nowrap;
}
.post div.pulldown-button {
border: 2px solid #247BA0;
}
.pulldown-button form {
display: inline;
}
div.pulldown-button button {
display: inline;
border: none;
border-radius: 0;
background-color: inherit;
color: #247BA0;
}
div.pulldown-button .pulldown-toggle {
padding-right: 0;
padding-left: 0;
position: relative;
left: -0.5em;
}
ul.pulldown button {
display: block;
text-align: left;
width: 100%;
border: none;
border-radius: 0;
background-color: white;
color: #247BA0;
}
.pulldown button[disabled] {
color: #aaa;
}
.pulldown button[disabled]:hover {
background-color: white;
}
.pulldown button:hover, .pulldown li:hover {
background-color: #ddd;
}
/* content area */
.content-container {
margin: 1rem;
}
.content-container > * {
padding-left: 1em;
padding-right: 1em;
}
#feed {
display: flex;
flex-direction: column;
padding-top: 70px;
position: relative;
z-index: 0;
margin-top: -2em;
}
/* row component */
.row {
display: flex;
flex-direction: row;
}
.row > * {
flex-grow: 1;
width: min-content;
margin-right: 1em;
}
.row > *:last-child {
margin-right: 0;
}
.row.shrink > * {
flex-grow: 0;
width: max-content;
}
.row.wrap {
flex-wrap: wrap;
}
.column {
display: flex;
flex-direction: column;
}
.column > * {
margin-bottom: 1em;
}
/* discover books page grid of covers */
.book-grid .book-cover {
height: 176px;
width: auto;
margin: 0 auto;
}
.book-grid .no-cover {
width: 115px;
}
.book-grid > * {
margin-bottom: 2em;
}
/* special case forms */
.review-form label {
display: block;
}
.review-form textarea {
width: 30rem;
height: 10rem;
}
.review-form.quote-form textarea#id_content {
height: 4rem;
}
.follow-requests .row {
margin-bottom: 0.5em;
}
.follow-requests .row > *:first-child {
width: 20em;
}
.login form {
margin-top: 1em;
}
.login form p {
display: flex;
flex-direction: row;
padding: 0.5em 0;
}
.login form label {
width: 0;
flex-grow: 1;
display: inline-block;
}
.book-form textarea {
display: block;
width: 100%;
font-size: 0.9em;
}
.book-form label {
display: inline-block;
width: 8rem;
vertical-align: top;
}
.book-form .row label {
width: max-content;
}
/* general form stuff */
input, button {
padding: 0.2em 0.5em;
}
button, input[type="submit"] {
cursor: pointer;
width: max-content;
}
.content-container button {
border: none;
background-color: #247BA0;
color: white;
padding: 0.3em 0.8em;
font-size: 0.9em;
border-radius: 0.3em;
}
button.secondary {
background-color: #EEE;
color: #247BA0;
}
.post button.secondary {
border: 2px solid #247BA0;
}
button.warning {
background-color: #FF1654;
}
form input {
flex-grow: 1;
}
form div {
margin-bottom: 1em;
}
textarea {
padding: 0.5em;
}
/* icons */
a .icon {
color: black;
text-decoration: none;
}
button .icon {
font-size: 1.1rem;
vertical-align: sub;
}
.hidden-text {
height: 0;
width: 0;
position: absolute;
overflow: hidden;
}
/* star ratings */
.stars {
letter-spacing: -0.15em;
display: inline-block;
}
.rate-stars .icon {
cursor: pointer;
color: goldenrod;
}
.rate-stars label.icon {
color: black;
}
.rate-stars form {
display: inline;
width: min-content;
}
.rate-stars button.icon {
background: none;
border: none;
padding: 0;
margin: 0;
display: inline;
}
.cover-container .stars {
display: block;
text-align: center;
}
.rate-stars:hover .icon:before {
content: '\e9d9';
}
.rate-stars form:hover ~ form .icon:before{
content: '\e9d7';
}
.review-form .rate-stars:hover .icon:before {
content: '\e9d9';
}
.review-form .rate-stars label {
display: inline;
}
.review-form .rate-stars input + .icon:before {
content: '\e9d9';
}
.review-form .rate-stars input:checked + .icon:before {
content: '\e9d9';
}
.review-form .rate-stars input:checked + * ~ .icon:before {
content: '\e9d7';
}
.review-form .rate-stars:hover label.icon:before {
content: '\e9d9';
}
.review-form .rate-stars label.icon:hover:before {
content: '\e9d9';
}
.review-form .rate-stars label.icon:hover ~ label.icon:before{
content: '\e9d7';
}
.review-form .rate-stars input[type="radio"] {
display: none;
}
/* re-usable tab styles */
.tabs {
display: flex;
flex-direction: row;
border-bottom: 3px solid #FF1654;
padding-left: 1em;
}
.tabs.secondary {
border-bottom: 3px solid #247BA0;
}
.tab {
padding: 0.5em 1em;
border-radius: 0.25em 0.25em 0 0;
}
.secondary .tab {
padding: 0.25em 0.5em;
}
.tabs .tab.active {
background-color: #FF1654;
}
.tabs.secondary .tab.active {
background-color: #247BA0;
}
.tab.active a {
color: black;
}
.user-pic {
width: 2em;
height: 2em;
border-radius: 50%;
vertical-align: top;
position: relative;
bottom: 0.35em;
}
.user-pic.large {
width: 5em;
height: 5em;
}
.user-profile .row > * {
flex-grow: 0;
}
.user-profile .row > *:last-child {
flex-grow: 1;
margin-left: 2em;
}
/* general book display */
.book-preview {
overflow: hidden;
z-index: 1;
text-align: center;
}
.book-preview.grid {
float: left;
}
.cover-container {
flex-grow: 0;
}
.cover-container button {
display: block;
margin: 0 auto;
}
.book-cover {
width: 180px;
height: auto;
}
.book-cover.small {
width: 50px;
height: auto;
}
.no-cover {
position: relative;
}
.no-cover div {
position: absolute;
padding: 1em;
color: white;
top: 0;
left: 0;
text-align: center;
}
.no-cover .title {
text-transform: uppercase;
margin-bottom: 1em;
}
dl {
font-size: 0.9em;
margin-top: 0.5em;
}
dt {
float: left;
margin-right: 0.5em;
}
dd {
margin-bottom: 0.25em;
}
.all-shelves {
display: flex;
flex-direction: row;
margin-left: 0;
position: relative;
z-index: 1;
overflow-y: auto;
}
.all-shelves h2 {
white-space: nowrap;
}
.all-shelves > div {
flex-grow: 0;
}
.all-shelves > div:last-child {
padding-right: 0;
flex-grow: 1;
}
.all-shelves > div > * {
padding: 0;
}
.all-shelves > div:first-child > * {
padding-left: 1em;
}
.covers-shelf {
display: flex;
flex-direction: row;
}
.covers-shelf .cover-container {
margin-right: 1em;
font-size: 0.9em;
overflow: unset;
width: min-content;
}
.covers-shelf .cover-container:last-child {
margin-right: 0;
}
.covers-shelf .book-cover:hover {
cursor: pointer;
box-shadow: #F3FFBD 0em 0em 1em 1em;
}
.covers-shelf .book-cover {
height: 11rem;
width: auto;
margin: 0;
}
.close {
float: right;
cursor: pointer;
padding: 1rem;
}
.all-shelves input[type='radio'] {
display: none;
}
.compose-popout input[type="radio"] {
display: none;
}
.compose-suggestion {
display: none;
box-shadow: 0 5px 10px rgba(0,0,0,0.15);
padding-bottom: 1em;
margin-top: 2em;
}
input:checked ~ .compose-suggestion {
display: block;
}
.compose .book-preview {
background-color: #EEE;
padding: 1em;
}
.compose button {
margin: 0;
}
.compose .stars {
text-align: left;
}
.tag {
display: inline-block;
padding: 0.2em;
border-radius: 0.2em;
background-color: #EEE;
}
.tag form {
display: inline;
}
.tag a {
text-decoration: none;
}
blockquote {
white-space: pre-line;
}
blockquote .icon-quote-open, blockquote .icon-quote-close, .quote blockquote:before, .quote blockquote:after {
font-size: 2rem;
margin-right: 0.5rem;
color: #888;
}
blockquote .icon-quote-open {
float: left;
}
.quote {
margin-bottom: 2em;
position: relative;
}
.quote blockquote {
background-color: white;
margin: 1em;
padding: 1em;
}
.quote blockquote:before, .quote blockquote:after {
font-family: 'icomoon';
position: absolute;
}
.quote blockquote:before {
content: "\e904";
top: 0;
left: 0;
}
.quote blockquote:after {
content: "\e903";
bottom: 1em;
right: 0;
}
.interaction {
background-color: #B2DBBF;
border-radius: 0 0 0.5em 0.5em;
display: flex;
flex-direction: row;
padding: 0.5em;
}
.interaction > * {
margin-right: 0.5em;
}
.interaction button:hover {
box-shadow: #247BA0 0em 0em 1em 0em;
color: #247BA0;
}
.interaction button {
background: white;
height: 2em;
min-width: 3em;
padding: 0;
color: #888;
}
.interaction .active button .icon {
color: #FF1654;
}
.interaction textarea {
height: 2em;
width: 23em;
float: left;
padding: 0.25em;
margin-right: 0.5em;
}
.interaction textarea:valid, .interaction textarea:focus {
height: 4em;
}
.hidden {
display: none;
}
table {
border-collapse: collapse;
margin: 1em;
}
tr {
vertical-align: top;
}
tr:nth-child(even) {
background-color: #EEE;
}
th {
font-weight: bold;
}
th, td {
padding: 1em;
text-align: left;
}
.errorlist {
list-style: none;
font-size: 0.8em;
color: #FF1654;
}
/* status css */
.time-ago {
float: right;
display: block;
text-align: right;
}
.post {
background-color: #EFEFEF;
padding-top: 1em;
padding-bottom: 1em;
}
.post h2, .compose-suggestion h2 {
position: relative;
right: 2em;
border: none;
}
.post .time-ago {
position: relative;
left: 2em;
}
.post .user-pic, .compose-suggestion .user-pic {
right: 0.25em;
}
.post h2 .subhead {
display: block;
margin-left: 2em;
}
.post .subhead .time-ago {
display: none;
}
/* status page with replies */
.comment-thread .reply h2 {
background: none;
}
.comment-thread .post {
margin-left: 4em;
border-left: 2px solid #247BA0;
}
.comment-thread .post.depth-1 {
margin-left: 0;
border: none;
}
.comment-thread .post.depth-2 {
margin-left: 1em;
}
.comment-thread .post.depth-3 {
margin-left: 2em;
}
.comment-thread .post.depth-4 {
margin-left: 3em;
}
/* pagination */
.pagination a {
text-decoration: none;
}
.pagination .next {
text-align: right;
}
/* special one-off "delete all data" banner */
#warning {
background-color: #FF1654;
text-align: center;
}

View file

@ -32,29 +32,19 @@ function rate_stars(e) {
}
function tabChange(e) {
e.preventDefault();
var target = e.target.parentElement;
var target = e.target.closest('li')
var identifier = target.getAttribute('data-id');
var options_class = target.getAttribute('data-category');
var options = document.getElementsByClassName(options_class);
for (var i = 0; i < options.length; i++) {
if (!options[i].className.includes('hidden')) {
options[i].className += ' hidden';
}
}
var tabs = target.parentElement.children;
for (i = 0; i < tabs.length; i++) {
if (tabs[i].getAttribute('data-id') == identifier) {
tabs[i].className += ' active';
tabs[i].className += ' is-active';
} else {
tabs[i].className = tabs[i].className.replace('active', '');
tabs[i].className = tabs[i].className.replace('is-active', '');
}
}
var el = document.getElementById(identifier);
el.className = el.className.replace('hidden', '');
}
function ajaxPost(form) {

View file

@ -1,21 +1,20 @@
{% extends 'layout.html' %}
{% block content %}
<div class="content-container">
<h2>About {{ site_settings.name }}</h2>
<div class="columns">
<div class="column block">
<h2 class="title">About {{ site_settings.name }}</h2>
<p>
{{ site_settings.instance_description }}
</p>
</div>
<p>
<small>
<a href="/login/">Login or Create an Account</a>
</small>
</p>
<h2>Code of Conduct</h2>
<div class="column block">
<h2 class="title">Code of Conduct</h2>
<p>
{{ site_settings.code_of_conduct }}
</p>
</div>
</div>
{% endblock %}

View file

@ -1,28 +1,19 @@
{% extends 'layout.html' %}
{% load fr_display %}
{% block content %}
<div class="content-container">
<h2>{{ author.display_name }}</h2>
<div class="block">
<h2 class="title">{{ author.display_name }}</h2>
{% if author.bio %}
<p>
{{ author.bio | author_bio }}
{{ author.bio }}
</p>
{% endif %}
</div>
<div class="content-container">
<h2>Books by {{ author.display_name }}</h2>
<div class="book-grid row shrink wrap">
{% for book in books %}
<div class="book-preview">
<a href="/book/{{ book.id }}">
{% include 'snippets/book_cover.html' with book=book %}
</a>
{% include 'snippets/shelve_button.html' with book=book %}
</div>
{% endfor %}
</div>
<div class="block">
<h3 class="title is-4">Books by {{ author.display_name }}</h3>
{% include 'snippets/book_tiles.html' with books=books %}
</div>
{% endblock %}

View file

@ -1,22 +1,27 @@
{% extends 'layout.html' %}
{% load fr_display %}
{% load humanize %}
{% block content %}
<div class="content-container">
<h2>
{% include 'snippets/book_titleby.html' with book=book %}
{% if request.user.is_authenticated %}
<a href="{{ book.id }}/edit" class="edit-link">edit
<span class="icon icon-pencil">
<span class="hidden-text">Edit Book</span>
</span>
</a>
{% endif %}
<div class="block">
<div class="level">
<h2 class="title level-left">
<span>{% include 'snippets/book_titleby.html' with book=book %}</span>
</h2>
<div class="row">
{% if request.user.is_authenticated %}
<div class="level-right">
<a href="{{ book.id }}/edit">edit
<span class="icon icon-pencil">
<span class="is-sr-only">Edit Book</span>
</span>
</a>
</div>
{% endif %}
</div>
<div class="cover-container">
<div class="columns">
<div class="column is-narrow">
{% include 'snippets/book_cover.html' with book=book size=large %}
{% include 'snippets/rate_action.html' with user=request.user book=book %}
{% include 'snippets/shelve_button.html' %}
@ -25,11 +30,11 @@
<form name="add-cover" method="POST" action="/upload_cover/{{ book.id }}" enctype="multipart/form-data">
{% csrf_token %}
{{ cover_form.as_p }}
<button type="submit">Add cover</button>
<button class="button" type="submit">Add cover</button>
</form>
{% endif %}
<dl>
<dl class="content">
{% for field in info_fields %}
{% if field.value %}
<dt>{{ field.name }}:</dt>
@ -40,56 +45,81 @@
</div>
<div class="column">
<h3>{{ active_tab }} rating: {% include 'snippets/stars.html' with rating=rating %}</h3>
<div class="block">
<h3 class="field is-grouped">{% include 'snippets/stars.html' with rating=rating %} ({{ reviews|length }} review{{ reviews|length|pluralize }})</h3>
{% include 'snippets/book_description.html' %}
{% if book.parent_work.edition_set.count > 1 %}
<p><a href="/editions/{{ book.parent_work.id }}">{{ book.parent_work.edition_set.count }} editions</a></p>
{% endif %}
</div>
{% if request.user.is_authenticated %}
<div class="compose">
<div class="block">
{% include 'snippets/create_status.html' with book=book hide_cover=True %}
</div>
<div>
<div class="block">
<h3>Tags</h3>
<form name="tag" action="/tag/" method="post">
{% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}">
<input type="text" name="name">
<button type="submit">Add tag</button>
<input class="input" type="text" name="name">
<button class="button" type="submit">Add tag</button>
</form>
</div>
<div class="tag-cloud">
{% endif %}
<div class="block">
<div class="field is-grouped is-grouped-multiline">
{% for tag in tags %}
{% include 'snippets/tag.html' with book=book tag=tag user_tags=user_tags %}
{% endfor %}
</div>
</div>
{% endif %}
</div>
</div>
</div>
{% if request.user.is_authenticated %}
<div class="content-container tabs">
{% include 'snippets/tabs.html' with tabs=feed_tabs active_tab=active_tab path=path %}
</div>
{% endif %}
{% if not reviews %}
<div class="content-container">
<div class="block">
<p>No reviews yet!</p>
</div>
{% endif %}
<div class="block">
{% for review in reviews %}
<div class="content-container">
<div class="block">
{% include 'snippets/status.html' with status=review hide_book=True depth=1 %}
</div>
{% endfor %}
<div class="block columns">
{% for rating in ratings %}
<div class="column">
<div class="media">
<div class="media-left">{% include 'snippets/avatar.html' %}</div>
<div class="media-content">
<div>
{% include 'snippets/username.html' %}
</div>
<div class="field is-grouped mb-0">
<div>rated it</div>
{% include 'snippets/stars.html' with rating=rating.rating %}
</div>
<div>
<a href="{{ rating.remote_id }}">{{ rating.published_date | naturaltime }}</a>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock %}

View file

@ -1,17 +0,0 @@
{% extends 'layout.html' %}
{% load fr_display %}
{% block content %}
<div class="content-container">
<h2>Recently Added Books</h2>
<div class="book-grid row wrap shrink">
{% for book in books %}
<div class="cover-container">
<a href="/book/{{ book.id }}">
{% include 'snippets/book_cover.html' with book=book %}
</a>
{% include 'snippets/shelve_button.html' with book=book %}
</div>
{% endfor %}
</div>
</div>
{% endblock %}

View file

@ -1,71 +1,83 @@
{% extends 'layout.html' %}
{% load humanize %}
{% block content %}
<div class="content-container">
<h2>
<div class="block">
<div class="level">
<h2 class="title level-left">
Edit "{{ book.title }}"
</h2>
<div class="level-right">
<a href="/book/{{ book.id }}">
<span class="edit-link icon icon-close">
<span class="hidden-text">Close</span>
<span class="is-sr-only">Close</span>
</span>
</a>
</h2>
<div class="book-preview row">
<div class="cover-container">
</div>
</div>
<div class="columns">
<div class="column is-narrow">
{% include 'snippets/book_cover.html' with book=book size="small" %}
</div>
<div>
<div class="column is-narrow">
<p>Added: {{ book.created_date | naturaltime }}</p>
<p>Updated: {{ book.updated_date | naturaltime }}</p>
</div>
</div>
</div>
<form class="book-form content-container" 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 %}
<h3>Data sync
<small>If sync is enabled, any changes will be over-written</small>
<div class="block">
<h3 class="title is-4">Data sync</h3>
<h4 class="subtitle is-5">If sync is enabled, any changes will be over-written</h4>
</h3>
<div>
<div class="row">
<p><label for="id_sync">Sync:</label> <input type="checkbox" name="sync" id="id_sync"></p>
<p><label for="id_sync_cover">Sync cover:</label> <input type="checkbox" name="sync_cover" id="id_sync_cover"></p>
<div class="columns">
<div class="column is-narrow">
<label class="checkbox" for="id_sync"><input class="checkbox" type="checkbox" name="sync" id="id_sync"> Sync</label>
</div>
<div class="column is-narrow">
<label class="checkbox" for="id_sync_cover"><input class="checkbox" type="checkbox" name="sync_cover" id="id_sync_cover"> Sync cover</label>
</div>
</div>
</div>
<h3>Cover</h3>
<div class="image-form">
<div class="columns">
<div class="block column">
<h3 class="title is-4">Book Identifiers</h3>
<p class="fields is-grouped"><label class="label"for="id_isbn_13">ISBN 13:</label> {{ form.isbn_13 }} </p>
<p class="fields is-grouped"><label class="label"for="id_isbn_10">ISBN 10:</label> {{ form.isbn_10 }} </p>
<p class="fields is-grouped"><label class="label"for="id_openlibrary_key">Openlibrary key:</label> {{ form.openlibrary_key }} </p>
<p class="fields is-grouped"><label class="label"for="id_librarything_key">Librarything key:</label> {{ form.librarything_key }} </p>
<p class="fields is-grouped"><label class="label"for="id_goodreads_key">Goodreads key:</label> {{ form.goodreads_key }} </p>
</div>
<div class="column">
<div class="block">
<h3 class="title is-4">Cover</h3>
<p>{{ form.cover }} </p>
</div>
<h3>Book Identifiers</h2>
<div>
<p><label for="id_isbn_13">ISBN 13:</label> {{ form.isbn_13 }} </p>
<p><label for="id_isbn_10">ISBN 10:</label> {{ form.isbn_10 }} </p>
<p><label for="id_openlibrary_key">Openlibrary key:</label> {{ form.openlibrary_key }} </p>
<p><label for="id_librarything_key">Librarything key:</label> {{ form.librarything_key }} </p>
<p><label for="id_goodreads_key">Goodreads key:</label> {{ form.goodreads_key }} </p>
<div class="block">
<h3 class="title is-4">Physical Properties</h3>
<p class="fields is-grouped"><label class="label"for="id_physical_format">Format:</label> {{ form.physical_format }} </p>
<p class="fields is-grouped"><label class="label"for="id_pages">Pages:</label> {{ form.pages }} </p>
</div>
</div>
</div>
<h3>Physical Properties</h3>
<div>
<p><label for="id_physical_format">Format:</label> {{ form.physical_format }} </p>
<p><label for="id_pages">Pages:</label> {{ form.pages }} </p>
<div class="block">
<h3 class="title is-4">Metadata</h3>
<p class="fields is-grouped"><label class="label"for="id_title">Title:</label> {{ form.title }} </p>
<p class="fields is-grouped"><label class="label"for="id_sort_title">Sort title:</label> {{ form.sort_title }} </p>
<p class="fields is-grouped"><label class="label"for="id_subtitle">Subtitle:</label> {{ form.subtitle }} </p>
<p class="fields is-grouped"><label class="label"for="id_description">Description:</label> {{ form.description }} </p>
<p class="fields is-grouped"><label class="label"for="id_series">Series:</label> {{ form.series }} </p>
<p class="fields is-grouped"><label class="label"for="id_series_number">Series number:</label> {{ form.series_number }} </p>
<p class="fields is-grouped"><label class="label"for="id_first_published_date">First published date:</label> {{ form.first_published_date }} </p>
<p class="fields is-grouped"><label class="label"for="id_published_date">Published date:</label> {{ form.published_date }} </p>
</div>
<h3>Metadata</h3>
<div>
<p><label for="id_title">Title:</label> {{ form.title }} </p>
<p><label for="id_sort_title">Sort title:</label> {{ form.sort_title }} </p>
<p><label for="id_subtitle">Subtitle:</label> {{ form.subtitle }} </p>
<p><label for="id_description">Description:</label> {{ form.description }} </p>
<p><label for="id_series">Series:</label> {{ form.series }} </p>
<p><label for="id_series_number">Series number:</label> {{ form.series_number }} </p>
<p><label for="id_first_published_date">First published date:</label> {{ form.first_published_date }} </p>
<p><label for="id_published_date">Published date:</label> {{ form.published_date }} </p>
</div>
<div>
<button type="submit">Save</button>
<div class="block">
<button class="button is-primary" type="submit">Save</button>
</div>
</form>

View file

@ -1,16 +1,11 @@
{% extends 'layout.html' %}
{% block content %}
<div class="content-container">
<div class="user-profile">
<h2>Edit Profile</h2>
<p>{% include 'snippets/avatar.html' with user=user %} {% if user.localname %}{{ user.localname }}{% else %}{{ user.username }}{% endif %}</p>
<div class="block">
<h2 class="title">Edit Profile</h2>
<form name="avatar" action="/edit_profile/" method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Update profile</button>
<button class="button is-primary" type="submit">Update profile</button>
</form>
</div>
</div>
{% endblock %}

View file

@ -1,18 +1,10 @@
{% extends 'layout.html' %}
{% load fr_display %}
{% block content %}
<div class="content-container">
<h2>Editions of <a href="/book/{{ work.id }}">"{{ work.title }}"</a></h2>
<ol class="book-grid row wrap">
{% for book in editions %}
<li class="book-preview">
<a href="/book/{{ book.id }}">
{% include 'snippets/book_cover.html' with book=book %}
</a>
{% include 'snippets/shelve_button.html' with book=book %}
</li>
{% endfor %}
</ol>
<div class="block">
<h2 class="title">Editions of <a href="/book/{{ work.id }}">"{{ work.title }}"</a></h2>
{% include 'snippets/book_tiles.html' with books=editions %}
</div>
{% endblock %}

View file

@ -1,8 +1,8 @@
{% extends 'layout.html' %}
{% block content %}
<div class="content-container">
<h2>Server Error</h2>
<div class="block">
<h2 class="title">Server Error</h2>
<p>Something went wrong! Sorry about that.</p>
</div>

View file

@ -2,23 +2,42 @@
{% load fr_display %}
{% block content %}
{% include 'snippets/covers_shelf.html' with shelves=shelves user=request.user %}
<div id="feed">
<div class="content-container tabs">
{% include 'snippets/tabs.html' with tabs=feed_tabs active_tab=active_tab %}
<div class="columns">
<div class="column is-one-third">
<h2 class="title is-4">Suggested books</h2>
<div class="tabs is-small is-toggle">
<ul>
{% for book in suggested_books %}
<li class="{% if forloop.first %}is-active{% endif %}" data-id="tab-book-{{ book.id }}">
<label for="book-{{ book.id }}" onclick="tabChange(event)"><a>{% include 'snippets/book_cover.html' with book=book size="medium" %}</a></label>
</li>
{% endfor %}
</ul>
</div>
{% for book in suggested_books %}
<div>
<input class="toggle-control" type="radio" name="recent-books" id="book-{{ book.id }}" {% if forloop.first %}checked{% endif %}>
<div class="toggle-content hidden">
<div class="block">
{% include 'snippets/book_titleby.html' with book=book %}
{% include 'snippets/shelve_button.html' with book=book %}
</div>
{% include 'snippets/create_status.html' with book=book %}
</div>
</div>
{% endfor %}
</div>
<div class="column is-two-thirds" id="feed">
{% for activity in activities %}
<div class="content-container">
<div class="block">
{% include 'snippets/status.html' with status=activity %}
</div>
{% endfor %}
<div class="content-container pagination row">
<nav class="pagination" role="navigation" aria-label="pagination">
{% if prev %}
<p>
<p class="pagination-previous">
<a href="{{ prev }}">
<span class="icon icon-arrow-left"></span>
Previous
@ -27,14 +46,14 @@
{% endif %}
{% if next %}
<p class="next">
<p class="pagination-next">
<a href="{{ next }}">
Next
<span class="icon icon-arrow-right"></span>
</a>
</p>
{% endif %}
</nav>
</div>
</div>
{% endblock %}

View file

@ -3,20 +3,22 @@
{% block content %}
{% include 'user_header.html' with user=user %}
<div class="content-container">
<h2>Followers</h2>
<div class="block">
<h2 class="title">Followers</h2>
{% for followers in followers %}
<div class="row shrink">
<div>
<div class="block">
<div class="field is-grouped">
<div class="control">
{% include 'snippets/avatar.html' with user=followers %}
</div>
<div>
<div class="control">
{% include 'snippets/username.html' with user=followers show_full=True %}
</div>
<div>
<div class="control">
{% include 'snippets/follow_button.html' with user=followers %}
</div>
</div>
</div>
{% endfor %}
{% if not followers.count %}
<div>{{ user|username }} has no followers</div>

View file

@ -3,20 +3,22 @@
{% block content %}
{% include 'user_header.html' %}
<div class="content-container">
<h2>Following</h2>
<div class="block">
<h2 class="title">Following</h2>
{% for follower in user.following.all %}
<div class="row shrink">
<div>
<div class="block">
<div class="field is-grouped">
<div class="control">
{% include 'snippets/avatar.html' with user=follower %}
</div>
<div>
<div class="control">
{% include 'snippets/username.html' with user=follower show_full=True %}
</div>
<div>
<div class="control">
{% include 'snippets/follow_button.html' with user=follower %}
</div>
</div>
</div>
{% endfor %}
{% if not following.count %}
<div>No one is following {{ user|username }}</div>

View file

@ -1,17 +1,22 @@
{% extends 'layout.html' %}
{% load humanize %}
{% block content %}
<div class="content-container">
<h2>Import Books from GoodReads</h2>
<div class="block">
<h2 class="title">Import Books from GoodReads</h2>
<form name="import" action="/import_data/" method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ import_form.as_p }}
<button type="submit">Import</button>
<button class="button" type="submit">Import</button>
</form>
<p>
Imports are limited in size, and only the first {{ limit }} items will be imported.
</div>
<h2>Recent Imports</h2>
<div class="content block">
<h2 class="title">Recent Imports</h2>
{% if not jobs %}
<p>No recent imports</p>
{% endif %}
<ul>
{% for job in jobs %}
<li><a href="/import_status/{{ job.id }}">{{ job.created_date | naturaltime }}</a></li>

View file

@ -2,9 +2,8 @@
{% load fr_display %}
{% load humanize %}
{% block content %}
<div id="content">
<div>
<h1>Import Status</h1>
<div class="block">
<h1 class="title">Import Status</h1>
<p>
Import started: {{ job.created_date | naturaltime }}
@ -16,7 +15,9 @@
<p>
{{ task.info }}
{% endif %}
</div>
<div class="block">
{% if job.import_status %}
{% include 'snippets/status.html' with status=job.import_status %}
{% endif %}
@ -25,8 +26,10 @@
<p>
(Hit reload to update!)
{% endif %}
</div>
<table>
<div class="block">
<table class="table">
<tr>
<th>
Book

View file

@ -4,8 +4,9 @@
<head>
<title>BookWyrm</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link type="text/css" rel="stylesheet" href="/static/format.css">
<link type="text/css" rel="stylesheet" href="/static/icons.css">
<link type="text/css" rel="stylesheet" href="/static/css/bulma.min.css">
<link type="text/css" rel="stylesheet" href="/static/css/format.css">
<link type="text/css" rel="stylesheet" href="/static/css/icons.css">
<link rel="shortcut icon" type="image/x-icon" href="/static/images/favicon.ico">
@ -16,63 +17,103 @@
<meta name="og:description" content="Federated Social Reading">
<meta name="twitter:creator" content="@tripofmice">
<meta name="twitter:site" content="@tripofmice">
</head>
<body>
<div id="top-bar">
<header class="row">
<div id="branding">
<a href="/">
<img id="logo" src="/static/images/logo-small.png" alt="BookWyrm"></img>
<nav class="navbar" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" href="/">
<img src="/static/images/logo-small.png" alt="BookWyrm" width="112" height="28">
</a>
</div>
<ul id="menu">
{% if request.user.is_authenticated %}
<li><a href="/user/{{request.user.localname}}/shelves">Your shelves</a></li>
{% endif %}
<li><a href="/#feed">Updates</a></li>
<li><a href="/books">Discover Books</a></li>
</ul>
<div id="actions">
<div id="search">
<form action="/search/">
<input type="text" name="q" placeholder="Search for a book or user">
<button type="submit">
<form class="navbar-item" action="/search/">
<div class="field is-grouped">
<input class="input" type="text" name="q" placeholder="Search for a book or user">
<button class="button" type="submit">
<span class="icon icon-search">
<span class="hidden-text">search</span>
<span class="is-sr-only">search</span>
</span>
</button>
</div>
</form>
</div>
{% if request.user.is_authenticated %}
<div id="notifications">
<a href="/notifications">
<span class="icon icon-bell">
<span class="hidden-text">Notitications</span>
</span>
{% if request.user|notification_count %}<span class="count">{{ request.user | notification_count }}</span>{% endif %}
</a>
</div>
<div class="pulldown-container">
{% include 'snippets/avatar.html' with user=request.user %}
<ul class="pulldown">
<li><a href="/user/{{ request.user }}">Your profile</a></li>
<li><a href="/user-edit/">Settings</a></li>
<li><a href="/import">Import Books</a></li>
<li><a href="/manage_invites/">Invites</a></li>
<li><a href="/logout/">Log out</a></li>
</ul>
</p>
{% endif %}
</div>
</header>
<label for="main-nav" role="button" class="navbar-burger burger" aria-label="menu" aria-expanded="false" data-target="mainNav">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</label>
</div>
<div id="main">
<input class="toggle-control" type="checkbox" id="main-nav">
<div id="mainNav" class="navbar-menu toggle-content">
<div class="navbar-start">
{% if request.user.is_authenticated %}
<a href="" class="navbar-item">
Lists
</a>
<a href="" class="navbar-item">
Groups
</a>
{% endif %}
</div>
<div class="navbar-end">
{% if request.user.is_authenticated %}
<div class="navbar-item has-dropdown is-hoverable">
<div class="navbar-link"><p>
{% include 'snippets/avatar.html' with user=user %}
{% include 'snippets/username.html' with user=request.user %}
</p></div>
<div class="navbar-dropdown">
<a href="/user/{{request.user.localname}}" class="navbar-item">
Profile
</a>
<a href="/user-edit" class="navbar-item">
Settings
</a>
<a href="/invite" class="navbar-item">
Invites
</a>
<a href="/import" class="navbar-item">
Import books
</a>
<hr class="navbar-divider">
<a href="/logout" class="navbar-item">
Log out
</a>
</div>
</div>
<div class="navbar-item">
<a href="/notifications">
<div class="tags has-addons">
<span class="tag is-medium">
<span class="icon icon-bell">
<span class="is-sr-only">Notitications</span>
</span>
</span>
{% if request.user|notification_count %}
<span class="tag is-danger is-medium">{{ request.user | notification_count }}</span>
{% endif %}
</div>
</a>
</div>
{% else %}
<div class="navbar-item">
<div class="buttons">
<a href="/register" class="button is-primary">
<strong>Sign up</strong>
</a>
<a href="/login" class="button is-light">
Log in
</a>
</div>
</div>
{% endif %}
</div>
</div>
</nav>
<div class="section">
{% block content %}
{% endblock %}
</div>

View file

@ -1,52 +1,45 @@
{% extends 'layout.html' %}
{% block content %}
<div class="content-container">
<h2>About {{ site_settings.name }}</h2>
<p>
<div class="columns">
<div class="column">
<h2 class="title">About {{ site_settings.name }}</h2>
<p class="block">
{{ site_settings.instance_description }}
</p>
<p>
<small>
<p class="block">
<a href="/about/">More about this site</a>
</small>
</p>
<p class="block">
<a href="/register" class="button is-link">Create an Account</a>
</p>
</div>
<div class="row">
<div class="content-container login">
<h2>Create an Account</h2>
<p><small>
With a BookWyrm account, you can track and share your reading activity with
friends here and on any other federated server, like Mastodon and PixelFed.
</small></p>
<div class="column">
<h2 class="title">Log in</h2>
{% if site_settings.allow_registration %}
<div>
<form name="register" method="post" action="/register">
{% csrf_token %}
{{ register_form.as_p }}
<button type="submit">Create account</button>
</form>
</div>
{% else %}
<small>
This instance is not open for registration.
</small>
{% endif %}
</div>
<div class="content-container login">
<h2>Log in</h2>
<div>
<div class="block">
<form name="login" method="post" action="/user-login">
{% csrf_token %}
{{ login_form.as_p }}
<button type="submit">Log in</button>
<div class="field">
<label class="label" for="id_username">Username:</label>
<div class="control">{{ login_form.username }}</div>
</div>
<div class="field">
<label class="label" for="id_password">Password:</label>
<div class="control">{{ login_form.password }}</div>
</div>
<div class="field is-grouped">
<div class="control">
<button class="button is-primary" type="submit">Log in</button>
</div>
<div class="control">
<small><a href="/reset-password">Forgot your password?</a></small>
</div>
</div>
</form>
<p><small><a href="/reset-password">Forgot your password?</a></small></p>
</div>
</div>

View file

@ -1,16 +1,18 @@
{% extends 'layout.html' %}
{% load humanize %}
{% block content %}
<div class="content-container">
<div class="manage-invites">
<h2>Invites</h2>
<table>
<div class="block">
<h2 class="title">Invites</h2>
<table class="table is-striped">
<tr>
<th>Link</th>
<th>Expires</th>
<th>Max uses</th>
<th>Times used</th>
</tr>
{% if not invites %}
<tr><td colspan="4">No active invites</td></tr>
{% endif %}
{% for invite in invites %}
<tr>
<td><a href="{{ invite.link }}">{{ invite.link }}</td>
@ -20,13 +22,32 @@
</tr>
{% endfor %}
</table>
<h2>Generate New Invite</h2>
</div>
<form name="avatar" action="/create_invite/" method="post">
<div class="block">
<h2 class="title is-4">Generate New Invite</h2>
<form name="invite" action="/create_invite/" method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Create Invite</button>
<div class="field">
<div class="control">
<label class="label" for="id_expiry">Expiry:</label>
</div>
<div class="select">
{{ form.expiry }}
</div>
</div>
<div class="field">
<div class="control">
<label class="label" for="id_use_limit">Use limit:</label>
</div>
<div class="select">
{{ form.use_limit }}
</div>
</div>
<button class="button is-primary" type="submit">Create Invite</button>
</form>
</div>
</div>
{% endblock %}

View file

@ -1,8 +1,8 @@
{% extends 'layout.html' %}
{% block content %}
<div class="content-container">
<h2>Not Found</h2>
<div class="block">
<h2 class="title">Not Found</h2>
<p>The page your requested doesn't seem to exist!</p>
</div>

View file

@ -1,20 +1,22 @@
{% extends 'layout.html' %}
{% load humanize %}l
{% block content %}
<div class="content-container">
<h2>Notifications</h2>
<div class="block">
<h2 class="title">Notifications</h2>
<form name="clear" action="/clear-notifications" method="POST">
{% csrf_token %}
<button type="submit" class="secondary">Delete notifications</button>
<button class="button is-danger" type="submit" class="secondary">Delete notifications</button>
</form>
</div>
<div class="content-container">
<div class="block">
{% for notification in notifications %}
<div class="notification{% if notification.id in unread %} unread{% endif %}">
<small class="time-ago">{{ notification.created_date | naturaltime }}</small>
<div class="notification level{% if notification.id in unread %} is-primary{% endif %}">
<div class="level-left">
<p>
{% if notification.related_user %}
{% include 'snippets/avatar.html' with user=notification.related_user %}
{% include 'snippets/username.html' with user=notification.related_user %}
{% if notification.notification_type == 'FAVORITE' %}
favorited your
@ -41,6 +43,10 @@
your <a href="/import_status/{{ notification.related_import.id }}">import</a> completed.
{% endif %}
</p>
</div>
<p class="level-right">{{ notification.created_date | naturaltime }}</p>
</div>
{% endfor %}
{% if not notifications %}

View file

@ -0,0 +1,49 @@
{% extends 'layout.html' %}
{% block content %}
<div class="columns">
<div class="column">
<h2 class="title">About {{ site_settings.name }}</h2>
<p class="block">
{{ site_settings.instance_description }}
</p>
<p class="block">
<a href="/about/">More about this site</a>
</p>
<p class="block">
<a href="/login" class="button is-link">Log In</a>
</p>
</div>
<div class="column">
<h2 class="title">Create an Account</h2>
<div class="block">
<form name="register" method="post" action="/user-register">
{% csrf_token %}
<div class="field">
<label class="label" for="id_username">Username:</label>
<div class="control">{{ register_form.username }}</div>
</div>
<div class="field">
<label class="label" for="id_email">Email address:</label>
<div class="control">{{ register_form.email }}</div>
</div>
<div class="field">
<label class="label" for="id_password">Password:</label>
<div class="control">{{ register_form.password }}</div>
</div>
<div class="field is-grouped">
<div class="control">
<button class="button is-primary" type="submit">Sign Up</button>
</div>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View file

@ -1,9 +1,19 @@
{% extends 'layout.html' %}
{% block content %}
<div id="content">
<div class="block">
<nav class="breadcrumb has-succeeds-separator" aria-label="breadcrumbs">
<ul>
<li><a href="/user/{{ user.username }}">{% include 'snippets/username.html' with user=user %}</a></li>
<li><a href="/user/{{ user.username }}/shelves">Shelves</a></li>
<li class="is-active"><a href="#" aria-current="page">{{ shelf.name }}</a></li>
</ul>
</nav>
</div>
<div class="block">
<div>
<h2>{% include 'snippets/username.html' with user=user %} > {{ shelf.name }}</h2>
<h2 class="title">{{ shelf.name }}</h2>
{% include 'snippets/shelf.html' with shelf=shelf ratings=ratings %}
</div>
</div>

View file

@ -1,2 +1,2 @@
<img class="user-pic{% if large %} large{% endif %}" src="{% if user.avatar %}/images/{{ user.avatar }}{% else %}/static/images/default_avi.jpg{% endif %}">
<img class="avatar image {% if large %}is-96x96{% else %}is-32x32{% endif %}" src="{% if user.avatar %}/images/{{ user.avatar }}{% else %}/static/images/default_avi.jpg{% endif %}">

View file

@ -1,12 +1,14 @@
{% load fr_display %}
<div class="cover-container is-{{ size }}">
{% if book.cover %}
<img class="book-cover {{ size }}" src="/images/{{ book.cover }}" alt="{% include 'snippets/cover_alt.html' with book=book %}">
<img class="book-cover" src="/images/{{ book.cover }}" alt="{% include 'snippets/cover_alt.html' with book=book %}">
{% else %}
<div class="no-cover book-cover {{ size }}">
<img class="book-cover {{ size }}" src="/static/images/no_cover.jpg" alt="No cover">
<div class="no-cover book-cover">
<img class="book-cover" src="/static/images/no_cover.jpg" alt="No cover">
<div>
<p class="title">{{ book.title }}</p>
<p>{{ book.title }}</p>
<p>({{ book|edition_info }})</p>
</div>
</div>
{% endif %}
</div>

View file

@ -1,7 +1,6 @@
{% load fr_display %}
{% if book.description %}
<blockquote>{{ book.description | description }}</blockquote>
<blockquote class="content">{{ book.description }}</blockquote>
{% elif book.parent_work.description %}
<blockquote>{{ book.parent_work.description | description }}</blockquote>
<blockquote>{{ book.parent_work.description }}</blockquote>
{% endif %}

View file

@ -0,0 +1,18 @@
<div class="columns">
{% for book in books %}
{% if forloop.counter0|divisibleby:"4" %}
</div>
<div class="columns">
{% endif %}
<div class="column is-narrow">
<div class="box">
<a href="/book/{{ book.id }}">
{% include 'snippets/book_cover.html' with book=book %}
</a>
{% include 'snippets/rate_action.html' with user=request.user book=book %}
{% include 'snippets/shelve_button.html' with book=book %}
</div>
</div>
{% endfor %}
</div>

View file

@ -1,8 +1,8 @@
<span class="title">
<span>
<a href="/book/{{ book.id }}">{{ book.title }}</a>
</span>
{% if book.authors %}
<span class="author">
<span>
by {% include 'snippets/authors.html' with book=book %}
</span>
{% endif %}

View file

@ -1,49 +0,0 @@
{% load fr_display %}
<div class="all-shelves content-container">
{% for shelf in shelves %}
{% if shelf.books %}
<div>
<h2>{{ shelf.name }}
{% if shelf.size > shelf.books|length %}
<small>(<a href="/shelf/{{ user | username }}/{{ shelf.identifier }}">See all {{ shelf.size }}</a>)</small>
{% endif %}
</h2>
<div class="covers-shelf {{ shelf.identifier }} ">
{% for book in shelf.books %}
<div class="cover-container">
<label for="book-{{ book.id }}-radio">
{% include 'snippets/book_cover.html' with book=book %}
</label>
{% include 'snippets/shelve_button.html' with book=book hide_pulldown=True %}
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% endfor %}
</div>
{% for shelf in shelves %}
{% for book in shelf.books %}
<div class="compose-popout">
<input name="book-popout" type="radio" id="book-{{ book.id }}-radio">
<div class="compose compose-suggestion" id="compose-book-{{ book.id }}">
<label class="close icon icon-close" for="book-{{ book.id }}-radio-close" onclick="hide_element(this)">
<span class="hidden-text">Close</span>
</label>
<input name="book-popout" type="radio" id="book-{{ book.id }}-radio-close">
<div class="content-container">
<h2>
{% include 'snippets/avatar.html' with user=user %}
Your thoughts on
a <a href="/book/{{ book.id }}">{{ book.title }}</a>
by {% include 'snippets/authors.html' with book=book %}
</h2>
{% include 'snippets/create_status.html' with book=book user=request.user %}
</div>
</div>
</div>
{% endfor %}
{% endfor %}

View file

@ -1,43 +1,79 @@
{% load humanize %}
{% load fr_display %}
<div class="tabs secondary">
<div class="tab active" data-id="tab-review-{{ book.id }}" data-category="tab-option-{{ book.id }}">
<a href="/book/{{ book.id }}/review" onclick="tabChange(event)">Review</a>
</div>
<div class="tab" data-id="tab-comment-{{ book.id }}" data-category="tab-option-{{ book.id }}">
<a href="/book/{{ book.id }}/comment" onclick="tabChange(event)">Comment</a>
</div>
<div class="tab" data-id="tab-quotation-{{ book.id }}" data-category="tab-option-{{ book.id }}">
<a href="/book/{{ book.id }}/quotation" onclick="tabChange(event)">Quote</a>
</div>
<div class="columns">
<div class="column">
<div class="tabs is-boxed">
<ul>
<li class="is-active" data-id="tab-review-{{ book.id }}" data-category="tab-option-{{ book.id }}">
<label for="review-{{ book.id }}" onclick="tabChange(event)"><a>Review</a></label>
</li>
<li data-id="tab-comment-{{ book.id }}" data-category="tab-option-{{ book.id }}">
<label for="comment-{{ book.id}}" onclick="tabChange(event)"><a>Comment</a></label>
</li>
<li data-id="tab-quotation-{{ book.id }}" data-category="tab-option-{{ book.id }}">
<label for="quote-{{ book.id }}" onclick="tabChange(event)"><a>Quote</a></label>
</li>
</ul>
</div>
<div class="book-preview row">
{% if not hide_cover %}
<div class="cover-container">
{% include 'snippets/book_cover.html' with book=book %}
<div>
<input class="toggle-control" type="radio" name="status-tabs-{{ book.id }}" id="review-{{ book.id }}" checked>
<form class="toggle-content hidden tab-option-{{ book.id }}" name="review" action="/review/" method="post" id="tab-review-{{ book.id }}">
{% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}">
<div class="control">
<label class="label" for="id_name_{{ book.id }}_review">Title:</label>
<input type="text" name="name" maxlength="255" class="input" required="" id="id_name_{{ book.id }}_review" placeholder="My review of '{{ book.title }}'">
</div>
{% endif %}
<form class="tab-option-{{ book.id }} review-form" name="review" action="/review/" method="post" id="tab-review-{{ book.id }}">
{% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}">
{% include 'snippets/rate_form.html' with book=book %}
{{ review_form.as_p }}
<button type="submit">post review</button>
</form>
<div class="control">
<label class="label" for="id_content_{{ book.id }}_review">Review:</label>
<form class="hidden tab-option-{{ book.id }} review-form" name="comment" action="/comment/" method="post" id="tab-comment-{{ book.id }}">
{% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}">
{{ comment_form.as_p }}
<button type="submit">post comment</button>
</form>
<span class="is-sr-only">Rating</span>
<div class="field is-grouped stars form-rate-stars">
<input class="hidden" type="radio" name="rating" value="" checked>
{% for i in '12345'|make_list %}
<input class="hidden" 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>
<form class="hidden tab-option-{{ book.id }} review-form quote-form" name="quotation" action="/quotate/" method="post" id="tab-quotation-{{ book.id }}">
{% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}">
{{ quotation_form.as_p }}
<button typr="submit">post quote</button>
<textarea name="content" class="textarea" id="id_content_{{ book.id }}_review"></textarea>
</div>
<button class="button is-primary" type="submit">post review</button>
</form>
</div>
<div>
<input class="toggle-control" type="radio" name="status-tabs-{{ book.id }}" id="comment-{{ book.id }}">
<form class="toggle-content hidden tab-option-{{ book.id }}" name="comment" action="/comment/" method="post" id="tab-comment-{{ book.id }}">
{% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}">
<div class="control">
<label class="label" for="id_content_{{ book.id }}_comment">Comment:</label>
<textarea name="content" class="textarea" id="id_content_{{ book.id }}_comment" placeholder="Some thoughts on '{{ book.title }}'"></textarea>
</div>
<button class="button is-primary" type="submit">post comment</button>
</form>
</div>
<div>
<input class="toggle-control" type="radio" name="status-tabs-{{ book.id }}" id="quote-{{ book.id }}">
<form class="toggle-content hidden tab-option-{{ book.id }}" name="quotation" action="/quotate/" method="post" id="tab-quotation-{{ book.id }}">
{% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}">
<div class="control">
<label class="label" for="id_quote_{{ book.id }}_quote">Quote:</label>
<textarea name="quote" class="textarea" required="" id="id_quote_{{ book.id }}_quote" placeholder="An except from '{{ book.title }}'"></textarea>
</div>
<div class="control">
<label class="label" for="id_content_{{ book.id }}_quote">Comment:</label>
<textarea name="content" class="textarea is-small" id="id_content_{{ book.id }}_quote"></textarea>
</div>
<button class="button is-primary" type="submit">post quote</button>
</form>
</div>
</div>
</div>

View file

@ -11,14 +11,14 @@ Follow request already sent.
{% csrf_token %}
<input type="hidden" name="user" value="{{ user.username }}">
{% if user.manually_approves_followers %}
<button type="submit">Send follow request</button>
<button class="button is-small" type="submit">Send follow request</button>
{% else %}
<button type="submit">Follow</button>
<button class="button is-small" type="submit">Follow</button>
{% endif %}
</form>
<form action="/unfollow/" method="POST" onsubmit="interact(event)" class="follow-{{ user.id }} {% if not request.user in user.followers.all %}hidden{%endif %}" data-id="follow-{{ user.id }}">
{% csrf_token %}
<input type="hidden" name="user" value="{{ user.username }}">
<button type="submit">Unfollow</button>
<button class="button is-small" type="submit">Unfollow</button>
</form>
{% endif %}

View file

@ -3,11 +3,11 @@
<form action="/accept_follow_request/" method="POST">
{% csrf_token %}
<input type="hidden" name="user" value="{{ user.username }}">
<button type="submit">Accept</button>
<button class="button is-small" type="submit">Accept</button>
</form>
<form action="/delete_follow_request/" method="POST">
{% csrf_token %}
<input type="hidden" name="user" value="{{ user.username }}">
<button type="submit" class="warning">Delete</button>
<button class="button is-small" type="submit" class="warning">Delete</button>
</form>
{% endif %}

View file

@ -1,64 +1,65 @@
{% load fr_display %}
<div class="interaction">
<div class="card-footer-item">
{% if request.user.is_authenticated %}
<form name="reply" action="/reply" method="post" onsubmit="return reply(event)">
<div class="field is-grouped">
{% csrf_token %}
<input type="hidden" name="parent" value="{{ activity.id }}">
<textarea name="content" placeholder="Leave a comment..." id="id_content" required="true"></textarea>
<button type="submit">
<textarea name="content" placeholder="Leave a comment..." id="id_content_{{ activity.id }}" required="true"></textarea>
<button class="button" type="submit">
<span class="icon icon-comment">
<span class="hidden-text">Comment</span>
<span class="is-sr-only">Comment</span>
</span>
</button>
</div>
</form>
<form name="boost" action="/boost/{{ activity.id }}" method="post" onsubmit="return interact(event)" class="boost-{{ status.id }} {% if request.user|boosted:status %}hidden{% endif %}" data-id="boost-{{ status.id }}">
{% csrf_token %}
<button type="submit">
<button class="button" type="submit">
<span class="icon icon-boost">
<span class="hidden-text">Boost status</span>
<span class="is-sr-only">Boost status</span>
</span>
</button>
</form>
<form name="unboost" action="/unboost/{{ activity.id }}" method="post" onsubmit="return interact(event)" class="boost-{{ status.id }} active {% if not request.user|boosted:status %}hidden{% endif %}" data-id="boost-{{ status.id }}">
{% csrf_token %}
<button type="submit">
<button class="button is-success" type="submit">
<span class="icon icon-boost">
<span class="hidden-text">Un-boost status</span>
<span class="is-sr-only">Un-boost status</span>
</span>
</button>
</form>
<form name="favorite" action="/favorite/{{ activity.id }}" method="POST" onsubmit="return interact(event)" class="fav-{{ status.id }} {% if request.user|liked:status %}hidden{% endif %}" data-id="fav-{{ status.id }}">
{% csrf_token %}
<button type="submit">
<button class="button" type="submit">
<span class="icon icon-heart">
<span class="hidden-text">Like status</span>
<span class="is-sr-only">Like status</span>
</span>
</button>
</form>
<form name="unfavorite" action="/unfavorite/{{ activity.id }}" method="POST" onsubmit="return interact(event)" class="fav-{{ status.id }} active {% if not request.user|liked:status %}hidden{% endif %}" data-id="fav-{{ status.id }}">
{% csrf_token %}
<button type="submit">
<button class="button is-success" type="submit">
<span class="icon icon-heart">
<span class="hidden-text">Un-like status</span>
<span class="is-sr-only">Un-like status</span>
</span>
</button>
</form>
{% else %}
<a href="/login">
<span class="icon icon-comment">
<span class="hidden-text">Comment</span>
<span class="is-sr-only">Comment</span>
</span>
<span class="icon icon-boost">
<span class="hidden-text">Boost status</span>
<span class="is-sr-only">Boost status</span>
</span>
<span class="icon icon-heart">
<span class="hidden-text">Like status</span>
<span class="is-sr-only">Like status</span>
</span>
</a>
{% endif %}
</div>

View file

@ -1,13 +1,13 @@
{% load fr_display %}
<span class="hidden-text">Leave a rating</span>
<div class="stars rate-stars">
<span class="is-sr-only">Leave a rating</span>
<div class="field is-grouped stars rate-stars">
{% for i in '12345'|make_list %}
<form name="rate" action="/rate/" method="POST" onsubmit="return rate_stars(event)">
{% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}">
<input type="hidden" name="rating" value="{{ forloop.counter }}">
<button type="submit" class="icon icon-star-{% if book|rating:user < forloop.counter %}empty{% else %}full{% endif %}">
<span class="hidden-text">{{ forloop.counter }} star{{ forloop.counter | pluralize }}</span>
<span class="is-sr-only">{{ forloop.counter }} star{{ forloop.counter | pluralize }}</span>
</button>
</form>
{% endfor %}

View file

@ -1,12 +0,0 @@
{% load fr_display %}
<span class="hidden-text">Rating</span>
<div class="stars rate-stars">
<input type="radio" name="rating" value="" checked>
{% for i in '12345'|make_list %}
<input 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="hidden-text">{{ forloop.counter }} star{{ forloop.counter | pluralize }}</span>
</label>
{% endfor %}
</div>

View file

@ -1,7 +1,8 @@
{% load humanize %}
{% load fr_display %}
{% if shelf.books %}
<table>
<table class="table is-striped is-fullwidth">
<tr class="book-preview">
<th>
Cover

View file

@ -1,25 +1,27 @@
{% load fr_display %}
{% if request.user.is_authenticated %}
<div class="pulldown-button">
<div class="field is-grouped">
<form name="shelve" action="/shelve/" method="post">
{% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}">
<input type="hidden" name="shelf" value="{% shelve_button_identifier book %}">
<button type="submit" style="">{% shelve_button_text book %}</button>
<button class="button is-small" type="submit" style="">{% shelve_button_text book %}</button>
</form>
<div class="dropdown is-hoverable">
{% if not hide_pulldown %}
<div class="pulldown-container">
<button class="pulldown-toggle">
<span class="icon icon-arrow-down"><span class="hidden-text">More shelves</span></span>
</button>
<ul class="pulldown">
<form name="shelve" action="/shelve/" method="post">
<div class="button dropdown-trigger is-small" >
<span class="icon icon-arrow-down"><span class="is-sr-only">More shelves</span></span>
</div>
<div class="dropdown-menu">
<ul class="dropdown-content">
<form class="dropdown-item" name="shelve" action="/shelve/" method="post">
{% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}">
{% for shelf in request.user.shelf_set.all %}
<li>
<button name="shelf" type="submit" value="{{ shelf.identifier }}" {% if shelf in book.shelf_set.all %} disabled {% endif %}>{{ shelf.name }} {% if shelf in book.shelf_set.all %} ✓ {% endif %}</button>
<button class="is-small" name="shelf" type="submit" value="{{ shelf.identifier }}" {% if shelf in book.shelf_set.all %} disabled {% endif %}>{{ shelf.name }} {% if shelf in book.shelf_set.all %} ✓ {% endif %}</button>
</li>
{% endfor %}
</form>
@ -28,4 +30,5 @@
{% endif %}
</div>
</div>
{% endif %}

View file

@ -1,5 +1,5 @@
<div class="stars">
<span class="hidden-text">{{ rating|floatformat }} star{{ rating|floatformat | pluralize }}</span>
<span class="is-sr-only">{{ rating|floatformat }} star{{ rating|floatformat | pluralize }}</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>

View file

@ -1,24 +1,31 @@
{% load humanize %}
{% load fr_display %}
<div class="post {{ status.status_type | lower }} depth-{{ depth }} {% if main %}main{% else %}reply{% endif %}">
<h2>
{% if status.boosted_status %}
{% include 'snippets/status_header.html' with status=status.boosted_status %}
<small class="subhead">{% include 'snippets/status_header.html' with status=status %}</small>
{% else %}
<div class="card">
<header class="card-header">
{% include 'snippets/status_header.html' with status=status %}
{% endif %}
</h2>
</header>
<div class="status-content">
{% include 'snippets/status_content.html' with status=status %}
</div>
</div>
<div class="card-content">
{% if status.status_type == 'Boost' %}
{% include 'snippets/interaction.html' with activity=status|boosted_status %}
{% include 'snippets/status_content.html' with status=status.boosted_status %}
{% else %}
{% include 'snippets/status_content.html' with status=status %}
{% endif %}
</div>
<footer class="card-footer">
{% if status.status_type == 'Boost' %}
{% include 'snippets/interaction.html' with activity=status.boosted_status %}
{% else %}
{% include 'snippets/interaction.html' with activity=status %}
{% endif %}
<div class="card-footer-item">
<span class="icon icon-public">
<span class="is-sr-only">Public post</span>
</span>
<a href="{{ status.remote_id }}">{{ status.published_date | naturaltime }}</a>
</div>
</footer>
</div>

View file

@ -1,38 +1,32 @@
{% load fr_display %}
<div class="media">
{% if not hide_book and status.mention_books.count %}
<div class="row">
<div class="media-left">
<div class="columns">
{% for book in status.mention_books.all|slice:"0:4" %}
<div class="row">
<div class="cover-container">
{% include 'snippets/book_cover.html' with book=book %}
<div class="column">
<a href="/book/{{ book.id }}">{% include 'snippets/book_cover.html' with book=book %}</a>
{% if status.mention_books.count > 1 %}
<p>{% include 'snippets/book_titleby.html' with book=book %}</p>
{% endif %}
{% include 'snippets/rate_action.html' with book=book user=request.user %}
{% include 'snippets/shelve_button.html' with book=book %}
</div>
{% if status.mention_books.count == 1 %}
<div>
<p>{% include 'snippets/book_titleby.html' with book=book %}</p>
{% include 'snippets/book_description.html' with book=book %}
</div>
{% endif %}
</div>
{% endfor %}
</div>
{% endif %}
<div class="row">
{% if not hide_book and status.book %}
<div class="cover-container">
{% include 'snippets/book_cover.html' with book=status.book %}
{% include 'snippets/rate_action.html' with book=status.book user=request.user %}
{% include 'snippets/shelve_button.html' with book=status.book %}
</div>
{% endif %}
{% if not hide_book and status.book %}
<div class="media-left">
<div>
<a href="/book/{{ status.book.id }}">{% include 'snippets/book_cover.html' with book=status.book %}</a>
{% include 'snippets/shelve_button.html' with book=status.book %}
</div>
</div>
{% endif %}
<div class="media-content">
{% if status.status_type == 'Review' %}
<h3>
{% if status.name %}{{ status.name }}<br>{% endif %}
@ -41,17 +35,21 @@
{% endif %}
{% if status.quote %}
<div class="quote">
<div class="quote block">
<blockquote>{{ status.quote }}</blockquote>
<p> &mdash; {% include 'snippets/book_titleby.html' with book=status.book %}</p>
</div>
{% endif %}
{% if status.content and status.status_type != 'Update' and status.status_type != 'Boost' %}
{% if status.content and status.status_type != 'GeneratedNote' and status.status_type != 'Boost' %}
<blockquote>{{ status.content | safe }}</blockquote>
{% endif %}
{% if status.mention_books.count == 1 and not status.book %}
{% include 'snippets/book_description.html' with book=status.mention_books.first %}
{% endif %}
{% if not status.content and status.book and not hide_book and status.status_type != 'Boost' %}
{% include 'snippets/book_description.html' with book=status.book %}
{% endif %}

View file

@ -1,10 +1,13 @@
{% load humanize %}
{% load fr_display %}
<div class="card-header-title">
<p>
{% include 'snippets/avatar.html' with user=status.user %}
{% include 'snippets/username.html' with user=status.user %}
{% if status.status_type == 'Update' %}
{{ status.content | safe }}
{% include 'snippets/username.html' with user=status.user %}
{% if status.status_type == 'GeneratedNote' %}
{{ status.content | safe }} {% include 'snippets/book_titleby.html' with book=status.mention_books.first %}
{% elif status.status_type == 'Boost' %}
boosted {% include 'snippets/avatar.html' with user=status.boosted_status.user %}{% include 'snippets/username.html' with user=status.boosted_status.user possessive=True %} status
{% elif status.status_type == 'Review' and not status.name and not status.content%}
rated <a href="/book/{{ status.book.id }}">{{ status.book.title }}</a>
{% elif status.status_type == 'Review' %}
@ -13,13 +16,10 @@
commented on <a href="/book/{{ status.book.id }}">{{ status.book.title }}</a>
{% elif status.status_type == 'Quotation' %}
quoted <a href="/book/{{ status.book.id }}">{{ status.book.title }}</a>
{% elif status.status_type == 'Boost' %}
boosted
{% elif status.reply_parent %}
{% with parent_status=status|parent %}
replied to {% include 'snippets/username.html' with user=parent_status.user possessive=True %} <a href="{{parent_status.remote_id }}">{{ parent_status.status_type | lower }}</a>
replied to {% include 'snippets/username.html' with user=parent_status.user possessive=True %} <a href="{{parent_status.remote_id }}">{% if parent_status.status_type == 'GeneratedNote' %}update{% else %}{{ parent_status.status_type | lower }}{% endif %}</a>
{% endwith %}
{% endif %}
<span class="time-ago">
<a href="{{ status.remote_id }}">{{ status.published_date | naturaltime }}</a>
</span>
</p>
</div>

View file

@ -1,6 +0,0 @@
{% for tab in tabs %}
<div class="tab {% if tab.id == active_tab %}active{% endif %}">
<a href="{{ path }}/{{ tab.id }}">{{ tab.display }}</a>
</div>
{% endfor %}

View file

@ -1,20 +1,24 @@
<div class="control">
<div class="tags has-addons">
<a class="tag is-link" href="/tag/{{ tag.identifier|urlencode }}">
{{ tag.name }}
</a>
<div class="tag">
<a href="/tag/{{ tag.identifier|urlencode }}">{{ tag.name }}</a>
{% if tag.identifier in user_tags %}
<form class="tag-form" name="tag" action="/untag/" method="post">
<form name="tag" action="/untag/" method="post">
{% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}">
<input type="hidden" name="name" value="{{ tag.name }}">
<button type="submit">x</button>
<button type="submit">x<span class="is-sr-only"> remove tag</span></button>
</form>
{% else %}
<form class="tag-form" name="tag" action="/tag/" method="post">
<form name="tag" action="/tag/" method="post">
{% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}">
<input type="hidden" name="name" value="{{ tag.name }}">
<button type="submit">+</button>
<button type="submit">+<span class="is-sr-only"> add tag</span></button>
</form>
{% endif %}
</div>
</div>
</div>

View file

@ -1,4 +1,6 @@
{% load fr_display %}
<div class="block">
{% with depth=depth|add:1 %}
{% if depth <= max_depth and status.reply_parent and direction <= 0 %}
{% with direction=-1 %}
@ -16,3 +18,5 @@
{% endfor %}
{% endif %}
{% endwith %}
</div>

View file

@ -1,11 +0,0 @@
<div>
<div>
{% include 'snippets/avatar.html' with user=user %}
{% include 'snippets/username.html' with user=user %}
<small>{{ user.username }}</small>
</div>
{% if not is_self %}
{% include 'snippets/follow_button.html' with user=user %}
{% endif %}
</div>

View file

@ -1,11 +1,9 @@
{% extends 'layout.html' %}
{% block content %}
<div id="content">
<div class="comment-thread">
<div class="block">
{% include 'snippets/thread.html' with status=status depth=0 max_depth=6 is_root=True direction=0 %}
</div>
</div>
{% endblock %}

View file

@ -1,20 +1,12 @@
{% extends 'layout.html' %}
{% load fr_display %}
{% block content %}
<div class="content-container">
<h2>Books tagged "{{ tag.name }}"</h2>
<div class="book-grid row wrap shrink">
{% for book in books.all %}
<div class="cover-container">
<a href="/book/{{ book.id }}">
{% include 'snippets/book_cover.html' with book=book %}
</a>
{% include 'snippets/rate_action.html' with user=request.user book=book %}
{% include 'snippets/shelve_button.html' with book=book %}
</div>
{% endfor %}
</div>
<div class="block">
<h2 class="title">Books tagged "{{ tag.name }}"</h2>
{% include 'snippets/book_tiles.html' with books=books.all %}
</div>
{% endblock %}

View file

@ -1,18 +1,43 @@
{% extends 'layout.html' %}
{% block content %}
<div class="block">
{% include 'user_header.html' with user=user %}
{% include 'snippets/covers_shelf.html' with shelves=shelves user=user %}
</div>
<div class="block">
<h2 class="title">Shelves</h2>
<div class="columns">
{% for shelf in shelves %}
<div class="column is-narrow">
<h3>{{ shelf.name }}
{% if shelf.size > 3 %}<small>(<a href="{{ shelf.remote_id }}">See all {{ shelf.size }}</a>)</small>{% endif %}</h3>
<div class="is-mobile field is-grouped">
{% for book in shelf.books %}
<div class="control">
<a href="/book/{{ book.id }}">
{% include 'snippets/book_cover.html' with book=book size="medium" %}
</a>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
<small><a href="/user/{{ user.localname }}/shelves">See all {{ shelf_count }} shelves</a></small>
</div>
<div>
<div class="content-container"><h2>User Activity</h2></div>
<div class="block">
<h2 class="title">User Activity</h2>
</div>
{% for activity in activities %}
<div class="content-container">
<div class="block">
{% include 'snippets/status.html' with status=activity %}
</div>
{% endfor %}
{% if not activities %}
<div class="content-container">
<div class="block">
<p>No activities yet!</a>
</div>
{% endif %}

View file

@ -1,22 +1,26 @@
{% load humanize %}
{% load fr_display %}
<div class="content-container user-profile">
<h2>User Profile
<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="hidden-text">Edit profile</span>
<span class="is-sr-only">Edit profile</span>
</span>
</a>
</div>
{% endif %}
</h2>
<div class="row">
<div class="pic-container">
{% include 'snippets/avatar.html' with user=user large=True %}
</div>
<div>
<div class="columns">
<div class="column is-narrow">
<div class="media">
<div class="media-left">
{% include 'snippets/avatar.html' with user=user large=True %}
</div>
<div class="media-content">
<p>{% if user.name %}{{ user.name }}{% else %}{{ user.localname }}{% endif %}</p>
<p>{{ user.username }}</p>
<p>Joined {{ user.created_date | naturaltime }}</p>
@ -24,11 +28,15 @@
<a href="/user/{{ user|username }}/followers">{{ user.followers.count }} follower{{ user.followers.count | pluralize }}</a>,
<a href="/user/{{ user|username }}/following">{{ user.following.count }} following</a></p>
</div>
</div>
</div>
<div class="column">
{% if user.summary %}
<blockquote><span class="icon icon-quote-open"></span>{{ user.summary | safe }}</blockquote>
{% endif %}
</div>
</div>
{% if not is_self %}
{% include 'snippets/follow_button.html' with user=user %}
{% endif %}

View file

@ -1,9 +1,14 @@
{% extends 'layout.html' %}
{% block content %}
<div class="content-container">
<div class="block">
<h2 class="title">User search results</h2>
{% if not results %}
<p>No results found for "{{ query }}"</p>
{% endif %}
{% for result in results %}
<div>
<h2>{{ result.username }}</h2>
<div class="block">
{% include 'snippets/avatar.html' with user=result %}</h2>
{% include 'snippets/username.html' with user=result show_full=True %}</h2>
{% include 'snippets/follow_button.html' with user=result %}
</div>
{% endfor %}

View file

@ -4,8 +4,8 @@
{% include 'user_header.html' with user=user %}
{% for shelf in shelves %}
<div class="content-container">
<h2>{{ shelf.name }}</h2>
<div class="block">
<h2 class="title">{{ shelf.name }}</h2>
{% include 'snippets/shelf.html' with shelf=shelf ratings=ratings %}
</div>
{% endfor %}

View file

@ -25,27 +25,6 @@ def get_rating(book, user):
return 0
@register.filter(name='description')
def description_format(description):
''' handle the various OL description formats '''
if not description:
return ''
if '----------' in description:
description = description.split('----------')[0]
return description.strip()
@register.filter(name='author_bio')
def bio_format(bio):
''' clean up OL author bios '''
if isinstance(bio, dict) and 'value' in bio:
bio = bio['value']
bio = bio.split('\n')
return bio[0].strip()
@register.filter(name='username')
def get_user_identifier(user):
''' use localname for local users, username for remote '''
@ -67,12 +46,6 @@ def get_replies(status):
).select_subclasses().all()[:10]
@register.filter(name='reply_count')
def get_reply_count(status):
''' how many replies does a status have? '''
return models.Status.objects.filter(reply_parent=status).count()
@register.filter(name='parent')
def get_parent(status):
''' get the reply parent for a status '''
@ -170,18 +143,6 @@ def shelve_button_text(context, book):
return 'Want to read'
@register.simple_tag(takes_context=True)
def current_shelf(context, book):
''' check what shelf a user has a book on, if any '''
try:
shelf = models.ShelfBook.objects.get(
shelf__user=context['user'],
book=book
)
except models.ShelfBook.DoesNotExist:
return None
return shelf.name
@register.simple_tag(takes_context=False)
def latest_read_through(book, user):
''' the most recent read activity '''

View file

@ -38,14 +38,13 @@ urlpatterns = [
# ui views
re_path(r'^login/?$', views.login_page),
re_path(r'^register/?$', views.register_page),
re_path(r'^about/?$', views.about_page),
re_path(r'^invite/?$', views.manage_invites),
re_path(r'^invite/(?P<code>[A-Za-z0-9]+)/?$', views.invite_page),
re_path(r'^manage_invites/?$', views.manage_invites),
path('', views.home),
re_path(r'^(?P<tab>home|local|federated)/?$', views.home_tab),
re_path(r'^notifications/?', views.notifications_page),
re_path(r'books/?$', views.books_page),
re_path(r'import/?$', views.import_page),
re_path(r'import_status/(\d+)/?$', views.import_status),
re_path(r'user-edit/?$', views.edit_profile_page),
@ -66,8 +65,6 @@ urlpatterns = [
# books
re_path(r'%s(.json)?/?$' % book_path, views.book_page),
re_path(r'%s/(?P<tab>friends|local|federated)?$' % \
book_path, views.book_page),
re_path(r'%s/edit/?$' % book_path, views.edit_book_page),
re_path(r'^editions/(?P<work_id>\d+)/?$', views.editions_page),
@ -84,7 +81,7 @@ urlpatterns = [
# internal action endpoints
re_path(r'^logout/?$', actions.user_logout),
re_path(r'^user-login/?$', actions.user_login),
re_path(r'^register/?$', actions.register),
re_path(r'^user-register/?$', actions.register),
re_path(r'^edit_profile/?$', actions.edit_profile),
re_path(r'^import_data/?', actions.import_data),

View file

@ -144,7 +144,7 @@ def resolve_book(request):
def edit_book(request, book_id):
''' edit a book cool '''
if not request.method == 'POST':
return redirect('/book/%s' % request.user.localname)
return redirect('/book/%s' % book_id)
try:
book = models.Edition.objects.get(id=book_id)
@ -444,4 +444,4 @@ def create_invite(request):
invite.user = request.user
invite.save()
return redirect('/manage_invites')
return redirect('/invite')

View file

@ -2,7 +2,7 @@
import re
from django.contrib.auth.decorators import login_required
from django.db.models import Avg, Q
from django.db.models import Avg, Count, Q
from django.http import HttpResponseBadRequest, HttpResponseNotFound,\
JsonResponse
from django.core.exceptions import PermissionDenied
@ -43,60 +43,54 @@ def not_found_page(request, _):
@login_required
def home(request):
''' this is the same as the feed on the home tab '''
return home_tab(request, 'home')
@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:
page = 1
shelves = []
shelves = get_user_shelf_preview(
request.user,
[('reading', 3), ('read', 1), ('to-read', 3)]
)
size = sum(len(s['books']) for s in shelves)
# books new to the instance, for discovery
if size < 6:
recent_books = models.Work.objects.order_by(
'-created_date'
)[:6 - size]
recent_books = [b.default_edition for b in recent_books]
shelves.append({
'name': 'Recently added',
'identifier': None,
'books': recent_books,
'count': 6 - size,
})
count = 5
querysets = [
# recemt currently reading
models.Edition.objects.filter(
shelves__user=request.user,
shelves__identifier='reading'
),
# read
models.Edition.objects.filter(
shelves__user=request.user,
shelves__identifier='read'
)[:2],
# to-read
models.Edition.objects.filter(
shelves__user=request.user,
shelves__identifier='to-read'
),
# popular books
models.Edition.objects.annotate(
shelf_count=Count('shelves')
).order_by('-shelf_count')
]
suggested_books = []
for queryset in querysets:
length = count - len(suggested_books)
suggested_books += list(queryset[:length])
if len(suggested_books) >= count:
break
# allows us to check if a user has shelved a book
user_books = models.Edition.objects.filter(shelves__user=request.user).all()
activities = get_activity_feed(request.user, tab)
activities = get_activity_feed(request.user, 'home')
activity_count = activities.count()
activities = activities[(page - 1) * page_size:page * page_size]
next_page = '/?page=%d' % (page + 1)
prev_page = '/?page=%d' % (page - 1)
next_page = '/?page=%d#feed' % (page + 1)
prev_page = '/?page=%d#feed' % (page - 1)
data = {
'user': request.user,
'shelves': shelves,
'user_books': user_books,
'suggested_books': suggested_books,
'activities': activities,
'feed_tabs': [
{'id': 'home', 'display': 'Home'},
{'id': 'local', 'display': 'Local'},
{'id': 'federated', 'display': 'Federated'}
],
'active_tab': tab,
'review_form': forms.ReviewForm(),
'quotation_form': forms.QuotationForm(),
'comment_form': forms.CommentForm(),
@ -147,9 +141,9 @@ def search(request):
query = request.GET.get('q')
if re.match(r'\w+@\w+.\w+', query):
# if something looks like a username, search with webfinger
results = [outgoing.handle_account_search(query)]
results = outgoing.handle_account_search(query)
return TemplateResponse(
request, 'user_results.html', {'results': results}
request, 'user_results.html', {'results': results, 'query': query}
)
# or just send the question over to book search
@ -163,23 +157,6 @@ def search(request):
return TemplateResponse(request, 'book_results.html', {'results': results})
def books_page(request):
''' discover books '''
recent_books = models.Work.objects
recent_books = recent_books.order_by('-created_date')[:50]
recent_books = [b.default_edition for b in recent_books]
if request.user.is_authenticated:
recent_books = models.Edition.objects.filter(
~Q(shelfbook__shelf__user=request.user),
id__in=[b.id for b in recent_books if b],
)
data = {
'books': recent_books,
}
return TemplateResponse(request, 'books.html', data)
@login_required
def import_page(request):
''' import history from goodreads '''
@ -216,6 +193,16 @@ def login_page(request):
return TemplateResponse(request, 'login.html', data)
def register_page(request):
''' authentication '''
# send user to the login page
data = {
'site_settings': models.SiteSettings.get(),
'register_form': forms.RegisterForm(),
}
return TemplateResponse(request, 'register.html', data)
def about_page(request):
''' more information about the instance '''
data = {
@ -276,8 +263,6 @@ def user_page(request, username, subpage=None):
return JsonResponse(user.to_activity(), encoder=ActivityEncoder)
# otherwise we're at a UI view
# TODO: change display with privacy and authentication considerations
data = {
'user': user,
'is_self': request.user.id == user.id,
@ -292,10 +277,22 @@ def user_page(request, username, subpage=None):
data['shelves'] = user.shelf_set.all()
return TemplateResponse(request, 'user_shelves.html', data)
shelves = get_user_shelf_preview(user)
data['shelf_count'] = user.shelf_set.count()
shelves = []
for shelf in user.shelf_set.all():
if not shelf.books.count():
continue
shelves.append({
'name': shelf.name,
'remote_id': shelf.remote_id,
'books': shelf.books.all()[:3],
'size': shelf.books.count(),
})
if len(shelves) > 2:
break
data['shelves'] = shelves
activities = get_activity_feed(user, 'self')[:15]
data['activities'] = activities
data['activities'] = get_activity_feed(user, 'self')[:15]
return TemplateResponse(request, 'user.html', data)
@ -398,7 +395,7 @@ def edit_profile_page(request):
return TemplateResponse(request, 'edit_user.html', data)
def book_page(request, book_id, tab='friends'):
def book_page(request, book_id):
''' info about a book '''
book = models.Book.objects.select_subclasses().get(id=book_id)
if is_api_request(request):
@ -413,33 +410,15 @@ def book_page(request, book_id, tab='friends'):
if not work:
return HttpResponseNotFound()
book_reviews = models.Review.objects.filter(book__in=work.edition_set.all())
reviews = models.Review.objects.filter(
book__in=work.edition_set.all(),
).order_by('-published_date')
user_tags = []
if request.user.is_authenticated:
user_reviews = book_reviews.filter(
user=request.user,
).all()
reviews = get_activity_feed(request.user, tab, model=book_reviews)
try:
# TODO: books can be on multiple shelves
shelf = models.Shelf.objects.filter(
user=request.user,
edition=book
).first()
except models.Shelf.DoesNotExist:
shelf = None
user_tags = models.Tag.objects.filter(
book=book, user=request.user
).values_list('identifier', flat=True)
else:
tab = 'public'
reviews = book_reviews.filter(privacy='public')
shelf = None
user_reviews = []
user_tags = []
rating = reviews.aggregate(Avg('rating'))
tags = models.Tag.objects.filter(
@ -450,20 +429,15 @@ def book_page(request, book_id, tab='friends'):
data = {
'book': book,
'shelf': shelf,
'user_reviews': user_reviews,
'reviews': reviews.distinct(),
'reviews': reviews.filter(content__isnull=False),
'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(),
'tag_form': forms.TagForm(),
'feed_tabs': [
{'id': 'friends', 'display': 'Friends'},
{'id': 'local', 'display': 'Local'},
{'id': 'federated', 'display': 'Federated'}
],
'active_tab': tab,
'path': '/book/%s' % book_id,
'cover_form': forms.CoverForm(instance=book),
'info_fields': [
@ -555,42 +529,3 @@ def shelf_page(request, username, shelf_identifier):
'user': user,
}
return TemplateResponse(request, 'shelf.html', data)
def get_user_shelf_preview(user, shelf_proportions=None):
''' data for the covers shelf (user page and feed page) '''
shelves = []
shelf_max = 6
if not shelf_proportions:
shelf_proportions = [('reading', 3), ('read', 2), ('to-read', -1)]
for (identifier, count) in shelf_proportions:
if shelf_max <= 0:
break
if count > shelf_max or count < 0:
count = shelf_max
try:
shelf = models.Shelf.objects.get(
user=user,
identifier=identifier,
)
except models.Shelf.DoesNotExist:
continue
if not shelf.books.count():
continue
books = models.ShelfBook.objects.filter(
shelf=shelf,
).order_by(
'-updated_date'
)[:count]
shelf_max -= len(books)
shelves.append({
'name': shelf.name,
'identifier': shelf.identifier,
'books': [b.book for b in books],
'size': shelf.books.count(),
})
return shelves

3
fr-dev
View file

@ -44,6 +44,9 @@ case "$1" in
test_report)
docker-compose exec web coverage report
;;
collectstatic)
docker-compose exec web python manage.py collectstatic
;;
*)
echo "Unrecognised command. Try: up, initdb, resetdb, makemigrations, migrate, shell, dbshell, restart_celery, test, test_report"
;;