Merge branch 'main' into inbox-refactor

This commit is contained in:
Mouse Reeve 2021-02-24 09:51:34 -08:00
commit cad19ee878
58 changed files with 472 additions and 317 deletions

View file

@ -112,36 +112,42 @@ Once the build is complete, you can access the instance at `localhost:1333`
## Installing in Production ## Installing in Production
This project is still young and isn't, at the momoment, very stable, so please procede with caution when running in production. This project is still young and isn't, at the momoment, very stable, so please procede with caution when running in production.
### Server setup ### Server setup
- Get a domain name and set up DNS for your server - Get a domain name and set up DNS for your server
- Set your server up with appropriate firewalls for running a web application (this instruction set is tested again Ubuntu 20.04) - Set your server up with appropriate firewalls for running a web application (this instruction set is tested again Ubuntu 20.04)
- Set up a mailgun account and the appropriate DNS settings - Set up an email service (such as mailgun) and the appropriate SMTP/DNS settings
- Install Docker and docker-compose - Install Docker and docker-compose
### Install and configure BookWyrm ### Install and configure BookWyrm
The `production` branch of BookWyrm contains a number of tools not on the `main` branch that are suited for running in production, such as `docker-compose` changes to update the default commands or configuration of containers, and indivudal changes to container config to enable things like SSL or regular backups.
Instructions for running BookWyrm in production:
- Get the application code: - Get the application code:
`git clone git@github.com:mouse-reeve/bookwyrm.git` `git clone git@github.com:mouse-reeve/bookwyrm.git`
- Switch to the `production` branch - Switch to the `production` branch
`git checkout production` `git checkout production`
- Create your environment variables file - Create your environment variables file
`cp .env.example .env` `cp .env.example .env`
- Add your domain, email address, mailgun credentials - Add your domain, email address, SMTP credentials
- Set a secure redis password and secret key - Set a secure redis password and secret key
- Set a secure database password for postgres - Set a secure database password for postgres
- Update your nginx configuration in `nginx/default.conf` - Update your nginx configuration in `nginx/default.conf`
- Replace `your-domain.com` with your domain name - Replace `your-domain.com` with your domain name
- Run the application (this should also set up a Certbot ssl cert for your domain) - Run the application (this should also set up a Certbot ssl cert for your domain) with
`docker-compose up --build` `docker-compose up --build`, and make sure all the images build successfully
Make sure all the images build successfully
- When docker has built successfully, stop the process with `CTRL-C` - When docker has built successfully, stop the process with `CTRL-C`
- Comment out the `command: certonly...` line in `docker-compose.yml` - Comment out the `command: certonly...` line in `docker-compose.yml`
- Run docker-compose in the background - Run docker-compose in the background with: `docker-compose up -d`
`docker-compose up -d` - Initialize the database with: `./bw-dev initdb`
- Initialize the database - Set up schedule backups with cron that runs that `docker-compose exec db pg_dump -U <databasename>` and saves the backup to a safe location
`./bw-dev initdb`
- Set up schedule backups with cron that runs that `docker-compose exec db pg_dump -U <databasename>` and saves the backup to a safe locationgi Congrats! You did it, go to your domain and enjoy the fruits of your labors.
- Congrats! You did it, go to your domain and enjoy the fruits of your labors
### Configure your instance ### Configure your instance
- Register a user account in the applcation UI - Register a user account in the application UI
- Make your account a superuser (warning: do *not* use django's `createsuperuser` command) - Make your account a superuser (warning: do *not* use django's `createsuperuser` command)
- On your server, open the django shell - On your server, open the django shell
`./bw-dev shell` `./bw-dev shell`

View file

@ -216,10 +216,6 @@ def get_data(url):
raise ConnectorException() raise ConnectorException()
if not resp.ok: if not resp.ok:
try:
resp.raise_for_status()
except requests.exceptions.HTTPError as e:
logger.exception(e)
raise ConnectorException() raise ConnectorException()
try: try:
data = resp.json() data = resp.json()

View file

@ -286,7 +286,7 @@ class OrderedCollectionPageMixin(ObjectMixin):
def to_ordered_collection(self, queryset, \ def to_ordered_collection(self, queryset, \
remote_id=None, page=False, collection_only=False, **kwargs): remote_id=None, page=False, collection_only=False, **kwargs):
''' an ordered collection of whatevers ''' 'pure=pure, '' an ordered collection of whatevers '''
if not queryset.ordered: if not queryset.ordered:
raise RuntimeError('queryset must be ordered') raise RuntimeError('queryset must be ordered')
@ -480,7 +480,7 @@ def sign_and_send(sender, data, destination):
# pylint: disable=unused-argument # pylint: disable=unused-argument
def to_ordered_collection_page( def to_ordered_collection_page(
queryset, remote_id, id_only=False, page=1, **kwargs): queryset, remote_id, id_only=False, page=1, pure=False, **kwargs):
''' serialize and pagiante a queryset ''' ''' serialize and pagiante a queryset '''
paginated = Paginator(queryset, PAGE_LENGTH) paginated = Paginator(queryset, PAGE_LENGTH)
@ -488,7 +488,7 @@ def to_ordered_collection_page(
if id_only: if id_only:
items = [s.remote_id for s in activity_page.object_list] items = [s.remote_id for s in activity_page.object_list]
else: else:
items = [s.to_activity() for s in activity_page.object_list] items = [s.to_activity(pure=pure) for s in activity_page.object_list]
prev_page = next_page = None prev_page = next_page = None
if activity_page.has_next(): if activity_page.has_next():

View file

@ -1,8 +1,7 @@
''' defines relationships between users ''' ''' defines relationships between users '''
from django.apps import apps from django.apps import apps
from django.db import models, transaction from django.db import models, transaction, IntegrityError
from django.db.models import Q from django.db.models import Q
from django.dispatch import receiver
from bookwyrm import activitypub from bookwyrm import activitypub
from .activitypub_mixin import ActivitypubMixin, ActivityMixin from .activitypub_mixin import ActivitypubMixin, ActivityMixin
@ -61,15 +60,26 @@ class UserFollows(ActivityMixin, UserRelationship):
''' Following a user ''' ''' Following a user '''
status = 'follows' status = 'follows'
def save(self, *args, **kwargs):
''' never broadcast a creation (that's handled by "accept"), only a
deletion (an unfollow, as opposed to "reject" and undo pending) '''
super().save(*args, broadcast=False, **kwargs)
def to_activity(self): def to_activity(self):
''' overrides default to manually set serializer ''' ''' overrides default to manually set serializer '''
return activitypub.Follow(**generate_activity(self)) return activitypub.Follow(**generate_activity(self))
def save(self, *args, **kwargs):
''' really really don't let a user follow someone who blocked them '''
# blocking in either direction is a no-go
if UserBlocks.objects.filter(
Q(
user_subject=self.user_subject,
user_object=self.user_object,
) | Q(
user_subject=self.user_object,
user_object=self.user_subject,
)
).exists():
raise IntegrityError()
# don't broadcast this type of relationship -- accepts and requests
# are handled by the UserFollowRequest model
super().save(*args, broadcast=False, **kwargs)
@classmethod @classmethod
def from_request(cls, follow_request): def from_request(cls, follow_request):
@ -88,24 +98,23 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship):
def save(self, *args, broadcast=True, **kwargs): def save(self, *args, broadcast=True, **kwargs):
''' make sure the follow or block relationship doesn't already exist ''' ''' make sure the follow or block relationship doesn't already exist '''
try: # don't create a request if a follow already exists
UserFollows.objects.get( if UserFollows.objects.filter(
user_subject=self.user_subject, user_subject=self.user_subject,
user_object=self.user_object, user_object=self.user_object,
) ).exists():
raise IntegrityError()
# blocking in either direction is a no-go # blocking in either direction is a no-go
UserBlocks.objects.get( if UserBlocks.objects.filter(
Q(
user_subject=self.user_subject, user_subject=self.user_subject,
user_object=self.user_object, user_object=self.user_object,
) ) | Q(
UserBlocks.objects.get(
user_subject=self.user_object, user_subject=self.user_object,
user_object=self.user_subject, user_object=self.user_subject,
) )
return ).exists():
except (UserFollows.DoesNotExist, UserBlocks.DoesNotExist): raise IntegrityError()
pass
super().save(*args, **kwargs) super().save(*args, **kwargs)
if broadcast and self.user_subject.local and not self.user_object.local: if broadcast and self.user_subject.local and not self.user_object.local:
@ -160,20 +169,15 @@ class UserBlocks(ActivityMixin, UserRelationship):
status = 'blocks' status = 'blocks'
activity_serializer = activitypub.Block activity_serializer = activitypub.Block
def save(self, *args, **kwargs):
@receiver(models.signals.post_save, sender=UserBlocks)
#pylint: disable=unused-argument
def execute_after_save(sender, instance, created, *args, **kwargs):
''' remove follow or follow request rels after a block is created ''' ''' remove follow or follow request rels after a block is created '''
super().save(*args, **kwargs)
UserFollows.objects.filter( UserFollows.objects.filter(
Q(user_subject=instance.user_subject, Q(user_subject=self.user_subject, user_object=self.user_object) | \
user_object=instance.user_object) | \ Q(user_subject=self.user_object, user_object=self.user_subject)
Q(user_subject=instance.user_object,
user_object=instance.user_subject)
).delete() ).delete()
UserFollowRequest.objects.filter( UserFollowRequest.objects.filter(
Q(user_subject=instance.user_subject, Q(user_subject=self.user_subject, user_object=self.user_object) | \
user_object=instance.user_object) | \ Q(user_subject=self.user_object, user_object=self.user_subject)
Q(user_subject=instance.user_object,
user_object=instance.user_subject)
).delete() ).delete()

