Merge pull request #1949 from bookwyrm-social/admin-setup

Flow for creating the admin account on a new instance
This commit is contained in:
Mouse Reeve 2022-02-17 16:04:30 -08:00 committed by GitHub
commit ebf905e20b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 607 additions and 42 deletions

View file

@ -41,7 +41,7 @@ REDIS_BROKER_PASSWORD=redispassword123
# Monitoring for celery
FLOWER_PORT=8888
FLOWER_USER=mouse
FLOWER_USER=admin
FLOWER_PASSWORD=changeme
# Email config

View file

@ -54,6 +54,13 @@ class RegisterForm(CustomForm):
help_texts = {f: None for f in fields}
widgets = {"password": PasswordInput()}
def clean(self):
"""Check if the username is taken"""
cleaned_data = super().clean()
localname = cleaned_data.get("localname").strip()
if models.User.objects.filter(localname=localname).first():
self.add_error("localname", _("User with this username already exists"))
class RatingForm(CustomForm):
class Meta:

View file

@ -0,0 +1,23 @@
""" Get your admin code to allow install """
from django.core.management.base import BaseCommand
from bookwyrm import models
def get_admin_code():
"""get that code"""
return models.SiteSettings.objects.get().admin_code
class Command(BaseCommand):
"""command-line options"""
help = "Gets admin code for configuring BookWyrm"
# pylint: disable=unused-argument
def handle(self, *args, **options):
"""execute init"""
self.stdout.write("*******************************************")
self.stdout.write("Use this code to create your admin account:")
self.stdout.write(get_admin_code())
self.stdout.write("*******************************************")

View file

