Merge branch 'main' into production

This commit is contained in:
Mouse Reeve 2021-01-30 09:20:19 -08:00
commit bfdfb846da
65 changed files with 861 additions and 386 deletions

View file

@ -92,6 +92,12 @@ class ReplyForm(CustomForm):
'user', 'content', 'content_warning', 'sensitive',
'reply_parent', 'privacy']
class StatusForm(CustomForm):
class Meta:
model = models.Status
fields = [
'user', 'content', 'content_warning', 'sensitive', 'privacy']
class EditUserForm(CustomForm):
class Meta:
@ -189,7 +195,14 @@ class ShelfForm(CustomForm):
model = models.Shelf
fields = ['user', 'name', 'privacy']
class GoalForm(CustomForm):
class Meta:
model = models.AnnualGoal
fields = ['user', 'year', 'goal', 'privacy']
class SiteForm(CustomForm):
class Meta:
model = models.SiteSettings
exclude = []

View file

@ -7,7 +7,7 @@ class InputHtmlParser(HTMLParser):#pylint: disable=abstract-method
def __init__(self):
HTMLParser.__init__(self)
self.allowed_tags = [
'p', 'br',
'p', 'blockquote', 'br',
'b', 'i', 'strong', 'em', 'pre',
'a', 'span', 'ul', 'ol', 'li'
]

View file