View file

@ -6,11 +6,10 @@ from django.apps import apps
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from django.db import models from django.db import models
from django.dispatch import receiver
from django.utils import timezone from django.utils import timezone
from bookwyrm import activitypub from bookwyrm import activitypub
from bookwyrm.connectors import get_data from bookwyrm.connectors import get_data, ConnectorException
from bookwyrm.models.shelf import Shelf from bookwyrm.models.shelf import Shelf
from bookwyrm.models.status import Status, Review from bookwyrm.models.status import Status, Review
from bookwyrm.settings import DOMAIN from bookwyrm.settings import DOMAIN
@ -113,6 +112,16 @@ class User(OrderedCollectionPageMixin, AbstractUser):
activity_serializer = activitypub.Person activity_serializer = activitypub.Person
@classmethod
def viewer_aware_objects(cls, viewer):
''' the user queryset filtered for the context of the logged in user '''
queryset = cls.objects.filter(is_active=True)
if viewer.is_authenticated:
queryset = queryset.exclude(
blocks=viewer
)
return queryset
def to_outbox(self, filter_type=None, **kwargs): def to_outbox(self, filter_type=None, **kwargs):
''' an ordered collection of statuses ''' ''' an ordered collection of statuses '''
if filter_type: if filter_type:
@ -172,15 +181,23 @@ class User(OrderedCollectionPageMixin, AbstractUser):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
''' populate fields for new local users ''' ''' populate fields for new local users '''
# this user already exists, no need to populate fields created = not bool(self.id)
if not self.local and not re.match(regex.full_username, self.username): if not self.local and not re.match(regex.full_username, self.username):
# generate a username that uses the domain (webfinger format) # generate a username that uses the domain (webfinger format)
actor_parts = urlparse(self.remote_id) actor_parts = urlparse(self.remote_id)
self.username = '%s@%s' % (self.username, actor_parts.netloc) self.username = '%s@%s' % (self.username, actor_parts.netloc)
return super().save(*args, **kwargs) super().save(*args, **kwargs)
if self.id or not self.local: # this user already exists, no need to populate fields
return super().save(*args, **kwargs) if not created:
super().save(*args, **kwargs)
return
# this is a new remote user, we need to set their remote server field
if not self.local:
super().save(*args, **kwargs)
set_remote_server.delay(self.id)
return
# populate fields for local users # populate fields for local users
self.remote_id = 'https://%s/user/%s' % (DOMAIN, self.localname) self.remote_id = 'https://%s/user/%s' % (DOMAIN, self.localname)
@ -188,7 +205,32 @@ class User(OrderedCollectionPageMixin, AbstractUser):
self.shared_inbox = 'https://%s/inbox' % DOMAIN self.shared_inbox = 'https://%s/inbox' % DOMAIN
self.outbox = '%s/outbox' % self.remote_id self.outbox = '%s/outbox' % self.remote_id
return super().save(*args, **kwargs) # an id needs to be set before we can proceed with related models
super().save(*args, **kwargs)
# create keys and shelves for new local users
self.key_pair = KeyPair.objects.create(
remote_id='%s/#main-key' % self.remote_id)
self.save(broadcast=False)
shelves = [{
'name': 'To Read',
'identifier': 'to-read',
}, {
'name': 'Currently Reading',
'identifier': 'reading',
}, {
'name': 'Read',
'identifier': 'read',
}]
for shelf in shelves:
Shelf(
name=shelf['name'],
identifier=shelf['identifier'],
user=self,
editable=False
).save(broadcast=False)
@property @property
def local_path(self): def local_path(self):
@ -280,42 +322,6 @@ class AnnualGoal(BookWyrmModel):
finish_date__year__gte=self.year).count() finish_date__year__gte=self.year).count()
@receiver(models.signals.post_save, sender=User)
#pylint: disable=unused-argument
def execute_after_save(sender, instance, created, *args, **kwargs):
''' create shelves for new users '''
if not created:
return
if not instance.local:
set_remote_server.delay(instance.id)
return
instance.key_pair = KeyPair.objects.create(
remote_id='%s/#main-key' % instance.remote_id)
instance.save(broadcast=False)
shelves = [{
'name': 'To Read',
'identifier': 'to-read',
}, {
'name': 'Currently Reading',
'identifier': 'reading',
}, {
'name': 'Read',
'identifier': 'read',
}]
for shelf in shelves:
Shelf(
name=shelf['name'],
identifier=shelf['identifier'],
user=instance,
editable=False
).save(broadcast=False)
@app.task @app.task
def set_remote_server(user_id): def set_remote_server(user_id):
''' figure out the user's remote server in the background ''' ''' figure out the user's remote server in the background '''
@ -323,7 +329,7 @@ def set_remote_server(user_id):
actor_parts = urlparse(user.remote_id) actor_parts = urlparse(user.remote_id)
user.federated_server = \ user.federated_server = \
get_or_create_remote_server(actor_parts.netloc) get_or_create_remote_server(actor_parts.netloc)
user.save() user.save(broadcast=False)
if user.bookwyrm_user: if user.bookwyrm_user:
get_remote_reviews.delay(user.outbox) get_remote_reviews.delay(user.outbox)
@ -337,19 +343,24 @@ def get_or_create_remote_server(domain):
except FederatedServer.DoesNotExist: except FederatedServer.DoesNotExist:
pass pass
try:
data = get_data('https://%s/.well-known/nodeinfo' % domain) data = get_data('https://%s/.well-known/nodeinfo' % domain)
try: try:
nodeinfo_url = data.get('links')[0].get('href') nodeinfo_url = data.get('links')[0].get('href')
except (TypeError, KeyError): except (TypeError, KeyError):
return None raise ConnectorException()
data = get_data(nodeinfo_url) data = get_data(nodeinfo_url)
application_type = data.get('software', {}).get('name')
application_version = data.get('software', {}).get('version')
except ConnectorException:
application_type = application_version = None
server = FederatedServer.objects.create( server = FederatedServer.objects.create(
server_name=domain, server_name=domain,
application_type=data['software']['name'], application_type=application_type,
application_version=data['software']['version'], application_version=application_version,
) )
return server return server

View file