@ -10,7 +10,9 @@ class Command(BaseCommand):
help = "Generate preview images"
# pylint: disable=no-self-use
def add_arguments(self, parser):
"""options for how the command is run"""
parser.add_argument(
"--all",
"-a",
@ -38,6 +40,7 @@ class Command(BaseCommand):
preview_images.generate_site_preview_image_task.delay()
self.stdout.write(" OK 🖼")
# pylint: disable=consider-using-f-string
if options["all"]:
# Users
users = models.User.objects.filter(

View file

@ -120,6 +120,7 @@ def init_settings():
models.SiteSettings.objects.create(
support_link="https://www.patreon.com/bookwyrm",
support_title="Patreon",
install_mode=True,
)

View file

@ -0,0 +1,24 @@
# Generated by Django 3.2.12 on 2022-02-17 17:08
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0135_auto_20220217_1624"),
]
operations = [
migrations.AddField(
model_name="sitesettings",
name="admin_code",
field=models.CharField(default=uuid.uuid4, max_length=50),
),
migrations.AddField(
model_name="sitesettings",
name="install_mode",
field=models.BooleanField(default=False),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2022-02-17 19:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0136_auto_20220217_1708"),
]
operations = [
migrations.AlterField(
model_name="sitesettings",
name="allow_registration",
field=models.BooleanField(default=False),
),
]

View file

@ -1,6 +1,7 @@
""" the particulars for this instance of BookWyrm """
import datetime
from urllib.parse import urljoin
import uuid
from django.db import models, IntegrityError
from django.dispatch import receiver
@ -24,6 +25,10 @@ class SiteSettings(models.Model):
instance_description = models.TextField(default="This instance has no description.")
instance_short_description = models.CharField(max_length=255, blank=True, null=True)
# admin setup options
install_mode = models.BooleanField(default=False)
admin_code = models.CharField(max_length=50, default=uuid.uuid4)
# about page
registration_closed_text = models.TextField(
default="We aren't taking new users at this time. You can find an open "
@ -38,7 +43,7 @@ class SiteSettings(models.Model):
privacy_policy = models.TextField(default="Add a privacy policy here.")
# registration
allow_registration = models.BooleanField(default=True)
allow_registration = models.BooleanField(default=False)
allow_invite_requests = models.BooleanField(default=True)
require_confirm_email = models.BooleanField(default=True)

View file

@ -32,6 +32,7 @@
{% block head_links %}{% endblock %}
</head>
<body>
{% block body %}
<nav class="navbar" aria-label="main navigation">
<div class="container">
<div class="navbar-brand">
@ -208,11 +209,8 @@
<div class="section is-flex-grow-1">
<div class="container">
{# almost every view needs to know the user shelves #}
{% with request.user.shelf_set.all as user_shelves %}
{% block content %}
{% endblock %}
{% endwith %}
</div>
</div>
@ -256,6 +254,7 @@
</div>
</div>
</footer>
{% endblock %}
<script>
var csrf_token = '{{ csrf_token }}';

View file

@ -0,0 +1,61 @@
{% extends 'setup/layout.html' %}
{% load i18n %}
{% block header %}
<h1 class="title">{% trans "Set up BookWyrm" %}</h1>
<div class="subtitle">
{% trans "Your account as a user and an admin" %}
</div>
{% endblock %}
{% block panel %}
<div class="block content">
<h2 class="title is-4">{% trans "Create your account" %}</h2>
<div class="columns">
<div class="column is-half">
<div class="box has-background-primary-light">
<form name="register" method="post" action="{% url 'setup-admin' %}">
<div class="field">
<label class="label" for="id_admin_key">
{% trans "Admin key:" %}
</label>
<div class="control">
<input
type="password"
name="admin_key"
class="input"
id="id_admin_key"
aria-describedby="desc_admin_key"
required
>
<p class="help" id="desc_admin_key">
{% blocktrans trimmed %}
An admin key was created when you installed BookWyrm.
You can get your admin key by running <code>./bw-dev admin_code</code> from the command line on your server.
{% endblocktrans %}
</p>
</div>
</div>
{% include 'snippets/register_form.html' %}
</form>
</div>
</div>
<div class="column">
<p>
{% blocktrans trimmed %}
As an admin, you'll be able to configure the instance name and information, and moderate your instance.
This means you will have access to private information about your users, and are responsible for responding to reports of bad behavior or spam.
{% endblocktrans %}
</p>
<p>
{% trans "Once the instance is set up, you can promote other users to moderator or admin roles from the admin panel." %}
</p>
<p>
<a href="https://docs.joinbookwyrm.com/moderation.html" target="_blank">
{% trans "Learn more about moderation" %}
</a>
</p>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,154 @@
{% extends 'setup/layout.html' %}
{% load i18n %}
{% block header %}
<h1 class="title">{% trans "Instance Configuration" %}</h1>
<div class="subtitle">
{% trans "Make sure everything looks right before proceeding" %}
</div>
{% endblock %}
{% block panel %}
<div class="block content">
{% if warnings.debug %}
<div class="notification is-danger is-flex is-align-items-start">
<span class="icon icon-warning is-size-4 pr-3" aria-hidden="true"></span>
<span>
{% blocktrans trimmed %}
You are running BookWyrm in <strong>debug</strong> mode.
This should <strong>never</strong> be used in a production environment.
{% endblocktrans %}
</span>
</div>
{% endif %}
{% if warnings.invalid_domain %}
<div class="notification is-danger is-flex is-align-items-start">
<span class="icon icon-warning is-size-4 pr-3" aria-hidden="true"></span>
<span>
{% blocktrans trimmed %}
Your domain appears to be misconfigured.
It should not include protocol or slashes.
{% endblocktrans %}
</span>
</div>
{% endif %}
{% if warnings.protocol %}
<div class="notification is-danger is-flex is-align-items-start">
<span class="icon icon-warning is-size-4 pr-3" aria-hidden="true"></span>
<span>
{% blocktrans trimmed %}
You are running BookWyrm in production mode without https.
<strong>USE_HTTPS</strong> should be enabled in production.
{% endblocktrans %}
</span>
</div>
{% endif %}
<div class="columns">
<div class="column is-half is-flex is-flex-direction-column">
<h2 class="title is-4">{% trans "Settings" %}</h2>
<div class="notification is-flex-grow-1">
<dl>
<dt class="is-pulled-left mr-5 has-text-weight-bold">
{% trans "Instance domain:" %}
</dt>
<dd>
{{ info.domain }}
</dd>
<dt class="is-pulled-left mr-5 has-text-weight-bold">
{% trans "Protocol:" %}
</dt>
<dd>
{% if info.use_https %}
<span class="tag is-success">https</span>
{% else %}
<span class="tag is-danger">http</span>
{% endif %}
</dd>
<dt class="is-pulled-left mr-5 has-text-weight-bold">
{% trans "Software version:" %}
</dt>
<dd>
{{ info.version }}
</dd>
<dt class="is-pulled-left mr-5 has-text-weight-bold">
{% trans "Using S3:" %}
</dt>
<dd>
{{ info.use_s3|yesno }}
</dd>
</dl>
</div>
</div>
<div class="column is-half is-flex is-flex-direction-column">
<h2 class="title is-4">{% trans "Display" %}</h2>
<div class="notification is-flex-grow-1">
<dl>
<dt class="is-pulled-left mr-5 has-text-weight-bold">
{% trans "Default interface language:" %}
</dt>
<dd>
{{ info.language }}
</dd>
<dt class="is-pulled-left mr-5 has-text-weight-bold">
{% trans "Email sender:" %}
</dt>
<dd>
{{ info.email_sender }}
</dd>
<dt class="is-pulled-left mr-5 has-text-weight-bold">
{% trans "Enable preview images:" %}
</dt>
<dd>
{{ info.preview_images|yesno }}
</dd>
<dt class="is-pulled-left mr-5 has-text-weight-bold">
{% trans "Enable image thumbnails:" %}
</dt>
<dd>
{{ info.thumbnails|yesno }}
</dd>
</dl>
</div>
</div>
</div>
</div>
<div class="block content">
<h2 class="title is-4">{% trans "Does everything look right?" %}</h2>
<p class="subtitle help">
{% blocktrans trimmed %}
This is your last chance to set your domain and protocol.
{% endblocktrans %}
</p>
<div class="box">
<div class="control">
<a class="button is-primary" href="{% url 'setup-admin' %}">
<span class="icon icon-check" aria-hidden="true"></span>
<span>{% trans "Continue" %}</span>
</a>
</div>
<p>
{% blocktrans trimmed %}
You can change your instance settings in the <code>.env</code> file on your server.
{% endblocktrans %}
<a href="https://docs.joinbookwyrm.com/installing-in-production.html" target="_blank">
{% trans "View installation instructions" %}
</a>
</p>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,37 @@
{% extends 'layout.html' %}
{% load i18n %}
{% load static %}
{% block title %}{% trans "Instance Setup" %}{% endblock %}
{% block body %}
<nav class="navbar" aria-label="main navigation">
<div class="container">
<div class="navbar-brand is-flex-grow-1">
<span class="navbar-item" href="/">
<img class="image logo" src="{% if site.logo_small %}{% get_media_prefix %}{{ site.logo_small }}{% else %}{% static "images/logo-small.png" %}{% endif %}" alt="{% blocktrans with site_name=site.name %}{{ site_name }} home page{% endblocktrans %}">
</span>
<div class="navbar-item is-align-items-start pt-5 is-flex-grow-1">
{% trans "Installing BookWyrm" %}
</div>
<div class="navbar-item is-align-items-start pt-5">
<a href="https://joinbookwyrm.com/get-involved/#dev-chat" target="_blank">{% trans "Need help?" %}</a>
</div>
</div>
</div>
</nav>
<div class="section is-flex-grow-1">
<div class="container">
<header class="block content">
{% block header %}{% endblock %}
</header>
<div class="block">
{% block panel %}{% endblock %}
</div>
</div>
</div>
{% endblock %}

View file

@ -3,27 +3,61 @@
<div class="field">
<label class="label" for="id_localname_register">{% trans "Username:" %}</label>
<div class="control">
<input type="text" name="localname" maxlength="150" class="input" required="" id="id_localname_register" value="{% if register_form.localname.value %}{{ register_form.localname.value }}{% endif %}" aria-describedby="desc_localname_register">
{% include 'snippets/form_errors.html' with errors_list=register_form.localname.errors id="desc_localname_register" %}
<input
type="text"
name="localname"
maxlength="150"
class="input"
required=""
id="id_localname_register"
value="{% if register_form.localname.value %}{{ register_form.localname.value }}{% endif %}"
aria-describedby="desc_localname_register_panel"
>
<div id="desc_localname_register_panel">
<p class="help">
{% trans "Choose wisely! Your username cannot be changed." %}
</p>
{% include 'snippets/form_errors.html' with errors_list=register_form.localname.errors id="desc_localname_register" %}
</div>
</div>
</div>
<div class="field">
<label class="label" for="id_email_register">{% trans "Email address:" %}</label>
<div class="control">
<input type="email" name="email" maxlength="254" class="input" id="id_email_register" value="{% if register_form.email.value %}{{ register_form.email.value }}{% endif %}" required aria-describedby="desc_email_register">
<input
type="email"
name="email"
maxlength="254"
class="input"
id="id_email_register"
value="{% if register_form.email.value %}{{ register_form.email.value }}{% endif %}"
required
aria-describedby="desc_email_register"
>
{% include 'snippets/form_errors.html' with errors_list=register_form.email.errors id="desc_email_register" %}
</div>
</div>
<div class="field">
<label class="label" for="id_password_register">{% trans "Password:" %}</label>
<div class="control">
<input type="password" name="password" maxlength="128" class="input" required="" id="id_password_register" aria-describedby="desc_password_register">
<input
type="password"
name="password"
maxlength="128"
class="input"
required=""
id="id_password_register"
aria-describedby="desc_password_register"
>
{% include 'snippets/form_errors.html' with errors_list=register_form.password.errors id="desc_password_register" %}
</div>
</div>
<div class="field is-grouped">
<div class="control">
<button class="button is-primary" type="submit">

View file

@ -37,7 +37,7 @@ class RegisterViews(TestCase):
self.anonymous_user.is_authenticated = False
self.settings = models.SiteSettings.objects.create(
id=1, require_confirm_email=False
id=1, require_confirm_email=False, allow_registration=True
)
def test_get_redirect(self, *_):

View file

@ -0,0 +1,79 @@
""" test for app action functionality """
from unittest.mock import patch
from django.core.exceptions import PermissionDenied
from django.template.response import TemplateResponse
from django.test import TestCase
from django.test.client import RequestFactory
from bookwyrm import forms, models, views
from bookwyrm.tests.validate_html import validate_html
class SetupViews(TestCase):
"""activity feed, statuses, dms"""
def setUp(self):
"""we need basic test data and mocks"""
self.factory = RequestFactory()
self.site = models.SiteSettings.objects.create(install_mode=True)
def test_instance_config_permission_denied(self):
"""there are so many views, this just makes sure it LOADS"""
self.site.install_mode = False
self.site.save()
view = views.InstanceConfig.as_view()
request = self.factory.get("")
with self.assertRaises(PermissionDenied):
view(request)
def test_instance_config(self):
"""there are so many views, this just makes sure it LOADS"""
view = views.InstanceConfig.as_view()
request = self.factory.get("")
result = view(request)
self.assertIsInstance(result, TemplateResponse)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
def test_create_admin_get(self):
"""there are so many views, this just makes sure it LOADS"""
view = views.CreateAdmin.as_view()
request = self.factory.get("")
result = view(request)
self.assertIsInstance(result, TemplateResponse)
validate_html(result.render())
self.assertEqual(result.status_code, 200)
def test_create_admin_post(self):
"""there are so many views, this just makes sure it LOADS"""
self.site.name = "hello"
self.site.save()
self.assertFalse(self.site.allow_registration)
self.assertTrue(self.site.require_confirm_email)
self.assertTrue(self.site.install_mode)
view = views.CreateAdmin.as_view()
form = forms.RegisterForm()
form.data["localname"] = "mouse"
form.data["password"] = "mouseword"
form.data["email"] = "aaa@bbb.ccc"
request = self.factory.post("", form.data)
with patch("bookwyrm.views.setup.login") as mock:
view(request)
self.assertTrue(mock.called)
self.site.refresh_from_db()
self.assertFalse(self.site.install_mode)
user = models.User.objects.get()
self.assertTrue(user.is_active)
self.assertTrue(user.is_superuser)
self.assertTrue(user.is_staff)
self.assertTrue(user.shelf_set.exists())

View file

@ -58,6 +58,9 @@ urlpatterns = [
views.get_unread_status_string,
name="stream-updates",
),
# instance setup
re_path(r"^setup/?$", views.InstanceConfig.as_view(), name="setup"),
re_path(r"^setup/admin/?$", views.CreateAdmin.as_view(), name="setup-admin"),
# authentication
re_path(r"^login/?$", views.Login.as_view(), name="login"),
re_path(r"^login/(?P<confirmed>confirmed)/?$", views.Login.as_view(), name="login"),

View file

@ -113,6 +113,7 @@ from .reading import ReadingStatus
from .report import Report
from .rss_feed import RssFeed
from .search import Search
from .setup import InstanceConfig, CreateAdmin
from .status import CreateStatus, EditStatus, DeleteStatus, update_progress
from .status import edit_readthrough
from .updates import get_notification_count, get_unread_status_string

View file

@ -1,8 +1,9 @@
""" non-interactive pages """
from django.shortcuts import redirect
from django.template.response import TemplateResponse
from django.views import View
from bookwyrm import forms
from bookwyrm import forms, models
from bookwyrm.views.feed import Feed
@ -15,6 +16,11 @@ class Home(View):
if request.user.is_authenticated:
feed_view = Feed.as_view()
return feed_view(request, "home")
site = models.SiteSettings.objects.get()
if site.install_mode:
return redirect("setup")
landing_view = Landing.as_view()
return landing_view(request)

View file

@ -25,6 +25,10 @@ class Register(View):
def post(self, request):
"""join the server"""
settings = models.SiteSettings.get()
# no registration allowed when the site is being installed
if settings.install_mode:
raise PermissionDenied()
if not settings.allow_registration:
invite_code = request.POST.get("invite_code")
@ -38,9 +42,16 @@ class Register(View):
invite = None
form = forms.RegisterForm(request.POST)
errors = False
if not form.is_valid():
errors = True
data = {
"login_form": forms.LoginForm(),
"register_form": form,
"invite": invite,
"valid": invite.valid() if invite else True,
}
if invite:
return TemplateResponse(request, "landing/invite.html", data)
return TemplateResponse(request, "landing/login.html", data)
localname = form.data["localname"].strip()
email = form.data["email"]
@ -52,22 +63,6 @@ class Register(View):
# treat this like a successful registration, but don't do anything
return redirect("confirm-email")
# check localname and email uniqueness
if models.User.objects.filter(localname=localname).first():
form.errors["localname"] = ["User with this username already exists"]
errors = True
if errors:
data = {
"login_form": forms.LoginForm(),
"register_form": form,
"invite": invite,
"valid": invite.valid() if invite else True,
}
if invite:
return TemplateResponse(request, "landing/invite.html", data)
return TemplateResponse(request, "landing/login.html", data)
username = f"{localname}@{DOMAIN}"
user = models.User.objects.create_user(
username,

99
bookwyrm/views/setup.py Normal file
View file

@ -0,0 +1,99 @@
""" Installation wizard 🧙 """
import re
from django.contrib.auth import login
from django.contrib.auth.models import Group
from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.shortcuts import redirect
from django.template.response import TemplateResponse
from django.views import View
from bookwyrm import forms, models
from bookwyrm import settings
from bookwyrm.utils import regex
# pylint: disable= no-self-use
class InstanceConfig(View):
"""make sure the instance looks correct before adding any data"""
def get(self, request):
"""Check out this cool instance"""
# only allow this view when an instance is being configured
site = models.SiteSettings.objects.get()
if not site.install_mode:
raise PermissionDenied()
# check for possible problems with the instance configuration
warnings = {}
warnings["debug"] = settings.DEBUG
warnings["invalid_domain"] = not re.match(rf"^{regex.DOMAIN}$", settings.DOMAIN)
warnings["protocol"] = not settings.DEBUG and not settings.USE_HTTPS
# pylint: disable=line-too-long
data = {
"warnings": warnings,
"info": {
"domain": settings.DOMAIN,
"version": settings.VERSION,
"use_https": settings.USE_HTTPS,
"language": settings.LANGUAGE_CODE,
"use_s3": settings.USE_S3,
"email_sender": f"{settings.EMAIL_SENDER_NAME}@{settings.EMAIL_SENDER_DOMAIN}",
"preview_images": settings.ENABLE_PREVIEW_IMAGES,
"thumbnails": settings.ENABLE_THUMBNAIL_GENERATION,
},
}
return TemplateResponse(request, "setup/config.html", data)
class CreateAdmin(View):
"""manage things like the instance name"""
def get(self, request):
"""Create admin user form"""
# only allow this view when an instance is being configured
site = models.SiteSettings.objects.get()
if not site.install_mode:
raise PermissionDenied()
data = {"register_form": forms.RegisterForm()}
return TemplateResponse(request, "setup/admin.html", data)
@transaction.atomic
def post(self, request):
"""Create that user"""
site = models.SiteSettings.objects.get()
# you can't create an admin user if you're in config mode
if not site.install_mode:
raise PermissionDenied()
form = forms.RegisterForm(request.POST)
if not form.is_valid():
data = {"register_form": form}
return TemplateResponse(request, "setup/admin.html", data)
localname = form.data["localname"].strip()
username = f"{localname}@{settings.DOMAIN}"
user = models.User.objects.create_superuser(
username,
form.data["email"],
form.data["password"],
localname=localname,
local=True,
deactivation_reason=None,
is_active=True,
)
# Set "admin" role
try:
user.groups.set(Group.objects.filter(name__in=["admin", "moderator"]))
except Group.DoesNotExist:
# this should only happen in tests
pass
login(request, user)
site.install_mode = False
site.save()
return redirect("settings-site")

38
bw-dev
View file

@ -21,8 +21,8 @@ function runweb {
docker-compose run --rm web "$@"
}
function execdb {
docker-compose exec db $@
function rundb {
docker-compose run --rm db $@
}
function execweb {
@ -30,12 +30,15 @@ function execweb {
}
function initdb {
runweb python manage.py migrate
runweb python manage.py initdb "$@"
}
function makeitblack {
docker-compose run --rm dev-tools black celerywyrm bookwyrm
function migrate {
runweb python manage.py migrate "$@"
}
function admin_code {
runweb python manage.py admin_code
}
function awscommand {
@ -65,16 +68,17 @@ case "$CMD" in
docker-compose run --rm --service-ports web
;;
initdb)
initdb "$@"
initdb "@"
;;
resetdb)
clean
# Start just the DB so no one else is using it
docker-compose up --build -d db
execdb dropdb -U ${POSTGRES_USER} ${POSTGRES_DB}
execdb createdb -U ${POSTGRES_USER} ${POSTGRES_DB}
rundb dropdb -U ${POSTGRES_USER} ${POSTGRES_DB}
rundb createdb -U ${POSTGRES_USER} ${POSTGRES_DB}
# Now start up web so we can run the migrations
docker-compose up --build -d web
migrate
initdb
clean
;;
@ -82,7 +86,7 @@ case "$CMD" in
runweb python manage.py makemigrations "$@"
;;
migrate)
runweb python manage.py migrate "$@"
migrate "$@"
;;
bash)
runweb bash
@ -91,13 +95,13 @@ case "$CMD" in
runweb python manage.py shell
;;
dbshell)
execdb psql -U ${POSTGRES_USER} ${POSTGRES_DB}
rundb psql -U ${POSTGRES_USER} ${POSTGRES_DB}
;;
restart_celery)
docker-compose restart celery_worker
;;
pytest)
runweb pytest --no-cov-on-fail "$@"
runweb --no-deps pytest --no-cov-on-fail "$@"
;;
collectstatic)
runweb python manage.py collectstatic --no-input
@ -132,7 +136,7 @@ case "$CMD" in
clean
;;
black)
makeitblack
docker-compose run --rm dev-tools black celerywyrm bookwyrm
;;
prettier)
docker-compose run --rm dev-tools npx prettier --write bookwyrm/static/js/*.js
@ -197,6 +201,15 @@ case "$CMD" in
--endpoint-url ${AWS_S3_ENDPOINT_URL}\
--cors-configuration file:///bw/$config_file" "$@"
;;
admin_code)
admin_code
;;
setup)
migrate
initdb
runweb python manage.py collectstatic --no-input
admin_code
;;
runweb)
runweb "$@"
;;
@ -225,6 +238,7 @@ case "$CMD" in
echo " stylelint"
echo " formatters"
echo " populate_streams [--stream=<stream name>]"
echo " populate_lists_streams"
echo " populate_suggestions"
echo " generate_thumbnails"
echo " generate_preview_images [--all]"

View file

@ -30,4 +30,6 @@ generate_thumbnails
generate_preview_images
copy_media_to_s3
set_cors_to_s3
setup
admin_code
runweb" -o bashdefault -o default bw-dev