forked from mirrors/bookwyrm
Merge pull request #129 from mouse-reeve/ratings
Ratings without reviews
This commit is contained in:
commit
930ca63fee
19 changed files with 228 additions and 31 deletions
|
@ -26,14 +26,19 @@ class RegisterForm(ModelForm):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class RatingForm(ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = models.Review
|
||||||
|
fields = ['rating']
|
||||||
|
|
||||||
|
|
||||||
class ReviewForm(ModelForm):
|
class ReviewForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Review
|
model = models.Review
|
||||||
fields = ['name', 'rating', 'content']
|
fields = ['name', 'content']
|
||||||
help_texts = {f: None for f in fields}
|
help_texts = {f: None for f in fields}
|
||||||
labels = {
|
labels = {
|
||||||
'name': 'Title',
|
'name': 'Title',
|
||||||
'rating': 'Rating (out of 5)',
|
|
||||||
'content': 'Review',
|
'content': 'Review',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
18
fedireads/migrations/0029_auto_20200403_1835.py
Normal file
18
fedireads/migrations/0029_auto_20200403_1835.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.0.3 on 2020-04-03 18:35
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('fedireads', '0028_auto_20200401_1824'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='review',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(max_length=255, null=True),
|
||||||
|
),
|
||||||
|
]
|
|
@ -58,7 +58,7 @@ class Comment(Status):
|
||||||
|
|
||||||
class Review(Status):
|
class Review(Status):
|
||||||
''' a book review '''
|
''' a book review '''
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255, null=True)
|
||||||
book = models.ForeignKey('Edition', on_delete=models.PROTECT)
|
book = models.ForeignKey('Edition', on_delete=models.PROTECT)
|
||||||
rating = models.IntegerField(
|
rating = models.IntegerField(
|
||||||
default=None,
|
default=None,
|
||||||
|
|
|
@ -10,7 +10,7 @@ from fedireads import activitypub
|
||||||
from fedireads import models
|
from fedireads import models
|
||||||
from fedireads.broadcast import get_recipients, broadcast
|
from fedireads.broadcast import get_recipients, broadcast
|
||||||
from fedireads.status import create_review, create_status, create_comment
|
from fedireads.status import create_review, create_status, create_comment
|
||||||
from fedireads.status import create_tag, create_notification
|
from fedireads.status import create_tag, create_notification, create_rating
|
||||||
from fedireads.remote_user import get_or_create_remote_user
|
from fedireads.remote_user import get_or_create_remote_user
|
||||||
|
|
||||||
|
|
||||||
|
@ -188,6 +188,12 @@ def handle_import_books(user, items):
|
||||||
broadcast(user, create_activity, recipients)
|
broadcast(user, create_activity, recipients)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_rate(user, book, rating):
|
||||||
|
''' a review that's just a rating '''
|
||||||
|
review = create_rating(user, book, rating)
|
||||||
|
# TODO: serialize and broadcast
|
||||||
|
|
||||||
|
|
||||||
def handle_review(user, book, name, content, rating):
|
def handle_review(user, book, name, content, rating):
|
||||||
''' 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
|
||||||
|
|
|
@ -314,6 +314,62 @@ button .icon {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
.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 */
|
/* re-usable tab styles */
|
||||||
.tabs {
|
.tabs {
|
||||||
|
@ -406,6 +462,20 @@ button .icon {
|
||||||
margin-bottom: 1em;
|
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 {
|
.all-shelves {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
|
@ -37,6 +37,17 @@ function reply(e) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function rate_stars(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
ajaxPost(e.target);
|
||||||
|
rating = e.target.rating.value;
|
||||||
|
var stars = e.target.parentElement.getElementsByClassName('icon');
|
||||||
|
for (var i = 0; i < stars.length ; i++) {
|
||||||
|
stars[i].className = rating > i ? 'icon icon-star-full' : 'icon icon-star-empty';
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function tabChange(e) {
|
function tabChange(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
var target = e.target.parentElement;
|
var target = e.target.parentElement;
|
||||||
|
|
|
@ -25,6 +25,17 @@ def create_review_from_activity(author, activity):
|
||||||
return review
|
return review
|
||||||
|
|
||||||
|
|
||||||
|
def create_rating(user, book, rating):
|
||||||
|
''' a review that's just a rating '''
|
||||||
|
if not rating or rating < 1 or rating > 5:
|
||||||
|
raise ValueError('Invalid rating')
|
||||||
|
return models.Review.objects.create(
|
||||||
|
user=user,
|
||||||
|
book=book,
|
||||||
|
rating=rating,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_review(user, book, name, content, rating):
|
def create_review(user, book, name, content, rating):
|
||||||
''' a book review has been added '''
|
''' a book review has been added '''
|
||||||
name = sanitize(name)
|
name = sanitize(name)
|
||||||
|
|
|
@ -40,7 +40,8 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<h3>{{ active_tab }} rating: {{ rating | stars }}</h3>
|
{% include 'snippets/rate_action.html' with user=request.user book=book %}
|
||||||
|
<h3>{{ active_tab }} rating: {% include 'snippets/stars.html' with rating=rating %}</h3>
|
||||||
|
|
||||||
{% include 'snippets/book_description.html' %}
|
{% include 'snippets/book_description.html' %}
|
||||||
|
|
||||||
|
@ -59,6 +60,7 @@
|
||||||
<h3>Leave a review</h3>
|
<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 %}
|
||||||
|
{% include 'snippets/rate_form.html' with book=book %}
|
||||||
<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>
|
||||||
|
|
|
@ -2,11 +2,22 @@
|
||||||
{% load humanize %}
|
{% load humanize %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="content-container">
|
<div class="content-container">
|
||||||
<h2>Edit "{{ book.title }}"</h2>
|
<h2>
|
||||||
<div class="book-preview">
|
Edit "{{ book.title }}"
|
||||||
{% include 'snippets/book_cover.html' with book=book size="small" %}
|
<a href="/book/{{ book.fedireads_key }}">
|
||||||
<p>Added: {{ book.created_date | naturaltime }}</p>
|
<span class="edit-link icon icon-close">
|
||||||
<p>Updated: {{ book.updated_date | naturaltime }}</p>
|
<span class="hidden-text">Close</span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</h2>
|
||||||
|
<div class="book-preview row">
|
||||||
|
<div class="cover-container">
|
||||||
|
{% include 'snippets/book_cover.html' with book=book size="small" %}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p>Added: {{ book.created_date | naturaltime }}</p>
|
||||||
|
<p>Updated: {{ book.updated_date | naturaltime }}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -54,7 +65,6 @@
|
||||||
<p><label for="id_first_published_date">First published date:</label> {{ form.first_published_date }} </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>
|
<p><label for="id_published_date">Published date:</label> {{ form.published_date }} </p>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit">Update book</button>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<h2>
|
<h2>
|
||||||
{% include 'snippets/avatar.html' with user=user %}
|
{% include 'snippets/avatar.html' with user=user %}
|
||||||
Your thoughts on
|
Your thoughts on
|
||||||
<a href="/book/{{ book.fedireads_key }}">{{ book.title }}</a>
|
a <a href="/book/{{ book.fedireads_key }}">{{ book.title }}</a>
|
||||||
by {% include 'snippets/authors.html' with book=book %}
|
by {% include 'snippets/authors.html' with book=book %}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
|
@ -27,6 +27,7 @@
|
||||||
<form class="tab-option-{{ book.id }} review-form" name="review" action="/review/" method="post" id="tab-review-{{ book.id }}">
|
<form class="tab-option-{{ book.id }} review-form" name="review" action="/review/" method="post" id="tab-review-{{ book.id }}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="book" value="{{ book.fedireads_key }}"></input>
|
<input type="hidden" name="book" value="{{ book.fedireads_key }}"></input>
|
||||||
|
{% include 'snippets/rate_form.html' with book=book %}
|
||||||
{{ review_form.as_p }}
|
{{ review_form.as_p }}
|
||||||
<button type="submit">post review</button>
|
<button type="submit">post review</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
14
fedireads/templates/snippets/rate_action.html
Normal file
14
fedireads/templates/snippets/rate_action.html
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
{% load fr_display %}
|
||||||
|
<span class="hidden-text">Leave a rating</span>
|
||||||
|
<div class="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.fedireads_key }}"></input>
|
||||||
|
<input type="hidden" name="rating" value="{{ forloop.counter }}"></input>
|
||||||
|
<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>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
12
fedireads/templates/snippets/rate_form.html
Normal file
12
fedireads/templates/snippets/rate_form.html
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{% load fr_display %}
|
||||||
|
<span class="hidden-text">Rating</span>
|
||||||
|
<div class="stars rate-stars">
|
||||||
|
<input type="radio" name="rating" value="" checked></input>
|
||||||
|
{% for i in '12345'|make_list %}
|
||||||
|
<input id="book{{book.id}}-star-{{ forloop.counter }}" type="radio" name="rating" value="{{ forloop.counter }}"></input>
|
||||||
|
<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>
|
||||||
|
|
|
@ -49,7 +49,7 @@
|
||||||
</td>
|
</td>
|
||||||
{% if ratings %}
|
{% if ratings %}
|
||||||
<td>
|
<td>
|
||||||
{{ ratings | dict_key:book.id | stars}}
|
{% include 'snippets/stars.html' with rating=ratings|dict_key:book.id %}
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
|
|
8
fedireads/templates/snippets/stars.html
Normal file
8
fedireads/templates/snippets/stars.html
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<div class="stars">
|
||||||
|
<span class="hidden-text">{{ 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>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
|
@ -33,15 +33,19 @@
|
||||||
<div>
|
<div>
|
||||||
{% if status.status_type == 'Review' %}
|
{% if status.status_type == 'Review' %}
|
||||||
<h3>
|
<h3>
|
||||||
{{ status.name }}<br>
|
{% if status.name %}{{ status.name }}<br>{% endif %}
|
||||||
{{ status.rating | stars }}
|
{% include 'snippets/stars.html' with rating=status.rating %}
|
||||||
</h3>
|
</h3>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if status.status_type != 'Update' and status.status_type != 'Boost' %}
|
{% if status.content and status.status_type != 'Update' and status.status_type != 'Boost' %}
|
||||||
<blockquote>{{ status.content | safe }}</blockquote>
|
<blockquote>{{ status.content | safe }}</blockquote>
|
||||||
{% endif %}
|
{% 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 %}
|
||||||
|
|
||||||
{% if status.status_type == 'Boost' %}
|
{% if status.status_type == 'Boost' %}
|
||||||
{% include 'snippets/status_content.html' with status=status|boosted_status %}
|
{% include 'snippets/status_content.html' with status=status|boosted_status %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -5,10 +5,12 @@
|
||||||
|
|
||||||
{% if status.status_type == 'Update' %}
|
{% if status.status_type == 'Update' %}
|
||||||
{{ status.content | safe }}
|
{{ status.content | safe }}
|
||||||
|
{% elif status.status_type == 'Review' and not status.name and not status.content%}
|
||||||
|
rated <a href="{{ status.book.absolute_id }}">{{ status.book.title }}</a>
|
||||||
{% elif status.status_type == 'Review' %}
|
{% elif status.status_type == 'Review' %}
|
||||||
reviewed {{ status.book.title }}
|
reviewed <a href="{{ status.book.absolute_id }}">{{ status.book.title }}</a>
|
||||||
{% elif status.status_type == 'Comment' %}
|
{% elif status.status_type == 'Comment' %}
|
||||||
commented on {{ status.book.title }}
|
commented on <a href="{{ status.book.absolute_id }}">{{ status.book.title }}</a>
|
||||||
{% elif status.status_type == 'Boost' %}
|
{% elif status.status_type == 'Boost' %}
|
||||||
boosted
|
boosted
|
||||||
{% elif status.reply_parent %}
|
{% elif status.reply_parent %}
|
||||||
|
|
|
@ -12,16 +12,17 @@ def dict_key(d, k):
|
||||||
return d.get(k) or 0
|
return d.get(k) or 0
|
||||||
|
|
||||||
|
|
||||||
@register.filter(name='stars')
|
@register.filter(name='rating')
|
||||||
def stars(number):
|
def get_rating(book, user):
|
||||||
''' turn integers into stars '''
|
''' get a user's rating of a book '''
|
||||||
try:
|
rating = models.Review.objects.filter(
|
||||||
number = int(number)
|
user=user,
|
||||||
except (ValueError, TypeError):
|
book=book,
|
||||||
number = 0
|
rating__isnull=False,
|
||||||
if not number:
|
).order_by('-published_date').first()
|
||||||
return ''
|
if rating:
|
||||||
return ('★' * number) + '☆' * (5 - number)
|
return rating.rating
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
@register.filter(name='description')
|
@register.filter(name='description')
|
||||||
|
|
|
@ -78,6 +78,7 @@ urlpatterns = [
|
||||||
re_path(r'^edit_book/(?P<book_id>\d+)/?', actions.edit_book),
|
re_path(r'^edit_book/(?P<book_id>\d+)/?', actions.edit_book),
|
||||||
re_path(r'^upload_cover/(?P<book_id>\d+)/?', actions.upload_cover),
|
re_path(r'^upload_cover/(?P<book_id>\d+)/?', actions.upload_cover),
|
||||||
|
|
||||||
|
re_path(r'^rate/?$', actions.rate),
|
||||||
re_path(r'^review/?$', actions.review),
|
re_path(r'^review/?$', actions.review),
|
||||||
re_path(r'^comment/?$', actions.comment),
|
re_path(r'^comment/?$', actions.comment),
|
||||||
re_path(r'^tag/?$', actions.tag),
|
re_path(r'^tag/?$', actions.tag),
|
||||||
|
|
|
@ -186,6 +186,23 @@ def shelve(request):
|
||||||
return redirect('/')
|
return redirect('/')
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def rate(request):
|
||||||
|
''' just a star rating for a book '''
|
||||||
|
form = forms.RatingForm(request.POST)
|
||||||
|
book_identifier = request.POST.get('book')
|
||||||
|
# TODO: better failure behavior
|
||||||
|
if not form.is_valid():
|
||||||
|
return redirect('/book/%s' % book_identifier)
|
||||||
|
|
||||||
|
rating = form.cleaned_data.get('rating')
|
||||||
|
# throws a value error if the book is not found
|
||||||
|
book = get_or_create_book(book_identifier)
|
||||||
|
|
||||||
|
outgoing.handle_rate(request.user, book, rating)
|
||||||
|
return redirect('/book/%s' % book_identifier)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def review(request):
|
def review(request):
|
||||||
''' create a book review '''
|
''' create a book review '''
|
||||||
|
@ -196,9 +213,13 @@ def review(request):
|
||||||
return redirect('/book/%s' % book_identifier)
|
return redirect('/book/%s' % book_identifier)
|
||||||
|
|
||||||
# TODO: validation, htmlification
|
# TODO: validation, htmlification
|
||||||
name = form.data.get('name')
|
name = form.cleaned_data.get('name')
|
||||||
content = form.data.get('content')
|
content = form.cleaned_data.get('content')
|
||||||
rating = form.cleaned_data.get('rating')
|
rating = form.data.get('rating', None)
|
||||||
|
try:
|
||||||
|
rating = int(rating)
|
||||||
|
except ValueError:
|
||||||
|
rating = None
|
||||||
|
|
||||||
# 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(book_identifier)
|
book = get_or_create_book(book_identifier)
|
||||||
|
|
Loading…
Reference in a new issue