Merge pull request #567 from mouse-reeve/organize-templates

Organize templates
This commit is contained in:
Mouse Reeve 2021-01-29 11:14:47 -08:00 committed by GitHub
commit 94974d9f73
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 430 additions and 373 deletions

View File

@ -35,8 +35,17 @@ window.onload = function() {
// polling // polling
document.querySelectorAll('[data-poll]') document.querySelectorAll('[data-poll]')
.forEach(el => polling(el)); .forEach(el => polling(el));
// browser back behavior
document.querySelectorAll('[data-back]')
.forEach(t => t.onclick = back);
}; };
function back(e) {
e.preventDefault();
history.back();
}
function polling(el) { function polling(el) {
let delay = 10000 + (Math.random() * 1000); let delay = 10000 + (Math.random() * 1000);
setTimeout(function() { setTimeout(function() {

View File

@ -1,5 +1,5 @@
{% extends 'layout.html' %} {% extends 'feed/feed_layout.html' %}
{% block content %} {% block panel %}
<div class="block"> <div class="block">
<h1 class="title">Direct Messages</h1> <h1 class="title">Direct Messages</h1>

View File

@ -0,0 +1,39 @@
{% extends 'feed/feed_layout.html' %}
{% load bookwyrm_tags %}
{% block panel %}
<h1 class="title">{{ tab | title }} Timeline</h1>
<div class="tabs">
<ul>
<li class="{% if tab == 'home' %}is-active{% endif %}">
<a href="/#feed">Home</a>
</li>
<li class="{% if tab == 'local' %}is-active{% endif %}">
<a href="/local#feed">Local</a>
</li>
<li class="{% if tab == 'federated' %}is-active{% endif %}">
<a href="/federated#feed">Federated</a>
</li>
</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">
{% include 'snippets/goal_card.html' with year=year %}
<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 %}
{% for activity in activities %}
<div class="block">
{% include 'snippets/status.html' with status=activity %}
</div>
{% endfor %}
{% endblock %}

View File

@ -3,6 +3,7 @@
{% block content %} {% block content %}
<div class="columns"> <div class="columns">
{% if user.is_authenticated %}
<div class="column is-one-third"> <div class="column is-one-third">
<h2 class="title is-5">Your books</h2> <h2 class="title is-5">Your books</h2>
{% if not suggested_books %} {% if not suggested_books %}
@ -69,43 +70,15 @@
</section> </section>
{% endif %} {% endif %}
</div> </div>
{% endif %}
<div class="column is-two-thirds" id="feed"> <div class="column is-two-thirds" id="feed">
<h1 class="title">{{ tab | title }} Timeline</h1> {% block panel %}{% endblock %}
<div class="tabs">
<ul>
<li class="{% if tab == 'home' %}is-active{% endif %}">
<a href="/#feed">Home</a>
</li>
<li class="{% if tab == 'local' %}is-active{% endif %}">
<a href="/local#feed">Local</a>
</li>
<li class="{% if tab == 'federated' %}is-active{% endif %}">
<a href="/federated#feed">Federated</a>
</li>
</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">
{% include 'snippets/goal_card.html' with year=year %}
<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 %}
{% for activity in activities %}
<div class="block">
{% include 'snippets/status.html' with status=activity %}
</div>
{% endfor %}
{% if activities %}
{% include 'snippets/pagination.html' with page=activities path='/'|add:tab anchor="#feed" %} {% include 'snippets/pagination.html' with page=activities path='/'|add:tab anchor="#feed" %}
{% endif %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,13 @@
{% extends 'feed/feed_layout.html' %}
{% block panel %}
<header class="block">
<a href="/#feed" class="button" data-back>
<span class="icon icon-arrow-left" aira-hidden="true"></span>
<span>Back</span>
</a>
</header>
{% include 'feed/thread.html' with status=status depth=0 max_depth=6 is_root=True direction=0 %}
{% endblock %}

View File

@ -81,7 +81,7 @@
</a> </a>
</li> </li>
<li> <li>
<a href="/edit-profile" class="navbar-item"> <a href="/preferences/profile" class="navbar-item">
Settings Settings
</a> </a>
</li> </li>

View File

@ -1,4 +1,4 @@
{% extends 'preferences_layout.html' %} {% extends 'preferences/preferences_layout.html' %}
{% block header %} {% block header %}
Blocked Users Blocked Users

View File

@ -1,4 +1,4 @@
{% extends 'preferences_layout.html' %} {% extends 'preferences/preferences_layout.html' %}
{% block header %} {% block header %}
Change Password Change Password
{% endblock %} {% endblock %}

View File

@ -1,4 +1,4 @@
{% extends 'preferences_layout.html' %} {% extends 'preferences/preferences_layout.html' %}
{% block header %} {% block header %}
Edit Profile Edit Profile
{% endblock %} {% endblock %}

View File

@ -10,16 +10,16 @@
<h2 class="menu-label">Account</h2> <h2 class="menu-label">Account</h2>
<ul class="menu-list"> <ul class="menu-list">
<li> <li>
<a href="/edit-profile"{% if '/edit-profile' in request.path %} class="is-active" aria-selected="true"{% endif %}>Profile</a> <a href="/preferences/profile"{% if '/preferences/profile' in request.path %} class="is-active" aria-selected="true"{% endif %}>Profile</a>
</li> </li>
<li> <li>
<a href="/change-password"{% if '/change-password' in request.path %} class="is-active" aria-selected="true"{% endif %}>Change password</a> <a href="/preferences/password"{% if '/preferences/password' in request.path %} class="is-active" aria-selected="true"{% endif %}>Change password</a>
</li> </li>
</ul> </ul>
<h2 class="menu-label">Relationships</h2> <h2 class="menu-label">Relationships</h2>
<ul class="menu-list"> <ul class="menu-list">
<li> <li>
<a href="/block"{% if '/block' in request.path %} class="is-active" aria-selected="true"{% endif %}>Blocked users</a> <a href="/preferences/block"{% if '/preferences/block' in request.path %} class="is-active" aria-selected="true"{% endif %}>Blocked users</a>
</li> </li>
</ul> </ul>
</nav> </nav>

View File

@ -1,4 +1,4 @@
{% extends 'snippets/components/modal.html' %} {% extends 'components/modal.html' %}
{% block modal-title %}Delete these read dates?{% endblock %} {% block modal-title %}Delete these read dates?{% endblock %}
{% block modal-body %} {% block modal-body %}
{% if readthrough.progress_updates|length > 0 %} {% if readthrough.progress_updates|length > 0 %}

View File

@ -1,4 +1,4 @@
{% extends 'snippets/components/modal.html' %} {% extends 'components/modal.html' %}
{% block modal-title %} {% block modal-title %}
Finish "<em>{{ book.title }}</em>" Finish "<em>{{ book.title }}</em>"

View File

@ -1,4 +1,4 @@
{% extends 'snippets/components/card.html' %} {% extends 'components/card.html' %}
{% block card-header %} {% block card-header %}
<h3 class="card-header-title has-background-primary has-text-white"> <h3 class="card-header-title has-background-primary has-text-white">

View File

@ -1,4 +1,4 @@
{% extends 'snippets/components/dropdown.html' %} {% extends 'components/dropdown.html' %}
{% block dropdown-trigger %} {% block dropdown-trigger %}
<span>Change shelf</span> <span>Change shelf</span>
<span class="icon icon-arrow-down" aria-hidden="true"></span> <span class="icon icon-arrow-down" aria-hidden="true"></span>

View File

@ -1,4 +1,4 @@
{% extends 'snippets/components/dropdown.html' %} {% extends 'components/dropdown.html' %}
{% block dropdown-trigger %} {% block dropdown-trigger %}
<span class="icon icon-arrow-down"> <span class="icon icon-arrow-down">
<span class="is-sr-only">More shelves</span> <span class="is-sr-only">More shelves</span>

View File

@ -1,4 +1,4 @@
{% extends 'snippets/components/modal.html' %} {% extends 'components/modal.html' %}
{% block modal-title %} {% block modal-title %}
Start "<em>{{ book.title }}</em>" Start "<em>{{ book.title }}</em>"

View File

@ -1,4 +1,4 @@
{% extends 'snippets/components/card.html' %} {% extends 'components/card.html' %}
{% load bookwyrm_tags %} {% load bookwyrm_tags %}
{% load humanize %} {% load humanize %}

View File

@ -1,4 +1,4 @@
{% extends 'snippets/components/dropdown.html' %} {% extends 'components/dropdown.html' %}
{% load bookwyrm_tags %} {% load bookwyrm_tags %}
{% block dropdown-trigger %} {% block dropdown-trigger %}

View File

@ -1,4 +1,4 @@
{% extends 'snippets/components/dropdown.html' %} {% extends 'components/dropdown.html' %}
{% load bookwyrm_tags %} {% load bookwyrm_tags %}
{% block dropdown-trigger %} {% block dropdown-trigger %}

View File

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

View File

@ -1,18 +1,17 @@
{% extends 'layout.html' %} {% extends 'user/user_layout.html' %}
{% load bookwyrm_tags %} {% load bookwyrm_tags %}
{% block content %}
<div class="block"> {% block header %}
<h1 class="title"> <h1 class="title">
{% if is_self %}Your {% if is_self %}Your
{% else %} {% else %}
{% include 'snippets/username.html' with user=user possessive=True %} {% include 'snippets/username.html' with user=user possessive=True %}
{% endif %} {% endif %}
followers followers
</h1> </h1>
</div> {% endblock %}
{% include 'snippets/user_header.html' with user=user %}
{% block panel %}
<div class="block"> <div class="block">
<h2 class="title">Followers</h2> <h2 class="title">Followers</h2>
{% for followers in followers %} {% for followers in followers %}
@ -34,5 +33,4 @@
<div>{{ user|username }} has no followers</div> <div>{{ user|username }} has no followers</div>
{% endif %} {% endif %}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,18 +1,17 @@
{% extends 'layout.html' %} {% extends 'user/user_layout.html' %}
{% load bookwyrm_tags %} {% load bookwyrm_tags %}
{% block content %}
<div class="block"> {% block header %}
<h1 class="title"> <h1 class="title">
Users following Users following
{% if is_self %}you {% if is_self %}you
{% else %} {% else %}
{% include 'snippets/username.html' with user=user %} {% include 'snippets/username.html' with user=user %}
{% endif %} {% endif %}
</h1> </h1>
</div> {% endblock %}
{% include 'snippets/user_header.html' with user=user %}
{% block panel %}
<div class="block"> <div class="block">
<h2 class="title">Following</h2> <h2 class="title">Following</h2>
{% for follower in user.following.all %} {% for follower in user.following.all %}
@ -34,5 +33,4 @@
<div>{{ user|username }} isn't following any users</div> <div>{{ user|username }} isn't following any users</div>
{% endif %} {% endif %}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,13 +1,13 @@
{% extends 'layout.html' %} {% extends 'user/user_layout.html' %}
{% block content %}
{% block header %}
<div class="columns"> <div class="columns">
<div class="column"> <div class="column">
<h1 class="title">User profile</h1> <h1 class="title">User profile</h1>
</div> </div>
{% if is_self %} {% if is_self %}
<div class="column is-narrow"> <div class="column is-narrow">
<a href="/edit-profile"> <a href="/preferences/profile">
<span class="icon icon-pencil" title="Edit profile"> <span class="icon icon-pencil" title="Edit profile">
<span class="is-sr-only">Edit profile</span> <span class="is-sr-only">Edit profile</span>
</span> </span>
@ -15,8 +15,9 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
{% endblock %}
{% include 'snippets/user_header.html' with user=user %} {% block panel %}
{% if user.bookwyrm_user %} {% if user.bookwyrm_user %}
<div class="block"> <div class="block">
<h2 class="title">Shelves</h2> <h2 class="title">Shelves</h2>

View File

@ -1,5 +1,13 @@
{% extends 'layout.html' %}
{% load humanize %} {% load humanize %}
{% load bookwyrm_tags %} {% load bookwyrm_tags %}
{% block content %}
<header class="block">
{% block header %}{% endblock %}
</header>
{# user bio #}
<div class="block"> <div class="block">
<div class="columns"> <div class="columns">
<div class="column is-narrow"> <div class="column is-narrow">
@ -60,3 +68,6 @@
{% endif %} {% endif %}
</div> </div>
{% block panel %}{% endblock %}
{% endblock %}

View File

@ -32,7 +32,7 @@ class BlockViews(TestCase):
request.user = self.local_user request.user = self.local_user
result = view(request) result = view(request)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'blocks.html') self.assertEqual(result.template_name, 'preferences/blocks.html')
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_block_post(self): def test_block_post(self):

View File

@ -1,28 +0,0 @@
''' test for app action functionality '''
from django.template.response import TemplateResponse
from django.test import TestCase
from django.test.client import RequestFactory
from bookwyrm import models
from bookwyrm import views
class DirectMessageViews(TestCase):
''' dms '''
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.mouse', 'password',
local=True, localname='mouse')
def test_direct_messages_page(self):
''' there are so many views, this just makes sure it LOADS '''
view = views.DirectMessage.as_view()
request = self.factory.get('')
request.user = self.local_user
result = view(request)
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'direct_messages.html')
self.assertEqual(result.status_code, 200)

View File

@ -0,0 +1,99 @@
''' test for app action functionality '''
from unittest.mock import patch
from django.template.response import TemplateResponse
from django.test import TestCase
from django.test.client import RequestFactory
from bookwyrm import models
from bookwyrm import views
from bookwyrm.activitypub import ActivitypubResponse
class FeedMessageViews(TestCase):
''' dms '''
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.mouse', 'password',
local=True, localname='mouse')
self.book = models.Edition.objects.create(
title='Example Edition',
remote_id='https://example.com/book/1',
)
def test_feed(self):
''' there are so many views, this just makes sure it LOADS '''
view = views.Feed.as_view()
request = self.factory.get('')
request.user = self.local_user
result = view(request, 'local')
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'feed/feed.html')
self.assertEqual(result.status_code, 200)
def test_status_page(self):
''' there are so many views, this just makes sure it LOADS '''
view = views.Status.as_view()
status = models.Status.objects.create(
content='hi', user=self.local_user)
request = self.factory.get('')
request.user = self.local_user
with patch('bookwyrm.views.feed.is_api_request') as is_api:
is_api.return_value = False
result = view(request, 'mouse', status.id)
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'feed/status.html')
self.assertEqual(result.status_code, 200)
with patch('bookwyrm.views.feed.is_api_request') as is_api:
is_api.return_value = True
result = view(request, 'mouse', status.id)
self.assertIsInstance(result, ActivitypubResponse)
self.assertEqual(result.status_code, 200)
def test_replies_page(self):
''' there are so many views, this just makes sure it LOADS '''
view = views.Replies.as_view()
status = models.Status.objects.create(
content='hi', user=self.local_user)
request = self.factory.get('')
request.user = self.local_user
with patch('bookwyrm.views.feed.is_api_request') as is_api:
is_api.return_value = False
result = view(request, 'mouse', status.id)
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'feed/status.html')
self.assertEqual(result.status_code, 200)
with patch('bookwyrm.views.feed.is_api_request') as is_api:
is_api.return_value = True
result = view(request, 'mouse', status.id)
self.assertIsInstance(result, ActivitypubResponse)
self.assertEqual(result.status_code, 200)
def test_direct_messages_page(self):
''' there are so many views, this just makes sure it LOADS '''
view = views.DirectMessage.as_view()
request = self.factory.get('')
request.user = self.local_user
result = view(request)
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'feed/direct_messages.html')
self.assertEqual(result.status_code, 200)
def test_get_suggested_book(self):
''' gets books the ~*~ algorithm ~*~ thinks you want to post about '''
models.ShelfBook.objects.create(
book=self.book,
added_by=self.local_user,
shelf=self.local_user.shelf_set.get(identifier='reading')
)
suggestions = views.feed.get_suggested_books(self.local_user)
self.assertEqual(suggestions[0]['name'], 'Currently Reading')
self.assertEqual(suggestions[0]['books'][0], self.book)

