forked from mirrors/bookwyrm
Merge branch 'main' into inbox-refactor
This commit is contained in:
commit
cad19ee878
58 changed files with 472 additions and 317 deletions
30
README.md
30
README.md
|
@ -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`
|
||||||
|
|
|
@ -216,11 +216,7 @@ def get_data(url):
|
||||||
raise ConnectorException()
|
raise ConnectorException()
|
||||||
|
|
||||||
if not resp.ok:
|
if not resp.ok:
|
||||||
try:
|
raise ConnectorException()
|
||||||
resp.raise_for_status()
|
|
||||||
except requests.exceptions.HTTPError as e:
|
|
||||||
logger.exception(e)
|
|
||||||
raise ConnectorException()
|
|
||||||
try:
|
try:
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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():
|
||||||
# blocking in either direction is a no-go
|
raise IntegrityError()
|
||||||
UserBlocks.objects.get(
|
# blocking in either direction is a no-go
|
||||||
user_subject=self.user_subject,
|
if UserBlocks.objects.filter(
|
||||||
user_object=self.user_object,
|
Q(
|
||||||
)
|
user_subject=self.user_subject,
|
||||||
UserBlocks.objects.get(
|
user_object=self.user_object,
|
||||||
user_subject=self.user_object,
|
) | Q(
|
||||||
user_object=self.user_subject,
|
user_subject=self.user_object,
|
||||||
)
|
user_object=self.user_subject,
|
||||||
return
|
)
|
||||||
except (UserFollows.DoesNotExist, UserBlocks.DoesNotExist):
|
).exists():
|
||||||
pass
|
raise IntegrityError()
|
||||||
|
|
||||||
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):
|
||||||
|
''' remove follow or follow request rels after a block is created '''
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
@receiver(models.signals.post_save, sender=UserBlocks)
|
UserFollows.objects.filter(
|
||||||
#pylint: disable=unused-argument
|
Q(user_subject=self.user_subject, user_object=self.user_object) | \
|
||||||
def execute_after_save(sender, instance, created, *args, **kwargs):
|
Q(user_subject=self.user_object, user_object=self.user_subject)
|
||||||
''' remove follow or follow request rels after a block is created '''
|
).delete()
|
||||||
UserFollows.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,
|
).delete()
|
||||||
user_object=instance.user_subject)
|
|
||||||
).delete()
|
|
||||||
UserFollowRequest.objects.filter(
|
|
||||||
Q(user_subject=instance.user_subject,
|
|
||||||
user_object=instance.user_object) | \
|
|
||||||
Q(user_subject=instance.user_object,
|
|
||||||
user_object=instance.user_subject)
|
|
||||||
).delete()
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
data = get_data('https://%s/.well-known/nodeinfo' % domain)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
nodeinfo_url = data.get('links')[0].get('href')
|
data = get_data('https://%s/.well-known/nodeinfo' % domain)
|
||||||
except (TypeError, KeyError):
|
try:
|
||||||
return None
|
nodeinfo_url = data.get('links')[0].get('href')
|
||||||
|
except (TypeError, KeyError):
|
||||||
|
raise ConnectorException()
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
data = get_data(nodeinfo_url)
|
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 %}
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
<label class="file-label">
|
<button class="button is-small is-primary" type="submit">Add</button>
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="control">
|
|
||||||
<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 %}
|
||||||
|
|
||||||
|
|
22
bookwyrm/templates/discover/about.html
Normal file
22
bookwyrm/templates/discover/about.html
Normal 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 %}
|
|
@ -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>
|
21
bookwyrm/templates/discover/icons.html
Normal file
21
bookwyrm/templates/discover/icons.html
Normal 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>
|
||||||
|
|
|
@ -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">
|
|
@ -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 %}
|
|
@ -62,7 +62,7 @@
|
||||||
<div class="column is-narrow">
|
<div class="column is-narrow">
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<h2 class="title is-4">Cover</h2>
|
<h2 class="title is-4">Cover</h2>
|
||||||
<p>{{ form.cover }} </p>
|
<p>{{ form.cover }}</p>
|
||||||
{% for error in form.cover.errors %}
|
{% for error in form.cover.errors %}
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
<p class="help is-danger">{{ error | escape }}</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -5,20 +5,29 @@
|
||||||
Follow request already sent.
|
Follow request already sent.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% elif user in request.user.blocks.all %}
|
||||||
|
{% include 'snippets/block_button.html' %}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
||||||
<form action="/follow/" method="POST" class="interaction follow-{{ user.id }} {% if request.user in user.followers.all %}hidden{%endif %}" data-id="follow-{{ user.id }}">
|
<div class="field has-addons">
|
||||||
{% csrf_token %}
|
<div class="control">
|
||||||
<input type="hidden" name="user" value="{{ user.username }}">
|
<form action="/follow/" method="POST" class="interaction follow-{{ user.id }} {% if request.user in user.followers.all %}hidden{%endif %}" data-id="follow-{{ user.id }}">
|
||||||
{% if user.manually_approves_followers %}
|
{% csrf_token %}
|
||||||
<button class="button is-small is-link" type="submit">Send follow request</button>
|
<input type="hidden" name="user" value="{{ user.username }}">
|
||||||
{% else %}
|
{% if user.manually_approves_followers %}
|
||||||
<button class="button is-small is-link" type="submit">Follow</button>
|
<button class="button is-small is-link" type="submit">Send follow request</button>
|
||||||
{% endif %}
|
{% else %}
|
||||||
</form>
|
<button class="button is-small is-link" type="submit">Follow</button>
|
||||||
<form action="/unfollow/" method="POST" class="interaction follow-{{ user.id }} {% if not request.user in user.followers.all %}hidden{%endif %}" data-id="follow-{{ user.id }}">
|
{% endif %}
|
||||||
{% csrf_token %}
|
</form>
|
||||||
<input type="hidden" name="user" value="{{ user.username }}">
|
<form action="/unfollow/" method="POST" class="interaction follow-{{ user.id }} {% if not request.user in user.followers.all %}hidden{%endif %}" data-id="follow-{{ user.id }}">
|
||||||
<button class="button is-small is-danger is-light" type="submit">Unfollow</button>
|
{% csrf_token %}
|
||||||
</form>
|
<input type="hidden" name="user" value="{{ user.username }}">
|
||||||
|
<button class="button is-small is-danger is-light" type="submit">Unfollow</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="control">
|
||||||
|
{% include 'snippets/user_options.html' with user=user class="is-small" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
|
@ -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 %}
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
|
@ -15,17 +15,13 @@
|
||||||
<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 %}
|
{% include 'snippets/username.html' with user=followers show_full=True %}
|
||||||
</div>
|
</div>
|
||||||
<div class="control">
|
<div class="column is-narrow">
|
||||||
{% include 'snippets/username.html' with user=followers show_full=True %}
|
{% include 'snippets/follow_button.html' with user=followers %}
|
||||||
</div>
|
|
||||||
<div class="control">
|
|
||||||
{% include 'snippets/follow_button.html' with user=followers %}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -15,17 +15,13 @@
|
||||||
<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 %}
|
{% include 'snippets/username.html' with user=follower show_full=True %}
|
||||||
</div>
|
</div>
|
||||||
<div class="control">
|
<div class="column">
|
||||||
{% include 'snippets/username.html' with user=follower show_full=True %}
|
{% include 'snippets/follow_button.html' with user=follower %}
|
||||||
</div>
|
|
||||||
<div class="control">
|
|
||||||
{% include 'snippets/follow_button.html' with user=follower %}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 }}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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">
|
{% include 'snippets/follow_button.html' with user=user %}
|
||||||
<div class="control">
|
|
||||||
{% 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 %}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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'])
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue