Merge branch 'master' into edition-task

This commit is contained in:
Mouse Reeve 2020-04-01 22:11:43 -07:00
commit 3a7c3724ad
28 changed files with 547 additions and 390 deletions

View file

@ -19,18 +19,24 @@ def get_comment(comment):
status = get_status(comment) status = get_status(comment)
status['inReplyToBook'] = comment.book.absolute_id status['inReplyToBook'] = comment.book.absolute_id
status['fedireadsType'] = comment.status_type status['fedireadsType'] = comment.status_type
status['name'] = comment.name
return status return status
def get_review_article(review): def get_review_article(review):
''' a book review formatted for a non-fedireads isntance (mastodon) ''' ''' a book review formatted for a non-fedireads isntance (mastodon) '''
status = get_status(review) status = get_status(review)
if review.rating:
name = 'Review of "%s" (%d stars): %s' % ( name = 'Review of "%s" (%d stars): %s' % (
review.book.title, review.book.title,
review.rating, review.rating,
review.name review.name
) )
else:
name = 'Review of "%s": %s' % (
review.book.title,
review.name
)
status['name'] = name status['name'] = name
return status return status
@ -38,11 +44,8 @@ def get_review_article(review):
def get_comment_article(comment): def get_comment_article(comment):
''' a book comment formatted for a non-fedireads isntance (mastodon) ''' ''' a book comment formatted for a non-fedireads isntance (mastodon) '''
status = get_status(comment) status = get_status(comment)
name = '%s (comment on "%s")' % ( status['content'] += '<br><br>(comment on <a href="%s">"%s"</a>)' % \
comment.name, (comment.book.absolute_id, comment.book.title)
comment.book.title
)
status['name'] = name
return status return status

View file

@ -188,7 +188,8 @@ class Connector(AbstractConnector):
} }
author = update_from_mappings(author, data, mappings) author = update_from_mappings(author, data, mappings)
# TODO this is making some BOLD assumption # TODO this is making some BOLD assumption
name = data['name'] name = data.get('name')
if name:
author.last_name = name.split(' ')[-1] author.last_name = name.split(' ')[-1]
author.first_name = ' '.join(name.split(' ')[:-1]) author.first_name = ' '.join(name.split(' ')[:-1])
author.save() author.save()
@ -223,8 +224,9 @@ def set_default_edition(work):
options = [e for e in options if e.cover] or options options = [e for e in options if e.cover] or options
options = sorted( options = sorted(
options, options,
key=lambda e: e.published_date.year if e.published_date else None key=lambda e: e.published_date.year if e.published_date else 3000
) )
if len(options):
options[0].default = True options[0].default = True
options[0].save() options[0].save()

View file

@ -31,9 +31,6 @@ class ReviewForm(ModelForm):
model = models.Review model = models.Review
fields = ['name', 'rating', 'content'] fields = ['name', 'rating', 'content']
help_texts = {f: None for f in fields} help_texts = {f: None for f in fields}
content = IntegerField(validators=[
MinValueValidator(0), MaxValueValidator(5)
])
labels = { labels = {
'name': 'Title', 'name': 'Title',
'rating': 'Rating (out of 5)', 'rating': 'Rating (out of 5)',
@ -44,10 +41,9 @@ class ReviewForm(ModelForm):
class CommentForm(ModelForm): class CommentForm(ModelForm):
class Meta: class Meta:
model = models.Comment model = models.Comment
fields = ['name', 'content'] fields = ['content']
help_texts = {f: None for f in fields} help_texts = {f: None for f in fields}
labels = { labels = {
'name': 'Title',
'content': 'Comment', 'content': 'Comment',
} }

View file

@ -0,0 +1,23 @@
# Generated by Django 3.0.3 on 2020-04-01 18:24
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fedireads', '0027_auto_20200330_2232'),
]
operations = [
migrations.RemoveField(
model_name='comment',
name='name',
),
migrations.AlterField(
model_name='review',
name='rating',
field=models.IntegerField(blank=True, default=None, null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(5)]),
),
]

View file

@ -48,12 +48,11 @@ class Status(FedireadsModel):
class Comment(Status): class Comment(Status):
''' like a review but without a rating and transient ''' ''' like a review but without a rating and transient '''
name = models.CharField(max_length=255)
book = models.ForeignKey('Edition', on_delete=models.PROTECT) book = models.ForeignKey('Edition', on_delete=models.PROTECT)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
self.status_type = 'Comment' self.status_type = 'Comment'
self.activity_type = 'Article' self.activity_type = 'Note'
super().save(*args, **kwargs) super().save(*args, **kwargs)
@ -62,8 +61,10 @@ class Review(Status):
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
book = models.ForeignKey('Edition', on_delete=models.PROTECT) book = models.ForeignKey('Edition', on_delete=models.PROTECT)
rating = models.IntegerField( rating = models.IntegerField(
default=0, default=None,
validators=[MinValueValidator(0), MaxValueValidator(5)] null=True,
blank=True,
validators=[MinValueValidator(1), MaxValueValidator(5)]
) )
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
@ -100,6 +101,7 @@ class Boost(Status):
self.status_type = 'Boost' self.status_type = 'Boost'
self.activity_type = 'Announce' self.activity_type = 'Announce'
super().save(*args, **kwargs) super().save(*args, **kwargs)
# This constraint can't work as it would cross tables. # This constraint can't work as it would cross tables.
# class Meta: # class Meta:
# unique_together = ('user', 'boosted_status') # unique_together = ('user', 'boosted_status')