@ -100,9 +100,6 @@
.cover-container.is-medium { .cover-container.is-medium {
height: 100px; height: 100px;
} }
.cover-container.is-small {
height: 70px;
}
} }
.cover-container.is-medium .no-cover div { .cover-container.is-medium .no-cover div {

View file

@ -1,16 +0,0 @@
{% extends 'layout.html' %}
{% block content %}
<div class="columns">
<div class="column block">
{% include 'snippets/about.html' %}
</div>
<div class="column block">
<h2 class="title">Code of Conduct</h2>
<div class="content">
{{ site.code_of_conduct | safe }}
</div>
</div>
</div>
{% endblock %}

View file

@ -2,7 +2,7 @@
{% load bookwyrm_tags %} {% load bookwyrm_tags %}
{% block content %} {% block content %}
<div class="block"> <div class="block">
<div class="columns"> <div class="columns is-mobile">
<div class="column"> <div class="column">
<h1 class="title">{{ author.name }}</h1> <h1 class="title">{{ author.name }}</h1>
</div> </div>

View file

@ -4,7 +4,7 @@
{% block content %} {% block content %}
<div class="block"> <div class="block">
<div class="columns"> <div class="columns is-mobile">
<div class="column"> <div class="column">
<h1 class="title"> <h1 class="title">
{{ book.title }}{% if book.subtitle %}: {{ book.title }}{% if book.subtitle %}:
@ -42,26 +42,10 @@
<h3 class="title is-6 mb-1">Add cover</h3> <h3 class="title is-6 mb-1">Add cover</h3>
<form name="add-cover" method="POST" action="/upload-cover/{{ book.id }}" enctype="multipart/form-data"> <form name="add-cover" method="POST" action="/upload-cover/{{ book.id }}" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
<div class="field has-addons"> <label class="label">
<div class="control"> <input type="file" name="cover" accept="image/*" enctype="multipart/form-data" id="id_cover" required>
<div class="file is-small mb-1">
<label class="file-label">
<input class="file-input" type="file" name="cover" accept="image/*" enctype="multipart/form-data" id="id_cover" required>
<span class="file-cta">
<span class="file-icon">
<i class="fas fa-upload"></i>
</span>
<span class="file-label">
Choose file...
</span>
</span>
</label> </label>
</div>
</div>
<div class="control">
<button class="button is-small is-primary" type="submit">Add</button> <button class="button is-small is-primary" type="submit">Add</button>
</div>
</div>
</form> </form>
</div> </div>
{% endif %} {% endif %}
@ -242,7 +226,7 @@
<div class="block" id="reviews"> <div class="block" id="reviews">
{% for review in reviews %} {% for review in reviews %}
<div class="block"> <div class="block">
{% include 'snippets/status.html' with status=review hide_book=True depth=1 %} {% include 'snippets/status/status.html' with status=review hide_book=True depth=1 %}
</div> </div>
{% endfor %} {% endfor %}

View file

@ -0,0 +1,22 @@
{% extends 'layout.html' %}
{% block content %}
<header class="block has-text-centered">
<h1 class="title">{{ site.name }}</h1>
<h2 class="subtitle">{{ site.instance_tagline }}</h2>
</header>
{% include 'discover/icons.html' %}
<section class="block">
{% include 'snippets/about.html' %}
</section>
<div class="block">
<h2 class="title">Code of Conduct</h2>
<div class="content">
{{ site.code_of_conduct | safe }}
</div>
</div>
{% endblock %}

View file

@ -1,33 +1,14 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% block content %} {% block content %}
{% if not request.user.is_authenticated %}
<header class="block has-text-centered"> <header class="block has-text-centered">
<h1 class="title">{{ site.name }}</h1> <h1 class="title">{{ site.name }}</h1>
<h2 class="subtitle">{{ site.instance_tagline }}</h2> <h2 class="subtitle">{{ site.instance_tagline }}</h2>
</header> </header>
<section class="level is-mobile"> {% include 'discover/icons.html' %}
<div class="level-item has-text-centered">
<div>
<p class="title has-text-weight-normal"><span class="icon icon-graphic-paperplane"></span></p>
<p class="heading">Decentralized</p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="title has-text-weight-normal"><span class="icon icon-graphic-heart"></span></p>
<p class="heading">Friendly</p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="title has-text-weight-normal"><span class="icon icon-graphic-banknote"></span></p>
<p class="heading">Anti-Corporate</p>
</div>
</div>
</section>
{% if not request.user.is_authenticated %}
<section class="tile is-ancestor"> <section class="tile is-ancestor">
<div class="tile is-7 is-parent"> <div class="tile is-7 is-parent">
<div class="tile is-child box"> <div class="tile is-child box">
@ -49,10 +30,6 @@
</div> </div>
</section> </section>
{% else %}
<div class="block">
<h1 class="title has-text-centered">Discover</h1>
</div>
{% endif %} {% endif %}
<div class="block is-hidden-tablet"> <div class="block is-hidden-tablet">
@ -63,18 +40,18 @@
<div class="tile is-vertical"> <div class="tile is-vertical">
<div class="tile is-parent"> <div class="tile is-parent">
<div class="tile is-child box has-background-white-ter"> <div class="tile is-child box has-background-white-ter">
{% include 'snippets/discover/large-book.html' with book=books.0 %} {% include 'discover/large-book.html' with book=books.0 %}
</div> </div>
</div> </div>
<div class="tile"> <div class="tile">
<div class="tile is-parent is-6"> <div class="tile is-parent is-6">
<div class="tile is-child box has-background-white-ter"> <div class="tile is-child box has-background-white-ter">
{% include 'snippets/discover/small-book.html' with book=books.1 %} {% include 'discover/small-book.html' with book=books.1 %}
</div> </div>
</div> </div>
<div class="tile is-parent is-6"> <div class="tile is-parent is-6">
<div class="tile is-child box has-background-white-ter"> <div class="tile is-child box has-background-white-ter">
{% include 'snippets/discover/small-book.html' with book=books.2 %} {% include 'discover/small-book.html' with book=books.2 %}
</div> </div>
</div> </div>
</div> </div>
@ -83,18 +60,18 @@
<div class="tile"> <div class="tile">
<div class="tile is-parent is-6"> <div class="tile is-parent is-6">
<div class="tile is-child box has-background-white-ter"> <div class="tile is-child box has-background-white-ter">
{% include 'snippets/discover/small-book.html' with book=books.3 %} {% include 'discover/small-book.html' with book=books.3 %}
</div> </div>
</div> </div>
<div class="tile is-parent is-6"> <div class="tile is-parent is-6">
<div class="tile is-child box has-background-white-ter"> <div class="tile is-child box has-background-white-ter">
{% include 'snippets/discover/small-book.html' with book=books.4 %} {% include 'discover/small-book.html' with book=books.4 %}
</div> </div>
</div> </div>
</div> </div>
<div class="tile is-parent"> <div class="tile is-parent">
<div class="tile is-child box has-background-white-ter"> <div class="tile is-child box has-background-white-ter">
{% include 'snippets/discover/large-book.html' with book=books.5 %} {% include 'discover/large-book.html' with book=books.5 %}
</div> </div>
</div> </div>
</div> </div>

View file

@ -0,0 +1,21 @@
<section class="level is-mobile">
<div class="level-item has-text-centered">
<div>
<p class="title has-text-weight-normal"><span class="icon icon-graphic-paperplane"></span></p>
<p class="heading">Decentralized</p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="title has-text-weight-normal"><span class="icon icon-graphic-heart"></span></p>
<p class="heading">Friendly</p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="title has-text-weight-normal"><span class="icon icon-graphic-banknote"></span></p>
<p class="heading">Anti-Corporate</p>
</div>
</div>
</section>

View file

@ -2,7 +2,7 @@
{% if book %} {% if book %}
<div class="columns"> <div class="columns">
<div class="column is-narrow"> <div class="column is-narrow">
{% include 'snippets/book_cover.html' with book=book size="large" %} <a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=book size="large" %}</a>
{% include 'snippets/stars.html' with rating=ratings|dict_key:book.id %} {% include 'snippets/stars.html' with rating=ratings|dict_key:book.id %}
</div> </div>
<div class="column"> <div class="column">

View file

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

View file

@ -16,7 +16,7 @@
{% endif %} {% endif %}
{% for activity in activities %} {% for activity in activities %}
<div class="block"> <div class="block">
{% include 'snippets/status.html' with status=activity %} {% include 'snippets/status/status.html' with status=activity %}
</div> </div>
{% endfor %} {% endfor %}

