Merge branch 'main' into production

This commit is contained in:
Mouse Reeve 2020-11-07 19:02:05 -08:00
commit 0574e36602
19 changed files with 194 additions and 223 deletions

View file

@ -1,10 +1,15 @@
FROM python:3 FROM python:3.9
ENV PYTHONUNBUFFERED 1
ENV PYTHONUNBUFFERED 1
RUN mkdir /app RUN mkdir /app
RUN mkdir /app/static RUN mkdir /app/static
RUN mkdir /app/images RUN mkdir /app/images
WORKDIR /app WORKDIR /app
COPY requirements.txt /app/ COPY requirements.txt /app/
RUN pip install -r requirements.txt RUN pip install -r requirements.txt
COPY ./bookwyrm /app COPY ./bookwyrm /app
COPY ./celerywyrm /app COPY ./celerywyrm /app

View file

@ -76,11 +76,12 @@ class ActivityObject:
if not isinstance(self, model.activity_serializer): if not isinstance(self, model.activity_serializer):
raise TypeError('Wrong activity type for model') raise TypeError('Wrong activity type for model')
# check for an existing instance # check for an existing instance, if we're not updating a known obj
try: if not instance:
return model.objects.get(remote_id=self.id) try:
except model.DoesNotExist: return model.objects.get(remote_id=self.id)
pass except model.DoesNotExist:
pass
model_fields = [m.name for m in model._meta.get_fields()] model_fields = [m.name for m in model._meta.get_fields()]
mapped_fields = {} mapped_fields = {}

View file

@ -269,7 +269,12 @@ def handle_favorite(activity):
@app.task @app.task
def handle_unfavorite(activity): def handle_unfavorite(activity):
''' approval of your good good post ''' ''' approval of your good good post '''
like = activitypub.Like(**activity['object']).to_model(models.Favorite) try:
like = models.Favorite.objects.filter(
remote_id=activity['object']['id']
).first()
except models.Favorite.DoesNotExist:
return
like.delete() like.delete()
@ -294,7 +299,7 @@ def handle_unboost(activity):
remote_id=activity['object']['id'] remote_id=activity['object']['id']
).first() ).first()
if boost: if boost:
status_builder.delete_status(boost) boost.delete()
@app.task @app.task

View file

@ -299,7 +299,8 @@ def handle_unfavorite(user, status):
# can't find that status, idk # can't find that status, idk
return return
fav_activity = activitypub.Undo(actor=user, object=favorite) fav_activity = favorite.to_undo_activity(user)
favorite.delete()
broadcast(user, fav_activity, direct_recipients=[status.user]) broadcast(user, fav_activity, direct_recipients=[status.user])
@ -319,6 +320,17 @@ def handle_boost(user, status):
broadcast(user, boost_activity) broadcast(user, boost_activity)
def handle_unboost(user, status):
''' a user regrets boosting a status '''
boost = models.Boost.objects.filter(
boosted_status=status, user=user
).first()
activity = boost.to_undo_activity(user)
boost.delete()
broadcast(user, activity)
def handle_update_book(user, book): def handle_update_book(user, book):
''' broadcast the news about our book ''' ''' broadcast the news about our book '''
broadcast(user, book.to_update_activity(user)) broadcast(user, book.to_update_activity(user))

View file

@ -1,6 +1,6 @@
{% load fr_display %} {% load fr_display %}
{% with activity.id|uuid as uuid %} {% with status.id|uuid as uuid %}
<form name="boost" action="/boost/{{ activity.id }}" method="post" onsubmit="return interact(event)" class="boost-{{ status.id }} {% if request.user|boosted:status %}hidden{% endif %}" data-id="boost-{{ status.id }}-{{ uuid }}"> <form name="boost" action="/boost/{{ status.id }}" method="post" onsubmit="return interact(event)" class="boost-{{ status.id }}-{{ uuid }} {% if request.user|boosted:status %}hidden{% endif %}" data-id="boost-{{ status.id }}-{{ uuid }}">
{% csrf_token %} {% csrf_token %}
<button class="button is-small" type="submit"> <button class="button is-small" type="submit">
<span class="icon icon-boost"> <span class="icon icon-boost">
@ -8,7 +8,7 @@
</span> </span>
</button> </button>
</form> </form>
<form name="unboost" action="/unboost/{{ activity.id }}" method="post" onsubmit="return interact(event)" class="boost-{{ status.id }} active {% if not request.user|boosted:status %}hidden{% endif %}" data-id="boost-{{ status.id }}-{{ uuid }}"> <form name="unboost" action="/unboost/{{ status.id }}" method="post" onsubmit="return interact(event)" class="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-success" type="submit">
<span class="icon icon-boost"> <span class="icon icon-boost">