View File

@ -18,10 +18,6 @@ class LandingViews(TestCase):
local=True, localname='mouse') local=True, localname='mouse')
self.anonymous_user = AnonymousUser self.anonymous_user = AnonymousUser
self.anonymous_user.is_authenticated = False self.anonymous_user.is_authenticated = False
self.book = models.Edition.objects.create(
title='Example Edition',
remote_id='https://example.com/book/1',
)
def test_home_page(self): def test_home_page(self):
@ -31,7 +27,7 @@ class LandingViews(TestCase):
request.user = self.local_user request.user = self.local_user
result = view(request) result = view(request)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
self.assertEqual(result.template_name, 'feed.html') self.assertEqual(result.template_name, 'feed/feed.html')
request.user = self.anonymous_user request.user = self.anonymous_user
result = view(request) result = view(request)
@ -51,17 +47,6 @@ class LandingViews(TestCase):
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_feed(self):
''' there are so many views, this just makes sure it LOADS '''
view = views.Feed.as_view()
request = self.factory.get('')
request.user = self.local_user
result = view(request, 'local')
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'feed.html')
self.assertEqual(result.status_code, 200)
def test_discover(self): def test_discover(self):
''' there are so many views, this just makes sure it LOADS ''' ''' there are so many views, this just makes sure it LOADS '''
view = views.Discover.as_view() view = views.Discover.as_view()
@ -70,15 +55,3 @@ class LandingViews(TestCase):
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'discover.html') self.assertEqual(result.template_name, 'discover.html')
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_get_suggested_book(self):
''' gets books the ~*~ algorithm ~*~ thinks you want to post about '''
models.ShelfBook.objects.create(
book=self.book,
added_by=self.local_user,
shelf=self.local_user.shelf_set.get(identifier='reading')
)
suggestions = views.landing.get_suggested_books(self.local_user)
self.assertEqual(suggestions[0]['name'], 'Currently Reading')
self.assertEqual(suggestions[0]['books'][0], self.book)