@ -35,8 +35,17 @@ window.onload = function() {
// polling
document.querySelectorAll('[data-poll]')
.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) {
let delay = 10000 + (Math.random() * 1000);
setTimeout(function() {

View file

@ -1,19 +0,0 @@
{% extends 'layout.html' %}
{% block content %}
<div class="block">
<h1 class="title">Direct Messages</h1>
{% if not activities %}
<p>You have no messages right now.</p>
{% endif %}
{% for activity in activities %}
<div class="block">
{% include 'snippets/status.html' with status=activity %}
</div>
{% endfor %}
{% include 'snippets/pagination.html' with page=activities path="direct-messages" %}
</div>
{% endblock %}

View file

@ -0,0 +1,26 @@
{% extends 'feed/feed_layout.html' %}
{% block panel %}
<header class="block">
<h1 class="title">Direct Messages{% if partner %} with {% include 'snippets/username.html' with user=partner %}{% endif %}</h1>
{% if partner %}<p class="subtitle"><a href="/direct-messages"><span class="icon icon-arrow-left" aria-hidden="true"></span> All messages</a></p>{% endif %}
</header>
<div class="box">
{% include 'snippets/create_status_form.html' with type="direct" uuid=1 mentions=partner %}
</div>
<section class="block">
{% if not activities %}
<p>You have no messages right now.</p>
{% endif %}
{% for activity in activities %}
<div class="block">
{% include 'snippets/status.html' with status=activity %}
</div>
{% endfor %}
{% include 'snippets/pagination.html' with page=activities path="direct-messages" %}
</section>
{% endblock %}

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 %}
<div class="columns">
{% if user.is_authenticated %}
<div class="column is-one-third">
<h2 class="title is-5">Your books</h2>
{% if not suggested_books %}
@ -69,43 +70,15 @@
</section>
{% endif %}
</div>
{% endif %}
<div class="column is-two-thirds" id="feed">
<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>
{% block panel %}{% endblock %}
{# 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>
{% if activities %}
{% include 'snippets/pagination.html' with page=activities path=path anchor="#feed" %}
{% 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 %}
{% include 'snippets/pagination.html' with page=activities path='/'|add:tab anchor="#feed" %}
</div>
</div>
{% 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>
</li>
<li>
<a href="/edit-profile" class="navbar-item">
<a href="/preferences/profile" class="navbar-item">
Settings
</a>
</li>
@ -90,13 +90,23 @@
Import books
</a>
</li>
{% if perms.bookwyrm.create_invites or perms.bookwyrm.edit_instance_settings%}
<hr class="navbar-divider">
{% endif %}
{% if perms.bookwyrm.create_invites %}
<li>
<a href="/invite" class="navbar-item">
<a href="{% url 'settings-invites' %}" class="navbar-item">
Invites
</a>
</li>
{% endif %}
{% if perms.bookwyrm.edit_instance_settings %}
<li>
<a href="{% url 'settings-site' %}" class="navbar-item">
Site Configuration
</a>
</li>
{% endif %}
<hr class="navbar-divider">
<li>
<a href="/logout" class="navbar-item">

View file

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

View file

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

View file

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

View file

@ -10,16 +10,16 @@
<h2 class="menu-label">Account</h2>
<ul class="menu-list">
<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>
<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>
</ul>
<h2 class="menu-label">Relationships</h2>
<ul class="menu-list">
<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>
</ul>
</nav>

View file

@ -0,0 +1,46 @@
{% extends 'layout.html' %}
{% block content %}
<header class="block column is-offset-one-quarter pl-1">
<h1 class="title">{% block header %}{% endblock %}</h1>
</header>
<div class="block columns">
<nav class="menu column is-one-quarter">
{% if perms.bookwyrm.create_invites %}
<h2 class="menu-label">Manage Users</h2>
<ul class="menu-list">
<li>
{% url 'settings-invites' as url %}
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>Invites</a>
</li>
<li>
{% url 'settings-federation' as url %}
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>Federated Servers</a>
</li>
</ul>
{% endif %}
{% if perms.bookwyrm.edit_instance_settings %}
<h2 class="menu-label">Instance Settings</h2>
<ul class="menu-list">
<li>
{% url 'settings-site' as url %}
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>Site Configuration</a>
{% if url in request.path %}
<ul class="emnu-list">
<li><a href="{{ url }}#instance-info">Instance Info</a></li>
<li><a href="{{ url }}#images">Images</a></li>
<li><a href="{{ url }}#footer">Footer Content</a></li>
<li><a href="{{ url }}#registration">Registration</a></li>
</ul>
{% endif %}
</li>
</ul>
{% endif %}
</nav>
<div class="column content">
{% block panel %}{% endblock %}
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,21 @@
{% extends 'settings/admin_layout.html' %}
{% block header %}Federated Servers{% endblock %}
{% block panel %}
<table class="table is-striped">
<tr>
<th>Server name</th>
<th>Software</th>
<th>Status</th>
</tr>
{% for server in servers %}
<tr>
<td>{{ server.server_name }}</td>
<td>{{ server.application_type }} ({{ server.application_version }})</td>
<td>{{ server.status }}</td>
</tr>
{% endfor %}
</table>
{% endblock %}

View file

@ -1,8 +1,8 @@
{% extends 'layout.html' %}
{% extends 'settings/admin_layout.html' %}
{% block header %}Invites{% endblock %}
{% load humanize %}
{% block content %}
<div class="block">
<h1 class="title">Invites</h1>
{% block panel %}
<section class="block">
<table class="table is-striped">
<tr>
<th>Link</th>
@ -22,12 +22,12 @@
</tr>
{% endfor %}
</table>
</div>
</section>
<div class="block">
<section class="block">
<h2 class="title is-4">Generate New Invite</h2>
<form name="invite" action="/invite/" method="post">
<form name="invite" action="{% url 'settings-invites' %}" method="post">
{% csrf_token %}
<div class="field is-grouped">
<div class="control">
@ -46,5 +46,5 @@
<button class="button is-primary" type="submit">Create Invite</button>
</form>
</div>
</section>
{% endblock %}

View file

@ -0,0 +1,84 @@
{% extends 'settings/admin_layout.html' %}
{% block header %}Site Configuration{% endblock %}
{% block panel %}
<form action="{% url 'settings-site' %}" method="POST" class="content">
{% csrf_token %}
<section class="block" id="instance-info">
<h2 class="title is-4">Instance Info</h2>
<div class="control">
<label class="label" for="id_name">Instance Name:</label>
{{ site_form.name }}
</div>
<div class="control">
<label class="label" for="id_instance_tagline">Tagline:</label>
{{ site_form.instance_tagline }}
</div>
<div class="control">
<label class="label" for="id_instance_description">Instance description:</label>
{{ site_form.instance_description }}
</div>
<div class="control">
<label class="label" for="id_code_of_conduct">Code of conduct:</label>
{{ site_form.code_of_conduct }}
</div>
</section>
<hr aria-hidden="true">
<section class="block" id="images">
<h2 class="title is-4">Images</h2>
<div class="field is-grouped">
<div class="control">
<label class="label" for="id_logo">Logo:</label>
{{ site_form.logo }}
</div>
<div class="control">
<label class="label" for="id_logo_small">Logo small:</label>
{{ site_form.logo_small }}
</div>
<div class="control">
<label class="label" for="id_favicon">Favicon:</label>
{{ site_form.favicon }}
</div>
</div>
</section>
<hr aria-hidden="true">
<section class="block" id="footer">
<h2 class="title is-4">Footer Content</h2>
<div class="control">
<label class="label" for="id_support_link">Support link:</label>
<input type="text" name="support_link" maxlength="255" class="input" id="id_support_link" placeholder="https://www.patreon.com/bookwyrm"{% if site.support_link %} value="{{ site.support_link }}"{% endif %}>
</div>
<div class="control">
<label class="label" for="id_support_title">Support title:</label>
<input type="text" name="support_title" maxlength="100" class="input" id="id_support_title" placeholder="Patreon"{% if site.support_title %} value="{{ site.support_title }}"{% endif %}>
</div>
<div class="control">
<label class="label" for="id_admin_email">Admin email:</label>
{{ site_form.admin_email }}
</div>
</section>
<hr aria-hidden="true">
<section class="block" id="registration">
<h2 class="title is-4">Registration</h2>
<div class="control">
<label class="label" for="id_allow_registration">Allow registration:
{{ site_form.allow_registration }}
</div>
<div class="control">
<label class="label" for="id_registration_closed_text">Registration closed text:</label>
{{ site_form.registration_closed_text }}
</div>
</section>
<footer class="block">
<button class="button is-primary" type="submit">Save Changes</button>
</footer>
</form>
{% endblock %}

View file

@ -1,5 +1,5 @@
{% load bookwyrm_tags %}
<form class="is-flex-grow-1" name="{{ type }}" action="/post/{{ type }}" method="post" id="tab-{{ type }}-{{ book.id }}{{ reply_parent.id }}">
<form class="is-flex-grow-1" name="{{ type }}" action="/post/{% if type == 'direct' %}status{% else %}{{ type }}{% endif %}" method="post" id="tab-{{ type }}-{{ book.id }}{{ reply_parent.id }}">
{% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}">
<input type="hidden" name="user" value="{{ request.user.id }}">
@ -11,7 +11,7 @@
</div>
{% endif %}
<div class="control">
{% if not type == 'reply' %}
{% if type != 'reply' and type != 'direct' %}
<label class="label" for="id_{% if type == 'quotation' %}quote{% else %}content{% endif %}_{{ book.id }}_{{ type }}">{{ type|title }}:</label>
{% endif %}
@ -35,7 +35,7 @@
<textarea name="quote" class="textarea" id="id_quote_{{ book.id }}_{{ type }}" placeholder="{{ placeholder }}" required></textarea>
{% else %}
{% include 'snippets/content_warning_field.html' with parent_status=status %}
<textarea name="content" class="textarea" id="id_content_{{ type }}-{{ book.id }}{{reply_parent.id}}" placeholder="{{ placeholder }}" {% if type == 'reply' %} aria-label="Reply"{% endif %} required>{% if reply_parent %}{{ reply_parent|mentions:request.user }}{% endif %}</textarea>
<textarea name="content" class="textarea" id="id_content_{{ type }}-{{ book.id }}{{reply_parent.id}}" placeholder="{{ placeholder }}" {% if type == 'reply' %} aria-label="Reply"{% endif %} required>{% if reply_parent %}{{ reply_parent|mentions:request.user }}{% endif %}{% if mentions %}@{{ mentions|username }} {% endif %}</textarea>
{% endif %}
</div>
{% if type == 'quotation' %}
@ -53,7 +53,12 @@
{% include 'snippets/toggle/toggle_button.html' with text="Include spoiler alert" icon="warning is-size-4" controls_text="spoilers" controls_uid=uuid focus="id_content_warning" checkbox="id_show_spoilers" class="toggle-button" pressed=status.content_warning %}
</div>
<div class="control">
{% include 'snippets/privacy_select.html' with current=reply_parent.privacy%}
{% if type == 'direct' %}
<input type="hidden" name="privacy" value="direct">
<button type="button" class="button" aria-label="Privacy" disabled>Private</button>
{% else %}
{% include 'snippets/privacy_select.html' with current=reply_parent.privacy %}
{% endif %}
</div>
</div>
<div class="column is-narrow">

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-body %}
{% if readthrough.progress_updates|length > 0 %}

View file

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

View file

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

View file

@ -0,0 +1 @@
{{ obj.pure_content | safe }}

View file

@ -0,0 +1,15 @@
{{ obj.user.display_name }}{% if obj.status_type == 'GeneratedNote' %}
{{ obj.content | safe }}
{% elif obj.status_type == 'Review' and not obj.name and not obj.content%}
rated
{% elif obj.status_type == 'Review' %}
reviewed
{% elif obj.status_type == 'Comment' %}
commented on
{% elif obj.status_type == 'Quotation' %}
quoted
{% endif %}
{% if obj.book %}{{ obj.book.title | safe}}
{% elif obj.mention_books %}
{{ obj.mention_books.first.title }}
{% endif %}

View file

@ -1,4 +1,4 @@
{% extends 'snippets/components/dropdown.html' %}
{% extends 'components/dropdown.html' %}
{% block dropdown-trigger %}
<span>Change shelf</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 %}
<span class="icon icon-arrow-down">
<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 %}
Start "<em>{{ book.title }}</em>"

View file

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

View file

@ -1,4 +1,4 @@
{% extends 'snippets/components/dropdown.html' %}
{% extends 'components/dropdown.html' %}
{% load bookwyrm_tags %}
{% block dropdown-trigger %}
@ -18,6 +18,9 @@
</form>
</li>
{% else %}
<li role="menuitem">
<a href="/direct-messages/{{ status.user|username }}" class="button is-fullwidth is-small">Send direct message</a>
</li>
<li role="menuitem">
{% include 'snippets/block_button.html' with user=status.user class="is-fullwidth" %}
</li>

View file

@ -1,4 +1,4 @@
{% extends 'snippets/components/dropdown.html' %}
{% extends 'components/dropdown.html' %}
{% load bookwyrm_tags %}
{% block dropdown-trigger %}
@ -8,6 +8,9 @@
{% endblock %}
{% block dropdown-list %}
<li role="menuitem">
<a href="/direct-messages/{{ user|username }}" class="button is-fullwidth is-small">Send direct message</a>
</li>
<li role="menuitem">
{% include 'snippets/block_button.html' with user=user class="is-fullwidth" %}
</li>

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

View file

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

View file

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

View file

@ -1,5 +1,13 @@
{% extends 'layout.html' %}
{% load humanize %}
{% load bookwyrm_tags %}
{% block content %}
<header class="block">
{% block header %}{% endblock %}
</header>
{# user bio #}
<div class="block">
<div class="columns">
<div class="column is-narrow">
@ -34,7 +42,7 @@
{% endif %}
</div>
</div>
{% if not is_self %}
{% if not is_self and request.user.is_authenticated %}
<div class="field has-addons">
<div class="control">
{% include 'snippets/follow_button.html' with user=user %}
@ -60,3 +68,6 @@
{% endif %}
</div>
{% block panel %}{% endblock %}
{% endblock %}

View file

@ -0,0 +1 @@
from . import *

View file

@ -32,7 +32,7 @@ class BlockViews(TestCase):
request.user = self.local_user
result = view(request)
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)
def test_block_post(self):

View file

@ -7,8 +7,8 @@ from bookwyrm import models
from bookwyrm import views
class DirectMessageViews(TestCase):
''' dms '''
class FederationViews(TestCase):
''' every response to a get request, html or json '''
def setUp(self):
''' we need basic test data and mocks '''
self.factory = RequestFactory()
@ -17,12 +17,13 @@ class DirectMessageViews(TestCase):
local=True, localname='mouse')
def test_direct_messages_page(self):
def test_federation_page(self):
''' there are so many views, this just makes sure it LOADS '''
view = views.DirectMessage.as_view()
view = views.Federation.as_view()
request = self.factory.get('')
request.user = self.local_user
request.user.is_superuser = True
result = view(request)
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'direct_messages.html')
self.assertEqual(result.template_name, 'settings/federation.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

@ -44,5 +44,5 @@ class InviteViews(TestCase):
request.user.is_superuser = True
result = view(request)
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'manage_invites.html')
self.assertEqual(result.template_name, 'settings/manage_invites.html')
self.assertEqual(result.status_code, 200)