View file

@ -32,7 +32,7 @@
{% endif %} {% endif %}
{% for activity in activities %} {% for activity in activities %}
<div class="block"> <div class="block">
{% include 'snippets/status.html' with status=activity %} {% include 'snippets/status/status.html' with status=activity %}
</div> </div>
{% endfor %} {% endfor %}

View file

@ -8,7 +8,7 @@
{% endwith %} {% endwith %}
{% endif %} {% endif %}
{% include 'snippets/status.html' with status=status main=is_root %} {% include 'snippets/status/status.html' with status=status main=is_root %}
{% if depth <= max_depth and direction >= 0 %} {% if depth <= max_depth and direction >= 0 %}
{% for reply in status|replies %} {% for reply in status|replies %}

View file

@ -50,7 +50,7 @@
<div class="column is-narrow"> <div class="column is-narrow">
<div class="box"> <div class="box">
<a href="{{ book.book.local_path }}"> <a href="{{ book.book.local_path }}">
{% include 'snippets/discover/small-book.html' with book=book.book rating=goal.ratings %} {% include 'discover/small-book.html' with book=book.book rating=goal.ratings %}
</a> </a>
</div> </div>
</div> </div>

View file

@ -19,7 +19,7 @@
{% for item in items %} {% for item in items %}
<li class="block pb-3"> <li class="block pb-3">
<div class="card"> <div class="card">
<div class="card-content columns p-0 mb-0"> <div class="card-content columns p-0 mb-0 is-mobile">
<div class="column is-narrow pt-0 pb-0"> <div class="column is-narrow pt-0 pb-0">
<a href="{{ item.book.local_path }}">{% include 'snippets/book_cover.html' with book=item.book size="medium" %}</a> <a href="{{ item.book.local_path }}">{% include 'snippets/book_cover.html' with book=item.book size="medium" %}</a>
</div> </div>
@ -73,7 +73,7 @@
{% endif %} {% endif %}
{% for book in suggested_books %} {% for book in suggested_books %}
{% if book %} {% if book %}
<div class="block columns"> <div class="block columns is-mobile">
<div class="column is-narrow"> <div class="column is-narrow">
<a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=book size="small" %}</a> <a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=book size="small" %}</a>
</div> </div>

View file

@ -2,7 +2,7 @@
{% load bookwyrm_tags %} {% load bookwyrm_tags %}
{% block content %} {% block content %}
<header class="columns content"> <header class="columns content is-mobile">
<div class="column"> <div class="column">
<h1 class="title">{{ list.name }} <span class="subtitle">{% include 'snippets/privacy-icons.html' with item=list %}</span></h1> <h1 class="title">{{ list.name }} <span class="subtitle">{% include 'snippets/privacy-icons.html' with item=list %}</span></h1>
<p class="subtitle help">Created {% if list.curation != 'open' %} and curated{% endif %} by {% include 'snippets/username.html' with user=list.user %}</p> <p class="subtitle help">Created {% if list.curation != 'open' %} and curated{% endif %} by {% include 'snippets/username.html' with user=list.user %}</p>

View file

@ -5,7 +5,7 @@
<h1 class="title">Lists</h1> <h1 class="title">Lists</h1>
</header> </header>
{% if request.user.is_authenticated and not lists.has_previous %} {% if request.user.is_authenticated and not lists.has_previous %}
<header class="block columns"> <header class="block columns is-mobile">
<div class="column"> <div class="column">
<h2 class="title">Your lists</h2> <h2 class="title">Your lists</h2>
</div> </div>

View file

@ -2,28 +2,6 @@
{% block header %}Invites{% endblock %} {% block header %}Invites{% endblock %}
{% load humanize %} {% load humanize %}
{% block panel %} {% block panel %}
<section class="block">
<table class="table is-striped">
<tr>
<th>Link</th>
<th>Expires</th>
<th>Max uses</th>
<th>Times used</th>
</tr>
{% if not invites %}
<tr><td colspan="4">No active invites</td></tr>
{% endif %}
{% for invite in invites %}
<tr>
<td><a href="{{ invite.link }}">{{ invite.link }}</td>
<td>{{ invite.expiry|naturaltime }}</td>
<td>{{ invite.use_limit }}</td>
<td>{{ invite.times_used }}</td>
</tr>
{% endfor %}
</table>
</section>
<section class="block"> <section class="block">
<h2 class="title is-4">Generate New Invite</h2> <h2 class="title is-4">Generate New Invite</h2>
@ -47,4 +25,27 @@
<button class="button is-primary" type="submit">Create Invite</button> <button class="button is-primary" type="submit">Create Invite</button>
</form> </form>
</section> </section>
<section class="block">
<table class="table is-striped">
<tr>
<th>Link</th>
<th>Expires</th>
<th>Max uses</th>
<th>Times used</th>
</tr>
{% if not invites %}
<tr><td colspan="4">No active invites</td></tr>
{% endif %}
{% for invite in invites %}
<tr>
<td><a href="{{ invite.link }}">{{ invite.link }}</td>
<td>{{ invite.expiry|naturaltime }}</td>
<td>{{ invite.use_limit }}</td>
<td>{{ invite.times_used }}</td>
</tr>
{% endfor %}
</table>
{% include 'snippets/pagination.html' with page=invites path=request.path %}
</section>
{% endblock %} {% endblock %}

View file

@ -11,7 +11,7 @@
</form> </form>
<form name="unboost" action="/unboost/{{ status.id }}" method="post" class="interaction boost-{{ status.id }}-{{ uuid }} active {% if not request.user|boosted:status %}hidden{% endif %}" data-id="boost-{{ status.id }}-{{ uuid }}"> <form name="unboost" action="/unboost/{{ status.id }}" method="post" class="interaction boost-{{ status.id }}-{{ uuid }} active {% if not request.user|boosted:status %}hidden{% endif %}" data-id="boost-{{ status.id }}-{{ uuid }}">
{% csrf_token %} {% csrf_token %}
<button class="button is-small is-success" type="submit"> <button class="button is-small is-primary" type="submit">
<span class="icon icon-boost" title="Un-boost status"> <span class="icon icon-boost" title="Un-boost status">
<span class="is-sr-only">Un-boost status</span> <span class="is-sr-only">Un-boost status</span>
</span> </span>

View file

@ -10,7 +10,7 @@
</form> </form>
<form name="unfavorite" action="/unfavorite/{{ status.id }}" method="POST" class="interaction fav-{{ status.id }}-{{ uuid }} active {% if not request.user|liked:status %}hidden{% endif %}" data-id="fav-{{ status.id }}-{{ uuid }}"> <form name="unfavorite" action="/unfavorite/{{ status.id }}" method="POST" class="interaction fav-{{ status.id }}-{{ uuid }} active {% if not request.user|liked:status %}hidden{% endif %}" data-id="fav-{{ status.id }}-{{ uuid }}">
{% csrf_token %} {% csrf_token %}
<button class="button is-success is-small" type="submit"> <button class="button is-primary is-small" type="submit">
<span class="icon icon-heart" title="Un-like status"> <span class="icon icon-heart" title="Un-like status">
<span class="is-sr-only">Un-like status</span> <span class="is-sr-only">Un-like status</span>
</span> </span>

View file

@ -5,8 +5,12 @@
Follow request already sent. Follow request already sent.
</div> </div>
{% elif user in request.user.blocks.all %}
{% include 'snippets/block_button.html' %}
{% else %} {% else %}
<div class="field has-addons">
<div class="control">
<form action="/follow/" method="POST" class="interaction follow-{{ user.id }} {% if request.user in user.followers.all %}hidden{%endif %}" data-id="follow-{{ user.id }}"> <form action="/follow/" method="POST" class="interaction follow-{{ user.id }} {% if request.user in user.followers.all %}hidden{%endif %}" data-id="follow-{{ user.id }}">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="user" value="{{ user.username }}"> <input type="hidden" name="user" value="{{ user.username }}">
@ -21,4 +25,9 @@ Follow request already sent.
<input type="hidden" name="user" value="{{ user.username }}"> <input type="hidden" name="user" value="{{ user.username }}">
<button class="button is-small is-danger is-light" type="submit">Unfollow</button> <button class="button is-small is-danger is-light" type="submit">Unfollow</button>
</form> </form>
</div>
<div class="control">
{% include 'snippets/user_options.html' with user=user class="is-small" %}
</div>
</div>
{% endif %} {% endif %}