View File

@ -106,7 +106,7 @@ class PasswordViews(TestCase):
result = view(request) result = view(request)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'change_password.html') self.assertEqual(result.template_name, 'preferences/change_password.html')
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)

View File

@ -36,48 +36,6 @@ class StatusViews(TestCase):
) )
def test_status_page(self):
''' there are so many views, this just makes sure it LOADS '''
view = views.Status.as_view()
status = models.Status.objects.create(
content='hi', user=self.local_user)
request = self.factory.get('')
request.user = self.local_user
with patch('bookwyrm.views.status.is_api_request') as is_api:
is_api.return_value = False
result = view(request, 'mouse', status.id)
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'status.html')
self.assertEqual(result.status_code, 200)
with patch('bookwyrm.views.status.is_api_request') as is_api:
is_api.return_value = True
result = view(request, 'mouse', status.id)
self.assertIsInstance(result, ActivitypubResponse)
self.assertEqual(result.status_code, 200)
def test_replies_page(self):
''' there are so many views, this just makes sure it LOADS '''
view = views.Replies.as_view()
status = models.Status.objects.create(
content='hi', user=self.local_user)
request = self.factory.get('')
request.user = self.local_user
with patch('bookwyrm.views.status.is_api_request') as is_api:
is_api.return_value = False
result = view(request, 'mouse', status.id)
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'status.html')
self.assertEqual(result.status_code, 200)
with patch('bookwyrm.views.status.is_api_request') as is_api:
is_api.return_value = True
result = view(request, 'mouse', status.id)
self.assertIsInstance(result, ActivitypubResponse)
self.assertEqual(result.status_code, 200)
def test_handle_status(self): def test_handle_status(self):
''' create a status ''' ''' create a status '''
view = views.CreateStatus.as_view() view = views.CreateStatus.as_view()