View file

@ -18,10 +18,6 @@ class LandingViews(TestCase):
local=True, localname='mouse')
self.anonymous_user = AnonymousUser
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):
@ -31,7 +27,7 @@ class LandingViews(TestCase):
request.user = self.local_user
result = view(request)
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
result = view(request)
@ -51,17 +47,6 @@ class LandingViews(TestCase):
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):
''' there are so many views, this just makes sure it LOADS '''
view = views.Discover.as_view()
@ -70,15 +55,3 @@ class LandingViews(TestCase):
self.assertIsInstance(result, TemplateResponse)
self.assertEqual(result.template_name, 'discover.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.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)
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)

View file

@ -45,7 +45,8 @@ class ReadingViews(TestCase):
'start_date': '2020-01-05',
})
request.user = self.local_user
views.start_reading(request, self.book.id)
with patch('bookwyrm.broadcast.broadcast_task.delay'):
views.start_reading(request, self.book.id)
self.assertEqual(shelf.books.get(), self.book)
@ -73,7 +74,8 @@ class ReadingViews(TestCase):
request = self.factory.post('')
request.user = self.local_user
views.start_reading(request, self.book.id)
with patch('bookwyrm.broadcast.broadcast_task.delay'):
views.start_reading(request, self.book.id)
self.assertFalse(to_read_shelf.books.exists())
self.assertEqual(shelf.books.get(), self.book)
@ -95,7 +97,9 @@ class ReadingViews(TestCase):
'id': readthrough.id,
})
request.user = self.local_user
views.finish_reading(request, self.book.id)
with patch('bookwyrm.broadcast.broadcast_task.delay'):
views.finish_reading(request, self.book.id)
self.assertEqual(shelf.books.get(), self.book)