View file

@ -9,7 +9,7 @@
<input type="hidden" name="privacy" value="public"> <input type="hidden" name="privacy" value="public">
<input type="hidden" name="rating" value="{{ forloop.counter }}"> <input type="hidden" name="rating" value="{{ forloop.counter }}">
<div class="field is-grouped stars form-rate-stars mb-1"> <div class="field is-grouped stars form-rate-stars mb-1 has-text-warning-dark">
<label class="is-sr-only" for="rating-no-rating-{{ book.id }}">No rating</label> <label class="is-sr-only" for="rating-no-rating-{{ book.id }}">No rating</label>
<input class="is-sr-only" type="radio" name="rating" value="" id="rating-no-rating-{{ book.id }}" checked> <input class="is-sr-only" type="radio" name="rating" value="" id="rating-no-rating-{{ book.id }}" checked>
{% for i in '12345'|make_list %} {% for i in '12345'|make_list %}

View file

@ -1,6 +1,7 @@
{% load humanize %} {% load humanize %}
{% load bookwyrm_tags %} {% load bookwyrm_tags %}
{% if books|length > 0 %} {% if books|length > 0 %}
<div class="table-container">
<table class="table is-striped is-fullwidth"> <table class="table is-striped is-fullwidth">
<tr class="book-preview"> <tr class="book-preview">
@ -74,6 +75,7 @@
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
</div>
{% else %} {% else %}
<p>This shelf is empty.</p> <p>This shelf is empty.</p>
{% if shelf.editable %} {% if shelf.editable %}

View file

@ -1,7 +1,7 @@
<p class="stars"> <p class="stars">
<span class="is-sr-only">{% if rating %}{{ rating|floatformat }} star{{ rating|floatformat | pluralize }}{% else %}No rating{% endif %}</span> <span class="is-sr-only">{% if rating %}{{ rating|floatformat }} star{{ rating|floatformat | pluralize }}{% else %}No rating{% endif %}</span>
{% for i in '12345'|make_list %} {% for i in '12345'|make_list %}
<span class="icon icon-star-{% if rating >= forloop.counter %}full{% elif rating|floatformat:0 >= forloop.counter|floatformat:0 %}half{% else %}empty{% endif %}" aria-hidden="true"> <span class="icon is-small mr-1 icon-star-{% if rating >= forloop.counter %}full{% elif rating|floatformat:0 >= forloop.counter|floatformat:0 %}half{% else %}empty{% endif %}" aria-hidden="true">
</span> </span>
{% endfor %} {% endfor %}
</p> </p>

View file

@ -3,6 +3,7 @@
<div class="column is-narrow"> <div class="column is-narrow">
<div> <div>
<a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=book %}</a> <a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=book %}</a>
{% include 'snippets/stars.html' with rating=book|rating:request.user %}
{% include 'snippets/shelve_button/shelve_button.html' with book=book %} {% include 'snippets/shelve_button/shelve_button.html' with book=book %}
</div> </div>
</div> </div>

View file

@ -4,8 +4,8 @@
{% include 'snippets/avatar.html' with user=status.user %} {% include 'snippets/avatar.html' with user=status.user %}
{% include 'snippets/username.html' with user=status.user %} {% include 'snippets/username.html' with user=status.user %}
boosted boosted
{% include 'snippets/status_body.html' with status=status|boosted_status %} {% include 'snippets/status/status_body.html' with status=status|boosted_status %}
{% else %} {% else %}
{% include 'snippets/status_body.html' with status=status %} {% include 'snippets/status/status_body.html' with status=status %}
{% endif %} {% endif %}
{% endif %} {% endif %}

View file

@ -5,13 +5,13 @@
{% block card-header %} {% block card-header %}
<h3 class="card-header-title has-background-white-ter is-block"> <h3 class="card-header-title has-background-white-ter is-block">
{% include 'snippets/status_header.html' with status=status %} {% include 'snippets/status/status_header.html' with status=status %}
</h3> </h3>
{% endblock %} {% endblock %}
{% block card-content %} {% block card-content %}
{% include 'snippets/status_content.html' with status=status %} {% include 'snippets/status/status_content.html' with status=status %}
{% endblock %} {% endblock %}
@ -55,7 +55,7 @@
<a href="{{ status.remote_id }}">{{ status.published_date | post_date }}</a> <a href="{{ status.remote_id }}">{{ status.published_date | post_date }}</a>
</div> </div>
<div class="card-footer-item"> <div class="card-footer-item">
{% include 'snippets/status_options.html' with class="is-small" right=True %} {% include 'snippets/status/status_options.html' with class="is-small" right=True %}
</div> </div>
{% endblock %} {% endblock %}

View file

@ -54,9 +54,9 @@
{% if status.book or status.mention_books.count %} {% if status.book or status.mention_books.count %}
<div class="{% if status.status_type != 'GeneratedNote' %}box has-background-white-bis{% endif %}"> <div class="{% if status.status_type != 'GeneratedNote' %}box has-background-white-bis{% endif %}">
{% if status.book %} {% if status.book %}
{% include 'snippets/book_preview.html' with book=status.book %} {% include 'snippets/status/book_preview.html' with book=status.book %}
{% elif status.mention_books.count %} {% elif status.mention_books.count %}
{% include 'snippets/book_preview.html' with book=status.mention_books.first %} {% include 'snippets/status/book_preview.html' with book=status.mention_books.first %}
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}

View file

@ -15,19 +15,15 @@
<div class="block"> <div class="block">
<h2 class="title">Followers</h2> <h2 class="title">Followers</h2>
{% for followers in followers %} {% for followers in followers %}
<div class="block"> <div class="block columns">
<div class="field is-grouped"> <div class="column">
<div class="control">
{% include 'snippets/avatar.html' with user=followers %} {% include 'snippets/avatar.html' with user=followers %}
</div>
<div class="control">
{% include 'snippets/username.html' with user=followers show_full=True %} {% include 'snippets/username.html' with user=followers show_full=True %}
</div> </div>
<div class="control"> <div class="column is-narrow">
{% include 'snippets/follow_button.html' with user=followers %} {% include 'snippets/follow_button.html' with user=followers %}
</div> </div>
</div> </div>
</div>
{% endfor %} {% endfor %}
{% if not followers.count %} {% if not followers.count %}
<div>{{ user|username }} has no followers</div> <div>{{ user|username }} has no followers</div>

View file

@ -15,19 +15,15 @@
<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 %}
<div class="block"> <div class="block columns">
<div class="field is-grouped"> <div class="column">
<div class="control">
{% include 'snippets/avatar.html' with user=follower %} {% include 'snippets/avatar.html' with user=follower %}
</div>
<div class="control">
{% include 'snippets/username.html' with user=follower show_full=True %} {% include 'snippets/username.html' with user=follower show_full=True %}
</div> </div>
<div class="control"> <div class="column">
{% include 'snippets/follow_button.html' with user=follower %} {% include 'snippets/follow_button.html' with user=follower %}
</div> </div>
</div> </div>
</div>
{% endfor %} {% endfor %}
{% if not following.count %} {% if not following.count %}
<div>{{ user|username }} isn't following any users</div> <div>{{ user|username }} isn't following any users</div>

View file

@ -1,7 +1,7 @@
{% extends 'user/user_layout.html' %} {% extends 'user/user_layout.html' %}
{% block header %} {% block header %}
<div class="columns"> <div class="columns is-mobile">
<div class="column"> <div class="column">
<h1 class="title"> <h1 class="title">
{% if is_self %}Your {% if is_self %}Your

View file

@ -38,7 +38,7 @@
{% include 'user/create_shelf_form.html' with controls_text='create-shelf-form' %} {% include 'user/create_shelf_form.html' with controls_text='create-shelf-form' %}
</div> </div>
<div class="block columns"> <div class="block columns is-mobile">
<div class="column"> <div class="column">
<h2 class="title is-3"> <h2 class="title is-3">
{{ shelf.name }} {{ shelf.name }}