View file

@ -163,6 +163,10 @@ def handle_import_books(user, items):
identifier=item.shelf, identifier=item.shelf,
user=user user=user
) )
if isinstance(item.book, models.Work):
item.book = item.book.default_edition
if not item.book:
continue
_, created = models.ShelfBook.objects.get_or_create( _, created = models.ShelfBook.objects.get_or_create(
book=item.book, shelf=desired_shelf, added_by=user) book=item.book, shelf=desired_shelf, added_by=user)
if created: if created:
@ -201,10 +205,10 @@ def handle_review(user, book, name, content, rating):
broadcast(user, article_create_activity, other_recipients) broadcast(user, article_create_activity, other_recipients)
def handle_comment(user, book, name, content): def handle_comment(user, book, content):
''' post a review ''' ''' post a review '''
# validated and saves the review in the database so it has an id # validated and saves the review in the database so it has an id
comment = create_comment(user, book, name, content) comment = create_comment(user, book, content)
comment_activity = activitypub.get_comment(comment) comment_activity = activitypub.get_comment(comment)
comment_create_activity = activitypub.get_create(user, comment_activity) comment_create_activity = activitypub.get_create(user, comment_activity)

View file

@ -7,7 +7,7 @@ from fedireads import models
def sync_book_data(): def sync_book_data():
''' update books with any changes to their canonical source ''' ''' update books with any changes to their canonical source '''
expiry = timezone.now() - timedelta(days=1) expiry = timezone.now() - timedelta(days=1)
books = models.Book.objects.filter( books = models.Edition.objects.filter(
sync=True, sync=True,
last_sync_date__lte=expiry last_sync_date__lte=expiry
).all() ).all()

View file