View File

@ -34,7 +34,7 @@ class UserViews(TestCase):
is_api.return_value = False is_api.return_value = False
result = view(request, 'mouse') result = view(request, 'mouse')
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'user.html') self.assertEqual(result.template_name, 'user/user.html')
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
with patch('bookwyrm.views.user.is_api_request') as is_api: with patch('bookwyrm.views.user.is_api_request') as is_api:
@ -65,7 +65,7 @@ class UserViews(TestCase):
is_api.return_value = False is_api.return_value = False
result = view(request, 'mouse') result = view(request, 'mouse')
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'followers.html') self.assertEqual(result.template_name, 'user/followers.html')
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
with patch('bookwyrm.views.user.is_api_request') as is_api: with patch('bookwyrm.views.user.is_api_request') as is_api:
@ -96,7 +96,7 @@ class UserViews(TestCase):
is_api.return_value = False is_api.return_value = False
result = view(request, 'mouse') result = view(request, 'mouse')
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'following.html') self.assertEqual(result.template_name, 'user/following.html')
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
with patch('bookwyrm.views.user.is_api_request') as is_api: with patch('bookwyrm.views.user.is_api_request') as is_api:
@ -125,7 +125,7 @@ class UserViews(TestCase):
request.user = self.local_user request.user = self.local_user
result = view(request) result = view(request)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'edit_user.html') self.assertEqual(result.template_name, 'preferences/edit_user.html')
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)