View file

@ -1,7 +1,7 @@
{% extends 'user/user_layout.html' %} {% extends 'user/user_layout.html' %}
{% block header %} {% block header %}
<div class="columns"> <div class="columns is-mobile">
<div class="column"> <div class="column">
<h1 class="title">User profile</h1> <h1 class="title">User profile</h1>
</div> </div>
@ -54,7 +54,7 @@
{% endif %} {% endif %}
<div> <div>
<div class="columns"> <div class="columns is-mobile">
<h2 class="title column">User Activity</h2> <h2 class="title column">User Activity</h2>
<div class="column is-narrow"> <div class="column is-narrow">
<a class="icon icon-rss" target="_blank" href="{{ user.local_path }}/rss"> <a class="icon icon-rss" target="_blank" href="{{ user.local_path }}/rss">
@ -64,7 +64,7 @@
</div> </div>
{% for activity in activities %} {% for activity in activities %}
<div class="block" id="feed"> <div class="block" id="feed">
{% include 'snippets/status.html' with status=activity %} {% include 'snippets/status/status.html' with status=activity %}
</div> </div>
{% endfor %} {% endfor %}
{% if not activities %} {% if not activities %}

View file

@ -43,14 +43,7 @@
</div> </div>
</div> </div>
{% if not is_self and request.user.is_authenticated %} {% 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 %} {% include 'snippets/follow_button.html' with user=user %}
</div>
<div class="control">
{% include 'snippets/user_options.html' with user=user class="is-small" %}
</div>
</div>
{% endif %} {% endif %}
{% if is_self and user.follower_requests.all %} {% if is_self and user.follower_requests.all %}

View file

@ -1,16 +1,18 @@
''' testing models ''' ''' testing models '''
from unittest.mock import patch from unittest.mock import patch
from django.test import TestCase from django.test import TestCase
import responses
from bookwyrm import models from bookwyrm import models
from bookwyrm.settings import DOMAIN from bookwyrm.settings import DOMAIN
# pylint: disable=missing-class-docstring
# pylint: disable=missing-function-docstring
class User(TestCase): class User(TestCase):
def setUp(self): def setUp(self):
self.user = models.User.objects.create_user( self.user = models.User.objects.create_user(
'mouse@%s' % DOMAIN, 'mouse@mouse.mouse', 'mouseword', 'mouse@%s' % DOMAIN, 'mouse@mouse.mouse', 'mouseword',
local=True, localname='mouse', name='hi') local=True, localname='mouse', name='hi', bookwyrm_user=False)
def test_computed_fields(self): def test_computed_fields(self):
''' username instead of id here ''' ''' username instead of id here '''
@ -28,7 +30,7 @@ class User(TestCase):
with patch('bookwyrm.models.user.set_remote_server.delay'): with patch('bookwyrm.models.user.set_remote_server.delay'):
user = models.User.objects.create_user( user = models.User.objects.create_user(
'rat', 'rat@rat.rat', 'ratword', local=False, 'rat', 'rat@rat.rat', 'ratword', local=False,
remote_id='https://example.com/dfjkg') remote_id='https://example.com/dfjkg', bookwyrm_user=False)
self.assertEqual(user.username, 'rat@example.com') self.assertEqual(user.username, 'rat@example.com')
@ -62,7 +64,7 @@ class User(TestCase):
self.assertEqual(activity['name'], self.user.name) self.assertEqual(activity['name'], self.user.name)
self.assertEqual(activity['inbox'], self.user.inbox) self.assertEqual(activity['inbox'], self.user.inbox)
self.assertEqual(activity['outbox'], self.user.outbox) self.assertEqual(activity['outbox'], self.user.outbox)
self.assertEqual(activity['bookwyrmUser'], True) self.assertEqual(activity['bookwyrmUser'], False)
self.assertEqual(activity['discoverable'], True) self.assertEqual(activity['discoverable'], True)
self.assertEqual(activity['type'], 'Person') self.assertEqual(activity['type'], 'Person')
@ -71,3 +73,83 @@ class User(TestCase):
self.assertEqual(activity['type'], 'OrderedCollection') self.assertEqual(activity['type'], 'OrderedCollection')
self.assertEqual(activity['id'], self.user.outbox) self.assertEqual(activity['id'], self.user.outbox)
self.assertEqual(activity['totalItems'], 0) self.assertEqual(activity['totalItems'], 0)
def test_set_remote_server(self):
server = models.FederatedServer.objects.create(
server_name=DOMAIN,
application_type='test type',
application_version=3
)
models.user.set_remote_server(self.user.id)
self.user.refresh_from_db()
self.assertEqual(self.user.federated_server, server)
@responses.activate
def test_get_or_create_remote_server(self):
responses.add(
responses.GET,
'https://%s/.well-known/nodeinfo' % DOMAIN,
json={'links': [{'href': 'http://www.example.com'}, {}]}
)
responses.add(
responses.GET,
'http://www.example.com',
json={'software': {'name': 'hi', 'version': '2'}},
)
server = models.user.get_or_create_remote_server(DOMAIN)
self.assertEqual(server.server_name, DOMAIN)
self.assertEqual(server.application_type, 'hi')
self.assertEqual(server.application_version, '2')
@responses.activate
def test_get_or_create_remote_server_no_wellknown(self):
responses.add(
responses.GET,
'https://%s/.well-known/nodeinfo' % DOMAIN,
status=404
)
server = models.user.get_or_create_remote_server(DOMAIN)
self.assertEqual(server.server_name, DOMAIN)
self.assertIsNone(server.application_type)
self.assertIsNone(server.application_version)
@responses.activate
def test_get_or_create_remote_server_no_links(self):
responses.add(
responses.GET,
'https://%s/.well-known/nodeinfo' % DOMAIN,
json={'links': [{'href': 'http://www.example.com'}, {}]}
)
responses.add(
responses.GET,
'http://www.example.com',
status=404
)
server = models.user.get_or_create_remote_server(DOMAIN)
self.assertEqual(server.server_name, DOMAIN)
self.assertIsNone(server.application_type)
self.assertIsNone(server.application_version)
@responses.activate
def test_get_or_create_remote_server_unknown_format(self):
responses.add(
responses.GET,
'https://%s/.well-known/nodeinfo' % DOMAIN,
json={'links': [{'href': 'http://www.example.com'}, {}]}
)
responses.add(
responses.GET,
'http://www.example.com',
json={'fish': 'salmon'}
)
server = models.user.get_or_create_remote_server(DOMAIN)
self.assertEqual(server.server_name, DOMAIN)
self.assertIsNone(server.application_type)
self.assertIsNone(server.application_version)

View file

@ -1,5 +1,6 @@
''' test for app action functionality ''' ''' test for app action functionality '''
from unittest.mock import patch from unittest.mock import patch
from django.utils import timezone
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
@ -30,6 +31,7 @@ class GoalViews(TestCase):
) )
self.anonymous_user = AnonymousUser self.anonymous_user = AnonymousUser
self.anonymous_user.is_authenticated = False self.anonymous_user.is_authenticated = False
models.SiteSettings.objects.create()
def test_goal_page_no_goal(self): def test_goal_page_no_goal(self):
@ -48,6 +50,7 @@ class GoalViews(TestCase):
request.user = self.local_user request.user = self.local_user
result = view(request, self.local_user.localname, 2020) result = view(request, self.local_user.localname, 2020)
result.render()
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
@ -62,16 +65,23 @@ class GoalViews(TestCase):
def test_goal_page_public(self): def test_goal_page_public(self):
''' view a user's public goal ''' ''' view a user's public goal '''
models.ReadThrough.objects.create(
finish_date=timezone.now(),
user=self.local_user,
book=self.book,
)
models.AnnualGoal.objects.create( models.AnnualGoal.objects.create(
user=self.local_user, user=self.local_user,
year=2020, year=timezone.now().year,
goal=128937123, goal=128937123,
privacy='public') privacy='public')
view = views.Goal.as_view() view = views.Goal.as_view()
request = self.factory.get('') request = self.factory.get('')
request.user = self.rat request.user = self.rat
result = view(request, self.local_user.localname, 2020) result = view(request, self.local_user.localname, timezone.now().year)
result.render()
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
def test_goal_page_private(self): def test_goal_page_private(self):