View file

@ -0,0 +1,52 @@
''' testing import '''
from unittest.mock import patch
from django.test import RequestFactory, TestCase
import responses
from bookwyrm import models
from bookwyrm.views import rss_feed
from bookwyrm.settings import DOMAIN
class RssFeedView(TestCase):
''' rss feed behaves as expected '''
def setUp(self):
self.site = models.SiteSettings.objects.create()
self.user = models.User.objects.create_user(
'rss_user', 'rss@test.rss', 'password', local=True)
work = models.Work.objects.create(title='Test Work')
self.book = models.Edition.objects.create(
title='Example Edition',
remote_id='https://example.com/book/1',
parent_work=work
)
self.review = models.Review.objects.create(
name='Review name', content='test content', rating=3,
user=self.user, book=self.book)
self.quote = models.Quotation.objects.create(
quote='a sickening sense', content='test content',
user=self.user, book=self.book)
self.generatednote = models.GeneratedNote.objects.create(
content='test content', user=self.user)
self.factory = RequestFactory()
def test_rss_feed(self):
view = rss_feed.RssFeed()
request = self.factory.get('/user/rss_user/rss')
with patch("bookwyrm.models.SiteSettings.objects.get") as site:
site.return_value = self.site
result = view(request, username=self.user.username)
self.assertEqual(result.status_code, 200)
self.assertIn(b"Status updates from rss_user", result.content)
self.assertIn( b"a sickening sense", result.content)
self.assertIn(b"Example Edition", result.content)