View file

@ -23,7 +23,7 @@
{% endfor %} {% endfor %}
</div> </div>
{% endif %} {% endif %}
<textarea name="{% if type == 'quote' %}quote{% else %}content{% endif %}" class="textarea" id="id_content_{{ book.id }}_{{ type }}" placeholder="{{ placeholder }}" required></textarea> <textarea name="{% if type == 'quote' %}quote{% else %}content{% endif %}" class="textarea" id="id_quote_{{ book.id }}_{{ type }}" placeholder="{{ placeholder }}" required></textarea>
</div> </div>
{% if type == 'quote' %} {% if type == 'quote' %}

View file

@ -1,6 +1,6 @@
{% load fr_display %} {% load fr_display %}
{% with activity.id|uuid as uuid %} {% with status.id|uuid as uuid %}
<form name="favorite" action="/favorite/{{ activity.id }}" method="POST" onsubmit="return interact(event)" class="fav-{{ status.id }} {% if request.user|liked:status %}hidden{% endif %}" data-id="fav-{{ status.id }}-{{ uuid }}"> <form name="favorite" action="/favorite/{{ status.id }}" method="POST" onsubmit="return interact(event)" class="fav-{{ status.id }}-{{ uuid }} {% if request.user|liked:status %}hidden{% endif %}" data-id="fav-{{ status.id }}-{{ uuid }}">
{% csrf_token %} {% csrf_token %}
<button class="button is-small" type="submit"> <button class="button is-small" type="submit">
<span class="icon icon-heart"> <span class="icon icon-heart">
@ -8,7 +8,7 @@
</span> </span>
</button> </button>
</form> </form>
<form name="unfavorite" action="/unfavorite/{{ activity.id }}" method="POST" onsubmit="return interact(event)" class="fav-{{ status.id }} active {% if not request.user|liked:status %}hidden{% endif %}" data-id="fav-{{ status.id }}-{{ uuid }}"> <form name="unfavorite" action="/unfavorite/{{ status.id }}" method="POST" onsubmit="return interact(event)" class="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-success is-small" type="submit">
<span class="icon icon-heart"> <span class="icon icon-heart">

View file

