Merge pull request #528 from mouse-reeve/reading-progress

Set annual reading goals
This commit is contained in:
Mouse Reeve 2021-01-16 14:37:31 -08:00 committed by GitHub
commit 8c4e652b2f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 586 additions and 80 deletions

View file

@ -188,3 +188,8 @@ class ShelfForm(CustomForm):
class Meta:
model = models.Shelf
fields = ['user', 'name', 'privacy']
class GoalForm(CustomForm):
class Meta:
model = models.AnnualGoal
fields = ['user', 'year', 'goal', 'privacy']

View file

@ -0,0 +1,32 @@
# Generated by Django 3.0.7 on 2021-01-16 18:43
import bookwyrm.models.fields
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('bookwyrm', '0035_edition_edition_rank'),
]
operations = [
migrations.CreateModel(
name='AnnualGoal',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_date', models.DateTimeField(auto_now_add=True)),
('updated_date', models.DateTimeField(auto_now=True)),
('remote_id', bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id])),
('goal', models.IntegerField()),
('year', models.IntegerField(default=2021)),
('privacy', models.CharField(choices=[('public', 'Public'), ('unlisted', 'Unlisted'), ('followers', 'Followers'), ('direct', 'Direct')], default='public', max_length=255)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
],
options={
'unique_together': {('user', 'year')},
},
),
]

View file

@ -17,7 +17,7 @@ from .readthrough import ReadThrough
from .tag import Tag, UserTag
from .user import User, KeyPair
from .user import User, KeyPair, AnnualGoal
from .relationship import UserFollows, UserFollowRequest, UserBlocks
from .federated_server import FederatedServer

View file

@ -6,6 +6,7 @@ from django.apps import apps
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.dispatch import receiver
from django.utils import timezone
from bookwyrm import activitypub
from bookwyrm.connectors import get_data
@ -18,7 +19,7 @@ from bookwyrm.utils import regex
from .base_model import OrderedCollectionPageMixin
from .base_model import ActivitypubMixin, BookWyrmModel
from .federated_server import FederatedServer
from . import fields
from . import fields, Review
class User(OrderedCollectionPageMixin, AbstractUser):
@ -221,6 +222,57 @@ class KeyPair(ActivitypubMixin, BookWyrmModel):
return activity_object
class AnnualGoal(BookWyrmModel):
''' set a goal for how many books you read in a year '''
user = models.ForeignKey('User', on_delete=models.PROTECT)
goal = models.IntegerField()
year = models.IntegerField(default=timezone.now().year)
privacy = models.CharField(
max_length=255,
default='public',
choices=fields.PrivacyLevels.choices
)
class Meta:
''' unqiueness constraint '''
unique_together = ('user', 'year')
def get_remote_id(self):
''' put the year in the path '''
return '%s/goal/%d' % (self.user.remote_id, self.year)
@property
def books(self):
''' the books you've read this year '''
return self.user.readthrough_set.filter(
finish_date__year__gte=self.year
).order_by('finish_date').all()
@property
def ratings(self):
''' ratings for books read this year '''
book_ids = [r.book.id for r in self.books]
reviews = Review.objects.filter(
user=self.user,
book__in=book_ids,
)
return {r.book.id: r.rating for r in reviews}
@property
def progress_percent(self):
return int(float(self.book_count / self.goal) * 100)
@property
def book_count(self):
''' how many books you've read this year '''
return self.user.readthrough_set.filter(
finish_date__year__gte=self.year).count()
@receiver(models.signals.post_save, sender=User)
#pylint: disable=unused-argument
def execute_after_save(sender, instance, created, *args, **kwargs):

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View file

@ -148,11 +148,11 @@ input.toggle-control:checked ~ .modal.toggle-content {
position: absolute;
}
.quote blockquote:before {
content: "\e905";
content: "\e906";
top: 0;
left: 0;
}
.quote blockquote:after {
content: "\e904";
content: "\e905";
right: 0;
}

View file