View file

@ -74,7 +74,7 @@ class ShelfViews(TestCase):
'name': 'To Read',
})
request.user = self.local_user
view(request, self.local_user.username, shelf.id)
view(request, self.local_user.username, shelf.identifier)
shelf.refresh_from_db()
self.assertEqual(shelf.privacy, 'unlisted')
@ -94,7 +94,7 @@ class ShelfViews(TestCase):
'name': 'cool name'
})
request.user = self.local_user
view(request, request.user.username, shelf.id)
view(request, request.user.username, shelf.identifier)
shelf.refresh_from_db()
self.assertEqual(shelf.name, 'cool name')
@ -114,7 +114,7 @@ class ShelfViews(TestCase):
'name': 'cool name'
})
request.user = self.local_user
view(request, request.user.username, shelf.id)
view(request, request.user.username, shelf.identifier)
self.assertEqual(shelf.name, 'To Read')

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):
''' create a status '''
view = views.CreateStatus.as_view()
@ -176,7 +134,8 @@ class StatusViews(TestCase):
reply = models.Status.replies(status).first()
self.assertEqual(reply.content, '<p>right</p>')
self.assertEqual(reply.user, user)
self.assertTrue(self.remote_user in reply.mention_users.all())
# the mentioned user in the parent post is only included if @'ed
self.assertFalse(self.remote_user in reply.mention_users.all())
self.assertTrue(self.local_user in reply.mention_users.all())
def test_find_mentions(self):