@ -1,57 +1,55 @@
/* some colors that are okay: #247BA0 #70C1B2 #B2DBBF #F3FFBD #FF1654 */ /* some colors that are okay: #247BA0 #70C1B2 #B2DBBF #F3FFBD #FF1654 */
/* general override */
* { * {
margin: 0; margin: 0;
padding: 0; padding: 0;
line-height: 1.3em; line-height: 1.3em;
font-family: sans-serif; font-family: sans-serif;
} }
html { html {
background-color: #FFF; background-color: #FFF;
color: black; color: black;
} }
body {
padding-top: 90px;
}
a { a {
color: #247BA0; color: #247BA0;
} }
input, button {
padding: 0.2em 0.5em;
}
button {
cursor: pointer;
width: max-content;
}
h1, h2, h3, h4 {
font-weight: normal;
}
h1 { h1 {
font-weight: normal;
font-size: 1.5rem; font-size: 1.5rem;
} }
h2 { h2 {
font-weight: normal;
font-size: 1rem; font-size: 1rem;
padding: 0.5rem 0.2rem; padding: 0.5rem 0.2rem;
margin-bottom: 1rem; margin-bottom: 1rem;
border-bottom: 3px solid #B2DBBF; 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 { h3 {
font-size: 1rem; font-size: 1rem;
margin: 1rem 0 0.5rem 0;
border-bottom: 3px solid #70C1B2;
font-weight: bold; font-weight: bold;
margin-bottom: 0.5em;
} }
h3 small { h3 small {
font-weight: normal; font-weight: normal;
} }
/* fixed display top bar */
body {
padding-top: 90px;
}
#top-bar { #top-bar {
overflow: visible; overflow: visible;
padding: 0.5rem; padding: 0.5rem;
@ -65,18 +63,31 @@ h3 small {
z-index: 2; z-index: 2;
} }
#warning { /* --- header bar content */
background-color: #FF1654; #branding {
flex-grow: 0;
}
#menu {
list-style: none;
text-align: center; 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;
} }
#branding a {
text-decoration: none;
}
#actions { #actions {
margin-top: 1em; margin-top: 1em;
} }
#actions > * { #actions > * {
display: inline-block; display: inline-block;
} }
@ -106,7 +117,6 @@ h3 small {
height: 1rem; height: 1rem;
width: 1rem; width: 1rem;
} }
.notification { .notification {
margin-bottom: 1em; margin-bottom: 1em;
padding: 1em 0; padding: 1em 0;
@ -116,11 +126,6 @@ h3 small {
background-color: #DDD; background-color: #DDD;
} }
button .icon {
font-size: 1.1rem;
vertical-align: sub;
}
#search button { #search button {
border: none; border: none;
background: none; background: none;
@ -131,29 +136,6 @@ button .icon {
max-width: 55rem; max-width: 55rem;
padding-right: 1em; padding-right: 1em;
} }
header {
display: flex;
flex-direction: row;
}
ul.menu {
list-style: none;
text-align: center;
margin-top: 1.5rem;
flex-grow: 1;
font-size: 0.9em;
}
ul.menu li {
display: inline-block;
background-color: white;
padding: 0 0.5em;
text-transform: uppercase;
}
ul.menu a {
color: #555;
text-decoration: none;
font-size: 0.9em;
}
.pulldown-container { .pulldown-container {
position: relative; position: relative;
@ -175,15 +157,24 @@ ul.menu a {
margin-bottom: 0.5em; margin-bottom: 0.5em;
} }
/* content area */
.content-container {
margin: 1rem;
}
.content-container > * {
padding-left: 1em;
padding-right: 1em;
}
#feed { #feed {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding-top: 70px; padding-top: 70px;
position: relative; position: relative;
top: -50px;
z-index: 0; z-index: 0;
} }
/* row component */
.row { .row {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -204,6 +195,16 @@ ul.menu a {
flex-wrap: wrap; flex-wrap: wrap;
} }
.column {
display: flex;
flex-direction: column;
}
.column > * {
margin-bottom: 1em;
}
/* discover books page grid of covers */
.book-grid .book-cover { .book-grid .book-cover {
height: 11em; height: 11em;
width: auto; width: auto;
@ -212,6 +213,17 @@ ul.menu a {
margin-bottom: 2em; margin-bottom: 2em;
} }
/* special case forms */
.review-form label {
display: block;
}
.review-form textarea {
width: 30rem;
height: 10rem;
}
.follow-requests .row { .follow-requests .row {
margin-bottom: 0.5em; margin-bottom: 0.5em;
} }
@ -219,6 +231,7 @@ ul.menu a {
width: 20em; width: 20em;
} }
.login form { .login form {
margin-top: 1em; margin-top: 1em;
} }
@ -247,14 +260,14 @@ ul.menu a {
.book-form .row label { .book-form .row label {
width: max-content; width: max-content;
} }
form input {
flex-grow: 1; /* general form stuff */
input, button {
padding: 0.2em 0.5em;
} }
form div { button, input[type="submit"] {
margin-bottom: 1em; cursor: pointer;
} width: max-content;
textarea {
padding: 0.5em;
} }
.content-container button { .content-container button {
border: none; border: none;
@ -273,6 +286,36 @@ button.warning {
background-color: #FF1654; 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;
}
/* re-usable tab styles */
.tabs { .tabs {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -299,9 +342,10 @@ button.warning {
color: black; color: black;
} }
.user-pic { .user-pic {
width: 2rem; width: 2em;
height: 2rem; height: 2em;
border-radius: 50%; border-radius: 50%;
vertical-align: top; vertical-align: top;
position: relative; position: relative;
@ -312,51 +356,54 @@ button.warning {
height: 5em; height: 5em;
} }
h2 .edit-link {
text-decoration: none;
font-size: 0.9em;
float: right;
}
h2 .edit-link .icon {
font-size: 1.2em;
}
.user-profile .row > * { .user-profile .row > * {
flex-grow: 0; flex-grow: 0;
} }
.user-profile .row > *:last-child { .user-profile .row > *:last-child {
flex-grow: 1; flex-grow: 1;
margin-left: 2em;
} }
.review-form label { /* general book display */
display: block;
}
.time-ago {
float: right;
display: block;
text-align: right;
}
.book-preview { .book-preview {
overflow: hidden; overflow: hidden;
z-index: 1; z-index: 1;
} }
.book-preview img {
float: left;
margin-right: 1em;
}
.book-preview.grid { .book-preview.grid {
float: left; float: left;
} }
.content-container { .cover-container {
margin: 1rem; flex-grow: 0;
} }
.content-container > * { .cover-container button {
padding-left: 1em; display: block;
padding-right: 1em; 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;
} }
.all-shelves { .all-shelves {
@ -379,51 +426,32 @@ h2 .edit-link .icon {
padding-left: 1em; padding-left: 1em;
} }
.user-shelves .covers-shelf {
flex-wrap: wrap;
}
.user-shelves > div {
margin: 1em 0;
padding: 0;
}
.user-shelves > div > * {
padding-left: 1em;
}
.user-shelves .covers-shelf .book-cover {
height: 9em;
}
.covers-shelf { .covers-shelf {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
} }
.covers-shelf .book-preview { .covers-shelf .cover-container {
margin-right: 1em; margin-right: 1em;
font-size: 0.9em; font-size: 0.9em;
overflow: unset; overflow: unset;
width: min-content; width: min-content;
position: relative; position: relative;
} }
.covers-shelf .book-preview button { .covers-shelf .cover-container:last-child {
display: block;
margin: 0 auto;
border: none;
}
.covers-shelf .book-preview:last-child {
margin-right: 0; margin-right: 0;
} }
.covers-shelf .book-preview:hover { .covers-shelf img:hover {
cursor: pointer; cursor: pointer;
}
.covers-shelf .book-preview:hover img {
box-shadow: #F3FFBD 0em 0em 1em 1em; box-shadow: #F3FFBD 0em 0em 1em 1em;
} }
.covers-shelf .book-cover { .covers-shelf .book-cover {
float: none;
height: 11rem; height: 11rem;
width: auto; width: auto;
margin: 0; margin: 0;
} }
.covers-shelf button {
border: none;
}
.close { .close {
float: right; float: right;
@ -437,29 +465,6 @@ h2 .edit-link .icon {
.compose-suggestion.visible { .compose-suggestion.visible {
display: block; display: block;
} }
.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;
}
.book-cover {
width: 180px;
}
.book-cover.small {
width: 50px;
height: auto;
}
.compose-suggestion .book-preview { .compose-suggestion .book-preview {
background-color: #EEE; background-color: #EEE;
padding: 1em; padding: 1em;
@ -476,14 +481,8 @@ h2 .edit-link .icon {
display: inline; display: inline;
} }
.review-form textarea {
width: 30rem;
height: 10rem;
}
blockquote { blockquote {
white-space: pre-line; white-space: pre-line;
margin-left: 2em;
} }
blockquote .icon-quote-open { blockquote .icon-quote-open {
float: left; float: left;
@ -557,8 +556,11 @@ th, td {
color: #FF1654; color: #FF1654;
} }
.comment-thread .reply h2 { /* status css */
background: none; .time-ago {
float: right;
display: block;
text-align: right;
} }
.post { .post {
background-color: #EFEFEF; background-color: #EFEFEF;
@ -577,7 +579,18 @@ th, td {
.post .user-pic, .compose-suggestion .user-pic { .post .user-pic, .compose-suggestion .user-pic {
right: 0.25em; 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 { .comment-thread .post {
margin-left: 4em; margin-left: 4em;
border-left: 2px solid #247BA0; border-left: 2px solid #247BA0;
@ -596,14 +609,16 @@ th, td {
margin-left: 3em; margin-left: 3em;
} }
a .icon { /* pagination */
color: black; .pagination a {
text-decoration: none; text-decoration: none;
} }
.pagination .next {
.hidden-text { text-align: right;
height: 0; }
width: 0;
position: absolute; /* special one-off "delete all data" banner */
overflow: hidden; #warning {
background-color: #FF1654;
text-align: center;
} }

View file

@ -1,11 +0,0 @@
function show_compose(element) {
var visible_compose_boxes = document.getElementsByClassName('visible');
for (var i = 0; i < visible_compose_boxes.length; i++) {
visible_compose_boxes[i].className = 'compose-suggestion';
}
var target_id = 'compose-' + element.id;
var target = document.getElementById(target_id);
target.className += ' visible';
}

View file

@ -1,3 +1,15 @@
function show_compose(element, e) {
e.preventDefault();
var visible_compose_boxes = document.getElementsByClassName('visible');
for (var i = 0; i < visible_compose_boxes.length; i++) {
visible_compose_boxes[i].className = 'compose-suggestion';
}
var target_id = 'compose-' + element.id;
var target = document.getElementById(target_id);
target.className += ' visible';
}
function hide_element(element) { function hide_element(element) {
var classes = element.parentElement.className; var classes = element.parentElement.className;
element.parentElement.className = classes.replace('visible', ''); element.parentElement.className = classes.replace('visible', '');

View file

@ -31,7 +31,11 @@ def create_review(user, possible_book, name, content, rating):
content = sanitize(content) content = sanitize(content)
# no ratings outside of 0-5 # no ratings outside of 0-5
rating = rating if 0 <= rating <= 5 else 0 try:
rating = int(rating)
rating = rating if 1 <= rating <= 5 else None
except ValueError:
rating = None
return models.Review.objects.create( return models.Review.objects.create(
user=user, user=user,
@ -46,19 +50,18 @@ def create_comment_from_activity(author, activity):
''' parse an activity json blob into a status ''' ''' parse an activity json blob into a status '''
book = activity['inReplyToBook'] book = activity['inReplyToBook']
book = book.split('/')[-1] book = book.split('/')[-1]
name = activity.get('name')
content = activity.get('content') content = activity.get('content')
published = activity.get('published') published = activity.get('published')
remote_id = activity['id'] remote_id = activity['id']
comment = create_comment(author, book, name, content) comment = create_comment(author, book, content)
comment.published_date = published comment.published_date = published
comment.remote_id = remote_id comment.remote_id = remote_id
comment.save() comment.save()
return comment return comment
def create_comment(user, possible_book, name, content): def create_comment(user, possible_book, content):
''' a book comment has been added ''' ''' a book comment has been added '''
# throws a value error if the book is not found # throws a value error if the book is not found
book = get_or_create_book(possible_book) book = get_or_create_book(possible_book)
@ -67,7 +70,6 @@ def create_comment(user, possible_book, name, content):
return models.Comment.objects.create( return models.Comment.objects.create(
user=user, user=user,
book=book, book=book,
name=name,
content=content, content=content,
) )

View file

@ -13,11 +13,16 @@
<div class="content-container"> <div class="content-container">
<h2>Books by {{ author.name }}</h2> <h2>Books by {{ author.name }}</h2>
<div class="book-grid row shrink wrap">
{% for book in books %} {% for book in books %}
<div class="book-preview"> <div class="book-preview">
{% include 'snippets/book.html' with book=book size=large description=True %} <a href="{{ book.absolute_id }}">
{% include 'snippets/book_cover.html' with book=book %}
</a>
{% include 'snippets/shelve_button.html' with book=book %}
</div> </div>
{% endfor %} {% endfor %}
</div>
</div> </div>
{% endblock %} {% endblock %}

View file

@ -1,9 +1,9 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% load fr_display %} {% load fr_display %}
{% block content %} {% block content %}
<div class="content-container user-profile"> <div class="content-container">
<h2><q>{{ book.title }}</q> by <h2>
{% include 'snippets/authors.html' with book=book %} {% include 'snippets/book_titleby.html' with book=book %}
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
<a href="{{ book.fedireads_key }}/edit" class="edit-link">edit <a href="{{ book.fedireads_key }}/edit" class="edit-link">edit
@ -13,27 +13,14 @@
</a> </a>
{% endif %} {% endif %}
</h2> </h2>
<div>
<div class="book-preview">
<div class="row">
<div class="cover-container">
{% include 'snippets/book_cover.html' with book=book size=large %} {% include 'snippets/book_cover.html' with book=book size=large %}
<p>{{ active_tab }} rating: {{ rating | stars }}</p>
{% if book.parent_work.description %}
<blockquote>{{ book.parent_work.description | description }}</blockquote>
{% endif %}
<div>
<div id="tag-cloud">
{% for tag in tags %}
{% include 'snippets/tag.html' with tag=tag user=request.user user_tags=user_tag_names %}
{% endfor %}
</div>
</div>
<p><a href="/editions/{{ book.parent_work.id }}">{{ book.parent_work.edition_set.count }} other editions</a></p>
{% include 'snippets/shelve_button.html' %} {% include 'snippets/shelve_button.html' %}
</div>
<div>
{% if request.user.is_authenticated and not book.cover %} {% if request.user.is_authenticated and not book.cover %}
<form name="add-cover" method="POST" action="/upload_cover/{{book.id}}" enctype="multipart/form-data"> <form name="add-cover" method="POST" action="/upload_cover/{{book.id}}" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
@ -41,21 +28,48 @@
<button type="submit">Add cover</button> <button type="submit">Add cover</button>
</form> </form>
{% endif %} {% endif %}
</div>
</div>
</div>
{% if request.user.is_authenticated %} <dl>
<div class="content-container"> {% for field in info_fields %}
<h2>Leave a review</h2> {% if field.value %}
<dt>{{ field.name }}:</dt>
<dd>{{ field.value }}</dd>
{% endif %}
{% endfor %}
</dl>
</div>
<div class="column">
<h3>{{ active_tab }} rating: {{ rating | stars }}</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 id="tag-cloud">
{% for tag in tags %}
{% include 'snippets/tag.html' with tag=tag user=request.user user_tags=user_tag_names %}
{% endfor %}
</div>
<div>
{% if request.user.is_authenticated %}
<h3>Leave a review</h3>
<form class="review-form" name="review" action="/review/" method="post"> <form class="review-form" name="review" action="/review/" method="post">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="book" value="{{ book.fedireads_key }}"></input> <input type="hidden" name="book" value="{{ book.fedireads_key }}"></input>
{{ review_form.as_p }} {{ review_form.as_p }}
<button type="submit">Post review</button> <button type="submit">Post review</button>
</form> </form>
{% endif %}
</div>
</div>
</div> </div>
{% if request.user.is_authenticated %}
<div class="content-container tabs"> <div class="content-container tabs">
{% include 'snippets/tabs.html' with tabs=feed_tabs active_tab=active_tab path=path %} {% include 'snippets/tabs.html' with tabs=feed_tabs active_tab=active_tab path=path %}
</div> </div>

View file

@ -2,22 +2,8 @@
{% load fr_display %} {% load fr_display %}
{% block content %} {% block content %}
<div class="all-shelves content-container"> {% include 'snippets/covers_shelf.html' with shelves=shelves user=request.user %}
{% include 'snippets/covers_shelf.html' with shelves=shelves user=request.user %}
</div>
{% for shelf in shelves %}
{% for book in shelf.books %}
<div class="compose-suggestion" id="compose-book-{{ book.id }}">
<span class="close icon icon-close" onclick="hide_element(this)">
<span class="hidden-text">Close</span>
</span>
<div class="content-container">
{% include 'snippets/create_status.html' with book=book user=request.user %}
</div>
</div>
{% endfor %}
{% endfor %}
<div id="feed"> <div id="feed">
<div class="content-container tabs"> <div class="content-container tabs">
@ -29,7 +15,26 @@
{% include 'snippets/status.html' with status=activity %} {% include 'snippets/status.html' with status=activity %}
</div> </div>
{% endfor %} {% endfor %}
<div class="content-container pagination row">
{% if prev %}
<p>
<a href="{{ prev }}">
<span class="icon icon-arrow-left"></span>
Previous
</a>
</p>
{% endif %}
{% if next %}
<p class="next">
<a href="{{ next }}">
Next
<span class="icon icon-arrow-right"></span>
</a>
</p>
{% endif %}
</div>
</div> </div>
<script src="/static/js/feed.js"></script>
{% endblock %} {% endblock %}

View file

@ -22,10 +22,14 @@
<body> <body>
<div id="top-bar"> <div id="top-bar">
<header> <header class="row">
<div id="branding"><a href="/"><img id="logo" src="/static/images/logo-small.png" alt="BookWyrm"></img></a></div> <div id="branding">
<a href="/">
<img id="logo" src="/static/images/logo-small.png" alt="BookWyrm"></img>
</a>
</div>
<ul class="menu"> <ul id="menu">
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
<li><a href="/user/{{request.user.localname}}/shelves">Your shelves</a></li> <li><a href="/user/{{request.user.localname}}/shelves">Your shelves</a></li>
{% endif %} {% endif %}

View file

@ -1,18 +0,0 @@
{% load fr_display %}
{% include 'snippets/book_cover.html' with book=book %}
<p class="title">
<a href="/book/{{ book.fedireads_key }}">{{ book.title }}</a>
</p>
<p>
by {% include 'snippets/authors.html' with book=book %}
</p>
{% if rating %}
{{ rating | stars }} {{ rating }}
{% endif %}
{% if description %}
<blockquote>{{ book.description | description }}</blockquote>
{% endif %}
{% include 'snippets/shelve_button.html' with book=book pulldown=shelf_pulldown %}

View file

@ -0,0 +1,7 @@
{% load fr_display %}
{% if book.description %}
<blockquote>{{ book.description | description }}</blockquote>
{% elif book.parent_work.description %}
<blockquote>{{ book.parent_work.description | description }}</blockquote>
{% endif %}

View file

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

View file

@ -1,20 +1,40 @@
{% load fr_display %} {% load fr_display %}
{% for shelf in shelves %}
{% if shelf.books %} <div class="all-shelves content-container">
<div> {% for shelf in shelves %}
{% if shelf.books %}
<div>
<h2>{{ shelf.name }} <h2>{{ shelf.name }}
{% if shelf.size > shelf.books|length %} {% if shelf.size > shelf.books|length %}
<small>(<a href="/shelf/{{ user | username }}/{{ shelf.identifier }}">See all {{ shelf.size }}</a>)</small> <small>(<a href="/shelf/{{ user | username }}/{{ shelf.identifier }}">See all {{ shelf.size }}</a>)</small>
{% endif %} {% endif %}
</h2> </h2>
<div class="covers-shelf {{ shelf.identifier }}"> <div class="covers-shelf {{ shelf.identifier }} ">
{% for book in shelf.books %} {% for book in shelf.books %}
<div class="book-preview" onclick="show_compose(this)" id="book-{{ book.id }}"> <div class="cover-container">
<div >
<a href="{{ book.absolute_id }}" onclick="show_compose(this, event)" id="book-{{ book.id }}">
{% include 'snippets/book_cover.html' with book=book %} {% include 'snippets/book_cover.html' with book=book %}
</a>
</div>
{% include 'snippets/shelve_button.html' with book=book %} {% include 'snippets/shelve_button.html' with book=book %}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
</div>
{% endif %}
{% endfor %}
</div> </div>
{% endif %}
{% for shelf in shelves %}
{% for book in shelf.books %}
<div class="compose-suggestion" id="compose-book-{{ book.id }}">
<span class="close icon icon-close" onclick="hide_element(this)">
<span class="hidden-text">Close</span>
</span>
<div class="content-container">
{% include 'snippets/create_status.html' with book=book user=request.user %}
</div>
</div>
{% endfor %}
{% endfor %} {% endfor %}

View file

@ -12,7 +12,7 @@
</button> </button>
</form> </form>
<form name="boost" action="/boost/{{ activity.id }}" method="post" onsubmit="return interact(event)" class="boost-{{ status.id }} {% if False %}hidden{% endif %}" data-id="boost-{{ status.id }}"> <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 %} {% csrf_token %}
<button type="submit"> <button type="submit">
<span class="icon icon-boost"> <span class="icon icon-boost">
@ -20,7 +20,7 @@
</span> </span>
</button> </button>
</form> </form>
<form name="unboost" action="/unboost/{{ activity.id }}" method="post" onsubmit="return interact(event)" class="boost-{{ status.id }} active {% if not False %}hidden{% endif %}" data-id="boost-{{ status.id }}"> <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 %} {% csrf_token %}
<button type="submit"> <button type="submit">
<span class="icon icon-boost"> <span class="icon icon-boost">

View file

@ -1,59 +1,24 @@
{% load humanize %}
{% load fr_display %} {% load fr_display %}
<div class="post {{ status.status_type|lower }} depth-{{ depth }} {% if main %}main{% else %}reply{% endif %}">
<div class="post {{ status.status_type | lower }} depth-{{ depth }} {% if main %}main{% else %}reply{% endif %}">
<h2> <h2>
{% include 'snippets/avatar.html' with user=status.user %} {% if status.boosted_status %}
{% include 'snippets/username.html' with user=status.user %} {% include 'snippets/status_header.html' with status=status.boosted_status %}
{% if status.status_type == 'Update' %} <small class="subhead">{% include 'snippets/status_header.html' with status=status %}</small>
{{ status.content | safe }} {% else %}
{% elif status.status_type == 'Review' %} {% include 'snippets/status_header.html' with status=status %}
reviewed {{ status.book.title }}
{% elif status.status_type == 'Comment' %}
commented on {{ status.book.title }}
{% 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.absolute_id }}">{{ parent_status.status_type|lower }}</a>
{% endwith %}
{% endif %} {% endif %}
<span class="time-ago">
<a href="{{ status.absolute_id }}">{{ status.published_date | naturaltime }}</a>
</span>
</h2> </h2>
{% if not hide_book and status.mention_books.count %} <div class="status-content">
{% for book in status.mention_books.all|slice:"0:3" %} {% include 'snippets/status_content.html' with status=status %}
<div class="book-preview">
{% if status.status_type == 'Review' %}
{% include 'snippets/book.html' with book=book %}
{% else %}
{% include 'snippets/book.html' with book=book description=True %}
{% endif %}
</div> </div>
{% endfor %}
{% endif %}
{% if not hide_book and status.book%}
<div class="book-preview">
{% if status.status_type == 'Review' %}
{% include 'snippets/book.html' with book=status.book %}
{% else %}
{% include 'snippets/book.html' with book=status.book description=True %}
{% endif %}
</div>
{% endif %}
{% if status.status_type == 'Review' %}<h4>{{ status.name }}
<small>{{ status.rating | stars }} stars, by {% include 'snippets/username.html' with user=status.user %}</small>
</h4>{% endif %}
{% if status.status_type != 'Update' and status.status_type != 'Boost' %}
<blockquote>{{ status.content | safe }}</blockquote>
{% endif %}
{% if status.status_type == 'Boost' %}
{% include 'snippets/status.html' with status=status.boosted_status depth=depth|add:1 %}
{% endif %}
{% if not max_depth and status.reply_parent or status|replies %}<p><a href="{{ status.absolute_id }}">Thread</a>{% endif %}
</div> </div>
{% if status.status_type != 'Boost' %} {% if status.status_type == 'Boost' %}
{% include 'snippets/interaction.html' with activity=status|boosted_status %}
{% else %}
{% include 'snippets/interaction.html' with activity=status %} {% include 'snippets/interaction.html' with activity=status %}
{% endif %} {% endif %}

View file

@ -0,0 +1,52 @@
{% load fr_display %}
{% if not hide_book and status.mention_books.count %}
<div class="row">
{% 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 %}
{% if status.mention_books.count > 1 %}
<p>{% include 'snippets/book_titleby.html' with book=book %}</p>
{% endif %}
{% 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/shelve_button.html' with book=status.book %}
</div>
{% endif %}
<div>
{% if status.status_type == 'Review' %}
<h3>
{{ status.name }}<br>
{{ status.rating | stars }}
</h3>
{% endif %}
{% if status.status_type != 'Update' and status.status_type != 'Boost' %}
<blockquote>{{ status.content | safe }}</blockquote>
{% endif %}
{% if status.status_type == 'Boost' %}
{% include 'snippets/status_content.html' with status=status|boosted_status %}
{% endif %}
{% if not max_depth and status.reply_parent or status|replies %}<p><a href="{{ status.absolute_id }}">Thread</a>{% endif %}
</div>
</div>

View file

@ -0,0 +1,21 @@
{% load humanize %}
{% load fr_display %}
{% include 'snippets/avatar.html' with user=status.user %}
{% include 'snippets/username.html' with user=status.user %}
{% if status.status_type == 'Update' %}
{{ status.content | safe }}
{% elif status.status_type == 'Review' %}
reviewed {{ status.book.title }}
{% elif status.status_type == 'Comment' %}
commented on {{ status.book.title }}
{% 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.absolute_id }}">{{ parent_status.status_type | lower }}</a>
{% endwith %}
{% endif %}
<span class="time-ago">
<a href="{{ status.absolute_id }}">{{ status.published_date | naturaltime }}</a>
</span>

View file

@ -2,9 +2,7 @@
{% block content %} {% block content %}
{% include 'user_header.html' with user=user %} {% include 'user_header.html' with user=user %}
<div class="all-shelves content-container"> {% include 'snippets/covers_shelf.html' with shelves=shelves user=user %}
{% include 'snippets/covers_shelf.html' with shelves=shelves user=user %}
</div>
<div> <div>
<div class="content-container"><h2>User Activity</h2></div> <div class="content-container"><h2>User Activity</h2></div>

View file

@ -88,6 +88,12 @@ def get_user_liked(user, status):
return False return False
@register.filter(name='boosted')
def get_user_boosted(user, status):
''' did the given user fav a status? '''
return user.id in status.boosters.all().values_list('user', flat=True)
@register.filter(name='follow_request_exists') @register.filter(name='follow_request_exists')
def follow_request_exists(user, requester): def follow_request_exists(user, requester):
''' see if there is a pending follow request for a user ''' ''' see if there is a pending follow request for a user '''
@ -101,6 +107,13 @@ def follow_request_exists(user, requester):
return False return False
@register.filter(name='boosted_status')
def get_boosted(boost):
''' load a boosted status. have to do this or it wont get foregin keys '''
return models.Status.objects.select_subclasses().filter(
id=boost.boosted_status.id
).get()
@register.simple_tag(takes_context=True) @register.simple_tag(takes_context=True)
def shelve_button_identifier(context, book): def shelve_button_identifier(context, book):

View file

@ -170,7 +170,7 @@ def review(request):
# TODO: validation, htmlification # TODO: validation, htmlification
name = form.data.get('name') name = form.data.get('name')
content = form.data.get('content') content = form.data.get('content')
rating = int(form.data.get('rating')) rating = form.data.get('rating')
outgoing.handle_review(request.user, book_identifier, name, content, rating) outgoing.handle_review(request.user, book_identifier, name, content, rating)
return redirect('/book/%s' % book_identifier) return redirect('/book/%s' % book_identifier)
@ -186,10 +186,9 @@ def comment(request):
return redirect('/book/%s' % book_identifier) return redirect('/book/%s' % book_identifier)
# TODO: validation, htmlification # TODO: validation, htmlification
name = form.data.get('name')
content = form.data.get('content') content = form.data.get('content')
outgoing.handle_comment(request.user, book_identifier, name, content) outgoing.handle_comment(request.user, book_identifier, content)
return redirect('/book/%s' % book_identifier) return redirect('/book/%s' % book_identifier)

View file

@ -44,6 +44,12 @@ def home(request):
@login_required @login_required
def home_tab(request, tab): def home_tab(request, tab):
''' user's homepage with activity feed ''' ''' user's homepage with activity feed '''
page_size = 15
try:
page = int(request.GET.get('page', 1))
except ValueError:
page = 1
shelves = [] shelves = []
shelves = get_user_shelf_preview( shelves = get_user_shelf_preview(
request.user, request.user,
@ -67,30 +73,13 @@ def home_tab(request, tab):
# allows us to check if a user has shelved a book # allows us to check if a user has shelved a book
user_books = models.Edition.objects.filter(shelves__user=request.user).all() user_books = models.Edition.objects.filter(shelves__user=request.user).all()
# status updates for your follow network activities = get_activity_feed(request.user, tab)
following = models.User.objects.filter(
Q(followers=request.user) | Q(id=request.user.id)
)
activities = models.Status.objects.order_by( activity_count = activities.count()
'-created_date' activities = activities[(page - 1) * page_size:page * page_size]
).select_subclasses()
if tab == 'home':
# people you follow and direct mentions
activities = activities.filter(
Q(user__in=following, privacy='public') | \
Q(mention_users=request.user)
)
elif tab == 'local':
# everyone on this instance
activities = activities.filter(user__local=True, privacy='public')
else:
# all activities from everyone you federate with
activities = activities.filter(privacy='public')
activities = activities[:10]
next_page = '/?page=%d' % (page + 1)
prev_page = '/?page=%d' % (page - 1)
data = { data = {
'user': request.user, 'user': request.user,
'shelves': shelves, 'shelves': shelves,
@ -104,10 +93,48 @@ def home_tab(request, tab):
'active_tab': tab, 'active_tab': tab,
'review_form': forms.ReviewForm(), 'review_form': forms.ReviewForm(),
'comment_form': forms.CommentForm(), 'comment_form': forms.CommentForm(),
'next': next_page if activity_count > (page_size * page) else None,
'prev': prev_page if page > 1 else None,
} }
return TemplateResponse(request, 'feed.html', data) return TemplateResponse(request, 'feed.html', data)
def get_activity_feed(user, filter_level, model=models.Status):
''' get a filtered queryset of statuses '''
# status updates for your follow network
following = models.User.objects.filter(
Q(followers=user) | Q(id=user.id)
)
activities = model
if hasattr(model, 'objects'):
activities = model.objects
activities = activities.order_by(
'-created_date'
)
if hasattr(activities, 'select_subclasses'):
activities = activities.select_subclasses()
# TODO: privacy relationshup between request.user and user
if filter_level in ['friends', 'home']:
# people you follow and direct mentions
activities = activities.filter(
Q(user__in=following, privacy='public') | \
Q(mention_users=user)
)
elif filter_level == 'self':
activities = activities.filter(user=user, privacy='public')
elif filter_level == 'local':
# everyone on this instance
activities = activities.filter(user__local=True, privacy='public')
else:
# all activities from everyone you federate with
activities = activities.filter(privacy='public')
return activities
def books_page(request): def books_page(request):
''' discover books ''' ''' discover books '''
recent_books = models.Work.objects recent_books = models.Work.objects
@ -188,11 +215,7 @@ def user_page(request, username, subpage=None):
else: else:
shelves = get_user_shelf_preview(user) shelves = get_user_shelf_preview(user)
data['shelves'] = shelves data['shelves'] = shelves
activities = models.Status.objects.filter( activities = get_activity_feed(user, 'self')[:15]
user=user,
).order_by(
'-created_date',
).select_subclasses().all()[:10]
data['activities'] = activities data['activities'] = activities
return TemplateResponse(request, 'user.html', data) return TemplateResponse(request, 'user.html', data)
@ -339,23 +362,7 @@ def book_page(request, book_identifier, tab='friends'):
user=request.user, user=request.user,
).all() ).all()
if tab == 'friends': reviews = get_activity_feed(request.user, tab, model=book_reviews)
reviews = book_reviews.filter(
Q(user__followers=request.user, privacy='public') | \
Q(user=request.user) | \
Q(mention_users=request.user),
)
elif tab == 'local':
reviews = book_reviews.filter(
Q(privacy='public') | \
Q(mention_users=request.user),
user__local=True,
)
else:
reviews = book_reviews.filter(
Q(privacy='public') | \
Q(mention_users=request.user),
)
try: try:
# TODO: books can be on multiple shelves # TODO: books can be on multiple shelves
@ -408,6 +415,14 @@ def book_page(request, book_identifier, tab='friends'):
'active_tab': tab, 'active_tab': tab,
'path': '/book/%s' % book_identifier, 'path': '/book/%s' % book_identifier,
'cover_form': forms.CoverForm(instance=book), 'cover_form': forms.CoverForm(instance=book),
'info_fields': [
{'name': 'ISBN', 'value': book.isbn},
{'name': 'OCLC number', 'value': book.oclc_number},
{'name': 'OpenLibrary ID', 'value': book.openlibrary_key},
{'name': 'Goodreads ID', 'value': book.goodreads_key},
{'name': 'Format', 'value': book.physical_format},
{'name': 'Pages', 'value': book.pages},
],
} }
return TemplateResponse(request, 'book.html', data) return TemplateResponse(request, 'book.html', data)

View file

@ -55,7 +55,7 @@ def nodeinfo(request):
return JsonResponse({ return JsonResponse({
"version": "2.0", "version": "2.0",
"software": { "software": {
"name": "mastodon", "name": "fedireads",
"version": "0.0.1" "version": "0.0.1"
}, },
"protocols": [ "protocols": [