View File

@ -5,7 +5,6 @@ from django.urls import path, re_path
from bookwyrm import incoming, settings, views, wellknown from bookwyrm import incoming, settings, views, wellknown
from bookwyrm.views.rss_feed import RssFeed
from bookwyrm.utils import regex from bookwyrm.utils import regex
user_path = r'^user/(?P<username>%s)' % regex.username user_path = r'^user/(?P<username>%s)' % regex.username
@ -49,7 +48,6 @@ urlpatterns = [
re_path(r'^password-reset/?$', views.PasswordResetRequest.as_view()), re_path(r'^password-reset/?$', views.PasswordResetRequest.as_view()),
re_path(r'^password-reset/(?P<code>[A-Za-z0-9]+)/?$', re_path(r'^password-reset/(?P<code>[A-Za-z0-9]+)/?$',
views.PasswordReset.as_view()), views.PasswordReset.as_view()),
re_path(r'^change-password/?$', views.ChangePassword.as_view()),
# invites # invites
re_path(r'^invite/?$', views.ManageInvites.as_view()), re_path(r'^invite/?$', views.ManageInvites.as_view()),
@ -58,9 +56,11 @@ urlpatterns = [
# landing pages # landing pages
re_path(r'^about/?$', views.About.as_view()), re_path(r'^about/?$', views.About.as_view()),
path('', views.Home.as_view()), path('', views.Home.as_view()),
re_path(r'^(?P<tab>home|local|federated)/?$', views.Feed.as_view()),
re_path(r'^discover/?$', views.Discover.as_view()), re_path(r'^discover/?$', views.Discover.as_view()),
re_path(r'^notifications/?$', views.Notifications.as_view()), re_path(r'^notifications/?$', views.Notifications.as_view()),
# feeds
re_path(r'^(?P<tab>home|local|federated)/?$', views.Feed.as_view()),
re_path(r'^direct-messages/?$', views.DirectMessage.as_view()), re_path(r'^direct-messages/?$', views.DirectMessage.as_view()),
# search # search
@ -76,9 +76,15 @@ urlpatterns = [
re_path(r'%s/shelves/?$' % user_path, views.user_shelves_page), re_path(r'%s/shelves/?$' % user_path, views.user_shelves_page),
re_path(r'%s/followers(.json)?/?$' % user_path, views.Followers.as_view()), re_path(r'%s/followers(.json)?/?$' % user_path, views.Followers.as_view()),
re_path(r'%s/following(.json)?/?$' % user_path, views.Following.as_view()), re_path(r'%s/following(.json)?/?$' % user_path, views.Following.as_view()),
re_path(r'^edit-profile/?$', views.EditUser.as_view()),
re_path(r'%s/rss' % user_path, views.rss_feed.RssFeed()), re_path(r'%s/rss' % user_path, views.rss_feed.RssFeed()),
# preferences
re_path(r'^preferences/profile/?$', views.EditUser.as_view()),
re_path(r'^preferences/password/?$', views.ChangePassword.as_view()),
re_path(r'^preferences/block/?$', views.Block.as_view()),
re_path(r'^block/(?P<user_id>\d+)/?$', views.Block.as_view()),
re_path(r'^unblock/(?P<user_id>\d+)/?$', views.unblock),
# reading goals # reading goals
re_path(r'%s/goal/(?P<year>\d{4})/?$' % user_path, views.Goal.as_view()), re_path(r'%s/goal/(?P<year>\d{4})/?$' % user_path, views.Goal.as_view()),
@ -140,7 +146,4 @@ urlpatterns = [
re_path(r'^accept-follow-request/?$', views.accept_follow_request), re_path(r'^accept-follow-request/?$', views.accept_follow_request),
re_path(r'^delete-follow-request/?$', views.delete_follow_request), re_path(r'^delete-follow-request/?$', views.delete_follow_request),
re_path(r'^block/?$', views.Block.as_view()),
re_path(r'^block/(?P<user_id>\d+)/?$', views.Block.as_view()),
re_path(r'^unblock/(?P<user_id>\d+)/?$', views.unblock),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@ -4,25 +4,26 @@ from .author import Author, EditAuthor
from .block import Block, unblock from .block import Block, unblock
from .books import Book, EditBook, Editions from .books import Book, EditBook, Editions
from .books import upload_cover, add_description, switch_edition, resolve_book from .books import upload_cover, add_description, switch_edition, resolve_book
from .direct_message import DirectMessage
from .error import not_found_page, server_error_page from .error import not_found_page, server_error_page
from .feed import DirectMessage, Feed, Replies, Status
from .follow import follow, unfollow from .follow import follow, unfollow
from .follow import accept_follow_request, delete_follow_request, handle_accept from .follow import accept_follow_request, delete_follow_request, handle_accept
from .goal import Goal from .goal import Goal
from .import_data import Import, ImportStatus from .import_data import Import, ImportStatus
from .interaction import Favorite, Unfavorite, Boost, Unboost from .interaction import Favorite, Unfavorite, Boost, Unboost
from .invite import ManageInvites, Invite from .invite import ManageInvites, Invite
from .landing import About, Home, Feed, Discover from .landing import About, Home, Discover
from .notifications import Notifications from .notifications import Notifications
from .outbox import Outbox from .outbox import Outbox
from .reading import edit_readthrough, create_readthrough, delete_readthrough from .reading import edit_readthrough, create_readthrough, delete_readthrough
from .reading import start_reading, finish_reading, delete_progressupdate from .reading import start_reading, finish_reading, delete_progressupdate
from .rss_feed import RssFeed
from .password import PasswordResetRequest, PasswordReset, ChangePassword from .password import PasswordResetRequest, PasswordReset, ChangePassword
from .tag import Tag, AddTag, RemoveTag from .tag import Tag, AddTag, RemoveTag
from .search import Search from .search import Search
from .shelf import Shelf from .shelf import Shelf
from .shelf import user_shelves_page, create_shelf, delete_shelf from .shelf import user_shelves_page, create_shelf, delete_shelf
from .shelf import shelve, unshelve from .shelf import shelve, unshelve
from .status import Status, Replies, CreateStatus, DeleteStatus from .status import CreateStatus, DeleteStatus
from .updates import Updates from .updates import Updates
from .user import User, EditUser, Followers, Following from .user import User, EditUser, Followers, Following

View File

@ -17,7 +17,7 @@ class Block(View):
def get(self, request): def get(self, request):
''' list of blocked users? ''' ''' list of blocked users? '''
return TemplateResponse( return TemplateResponse(
request, 'blocks.html', {'title': 'Blocked Users'}) request, 'preferences/blocks.html', {'title': 'Blocked Users'})
def post(self, request, user_id): def post(self, request, user_id):
''' block a user ''' ''' block a user '''
@ -31,7 +31,7 @@ class Block(View):
privacy='direct', privacy='direct',
direct_recipients=[to_block] direct_recipients=[to_block]
) )
return redirect('/block') return redirect('/preferences/block')
@require_POST @require_POST
@ -55,4 +55,4 @@ def unblock(request, user_id):
direct_recipients=[to_unblock] direct_recipients=[to_unblock]
) )
block.delete() block.delete()
return redirect('/block') return redirect('/preferences/block')