View file

@ -56,12 +56,14 @@ class ViewsHelpers(TestCase):
def test_get_user_from_username(self): def test_get_user_from_username(self):
''' works for either localname or username ''' ''' works for either localname or username '''
self.assertEqual( self.assertEqual(
views.helpers.get_user_from_username('mouse'), self.local_user) views.helpers.get_user_from_username(
self.local_user, 'mouse'), self.local_user)
self.assertEqual( self.assertEqual(
views.helpers.get_user_from_username( views.helpers.get_user_from_username(
'mouse@local.com'), self.local_user) self.local_user, 'mouse@local.com'), self.local_user)
with self.assertRaises(models.User.DoesNotExist): with self.assertRaises(models.User.DoesNotExist):
views.helpers.get_user_from_username('mojfse@example.com') views.helpers.get_user_from_username(
self.local_user, 'mojfse@example.com')
def test_is_api_request(self): def test_is_api_request(self):
@ -188,18 +190,18 @@ class ViewsHelpers(TestCase):
def test_is_bookwyrm_request(self): def test_is_bookwyrm_request(self):
''' checks if a request came from a bookwyrm instance ''' ''' checks if a request came from a bookwyrm instance '''
request = self.factory.get('', {'q': 'Test Book'}) request = self.factory.get('', {'q': 'Test Book'})
self.assertFalse(views.helpers.is_bookworm_request(request)) self.assertFalse(views.helpers.is_bookwyrm_request(request))
request = self.factory.get( request = self.factory.get(
'', {'q': 'Test Book'}, '', {'q': 'Test Book'},
HTTP_USER_AGENT=\ HTTP_USER_AGENT=\
"http.rb/4.4.1 (Mastodon/3.3.0; +https://mastodon.social/)" "http.rb/4.4.1 (Mastodon/3.3.0; +https://mastodon.social/)"
) )
self.assertFalse(views.helpers.is_bookworm_request(request)) self.assertFalse(views.helpers.is_bookwyrm_request(request))
request = self.factory.get( request = self.factory.get(
'', {'q': 'Test Book'}, HTTP_USER_AGENT=USER_AGENT) '', {'q': 'Test Book'}, HTTP_USER_AGENT=USER_AGENT)
self.assertTrue(views.helpers.is_bookworm_request(request)) self.assertTrue(views.helpers.is_bookwyrm_request(request))
def test_existing_user(self): def test_existing_user(self):

View file

@ -7,6 +7,7 @@ from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
from bookwyrm import models, views from bookwyrm import models, views
from bookwyrm.settings import USER_AGENT
# pylint: disable=too-many-public-methods # pylint: disable=too-many-public-methods
@ -90,3 +91,39 @@ class OutboxView(TestCase):
data = json.loads(result.content) data = json.loads(result.content)
self.assertEqual(data['type'], 'OrderedCollection') self.assertEqual(data['type'], 'OrderedCollection')
self.assertEqual(data['totalItems'], 1) self.assertEqual(data['totalItems'], 1)
def test_outbox_bookwyrm_request_true(self):
''' should differentiate between bookwyrm and outside requests '''
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
models.Review.objects.create(
name='hi',
content='look at this',
user=self.local_user,
book=self.book,
privacy='public',
)
request = self.factory.get('', {'page': 1}, HTTP_USER_AGENT=USER_AGENT)
result = views.Outbox.as_view()(request, 'mouse')
data = json.loads(result.content)
self.assertEqual(len(data['orderedItems']), 1)
self.assertEqual(data['orderedItems'][0]['type'], 'Review')
def test_outbox_bookwyrm_request_false(self):
''' should differentiate between bookwyrm and outside requests '''
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
models.Review.objects.create(
name='hi',
content='look at this',
user=self.local_user,
book=self.book,
privacy='public',
)
request = self.factory.get('', {'page': 1})
result = views.Outbox.as_view()(request, 'mouse')
data = json.loads(result.content)
self.assertEqual(len(data['orderedItems']), 1)
self.assertEqual(data['orderedItems'][0]['type'], 'Article')

View file

@ -41,6 +41,7 @@ class RssFeedView(TestCase):
''' load an rss feed ''' ''' load an rss feed '''
view = rss_feed.RssFeed() view = rss_feed.RssFeed()
request = self.factory.get('/user/rss_user/rss') request = self.factory.get('/user/rss_user/rss')
request.user = self.user
with patch("bookwyrm.models.SiteSettings.objects.get") as site: with patch("bookwyrm.models.SiteSettings.objects.get") as site:
site.return_value = self.site site.return_value = self.site
result = view(request, username=self.user.username) result = view(request, username=self.user.username)

View file

