Merge pull request #129 from mouse-reeve/ratings

Ratings without reviews
This commit is contained in:
Mouse Reeve 2020-04-04 11:34:02 -07:00 committed by GitHub
commit 930ca63fee
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 228 additions and 31 deletions

View file

@ -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',
} }

View 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),
),
]

View file

@ -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,

View file

@ -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

View file

@ -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;

View file

@ -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;

View file

@ -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)

View file

@ -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>

View file

@ -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 %}

View file

@ -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>

View 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>

View 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>

View file

@ -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>

View 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>

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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')

View file

@ -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),

View file

@ -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)