View File

@ -1,26 +0,0 @@
''' non-interactive pages '''
from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator
from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator
from django.views import View
from bookwyrm.settings import PAGE_LENGTH
from .helpers import get_activity_feed
# pylint: disable= no-self-use
@method_decorator(login_required, name='dispatch')
class DirectMessage(View):
''' dm view '''
def get(self, request, page=1):
''' like a feed but for dms only '''
activities = get_activity_feed(request.user, 'direct')
paginated = Paginator(activities, PAGE_LENGTH)
activity_page = paginated.page(page)
data = {
'title': 'Direct Messages',
'user': request.user,
'activities': activity_page,
}
return TemplateResponse(request, 'direct_messages.html', data)

156
bookwyrm/views/feed.py Normal file
View File

@ -0,0 +1,156 @@
''' non-interactive pages '''
from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator
from django.http import HttpResponseNotFound
from django.template.response import TemplateResponse
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.views import View
from bookwyrm import forms, models
from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.settings import PAGE_LENGTH
from .helpers import get_activity_feed
from .helpers import get_user_from_username
from .helpers import is_api_request, is_bookworm_request, object_visible_to_user
# pylint: disable= no-self-use
@method_decorator(login_required, name='dispatch')
class Feed(View):
''' activity stream '''
def get(self, request, tab):
''' user's homepage with activity feed '''
try:
page = int(request.GET.get('page', 1))
except ValueError:
page = 1
if tab == 'home':
activities = get_activity_feed(
request.user, ['public', 'unlisted', 'followers'],
following_only=True)
elif tab == 'local':
activities = get_activity_feed(
request.user, ['public', 'followers'], local_only=True)
else:
activities = get_activity_feed(
request.user, ['public', 'followers'])
paginated = Paginator(activities, PAGE_LENGTH)
data = {**feed_page_data(request.user), **{
'title': 'Updates Feed',
'user': request.user,
'activities': paginated.page(page),
'tab': tab,
'goal_form': forms.GoalForm(),
}}
return TemplateResponse(request, 'feed/feed.html', data)
@method_decorator(login_required, name='dispatch')
class DirectMessage(View):
''' dm view '''
def get(self, request):
''' like a feed but for dms only '''
try:
page = int(request.GET.get('page', 1))
except ValueError:
page = 1
activities = get_activity_feed(request.user, 'direct')
paginated = Paginator(activities, PAGE_LENGTH)
activity_page = paginated.page(page)
data = {**feed_page_data(request.user), **{
'title': 'Direct Messages',
'user': request.user,
'activities': activity_page,
}}
return TemplateResponse(request, 'feed/direct_messages.html', data)
class Status(View):
''' get posting '''
def get(self, request, username, status_id):
''' display a particular status (and replies, etc) '''
try:
user = get_user_from_username(username)
status = models.Status.objects.select_subclasses().get(
id=status_id, deleted=False)
except ValueError:
return HttpResponseNotFound()
# the url should have the poster's username in it
if user != status.user:
return HttpResponseNotFound()
# make sure the user is authorized to see the status
if not object_visible_to_user(request.user, status):
return HttpResponseNotFound()
if is_api_request(request):
return ActivitypubResponse(
status.to_activity(pure=not is_bookworm_request(request)))
data = {**feed_page_data(request.user), **{
'title': 'Status by %s' % user.username,
'status': status,
}}
return TemplateResponse(request, 'feed/status.html', data)
class Replies(View):
''' replies page (a json view of status) '''
def get(self, request, username, status_id):
''' ordered collection of replies to a status '''
# the html view is the same as Status
if not is_api_request(request):
status_view = Status.as_view()
return status_view(request, username, status_id)
# the json view is different than Status
status = models.Status.objects.get(id=status_id)
if status.user.localname != username:
return HttpResponseNotFound()
return ActivitypubResponse(status.to_replies(**request.GET))
def feed_page_data(user):
''' info we need for every feed page '''
if not user.is_authenticated:
return {}
goal = models.AnnualGoal.objects.filter(
user=user, year=timezone.now().year
).first()
return {
'suggested_books': get_suggested_books(user),
'goal': goal,
'goal_form': forms.GoalForm(),
}
def get_suggested_books(user, max_books=5):
''' helper to get a user's recent books '''
book_count = 0
preset_shelves = [
('reading', max_books), ('read', 2), ('to-read', max_books)
]
suggested_books = []
for (preset, shelf_max) in preset_shelves:
limit = shelf_max if shelf_max < (max_books - book_count) \
else max_books - book_count
shelf = user.shelf_set.get(identifier=preset)
shelf_books = shelf.shelfbook_set.order_by(
'-updated_date'
).all()[:limit]
if not shelf_books:
continue
shelf_preview = {
'name': shelf.name,
'books': [s.book for s in shelf_books]
}
suggested_books.append(shelf_preview)
book_count += len(shelf_preview['books'])
return suggested_books