@ -1,10 +1,10 @@
@font-face {
font-family: 'icomoon';
src: url('fonts/icomoon.eot?rd4abb');
src: url('fonts/icomoon.eot?rd4abb#iefix') format('embedded-opentype'),
url('fonts/icomoon.ttf?rd4abb') format('truetype'),
url('fonts/icomoon.woff?rd4abb') format('woff'),
url('fonts/icomoon.svg?rd4abb#icomoon') format('svg');
src: url('fonts/icomoon.eot?uh765c');
src: url('fonts/icomoon.eot?uh765c#iefix') format('embedded-opentype'),
url('fonts/icomoon.ttf?uh765c') format('truetype'),
url('fonts/icomoon.woff?uh765c') format('woff'),
url('fonts/icomoon.svg?uh765c#icomoon') format('svg');
font-weight: normal;
font-style: normal;
font-display: block;
@ -25,81 +25,102 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-dots-three-vertical:before {
content: "\e918";
.icon-sparkle:before {
content: "\e91a";
}
.icon-check:before {
content: "\e917";
.icon-warning:before {
content: "\e91b";
}
.icon-dots-three:before {
content: "\e916";
}
.icon-envelope:before {
.icon-book:before {
content: "\e900";
}
.icon-arrow-right:before {
.icon-bookmark:before {
content: "\e91c";
}
.icon-envelope:before {
content: "\e901";
}
.icon-bell:before {
.icon-arrow-right:before {
content: "\e902";
}
.icon-x:before {
.icon-bell:before {
content: "\e903";
}
.icon-quote-close:before {
.icon-x:before {
content: "\e904";
}
.icon-quote-open:before {
.icon-quote-close:before {
content: "\e905";
}
.icon-image:before {
.icon-quote-open:before {
content: "\e906";
}
.icon-pencil:before {
.icon-image:before {
content: "\e907";
}
.icon-list:before {
.icon-pencil:before {
content: "\e908";
}
.icon-unlock:before {
.icon-list:before {
content: "\e909";
}
.icon-globe:before {
.icon-unlock:before {
content: "\e90a";
}
.icon-lock:before {
.icon-unlisted:before {
content: "\e90a";
}
.icon-globe:before {
content: "\e90b";
}
.icon-chain-broken:before {
.icon-public:before {
content: "\e90b";
}
.icon-lock:before {
content: "\e90c";
}
.icon-chain:before {
.icon-followers:before {
content: "\e90c";
}
.icon-chain-broken:before {
content: "\e90d";
}
.icon-comments:before {
.icon-chain:before {
content: "\e90e";
}
.icon-comment:before {
.icon-comments:before {
content: "\e90f";
}
.icon-boost:before {
.icon-comment:before {
content: "\e910";
}
.icon-arrow-left:before {
.icon-boost:before {
content: "\e911";
}
.icon-arrow-up:before {
.icon-arrow-left:before {
content: "\e912";
}
.icon-arrow-down:before {
.icon-arrow-up:before {
content: "\e913";
}
.icon-home:before {
.icon-arrow-down:before {
content: "\e914";
}
.icon-local:before {
.icon-home:before {
content: "\e915";
}
.icon-local:before {
content: "\e916";
}
.icon-dots-three:before {
content: "\e917";
}
.icon-check:before {
content: "\e918";
}
.icon-dots-three-vertical:before {
content: "\e919";
}
.icon-search:before {
content: "\e986";
}

View file

@ -21,11 +21,38 @@ window.onload = function() {
// handle aria settings on menus
Array.from(document.getElementsByClassName('pulldown-menu'))
.forEach(t => t.onclick = toggleMenu);
// display based on localstorage vars
document.querySelectorAll('[data-hide]')
.forEach(t => setDisplay(t));
// update localstorage
Array.from(document.getElementsByClassName('set-display'))
.forEach(t => t.onclick = updateDisplay);
};
function updateDisplay(e) {
var key = e.target.getAttribute('data-id');
var value = e.target.getAttribute('data-value');
window.localStorage.setItem(key, value);
document.querySelectorAll('[data-hide="' + key + '"]')
.forEach(t => setDisplay(t));
}
function setDisplay(el) {
var key = el.getAttribute('data-hide');
var value = window.localStorage.getItem(key)
if (!value) {
el.className = el.className.replace('hidden', '');
} else if (value != null && !!value) {
el.className += ' hidden';
}
}
function toggleAction(e) {
// set hover, if appropriate
var hover = e.target.getAttribute('data-hover-target')
var hover = e.target.getAttribute('data-hover-target');
if (hover) {
document.getElementById(hover).focus();
}

View file

@ -47,8 +47,8 @@
<div class="card">
<div class="card-header">
<p class="card-header-title">
<span>{% include 'snippets/book_titleby.html' with book=book %}</span>
</>
<span>{% include 'snippets/book_titleby.html' with book=book %}</span>
</p>
<div class="card-header-icon is-hidden-tablet">
{% include 'snippets/toggle/toggle_button.html' with label="close" controls_text="no-book" class="delete" %}
</div>
@ -67,6 +67,15 @@
<input class="toggle-control" type="radio" name="recent-books" id="no-book">
</div>
{% endif %}
{% if goal %}
<section class="section">
<div class="block">
<h3 class="title is-4">{{ goal.year }} Reading Goal</h3>
{% include 'snippets/goal_progress.html' with goal=goal %}
</div>
</section>
{% endif %}
</div>
<div class="column is-two-thirds" id="feed">
@ -85,6 +94,33 @@
</ul>
</div>
{# announcements and system messages #}
{% if not goal and tab == 'home' %}
{% now 'Y' as year %}
<section class="block hidden" aria-title="Announcements" data-hide="hide-{{ year }}-reading-goal">
<article class="card">
<header class="card-header">
<h3 class="card-header-title has-background-primary has-text-white">
<span class="icon icon-book is-size-3 mr-2" aria-hidden="true"></span> {{ year }} reading goal
</h3>
</header>
<section class="card-content content">
<p>Set a goal for how many books you'll finish reading in {{ year }}, and track your progress throughout the year.</p>
{% include 'snippets/goal_form.html' %}
</section>
<footer class="card-footer has-background-white-bis">
<div class="card-footer-item is-flex-direction-column">
<button class="button is-danger is-light is-block set-display" data-id="hide-{{ year }}-reading-goal" data-value="true">Dismiss message</button>
<p class="help">You can set or change your reading goal any time from your <a href="{{ request.user.local_path }}">profile page</a></p>
</div>
</footer>
</article>
<hr>
</section>
{% endif %}
{# activity feed #}
{% if not activities %}
<p>There aren't any activities right now! Try following a user to get started</p>
{% endif %}

View file

@ -0,0 +1,60 @@
{% extends 'layout.html' %}
{% block content %}
<section class="block">
<h1 class="title">{{ year }} Reading Progress</h1>
{% if user == request.user %}
<div class="block">
{% if goal %}
<input type="radio" class="toggle-control" name="edit-goal" id="hide-edit-goal" checked>
<div class="toggle-content hidden">
{% include 'snippets/toggle/toggle_button.html' with text="Edit goal" controls_text="show-edit-goal" %}
</div>
{% endif %}
</div>
<div class="block">
<input type="radio" class="toggle-control" name="edit-goal" id="show-edit-goal" data-hover-target="edit-form-header">
<div class="toggle-content{% if goal %} hidden{% endif %}">
{% now 'Y' as year %}
<section class="card">
<header class="card-header">
<h2 class="card-header-title has-background-primary has-text-white" tabindex="0" id="edit-form-header">
<span class="icon icon-book is-size-3 mr-2" aria-hidden="true"></span> {{ year }} reading goal
</h2>
</header>
<section class="card-content content">
<p>Set a goal for how many books you'll finish reading in {{ year }}, and track your progress throughout the year.</p>
{% include 'snippets/goal_form.html' with goal=goal year=year %}
</section>
</section>
</div>
</div>
{% endif %}
{% if not goal and user != request.user %}
<p>{{ user.display_name }} hasn't set a reading goal for {{ year }}.</p>
{% endif %}
{% if goal %}
{% include 'snippets/goal_progress.html' with goal=goal %}
{% endif %}
</section>
{% if goal.books %}
<section>
<h2 class="title">{% if goal.user == request.user %}Your{% else %}{{ goal.user.display_name }}'s{% endif %} {{ year }} Books</h2>
<div class="columns is-multiline">
{% for book in goal.books %}
<div class="column is-narrow">
<div class="box">
<a href="{{ book.book.local_path }}">
{% include 'snippets/discover/small-book.html' with book=book.book rating=goal.ratings %}
</a>
</div>
</div>
{% endfor %}
</div>
</section>
{% endif %}
{% endblock %}

View file

@ -1,7 +1,9 @@
{% load bookwyrm_tags %}
{% if book %}
{% include 'snippets/book_cover.html' with book=book %}
{% if ratings %}
{% include 'snippets/stars.html' with rating=ratings|dict_key:book.id %}
{% endif %}
<h3 class="title is-6"><a href="/book/{{ book.id }}">{{ book.title }}</a></h3>
{% if book.authors %}

View file

@ -29,8 +29,8 @@
<footer class="modal-card-foot">
<div class="columns">
<div class="column field">
<label for="post-status">
<input type="checkbox" name="post-status" class="checkbox" checked>
<label for="post_status-{{ uuid }}">
<input type="checkbox" name="post-status" class="checkbox" id="post_status-{{ uuid }}" checked>
Post to feed
</label>
{% include 'snippets/privacy_select.html' %}

View file

@ -0,0 +1,34 @@
<form method="post" name="goal" action="{{ request.user.local_path }}/goal/{{ year }}">
{% csrf_token %}
<input type="hidden" name="year" value="{% if goal %}{{ goal.year }}{% else %}{{ year }}{% endif %}">
<input type="hidden" name="user" value="{{ request.user.id }}">
<div class="columns">
<div class="column">
<label class="label" for="id_goal">Reading goal:</label>
<div class="field has-addons">
<div class="control">
<input type="number" class="input" name="goal" id="id_goal" value="{% if goal %}{{ goal.goal }}{% else %}12{% endif %}">
</div>
<p class="button is-static" aria-hidden="true">books</p>
</div>
</div>
<div class="column">
<label class="label"><p class="mb-2">Goal privacy:</p>
{% include 'snippets/privacy_select.html' with no_label=True current=goal.privacy %}
</label>
</div>
</div>
<label for="post_status" class="label">
<input type="checkbox" name="post-status" id="post_status" class="checkbox" checked>
Post to feed
</label>
<p>
<button type="submit" class="button is-link">Set goal</button>
{% if goal %}
{% include 'snippets/toggle/toggle_button.html' with text="Cancel" controls_text="hide-edit-goal" %}
{% endif %}
</p>
</form>

View file

@ -0,0 +1,10 @@
<p>
{% if goal.progress_percent >= 100 %}
Success!
{% elif goal.progress_percent %}
{{ goal.progress_percent }}% complete!
{% endif %}
{% if goal.user == request.user %}You've{% else %}{{ goal.user.display_name }} has{% endif %} read {% if request.path != goal.local_path %}<a href="{{ goal.local_path }}">{% endif %}{{ goal.book_count }} of {{ goal.goal }} books{% if request.path != goal.local_path %}</a>{% endif %}.
</p>
<progress class="progress is-large" value="{{ goal.book_count }}" max="{{ goal.goal }}" aria-hidden="true">{{ goal.progress_percent }}%</progress>

View file

@ -20,8 +20,8 @@
<footer class="modal-card-foot">
<div class="columns">
<div class="column field">
<label for="post-status">
<input type="checkbox" name="post-status" class="checkbox" checked>
<label for="post_status_start-{{ uuid }}">
<input type="checkbox" name="post-status" class="checkbox" id="post_status_start-{{ uuid }}" checked>
Post to feed
</label>
{% include 'snippets/privacy_select.html' %}

View file

@ -40,6 +40,17 @@
<small><a href="{{ user.local_path }}/shelves">See all {{ shelf_count }} shelves</a></small>
</div>
{% if goal %}
<div class="block">
<h2 class="title">{% now 'Y' %} Reading Goal</h2>
{% include 'snippets/goal_progress.html' with goal=goal %}
</div>
{% elif user == request.user %}
<div class="block">
<h2 class="title is-4"><a href="{{ user.local_path }}/goal/{% now 'Y' %}">Set a reading goal for {% now 'Y' %}</a></h2>
</div>
{% endif %}
<div>
<div class="block">
<h2 class="title">User Activity</h2>

View file

@ -0,0 +1,114 @@
''' test for app action functionality '''
from unittest.mock import patch
from django.contrib.auth.models import AnonymousUser
from django.template.response import TemplateResponse
from django.test import TestCase
from django.test.client import RequestFactory
from bookwyrm import models, views
class GoalViews(TestCase):
''' viewing and creating statuses '''
def setUp(self):
''' we need basic test data and mocks '''
self.factory = RequestFactory()
self.local_user = models.User.objects.create_user(
'mouse@local.com', 'mouse@mouse.com', 'mouseword',
local=True, localname='mouse',
remote_id='https://example.com/users/mouse',
)
self.rat = models.User.objects.create_user(
'rat@local.com', 'rat@rat.com', 'ratword',
local=True, localname='rat',
remote_id='https://example.com/users/rat',
)
self.book = models.Edition.objects.create(
title='Example Edition',
remote_id='https://example.com/book/1',
)
self.anonymous_user = AnonymousUser
self.anonymous_user.is_authenticated = False
def test_goal_page_no_goal(self):
''' view a reading goal page for another's unset goal '''
view = views.Goal.as_view()
request = self.factory.get('')
request.user = self.rat
result = view(request, self.local_user.localname, 2020)
self.assertEqual(result.status_code, 404)
def test_goal_page_no_goal_self(self):
''' view a reading goal page for your own unset goal '''
view = views.Goal.as_view()
request = self.factory.get('')
request.user = self.local_user
result = view(request, self.local_user.localname, 2020)
self.assertIsInstance(result, TemplateResponse)
def test_goal_page_anonymous(self):
''' can't view it without login '''
view = views.Goal.as_view()
request = self.factory.get('')
request.user = self.anonymous_user
result = view(request, self.local_user.localname, 2020)
self.assertEqual(result.status_code, 302)
def test_goal_page_public(self):
''' view a user's public goal '''
models.AnnualGoal.objects.create(
user=self.local_user,
year=2020,
goal=128937123,
privacy='public')
view = views.Goal.as_view()
request = self.factory.get('')
request.user = self.rat
result = view(request, self.local_user.localname, 2020)
self.assertIsInstance(result, TemplateResponse)
def test_goal_page_private(self):
''' view a user's private goal '''
models.AnnualGoal.objects.create(
user=self.local_user,
year=2020,
goal=15,
privacy='followers')
view = views.Goal.as_view()
request = self.factory.get('')
request.user = self.rat
result = view(request, self.local_user.localname, 2020)
self.assertEqual(result.status_code, 404)
def test_create_goal(self):
''' create a new goal '''
view = views.Goal.as_view()
request = self.factory.post('', {
'user': self.local_user.id,
'goal': 10,
'year': 2020,
'privacy': 'unlisted',
'post-status': True
})
request.user = self.local_user
with patch('bookwyrm.broadcast.broadcast_task.delay'):
view(request, self.local_user.localname, 2020)
goal = models.AnnualGoal.objects.get()
self.assertEqual(goal.user, self.local_user)
self.assertEqual(goal.goal, 10)
self.assertEqual(goal.year, 2020)
self.assertEqual(goal.privacy, 'unlisted')
status = models.GeneratedNote.objects.get()
self.assertEqual(status.user, self.local_user)
self.assertEqual(status.privacy, 'unlisted')

View file

@ -75,6 +75,9 @@ urlpatterns = [
re_path(r'%s/following(.json)?/?$' % user_path, views.Following.as_view()),
re_path(r'^edit-profile/?$', views.EditUser.as_view()),
# reading goals
re_path(r'%s/goal/(?P<year>\d{4})/?$' % user_path, views.Goal.as_view()),
# statuses
re_path(r'%s(.json)?/?$' % status_path, views.Status.as_view()),
re_path(r'%s/activity/?$' % status_path, views.Status.as_view()),

View file

@ -7,6 +7,7 @@ from .direct_message import DirectMessage
from .error import not_found_page, server_error_page
from .follow import follow, unfollow
from .follow import accept_follow_request, delete_follow_request, handle_accept
from .goal import Goal
from .import_data import Import, ImportStatus
from .interaction import Favorite, Unfavorite, Boost, Unboost
from .invite import ManageInvites, Invite

79
bookwyrm/views/goal.py Normal file
View file

@ -0,0 +1,79 @@
''' non-interactive pages '''
from django.contrib.auth.decorators import login_required
from django.http import HttpResponseNotFound
from django.shortcuts import redirect
from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator
from django.views import View
from bookwyrm import forms, models
from bookwyrm.broadcast import broadcast
from bookwyrm.status import create_generated_note
from .helpers import get_user_from_username, object_visible_to_user
# pylint: disable= no-self-use
@method_decorator(login_required, name='dispatch')
class Goal(View):
''' track books for the year '''
def get(self, request, username, year):
''' reading goal page '''
user = get_user_from_username(username)
year = int(year)
goal = models.AnnualGoal.objects.filter(
year=year, user=user
).first()
if not goal and user != request.user:
return HttpResponseNotFound()
if goal and not object_visible_to_user(request.user, goal):
return HttpResponseNotFound()
data = {
'title': '%s\'s %d Reading' % (user.display_name, year),
'goal_form': forms.GoalForm(instance=goal),
'goal': goal,
'user': user,
'year': year,
}
return TemplateResponse(request, 'goal.html', data)
def post(self, request, username, year):
''' update or create an annual goal '''
user = get_user_from_username(username)
if user != request.user:
return HttpResponseNotFound()
year = int(year)
goal = models.AnnualGoal.objects.filter(
year=year, user=request.user
).first()
form = forms.GoalForm(request.POST, instance=goal)
if not form.is_valid():
data = {
'title': '%s\'s %d Reading' % (goal.user.display_name, year),
'goal_form': form,
'goal': goal,
'year': year,
}
return TemplateResponse(request, 'goal.html', data)
goal = form.save()
if request.POST.get('post-status'):
# create status, if appropraite
status = create_generated_note(
request.user,
'set a goal to read %d books in %d' % (goal.goal, goal.year),
privacy=goal.privacy
)
broadcast(
request.user,
status.to_create_activity(request.user),
software='bookwyrm')
# re-format the activity for non-bookwyrm servers
remote_activity = status.to_create_activity(request.user, pure=True)
broadcast(request.user, remote_activity, software='other')
return redirect(request.headers.get('Referer', '/'))

View file

@ -34,16 +34,17 @@ def is_bookworm_request(request):
return True
def status_visible_to_user(viewer, status):
''' is a user authorized to view a status? '''
if viewer == status.user or status.privacy in ['public', 'unlisted']:
def object_visible_to_user(viewer, obj):
''' is a user authorized to view an object? '''
if viewer == obj.user or obj.privacy in ['public', 'unlisted']:
return True
if status.privacy == 'followers' and \
status.user.followers.filter(id=viewer.id).first():
return True
if status.privacy == 'direct' and \
status.mention_users.filter(id=viewer.id).first():
if obj.privacy == 'followers' and \
obj.user.followers.filter(id=viewer.id).first():
return True
if isinstance(obj, models.Status):
if obj.privacy == 'direct' and \
obj.mention_users.filter(id=viewer.id).first():
return True
return False
def get_activity_feed(

View file

@ -3,6 +3,7 @@ from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator
from django.db.models import Avg, Max
from django.template.response import TemplateResponse
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.views import View
@ -86,12 +87,18 @@ class Feed(View):
activities = get_activity_feed(
request.user, ['public', 'followers'])
paginated = Paginator(activities, PAGE_LENGTH)
goal = models.AnnualGoal.objects.filter(
user=request.user, year=timezone.now().year
).first()
data = {
'title': 'Updates Feed',
'user': request.user,
'suggested_books': suggested_books,
'activities': paginated.page(page),
'tab': tab,
'goal': goal,
'goal_form': forms.GoalForm(),
}
return TemplateResponse(request, 'feed.html', data)

View file

@ -16,7 +16,7 @@ from bookwyrm.settings import DOMAIN
from bookwyrm.status import create_notification, delete_status
from bookwyrm.utils import regex
from .helpers import get_user_from_username, handle_remote_webfinger
from .helpers import is_api_request, is_bookworm_request, status_visible_to_user
from .helpers import is_api_request, is_bookworm_request, object_visible_to_user
# pylint: disable= no-self-use
@ -35,7 +35,7 @@ class Status(View):
return HttpResponseNotFound()
# make sure the user is authorized to see the status
if not status_visible_to_user(request.user, status):
if not object_visible_to_user(request.user, status):
return HttpResponseNotFound()
if is_api_request(request):

View file

@ -9,6 +9,7 @@ from django.core.paginator import Paginator
from django.http import HttpResponseNotFound
from django.shortcuts import redirect
from django.template.response import TemplateResponse
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.views import View
@ -17,6 +18,7 @@ from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.broadcast import broadcast
from bookwyrm.settings import PAGE_LENGTH
from .helpers import get_activity_feed, get_user_from_username, is_api_request
from .helpers import object_visible_to_user
# pylint: disable= no-self-use
@ -70,6 +72,10 @@ class User(View):
queryset=models.Status.objects.filter(user=user)
)
paginated = Paginator(activities, PAGE_LENGTH)
goal = models.AnnualGoal.objects.filter(
user=user, year=timezone.now().year).first()
if not object_visible_to_user(request.user, goal):
goal = None
data = {
'title': user.name,
'user': user,
@ -77,6 +83,7 @@ class User(View):
'shelves': shelf_preview,
'shelf_count': shelves.count(),
'activities': paginated.page(page),
'goal': goal,
}
return TemplateResponse(request, 'user.html', data)