View file

@ -34,7 +34,7 @@ class UserViews(TestCase):
is_api.return_value = False
result = view(request, 'mouse')
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)
with patch('bookwyrm.views.user.is_api_request') as is_api:
@ -65,7 +65,7 @@ class UserViews(TestCase):
is_api.return_value = False
result = view(request, 'mouse')
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)
with patch('bookwyrm.views.user.is_api_request') as is_api:
@ -96,7 +96,7 @@ class UserViews(TestCase):
is_api.return_value = False
result = view(request, 'mouse')
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)
with patch('bookwyrm.views.user.is_api_request') as is_api:
@ -125,7 +125,7 @@ class UserViews(TestCase):
request.user = self.local_user
result = view(request)
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)

View file

@ -3,6 +3,7 @@ from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path, re_path
from bookwyrm import incoming, settings, views, wellknown
from bookwyrm.utils import regex
@ -47,19 +48,27 @@ urlpatterns = [
re_path(r'^password-reset/?$', views.PasswordResetRequest.as_view()),
re_path(r'^password-reset/(?P<code>[A-Za-z0-9]+)/?$',
views.PasswordReset.as_view()),
re_path(r'^change-password/?$', views.ChangePassword.as_view()),
# invites
re_path(r'^invite/?$', views.ManageInvites.as_view()),
# admin
re_path(r'^settings/site-settings',
views.Site.as_view(), name='settings-site'),
re_path(r'^settings/federation',
views.Federation.as_view(), name='settings-federation'),
re_path(r'^settings/invites/?$',
views.ManageInvites.as_view(), name='settings-invites'),
re_path(r'^invite/(?P<code>[A-Za-z0-9]+)/?$', views.Invite.as_view()),
# landing pages
re_path(r'^about/?$', views.About.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'^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/(?P<username>%s)?$' % regex.username,
views.DirectMessage.as_view()),
# search
re_path(r'^search/?$', views.Search.as_view()),
@ -74,7 +83,14 @@ urlpatterns = [
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/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()),
# 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
re_path(r'%s/goal/(?P<year>\d{4})/?$' % user_path, views.Goal.as_view()),
@ -137,7 +153,4 @@ urlpatterns = [
re_path(r'^accept-follow-request/?$', views.accept_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)

View file

@ -4,25 +4,28 @@ from .author import Author, EditAuthor
from .block import Block, unblock
from .books import Book, EditBook, Editions
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 .federation import Federation
from .feed import DirectMessage, Feed, Replies, Status
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
from .landing import About, Home, Feed, Discover
from .landing import About, Home, Discover
from .notifications import Notifications
from .outbox import Outbox
from .reading import edit_readthrough, create_readthrough, delete_readthrough
from .reading import start_reading, finish_reading, delete_progressupdate
from .rss_feed import RssFeed
from .password import PasswordResetRequest, PasswordReset, ChangePassword
from .tag import Tag, AddTag, RemoveTag
from .search import Search
from .shelf import Shelf
from .shelf import user_shelves_page, create_shelf, delete_shelf
from .shelf import shelve, unshelve
from .status import Status, Replies, CreateStatus, DeleteStatus
from .site import Site
from .status import CreateStatus, DeleteStatus
from .updates import Updates
from .user import User, EditUser, Followers, Following

View file

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

View file

@ -0,0 +1,24 @@
''' manage federated servers '''
from django.contrib.auth.decorators import login_required, permission_required
from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator
from django.views import View
from bookwyrm import models
# pylint: disable= no-self-use
@method_decorator(login_required, name='dispatch')
@method_decorator(
permission_required('bookwyrm.control_federation', raise_exception=True),
name='dispatch')
class Federation(View):
''' what servers do we federate with '''
def get(self, request):
''' edit form '''
servers = models.FederatedServer.objects.all()
data = {
'title': 'Federated Servers',
'servers': servers
}
return TemplateResponse(request, 'settings/federation.html', data)

173
bookwyrm/views/feed.py Normal file
View file

@ -0,0 +1,173 @@
''' non-interactive pages '''
from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator
from django.db.models import Q
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(),
'path': '/%s' % tab,
}}
return TemplateResponse(request, 'feed/feed.html', data)
@method_decorator(login_required, name='dispatch')
class DirectMessage(View):
''' dm view '''
def get(self, request, username=None):
''' like a feed but for dms only '''
try:
page = int(request.GET.get('page', 1))
except ValueError:
page = 1
queryset = models.Status.objects
user = None
if username:
try:
user = get_user_from_username(username)
except models.User.DoesNotExist:
pass
if user:
queryset = queryset.filter(Q(user=user) | Q(mention_users=user))
activities = get_activity_feed(
request.user, 'direct', queryset=queryset)
paginated = Paginator(activities, PAGE_LENGTH)
activity_page = paginated.page(page)
data = {**feed_page_data(request.user), **{
'title': 'Direct Messages',
'user': request.user,
'partner': user,
'activities': activity_page,
'path': '/direct-messages',
}}
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