View File

@ -1,14 +1,10 @@
''' non-interactive pages ''' ''' non-interactive pages '''
from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator
from django.db.models import Avg, Max from django.db.models import Avg, Max
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.views import View from django.views import View
from bookwyrm import forms, models from bookwyrm import forms, models
from bookwyrm.settings import PAGE_LENGTH from .feed import Feed
from .helpers import get_activity_feed from .helpers import get_activity_feed
@ -61,68 +57,3 @@ class Discover(View):
'ratings': ratings 'ratings': ratings
} }
return TemplateResponse(request, 'discover.html', data) return TemplateResponse(request, 'discover.html', data)
@method_decorator(login_required, name='dispatch')
class Feed(View):
''' activity stream '''
def get(self, request, tab):
''' user's homepage with activity feed '''
try:
page = int(request.GET.get('page', 1))
except ValueError:
page = 1
suggested_books = get_suggested_books(request.user)
if tab == 'home':
activities = get_activity_feed(
request.user, ['public', 'unlisted', 'followers'],
following_only=True)
elif tab == 'local':
activities = get_activity_feed(
request.user, ['public', 'followers'], local_only=True)
else:
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)
def get_suggested_books(user, max_books=5):
''' helper to get a user's recent books '''
book_count = 0
preset_shelves = [
('reading', max_books), ('read', 2), ('to-read', max_books)
]
suggested_books = []
for (preset, shelf_max) in preset_shelves:
limit = shelf_max if shelf_max < (max_books - book_count) \
else max_books - book_count
shelf = user.shelf_set.get(identifier=preset)
shelf_books = shelf.shelfbook_set.order_by(
'-updated_date'
).all()[:limit]
if not shelf_books:
continue
shelf_preview = {
'name': shelf.name,
'books': [s.book for s in shelf_books]
}
suggested_books.append(shelf_preview)
book_count += len(shelf_preview['books'])
return suggested_books

View File

@ -94,7 +94,8 @@ class ChangePassword(View):
'title': 'Change Password', 'title': 'Change Password',
'user': request.user, 'user': request.user,
} }
return TemplateResponse(request, 'change_password.html', data) return TemplateResponse(
request, 'preferences/change_password.html', data)
def post(self, request): def post(self, request):
''' allow a user to change their password ''' ''' allow a user to change their password '''
@ -102,9 +103,9 @@ class ChangePassword(View):
confirm_password = request.POST.get('confirm-password') confirm_password = request.POST.get('confirm-password')
if new_password != confirm_password: if new_password != confirm_password:
return redirect('/edit-profile') return redirect('preferences/password')
request.user.set_password(new_password) request.user.set_password(new_password)
request.user.save() request.user.save()
login(request, request.user) login(request, request.user)
return redirect('/user/%s' % request.user.localname) return redirect(request.user.local_path)

View File

@ -1,29 +1,35 @@
''' ''' ''' serialize user's posts in rss feed '''
from django.contrib.syndication.views import Feed from django.contrib.syndication.views import Feed
from django.urls import reverse
from bookwyrm.models.user import User
from .helpers import get_activity_feed, get_user_from_username from .helpers import get_activity_feed, get_user_from_username
# pylint: disable=no-self-use, unused-argument
class RssFeed(Feed): class RssFeed(Feed):
''' serialize user's posts in rss feed '''
description_template = "snippets/rss_content.html" description_template = 'snippets/rss_content.html'
title_template = "snippets/rss_title.html" title_template = 'snippets/rss_title.html'
def get_object(self, request, username): def get_object(self, request, username):
''' the user who's posts get serialized '''
return get_user_from_username(username) return get_user_from_username(username)
def link(self, obj): def link(self, obj):
''' link to the user's profile '''
return obj.local_path return obj.local_path
def title(self, obj): def title(self, obj):
return f"Status updates from {obj.display_name}" ''' title of the rss feed entry '''
return f'Status updates from {obj.display_name}'
def items(self, obj): def items(self, obj):
return get_activity_feed(obj, ['public', 'unlisted'], queryset=obj.status_set) ''' the user's activity feed '''
return get_activity_feed(
obj, ['public', 'unlisted'], queryset=obj.status_set)
def item_link(self, item): def item_link(self, item):
''' link to the status '''
return item.local_path return item.local_path