@ -1,142 +1,11 @@
{% load humanize %}
{% load fr_display %} {% load fr_display %}
{% if not status.deleted %} {% if not status.deleted %}
<div class="card"> {% if status.status_type == 'Boost' %}
<header class="card-header">
<div class="card-header-title">
<div class="columns">
<div class="column is-narrow">
{% if status.status_type == 'Boost' %}
{% include 'snippets/avatar.html' with user=status.user %}
{% include 'snippets/username.html' with user=status.user %}
boosted
</div>
<div class="column">
{% include 'snippets/status_header.html' with status=status|boosted_status %}
{% else %}
{% include 'snippets/status_header.html' with status=status %}
{% endif %}
</div>
</div>
</div>
</header>
<div class="card-content">
{% if status.status_type == 'Boost' %}
{% include 'snippets/status_content.html' with status=status|boosted_status %}
{% else %}
{% include 'snippets/status_content.html' with status=status %}
{% endif %}
</div>
<footer>
{% if request.user.is_authenticated %}
<input class="toggle-control" type="checkbox" name="show-comment-{{ status.id }}" id="show-comment-{{ status.id }}">
<div class="toggle-content hidden">
<div class="card-footer">
<div class="card-footer-item">
{% if status.status_type == 'Boost' %}
{% include 'snippets/reply_form.html' with status=status|boosted_status %}
{% else %}
{% include 'snippets/reply_form.html' with status=status %}
{% endif %}
</div>
</div>
</div>
{% endif %}
<div class="card-footer">
<div class="card-footer-item">
{% if request.user.is_authenticated %}
<label class="button is-small" for="show-comment-{{ status.id }}">
<span class="icon icon-comment"><span class="is-sr-only">Comment</span></span>
</label>
{% if status.status_type == 'Boost' %}
{% include 'snippets/boost_button.html' with status=status|boosted_status %}
{% include 'snippets/fav_button.html' with status=status|boosted_status %}
{% else %}
{% include 'snippets/boost_button.html' with status=status %}
{% include 'snippets/fav_button.html' with status=status %}
{% endif %}
{% else %}
<a href="/login">
<span class="icon icon-comment">
<span class="is-sr-only">Comment</span>
</span>
<span class="icon icon-boost">
<span class="is-sr-only">Boost status</span>
</span>
<span class="icon icon-heart">
<span class="is-sr-only">Like status</span>
</span>
</a>
{% endif %}
</div>
<div class="card-footer-item">
{% if status.privacy == 'public' %}
<span class="icon icon-globe">
<span class="is-sr-only">Public post</span>
</span>
{% elif status.privacy == 'unlisted' %}
<span class="icon icon-unlock">
<span class="is-sr-only">Unlisted post</span>
</span>
{% elif status.privacy == 'followers' %}
<span class="icon icon-lock">
<span class="is-sr-only">Followers-only post</span>
</span>
{% else %}
<span class="icon icon-envelope">
<span class="is-sr-only">Private post</span>
</span>
{% endif %}
</div>
<div class="card-footer-item">
<a href="{{ status.remote_id }}">{{ status.published_date | post_date }}</a>
</div>
{% if status.user == request.user %}
<div class="card-footer-item">
<label class="button" for="more-info-{{ status.id }}">
<div class="icon icon-dots-three">
<span class="is-sr-only">More options</span>
</div>
</label>
</div>
{% endif %}
</div>
<div>
<input class="toggle-control" type="checkbox" name="more-info-{{ status.id }}" id="more-info-{{ status.id }}">
<div class="toggle-content hidden card-footer">
{% if status.user == request.user %}
<div class="card-footer-item">
<form name="delete-{{status.id}}" action="/delete-status" method="post">
{% csrf_token %}
<input type="hidden" name="status" value="{{ status.id }}">
<button class="button is-danger" type="submit">
Delete post
</button>
</form>
</div>
{% endif %}
</div>
</div>
</footer>
</div>
{% else %}
<div class="card">
<header class="card-header">
<p>
{% 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 %}
deleted this status boosted
</p> {% include 'snippets/status_body.html' with status=status|boosted_status %}
</header> {% else %}
</div> {% include 'snippets/status_body.html' with status=status %}
{% endif %}
{% endif %} {% endif %}

View file