@ -13,7 +13,7 @@ from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.settings import PAGE_LENGTH from bookwyrm.settings import PAGE_LENGTH
from .helpers import get_activity_feed from .helpers import get_activity_feed
from .helpers import get_user_from_username from .helpers import get_user_from_username
from .helpers import is_api_request, is_bookworm_request, object_visible_to_user from .helpers import is_api_request, is_bookwyrm_request, object_visible_to_user
# pylint: disable= no-self-use # pylint: disable= no-self-use
@ -65,7 +65,7 @@ class DirectMessage(View):
user = None user = None
if username: if username:
try: try:
user = get_user_from_username(username) user = get_user_from_username(request.user, username)
except models.User.DoesNotExist: except models.User.DoesNotExist:
pass pass
if user: if user:
@ -91,7 +91,7 @@ class Status(View):
def get(self, request, username, status_id): def get(self, request, username, status_id):
''' display a particular status (and replies, etc) ''' ''' display a particular status (and replies, etc) '''
try: try:
user = get_user_from_username(username) user = get_user_from_username(request.user, username)
status = models.Status.objects.select_subclasses().get( status = models.Status.objects.select_subclasses().get(
id=status_id, deleted=False) id=status_id, deleted=False)
except ValueError: except ValueError:
@ -107,7 +107,7 @@ class Status(View):
if is_api_request(request): if is_api_request(request):
return ActivitypubResponse( return ActivitypubResponse(
status.to_activity(pure=not is_bookworm_request(request))) status.to_activity(pure=not is_bookwyrm_request(request)))
data = {**feed_page_data(request.user), **{ data = {**feed_page_data(request.user), **{
'title': 'Status by %s' % user.username, 'title': 'Status by %s' % user.username,

View file

@ -14,7 +14,7 @@ def follow(request):
''' follow another user, here or abroad ''' ''' follow another user, here or abroad '''
username = request.POST['user'] username = request.POST['user']
try: try:
to_follow = get_user_from_username(username) to_follow = get_user_from_username(request.user, username)
except models.User.DoesNotExist: except models.User.DoesNotExist:
return HttpResponseBadRequest() return HttpResponseBadRequest()
@ -35,7 +35,7 @@ def unfollow(request):
''' unfollow a user ''' ''' unfollow a user '''
username = request.POST['user'] username = request.POST['user']
try: try:
to_unfollow = get_user_from_username(username) to_unfollow = get_user_from_username(request.user, username)
except models.User.DoesNotExist: except models.User.DoesNotExist:
return HttpResponseBadRequest() return HttpResponseBadRequest()
@ -52,7 +52,7 @@ def accept_follow_request(request):
''' a user accepts a follow request ''' ''' a user accepts a follow request '''
username = request.POST['user'] username = request.POST['user']
try: try:
requester = get_user_from_username(username) requester = get_user_from_username(request.user, username)
except models.User.DoesNotExist: except models.User.DoesNotExist:
return HttpResponseBadRequest() return HttpResponseBadRequest()
@ -75,7 +75,7 @@ def delete_follow_request(request):
''' a user rejects a follow request ''' ''' a user rejects a follow request '''
username = request.POST['user'] username = request.POST['user']
try: try:
requester = get_user_from_username(username) requester = get_user_from_username(request.user, username)
except models.User.DoesNotExist: except models.User.DoesNotExist:
return HttpResponseBadRequest() return HttpResponseBadRequest()

View file

@ -18,7 +18,7 @@ class Goal(View):
''' track books for the year ''' ''' track books for the year '''
def get(self, request, username, year): def get(self, request, username, year):
''' reading goal page ''' ''' reading goal page '''
user = get_user_from_username(username) user = get_user_from_username(request.user, username)
year = int(year) year = int(year)
goal = models.AnnualGoal.objects.filter( goal = models.AnnualGoal.objects.filter(
year=year, user=user year=year, user=user
@ -42,7 +42,7 @@ class Goal(View):
def post(self, request, username, year): def post(self, request, username, year):
''' update or create an annual goal ''' ''' update or create an annual goal '''
user = get_user_from_username(username) user = get_user_from_username(request.user, username)
if user != request.user: if user != request.user:
return HttpResponseNotFound() return HttpResponseNotFound()

View file

@ -9,13 +9,13 @@ from bookwyrm.status import create_generated_note
from bookwyrm.utils import regex from bookwyrm.utils import regex
def get_user_from_username(username): def get_user_from_username(viewer, username):
''' helper function to resolve a localname or a username to a user ''' ''' helper function to resolve a localname or a username to a user '''
# raises DoesNotExist if user is now found # raises DoesNotExist if user is now found
try: try:
return models.User.objects.get(localname=username) return models.User.viewer_aware_objects(viewer).get(localname=username)
except models.User.DoesNotExist: except models.User.DoesNotExist:
return models.User.objects.get(username=username) return models.User.viewer_aware_objects(viewer).get(username=username)
def is_api_request(request): def is_api_request(request):
@ -24,8 +24,8 @@ def is_api_request(request):
request.path[-5:] == '.json' request.path[-5:] == '.json'
def is_bookworm_request(request): def is_bookwyrm_request(request):
''' check if the request is coming from another bookworm instance ''' ''' check if the request is coming from another bookwyrm instance '''
user_agent = request.headers.get('User-Agent') user_agent = request.headers.get('User-Agent')
if user_agent is None or \ if user_agent is None or \
re.search(regex.bookwyrm_user_agent, user_agent) is None: re.search(regex.bookwyrm_user_agent, user_agent) is None:

View file

@ -1,5 +1,6 @@
''' invites when registration is closed ''' ''' invites when registration is closed '''
from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.decorators import login_required, permission_required
from django.core.paginator import Paginator
from django.http import HttpResponseBadRequest 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.template.response import TemplateResponse
@ -7,6 +8,7 @@ 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
# pylint: disable= no-self-use # pylint: disable= no-self-use
@ -18,10 +20,18 @@ class ManageInvites(View):
''' create invites ''' ''' create invites '''
def get(self, request): def get(self, request):
''' invite management page ''' ''' invite management page '''
try:
page = int(request.GET.get('page', 1))
except ValueError:
page = 1
paginated = Paginator(models.SiteInvite.objects.filter(
user=request.user
).order_by('-created_date'), PAGE_LENGTH)
data = { data = {
'title': 'Invitations', 'title': 'Invitations',
'invites': models.SiteInvite.objects.filter( 'invites': paginated.page(page),
user=request.user).order_by('-created_date'),
'form': forms.CreateInviteForm(), 'form': forms.CreateInviteForm(),
} }
return TemplateResponse(request, 'settings/manage_invites.html', data) return TemplateResponse(request, 'settings/manage_invites.html', data)
@ -36,7 +46,15 @@ class ManageInvites(View):
invite.user = request.user invite.user = request.user
invite.save() invite.save()
return redirect('/settings/invites') paginated = Paginator(models.SiteInvite.objects.filter(
user=request.user
).order_by('-created_date'), PAGE_LENGTH)
data = {
'title': 'Invitations',
'invites': paginated.page(1),
'form': form
}
return TemplateResponse(request, 'settings/manage_invites.html', data)
class Invite(View): class Invite(View):

View file

@ -16,7 +16,7 @@ class About(View):
data = { data = {
'title': 'About', 'title': 'About',
} }
return TemplateResponse(request, 'about.html', data) return TemplateResponse(request, 'discover/about.html', data)
class Home(View): class Home(View):
''' discover page or home feed depending on auth ''' ''' discover page or home feed depending on auth '''
@ -56,4 +56,4 @@ class Discover(View):
'books': list(set(books)), 'books': list(set(books)),
'ratings': ratings 'ratings': ratings
} }
return TemplateResponse(request, 'discover.html', data) return TemplateResponse(request, 'discover/discover.html', data)

View file

@ -65,7 +65,7 @@ class UserLists(View):
page = int(request.GET.get('page', 1)) page = int(request.GET.get('page', 1))
except ValueError: except ValueError:
page = 1 page = 1
user = get_user_from_username(username) user = get_user_from_username(request.user, username)
lists = models.List.objects.filter(user=user).all() lists = models.List.objects.filter(user=user).all()
lists = privacy_filter( lists = privacy_filter(
request.user, lists, ['public', 'followers', 'unlisted']) request.user, lists, ['public', 'followers', 'unlisted'])

View file

@ -4,6 +4,7 @@ from django.shortcuts import get_object_or_404
from django.views import View from django.views import View
from bookwyrm import activitypub, models from bookwyrm import activitypub, models
from .helpers import is_bookwyrm_request
# pylint: disable= no-self-use # pylint: disable= no-self-use
@ -17,6 +18,10 @@ class Outbox(View):
filter_type = None filter_type = None
return JsonResponse( return JsonResponse(
user.to_outbox(**request.GET, filter_type=filter_type), user.to_outbox(
**request.GET,
filter_type=filter_type,
pure=not is_bookwyrm_request(request)
),
encoder=activitypub.ActivityEncoder encoder=activitypub.ActivityEncoder
) )

View file

@ -11,7 +11,7 @@ class RssFeed(Feed):
def get_object(self, request, username): def get_object(self, request, username):
''' the user who's posts get serialized ''' ''' the user who's posts get serialized '''
return get_user_from_username(username) return get_user_from_username(request.user, username)
def link(self, obj): def link(self, obj):

View file

@ -33,7 +33,7 @@ class Search(View):
handle_remote_webfinger(query) handle_remote_webfinger(query)
# do a user search # do a user search
user_results = models.User.objects.annotate( user_results = models.User.viewer_aware_objects(request.user).annotate(
similarity=Greatest( similarity=Greatest(
TrigramSimilarity('username', query), TrigramSimilarity('username', query),
TrigramSimilarity('localname', query), TrigramSimilarity('localname', query),

View file

@ -19,7 +19,7 @@ class Shelf(View):
def get(self, request, username, shelf_identifier): def get(self, request, username, shelf_identifier):
''' display a shelf ''' ''' display a shelf '''
try: try:
user = get_user_from_username(username) user = get_user_from_username(request.user, username)
except models.User.DoesNotExist: except models.User.DoesNotExist:
return HttpResponseNotFound() return HttpResponseNotFound()

View file

@ -26,7 +26,7 @@ class User(View):
def get(self, request, username): def get(self, request, username):
''' profile page for a user ''' ''' profile page for a user '''
try: try:
user = get_user_from_username(username) user = get_user_from_username(request.user, username)
except models.User.DoesNotExist: except models.User.DoesNotExist:
return HttpResponseNotFound() return HttpResponseNotFound()
@ -96,7 +96,7 @@ class Followers(View):
def get(self, request, username): def get(self, request, username):
''' list of followers ''' ''' list of followers '''
try: try:
user = get_user_from_username(username) user = get_user_from_username(request.user, username)
except models.User.DoesNotExist: except models.User.DoesNotExist:
return HttpResponseNotFound() return HttpResponseNotFound()
@ -121,7 +121,7 @@ class Following(View):
def get(self, request, username): def get(self, request, username):
''' list of followers ''' ''' list of followers '''
try: try:
user = get_user_from_username(username) user = get_user_from_username(request.user, username)
except models.User.DoesNotExist: except models.User.DoesNotExist:
return HttpResponseNotFound() return HttpResponseNotFound()