View File

@ -1,55 +1,22 @@
''' what are we here for if not for posting ''' ''' what are we here for if not for posting '''
import re import re
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.http import HttpResponseBadRequest, HttpResponseNotFound from django.http import HttpResponseBadRequest
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views import View from django.views import View
from markdown import markdown from markdown import markdown
from bookwyrm import forms, models from bookwyrm import forms, models
from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.broadcast import broadcast from bookwyrm.broadcast import broadcast
from bookwyrm.sanitize_html import InputHtmlParser from bookwyrm.sanitize_html import InputHtmlParser
from bookwyrm.settings import DOMAIN from bookwyrm.settings import DOMAIN
from bookwyrm.status import create_notification, delete_status from bookwyrm.status import create_notification, delete_status
from bookwyrm.utils import regex from bookwyrm.utils import regex
from .helpers import get_user_from_username, handle_remote_webfinger from .helpers import handle_remote_webfinger
from .helpers import is_api_request, is_bookworm_request, object_visible_to_user
# pylint: disable= no-self-use # pylint: disable= no-self-use
class Status(View):
''' get posting '''
def get(self, request, username, status_id):
''' display a particular status (and replies, etc) '''
try:
user = get_user_from_username(username)
status = models.Status.objects.select_subclasses().get(
id=status_id, deleted=False)
except ValueError:
return HttpResponseNotFound()
# the url should have the poster's username in it
if user != status.user:
return HttpResponseNotFound()
# make sure the user is authorized to see the status
if not object_visible_to_user(request.user, status):
return HttpResponseNotFound()
if is_api_request(request):
return ActivitypubResponse(
status.to_activity(pure=not is_bookworm_request(request)))
data = {
'title': 'Status by %s' % user.username,
'status': status,
}
return TemplateResponse(request, 'status.html', data)
@method_decorator(login_required, name='dispatch') @method_decorator(login_required, name='dispatch')
class CreateStatus(View): class CreateStatus(View):
''' the view for *posting* ''' ''' the view for *posting* '''
@ -144,23 +111,6 @@ class DeleteStatus(View):
broadcast(request.user, status.to_delete_activity(request.user)) broadcast(request.user, status.to_delete_activity(request.user))
return redirect(request.headers.get('Referer', '/')) return redirect(request.headers.get('Referer', '/'))
class Replies(View):
''' replies page (a json view of status) '''
def get(self, request, username, status_id):
''' ordered collection of replies to a status '''
# the html view is the same as Status
if not is_api_request(request):
status_view = Status.as_view()
return status_view(request, username, status_id)
# the json view is different than Status
status = models.Status.objects.get(id=status_id)
if status.user.localname != username:
return HttpResponseNotFound()
return ActivitypubResponse(status.to_replies(**request.GET))
def find_mentions(content): def find_mentions(content):
''' detect @mentions in raw status content ''' ''' detect @mentions in raw status content '''
for match in re.finditer(regex.strict_username, content): for match in re.finditer(regex.strict_username, content):

View File

@ -90,7 +90,7 @@ class User(View):
'goal': goal, 'goal': goal,
} }
return TemplateResponse(request, 'user.html', data) return TemplateResponse(request, 'user/user.html', data)
class Followers(View): class Followers(View):
''' list of followers view ''' ''' list of followers view '''
@ -115,7 +115,7 @@ class Followers(View):
'is_self': request.user.id == user.id, 'is_self': request.user.id == user.id,
'followers': user.followers.all(), 'followers': user.followers.all(),
} }
return TemplateResponse(request, 'followers.html', data) return TemplateResponse(request, 'user/followers.html', data)
class Following(View): class Following(View):
''' list of following view ''' ''' list of following view '''
@ -140,7 +140,7 @@ class Following(View):
'is_self': request.user.id == user.id, 'is_self': request.user.id == user.id,
'following': user.following.all(), 'following': user.following.all(),
} }
return TemplateResponse(request, 'following.html', data) return TemplateResponse(request, 'user/following.html', data)
@method_decorator(login_required, name='dispatch') @method_decorator(login_required, name='dispatch')
@ -153,7 +153,7 @@ class EditUser(View):
'form': forms.EditUserForm(instance=request.user), 'form': forms.EditUserForm(instance=request.user),
'user': request.user, 'user': request.user,
} }
return TemplateResponse(request, 'edit_user.html', data) return TemplateResponse(request, 'preferences/edit_user.html', data)
def post(self, request): def post(self, request):
''' les get fancy with images ''' ''' les get fancy with images '''
@ -161,7 +161,7 @@ class EditUser(View):
request.POST, request.FILES, instance=request.user) request.POST, request.FILES, instance=request.user)
if not form.is_valid(): if not form.is_valid():
data = {'form': form, 'user': request.user} data = {'form': form, 'user': request.user}
return TemplateResponse(request, 'edit_user.html', data) return TemplateResponse(request, 'preferences/edit_user.html', data)
user = form.save(commit=False) user = form.save(commit=False)