@ -24,7 +24,7 @@ class ManageInvites(View):
user=request.user).order_by('-created_date'),
'form': forms.CreateInviteForm(),
}
return TemplateResponse(request, 'manage_invites.html', data)
return TemplateResponse(request, 'settings/manage_invites.html', data)
def post(self, request):
''' creates an invite database entry '''
@ -36,7 +36,7 @@ class ManageInvites(View):
invite.user = request.user
invite.save()
return redirect('/invite')
return redirect('/settings/invites')
class Invite(View):

View file

@ -1,14 +1,10 @@
''' 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.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.settings import PAGE_LENGTH
from .feed import Feed
from .helpers import get_activity_feed
@ -61,68 +57,3 @@ class Discover(View):
'ratings': ratings
}
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',
'user': request.user,
}
return TemplateResponse(request, 'change_password.html', data)
return TemplateResponse(
request, 'preferences/change_password.html', data)
def post(self, request):
''' allow a user to change their password '''
@ -102,9 +103,9 @@ class ChangePassword(View):
confirm_password = request.POST.get('confirm-password')
if new_password != confirm_password:
return redirect('/edit-profile')
return redirect('preferences/password')
request.user.set_password(new_password)
request.user.save()
login(request, request.user)
return redirect('/user/%s' % request.user.localname)
return redirect(request.user.local_path)

View file

@ -30,8 +30,8 @@ def start_reading(request, book_id):
if readthrough:
readthrough.save()
# create a progress update if we have a page
readthrough.create_update()
# create a progress update if we have a page
readthrough.create_update()
# shelve the book
if request.POST.get('reshelve', True):

View file

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

View file

@ -65,6 +65,7 @@ class Shelf(View):
return TemplateResponse(request, 'shelf.html', data)
@method_decorator(login_required, name='dispatch')
# pylint: disable=unused-argument
def post(self, request, username, shelf_identifier):
''' edit a shelf '''
try:

40
bookwyrm/views/site.py Normal file
View file

@ -0,0 +1,40 @@
''' manage site settings '''
from django.contrib.auth.decorators import login_required, permission_required
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
# pylint: disable= no-self-use
@method_decorator(login_required, name='dispatch')
@method_decorator(
permission_required(
'bookwyrm.edit_instance_settings', raise_exception=True),
name='dispatch')
class Site(View):
''' manage things like the instance name '''
def get(self, request):
''' edit form '''
site = models.SiteSettings.objects.get()
data = {
'title': 'Site Settings',
'site_form': forms.SiteForm(instance=site)
}
return TemplateResponse(request, 'settings/site.html', data)
def post(self, request):
''' edit the site settings '''
site = models.SiteSettings.objects.get()
form = forms.SiteForm(request.POST, instance=site)
if not form.is_valid():
data = {
'title': 'Site Settings',
'site_form': form
}
return TemplateResponse(request, 'settings/site.html', data)
form.save()
return redirect('settings-site')

View file

@ -1,55 +1,22 @@
''' what are we here for if not for posting '''
import re
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.template.response import TemplateResponse
from django.utils.decorators import method_decorator
from django.views import View
from markdown import markdown
from bookwyrm import forms, models
from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.broadcast import broadcast
from bookwyrm.sanitize_html import InputHtmlParser
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, object_visible_to_user
from .helpers import handle_remote_webfinger
# 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')
class CreateStatus(View):
''' the view for *posting* '''
@ -144,23 +111,6 @@ class DeleteStatus(View):
broadcast(request.user, status.to_delete_activity(request.user))
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):
''' detect @mentions in raw status content '''
for match in re.finditer(regex.strict_username, content):

View file

@ -73,7 +73,7 @@ class User(View):
activities = get_activity_feed(
request.user,
['public', 'unlisted', 'followers'],
queryset=models.Status.objects.filter(user=user)
queryset=user.status_set
)
paginated = Paginator(activities, PAGE_LENGTH)
goal = models.AnnualGoal.objects.filter(
@ -90,7 +90,7 @@ class User(View):
'goal': goal,
}
return TemplateResponse(request, 'user.html', data)
return TemplateResponse(request, 'user/user.html', data)
class Followers(View):
''' list of followers view '''
@ -115,7 +115,7 @@ class Followers(View):
'is_self': request.user.id == user.id,
'followers': user.followers.all(),
}
return TemplateResponse(request, 'followers.html', data)
return TemplateResponse(request, 'user/followers.html', data)
class Following(View):
''' list of following view '''
@ -140,7 +140,7 @@ class Following(View):
'is_self': request.user.id == user.id,
'following': user.following.all(),
}
return TemplateResponse(request, 'following.html', data)
return TemplateResponse(request, 'user/following.html', data)
@method_decorator(login_required, name='dispatch')
@ -153,7 +153,7 @@ class EditUser(View):
'form': forms.EditUserForm(instance=request.user),
'user': request.user,
}
return TemplateResponse(request, 'edit_user.html', data)
return TemplateResponse(request, 'preferences/edit_user.html', data)
def post(self, request):
''' les get fancy with images '''
@ -161,7 +161,7 @@ class EditUser(View):
request.POST, request.FILES, instance=request.user)
if not form.is_valid():
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)