@ -0,0 +1,120 @@
{% load fr_display %}
{% load humanize %}
{% if not status.deleted %}
<div class="card">
<header class="card-header">
<div class="card-header-title">
<div class="columns">
<div class="column is-narrow">
{% include 'snippets/status_header.html' with status=status %}
</div>
</div>
</div>
</header>
<div class="card-content">
{% include 'snippets/status_content.html' with status=status %}
</div>
<footer>
{% if request.user.is_authenticated %}
<input class="toggle-control" type="checkbox" name="show-comment-{{ status.id }}" id="show-comment-{{ status.id }}">
<div class="toggle-content hidden">
<div class="card-footer">
<div class="card-footer-item">
{% include 'snippets/reply_form.html' with status=status %}
</div>
</div>
</div>
{% endif %}
<div class="card-footer">
<div class="card-footer-item">
{% if request.user.is_authenticated %}
<label class="button is-small" for="show-comment-{{ status.id }}">
<span class="icon icon-comment"><span class="is-sr-only">Comment</span></span>
</label>
{% include 'snippets/boost_button.html' with status=status %}
{% include 'snippets/fav_button.html' with status=status %}
{% else %}
<a href="/login">
<span class="icon icon-comment">
<span class="is-sr-only">Comment</span>
</span>
<span class="icon icon-boost">
<span class="is-sr-only">Boost status</span>
</span>
<span class="icon icon-heart">
<span class="is-sr-only">Like status</span>
</span>
</a>
{% endif %}
</div>
<div class="card-footer-item">
{% if status.privacy == 'public' %}
<span class="icon icon-globe">
<span class="is-sr-only">Public post</span>
</span>
{% elif status.privacy == 'unlisted' %}
<span class="icon icon-unlock">
<span class="is-sr-only">Unlisted post</span>
</span>
{% elif status.privacy == 'followers' %}
<span class="icon icon-lock">
<span class="is-sr-only">Followers-only post</span>
</span>
{% else %}
<span class="icon icon-envelope">
<span class="is-sr-only">Private post</span>
</span>
{% endif %}
</div>
<div class="card-footer-item">
<a href="{{ status.remote_id }}">{{ status.published_date | post_date }}</a>
</div>
{% if status.user == request.user %}
<div class="card-footer-item">
<label class="button" for="more-info-{{ status.id }}">
<div class="icon icon-dots-three">
<span class="is-sr-only">More options</span>
</div>
</label>
</div>
{% endif %}
</div>
<div>
<input class="toggle-control" type="checkbox" name="more-info-{{ status.id }}" id="more-info-{{ status.id }}">
<div class="toggle-content hidden card-footer">
{% if status.user == request.user %}
<div class="card-footer-item">
<form name="delete-{{status.id}}" action="/delete-status" method="post">
{% csrf_token %}
<input type="hidden" name="status" value="{{ status.id }}">
<button class="button is-danger" type="submit">
Delete post
</button>
</form>
</div>
{% endif %}
</div>
</div>
</footer>
</div>
{% else %}
<div class="card">
<header class="card-header">
<p>
{% include 'snippets/avatar.html' with user=status.user %}
{% include 'snippets/username.html' with user=status.user %}
deleted this status
</p>
</header>
</div>
{% endif %}

View file

@ -64,8 +64,10 @@ class SelfConnector(TestCase):
def test_search_default_filter(self): def test_search_default_filter(self):
self.edition.default = True ''' it should get rid of duplicate editions for the same work '''
self.edition.save() self.work.default_edition = self.edition
self.work.save()
results = self.connector.search('Anonymous') results = self.connector.search('Anonymous')
self.assertEqual(len(results), 1) self.assertEqual(len(results), 1)
self.assertEqual(results[0].title, 'Edition of Example Work') self.assertEqual(results[0].title, 'Edition of Example Work')

View file

@ -17,6 +17,7 @@ class Favorite(TestCase):
self.local_user = models.User.objects.create_user( self.local_user = models.User.objects.create_user(
'mouse', 'mouse@mouse.com', 'mouseword', 'mouse', 'mouse@mouse.com', 'mouseword',
remote_id='http://local.com/user/mouse') remote_id='http://local.com/user/mouse')
self.status = models.Status.objects.create( self.status = models.Status.objects.create(
user=self.local_user, user=self.local_user,
content='Test status', content='Test status',
@ -33,24 +34,13 @@ class Favorite(TestCase):
def test_handle_favorite(self): def test_handle_favorite(self):
activity = { activity = {
'@context': 'https://www.w3.org/ns/activitystreams', '@context': 'https://www.w3.org/ns/activitystreams',
'id': 'http://example.com/activity/1', 'id': 'http://example.com/fav/1',
'type': 'Create',
'actor': 'https://example.com/users/rat', 'actor': 'https://example.com/users/rat',
'published': 'Mon, 25 May 2020 19:31:20 GMT', 'published': 'Mon, 25 May 2020 19:31:20 GMT',
'to': ['https://example.com/user/rat/followers'], 'object': 'http://local.com/status/1',
'cc': ['https://www.w3.org/ns/activitystreams#Public'],
'object': {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': 'http://example.com/fav/1',
'type': 'Like',
'actor': 'https://example.com/users/rat',
'object': 'http://local.com/status/1',
},
'signature': {}
} }
result = incoming.handle_favorite(activity) incoming.handle_favorite(activity)
fav = models.Favorite.objects.get(remote_id='http://example.com/fav/1') fav = models.Favorite.objects.get(remote_id='http://example.com/fav/1')
self.assertEqual(fav.status, self.status) self.assertEqual(fav.status, self.status)

View file

@ -1,3 +1,4 @@
''' when a remote user changes their profile '''
import json import json
import pathlib import pathlib
from django.test import TestCase from django.test import TestCase

View file

@ -39,17 +39,8 @@ class Book(TestCase):
title='Invalid Book' title='Invalid Book'
) )
def test_default_edition(self):
''' a work should always be able to produce a deafult edition '''
self.assertIsInstance(self.work.default_edition, models.Edition)
self.assertEqual(self.work.default_edition, self.first_edition)
self.second_edition.default = True
self.second_edition.save()
self.assertEqual(self.work.default_edition, self.second_edition)
def test_isbn_10_to_13(self): def test_isbn_10_to_13(self):
''' checksums and so on '''
isbn_10 = '178816167X' isbn_10 = '178816167X'
isbn_13 = isbn_10_to_13(isbn_10) isbn_13 = isbn_10_to_13(isbn_10)
self.assertEqual(isbn_13, '9781788161671') self.assertEqual(isbn_13, '9781788161671')
@ -59,8 +50,8 @@ class Book(TestCase):
self.assertEqual(isbn_13, '9781788161671') self.assertEqual(isbn_13, '9781788161671')
def test_isbn_13_to_10(self): def test_isbn_13_to_10(self):
''' checksums and so on '''
isbn_13 = '9781788161671' isbn_13 = '9781788161671'
isbn_10 = isbn_13_to_10(isbn_13) isbn_10 = isbn_13_to_10(isbn_13)
self.assertEqual(isbn_10, '178816167X') self.assertEqual(isbn_10, '178816167X')

View file

@ -38,16 +38,6 @@ class Shelving(TestCase):
# make sure the book is on the shelf # make sure the book is on the shelf
self.assertEqual(shelf.books.get(), self.book) self.assertEqual(shelf.books.get(), self.book)
# it should have posted a status about this
status = models.GeneratedNote.objects.get()
self.assertEqual(status.content, 'wants to read')
self.assertEqual(status.user, self.user)
self.assertEqual(status.mention_books.count(), 1)
self.assertEqual(status.mention_books.first(), self.book)
# and it should not create a read-through
self.assertEqual(models.ReadThrough.objects.count(), 0)
def test_handle_shelve_reading(self): def test_handle_shelve_reading(self):
shelf = models.Shelf.objects.get(identifier='reading') shelf = models.Shelf.objects.get(identifier='reading')
@ -56,20 +46,6 @@ class Shelving(TestCase):
# make sure the book is on the shelf # make sure the book is on the shelf
self.assertEqual(shelf.books.get(), self.book) self.assertEqual(shelf.books.get(), self.book)
# it should have posted a status about this
status = models.GeneratedNote.objects.order_by('-published_date').first()
self.assertEqual(status.content, 'started reading')
self.assertEqual(status.user, self.user)
self.assertEqual(status.mention_books.count(), 1)
self.assertEqual(status.mention_books.first(), self.book)
# and it should create a read-through
readthrough = models.ReadThrough.objects.get()
self.assertEqual(readthrough.user, self.user)
self.assertEqual(readthrough.book.id, self.book.id)
self.assertIsNotNone(readthrough.start_date)
self.assertIsNone(readthrough.finish_date)
def test_handle_shelve_read(self): def test_handle_shelve_read(self):
shelf = models.Shelf.objects.get(identifier='read') shelf = models.Shelf.objects.get(identifier='read')
@ -78,20 +54,6 @@ class Shelving(TestCase):
# make sure the book is on the shelf # make sure the book is on the shelf
self.assertEqual(shelf.books.get(), self.book) self.assertEqual(shelf.books.get(), self.book)
# it should have posted a status about this
status = models.GeneratedNote.objects.order_by('-published_date').first()
self.assertEqual(status.content, 'finished reading')
self.assertEqual(status.user, self.user)
self.assertEqual(status.mention_books.count(), 1)
self.assertEqual(status.mention_books.first(), self.book)
# and it should update the existing read-through
readthrough = models.ReadThrough.objects.get()
self.assertEqual(readthrough.user, self.user)
self.assertEqual(readthrough.book.id, self.book.id)
self.assertIsNotNone(readthrough.start_date)
self.assertIsNotNone(readthrough.finish_date)
def test_handle_unshelve(self): def test_handle_unshelve(self):
self.shelf.books.add(self.book) self.shelf.books.add(self.book)

View file

@ -15,6 +15,8 @@ class Book(TestCase):
title='Example Edition', title='Example Edition',
parent_work=self.work parent_work=self.work
) )
self.work.default_edition = self.edition
self.work.save()
self.connector = models.Connector.objects.create( self.connector = models.Connector.objects.create(
identifier='test_connector', identifier='test_connector',

View file

@ -39,6 +39,7 @@ class Signature(TestCase):
) )
def send(self, signature, now, data, digest): def send(self, signature, now, data, digest):
''' test request '''
c = Client() c = Client()
return c.post( return c.post(
urlsplit(self.rat.inbox).path, urlsplit(self.rat.inbox).path,
@ -73,13 +74,13 @@ class Signature(TestCase):
def test_wrong_signature(self): def test_wrong_signature(self):
''' Messages must be signed by the right actor. ''' Messages must be signed by the right actor.
(cat cannot sign messages on behalf of mouse) (cat cannot sign messages on behalf of mouse) '''
'''
response = self.send_test_request(sender=self.mouse, signer=self.cat) response = self.send_test_request(sender=self.mouse, signer=self.cat)
self.assertEqual(response.status_code, 401) self.assertEqual(response.status_code, 401)
@responses.activate @responses.activate
def test_remote_signer(self): def test_remote_signer(self):
''' signtures for remote users '''
datafile = pathlib.Path(__file__).parent.joinpath('data/ap_user.json') datafile = pathlib.Path(__file__).parent.joinpath('data/ap_user.json')
data = json.loads(datafile.read_bytes()) data = json.loads(datafile.read_bytes())
data['id'] = self.fake_remote.remote_id data['id'] = self.fake_remote.remote_id
@ -138,7 +139,6 @@ class Signature(TestCase):
json=data, json=data,
status=200) status=200)
# Key correct: # Key correct:
response = self.send_test_request(sender=self.fake_remote) response = self.send_test_request(sender=self.fake_remote)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)

View file

@ -116,6 +116,7 @@ urlpatterns = [
re_path(r'^favorite/(?P<status_id>\d+)/?$', actions.favorite), re_path(r'^favorite/(?P<status_id>\d+)/?$', actions.favorite),
re_path(r'^unfavorite/(?P<status_id>\d+)/?$', actions.unfavorite), re_path(r'^unfavorite/(?P<status_id>\d+)/?$', actions.unfavorite),
re_path(r'^boost/(?P<status_id>\d+)/?$', actions.boost), re_path(r'^boost/(?P<status_id>\d+)/?$', actions.boost),
re_path(r'^unboost/(?P<status_id>\d+)/?$', actions.unboost),
re_path(r'^delete-status/?$', actions.delete_status), re_path(r'^delete-status/?$', actions.delete_status),

View file

@ -508,6 +508,7 @@ def unfavorite(request, status_id):
outgoing.handle_unfavorite(request.user, status) outgoing.handle_unfavorite(request.user, status)
return redirect(request.headers.get('Referer', '/')) return redirect(request.headers.get('Referer', '/'))
@login_required @login_required
def boost(request, status_id): def boost(request, status_id):
''' boost a status ''' ''' boost a status '''
@ -516,6 +517,14 @@ def boost(request, status_id):
return redirect(request.headers.get('Referer', '/')) return redirect(request.headers.get('Referer', '/'))
@login_required
def unboost(request, status_id):
''' boost a status '''
status = models.Status.objects.get(id=status_id)
outgoing.handle_unboost(request.user, status)
return redirect(request.headers.get('Referer', '/'))
@login_required @login_required
def delete_status(request): def delete_status(request):
''' delete and tombstone a status ''' ''' delete and tombstone a status '''

View file

@ -71,7 +71,8 @@ services:
- redis - redis
restart: on-failure restart: on-failure
flower: flower:
image: mher/flower build: .
command: flower --port=8888
env_file: .env env_file: .env
environment: environment:
- CELERY_BROKER_URL=${CELERY_BROKER} - CELERY_BROKER_URL=${CELERY_BROKER}