Repository: wagtail/wagtail

Issue:
Missing staticfiles manifest from Wagtail checks before collectstatic
### Issue Summary

With Wagtail 2.13rc1, running certain Django commands which use the checks framework *before* running collectstatic will result in an exception. Quick example:

```shell
./manage.py makemigrations --settings=demo.settings.production --dry-run --check
...
  File "/Users/tomkins/.virtualenvs/tmp-ec5df8b3f9892a8/lib/python3.9/site-packages/django/contrib/staticfiles/storage.py", line 417, in stored_name
    raise ValueError("Missing staticfiles manifest entry for '%s'" % clean_name)
ValueError: Missing staticfiles manifest entry for 'wagtailadmin/js/telepath/telepath.js'
```

This might be a little bit obscure, and is something that can be worked around. My particular use case is that we run commands such as makemigrations to prevent missed migrations, and validate_templates from django-extensions - these are done before collectstatic.

The collectstatic command *does* work as expected though.

### Steps to Reproduce

```shell
pip install Wagtail==2.13rc1
wagtail start demo
cd demo
echo "SECRET_KEY='secret'" > demo/settings/local.py
cat > home/models.py <<EOF
from wagtail.admin.edit_handlers import StreamFieldPanel
from wagtail.core.blocks import RichTextBlock
from wagtail.core.fields import StreamField
from wagtail.core.models import Page


class DemoPage(Page):
    content = StreamField([
        ('paragraph', RichTextBlock()),
    ])

    content_panels = Page.content_panels + [StreamFieldPanel("content")]
EOF
```

Then:

```
./manage.py makemigrations --settings=demo.settings.production --dry-run --check
Traceback (most recent call last):
  File "/Users/tomkins/.virtualenvs/tmp-ec5df8b3f9892a8/demo/./manage.py", line 10, in <module>
    execute_from_command_line(sys.argv)
  File "/Users/tomkins/.virtualenvs/tmp-ec5df8b3f9892a8/lib/python3.9/site-packages/django/core/management/__init__.py", line 419, in execute_from_command_line
    utility.execute()
  File "/Users/tomkins/.virtualenvs/tmp-ec5df8b3f9892a8/lib/python3.9/site-packages/django/core/management/__init__.py", line 413, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "/Users/tomkins/.virtualenvs/tmp-ec5df8b3f9892a8/lib/python3.9/site-packages/django/core/management/base.py", line 354, in run_from_argv
    self.execute(*args, **cmd_options)
  File "/Users/tomkins/.virtualenvs/tmp-ec5df8b3f9892a8/lib/python3.9/site-packages/django/core/management/base.py", line 393, in execute
    self.check()
  File "/Users/tomkins/.virtualenvs/tmp-ec5df8b3f9892a8/lib/python3.9/site-packages/django/core/management/base.py", line 419, in check
    all_issues = checks.run_checks(
  File "/Users/tomkins/.virtualenvs/tmp-ec5df8b3f9892a8/lib/python3.9/site-packages/django/core/checks/registry.py", line 76, in run_checks
    new_errors = check(app_configs=app_configs, databases=databases)
  File "/Users/tomkins/.virtualenvs/tmp-ec5df8b3f9892a8/lib/python3.9/site-packages/wagtail/admin/checks.py", line 63, in get_form_class_check
    if not issubclass(edit_handler.get_form_class(), WagtailAdminPageForm):
  File "/Users/tomkins/.virtualenvs/tmp-ec5df8b3f9892a8/lib/python3.9/site-packages/wagtail/admin/edit_handlers.py", line 355, in get_form_class
    return get_form_for_model(
  File "/Users/tomkins/.virtualenvs/tmp-ec5df8b3f9892a8/lib/python3.9/site-packages/wagtail/admin/edit_handlers.py", line 67, in get_form_for_model
    return metaclass(class_name, (form_class,), form_class_attrs)
  File "/Users/tomkins/.virtualenvs/tmp-ec5df8b3f9892a8/lib/python3.9/site-packages/wagtail/admin/forms/models.py", line 75, in __new__
    new_class = super(WagtailAdminModelFormMetaclass, cls).__new__(cls, name, bases, attrs)
  File "/Users/tomkins/.virtualenvs/tmp-ec5df8b3f9892a8/lib/python3.9/site-packages/modelcluster/forms.py", line 234, in __new__
    new_class = super(ClusterFormMetaclass, cls).__new__(cls, name, bases, attrs)
  File "/Users/tomkins/.virtualenvs/tmp-ec5df8b3f9892a8/lib/python3.9/site-packages/django/forms/models.py", line 261, in __new__
    fields = fields_for_model(
  File "/Users/tomkins/.virtualenvs/tmp-ec5df8b3f9892a8/lib/python3.9/site-packages/django/forms/models.py", line 187, in fields_for_model
    formfield = formfield_callback(f, **kwargs)
  File "/Users/tomkins/.virtualenvs/tmp-ec5df8b3f9892a8/lib/python3.9/site-packages/wagtail/admin/forms/models.py", line 56, in formfield_for_dbfield
    return db_field.formfield(**kwargs)
  File "/Users/tomkins/.virtualenvs/tmp-ec5df8b3f9892a8/lib/python3.9/site-packages/wagtail/core/fields.py", line 148, in formfield
    return super().formfield(**defaults)
  File "/Users/tomkins/.virtualenvs/tmp-ec5df8b3f9892a8/lib/python3.9/site-packages/django/db/models/fields/__init__.py", line 948, in formfield
    return form_class(**defaults)
  File "/Users/tomkins/.virtualenvs/tmp-ec5df8b3f9892a8/lib/python3.9/site-packages/wagtail/core/blocks/base.py", line 531, in __init__
    kwargs['widget'] = BlockWidget(block)
  File "/Users/tomkins/.virtualenvs/tmp-ec5df8b3f9892a8/lib/python3.9/site-packages/wagtail/core/blocks/base.py", line 478, in __init__
    self.js_context = JSContext()
  File "/Users/tomkins/.virtualenvs/tmp-ec5df8b3f9892a8/lib/python3.9/site-packages/telepath/__init__.py", line 181, in __init__
    self.media = self.base_media
  File "/Users/tomkins/.virtualenvs/tmp-ec5df8b3f9892a8/lib/python3.9/site-packages/wagtail/core/telepath.py", line 11, in base_media
    versioned_static(self.telepath_js_path),
  File "/Users/tomkins/.virtualenvs/tmp-ec5df8b3f9892a8/lib/python3.9/site-packages/wagtail/admin/staticfiles.py", line 48, in versioned_static
    base_url = static(path)
  File "/Users/tomkins/.virtualenvs/tmp-ec5df8b3f9892a8/lib/python3.9/site-packages/django/templatetags/static.py", line 167, in static
    return StaticNode.handle_simple(path)
  File "/Users/tomkins/.virtualenvs/tmp-ec5df8b3f9892a8/lib/python3.9/site-packages/django/templatetags/static.py", line 118, in handle_simple
    return staticfiles_storage.url(path)
  File "/Users/tomkins/.virtualenvs/tmp-ec5df8b3f9892a8/lib/python3.9/site-packages/django/contrib/staticfiles/storage.py", line 147, in url
    return self._url(self.stored_name, name, force)
  File "/Users/tomkins/.virtualenvs/tmp-ec5df8b3f9892a8/lib/python3.9/site-packages/django/contrib/staticfiles/storage.py", line 126, in _url
    hashed_name = hashed_name_func(*args)
  File "/Users/tomkins/.virtualenvs/tmp-ec5df8b3f9892a8/lib/python3.9/site-packages/django/contrib/staticfiles/storage.py", line 417, in stored_name
    raise ValueError("Missing staticfiles manifest entry for '%s'" % clean_name)
ValueError: Missing staticfiles manifest entry for 'wagtailadmin/js/telepath/telepath.js'
```


### Technical details

* Python version: 3.9
* Django version: 3.2
* Wagtail version: 2.13rc1



Discussion:

Resolving patch:
diff --git a/wagtail/core/blocks/base.py b/wagtail/core/blocks/base.py
--- a/wagtail/core/blocks/base.py
+++ b/wagtail/core/blocks/base.py
@@ -10,6 +10,7 @@
 from django.core.exceptions import ImproperlyConfigured
 from django.template.loader import render_to_string
 from django.utils.encoding import force_str
+from django.utils.functional import cached_property
 from django.utils.html import format_html
 from django.utils.safestring import mark_safe
 from django.utils.text import capfirst
@@ -475,8 +476,25 @@ class BlockWidget(forms.Widget):
     def __init__(self, block_def, attrs=None):
         super().__init__(attrs=attrs)
         self.block_def = block_def
-        self.js_context = JSContext()
-        self.block_json = json.dumps(self.js_context.pack(self.block_def))
+        self._js_context = None
+
+    def _build_block_json(self):
+        self._js_context = JSContext()
+        self._block_json = json.dumps(self._js_context.pack(self.block_def))
+
+    @property
+    def js_context(self):
+        if self._js_context is None:
+            self._build_block_json()
+
+        return self._js_context
+
+    @property
+    def block_json(self):
+        if self._js_context is None:
+            self._build_block_json()
+
+        return self._block_json
 
     def render_with_errors(self, name, value, attrs=None, errors=None, renderer=None):
         value_json = json.dumps(self.block_def.get_form_state(value))
@@ -499,7 +517,7 @@ def render_with_errors(self, name, value, attrs=None, errors=None, renderer=None
     def render(self, name, value, attrs=None, renderer=None):
         return self.render_with_errors(name, value, attrs=attrs, errors=None, renderer=renderer)
 
-    @property
+    @cached_property
     def media(self):
         return self.js_context.media + forms.Media(
             js=[


Test patch:
diff --git a/wagtail/tests/testapp/migrations/0059_deadlystreampage.py b/wagtail/tests/testapp/migrations/0059_deadlystreampage.py
new file mode 100644
--- /dev/null
+++ b/wagtail/tests/testapp/migrations/0059_deadlystreampage.py
@@ -0,0 +1,28 @@
+# Generated by Django 3.1.8 on 2021-04-22 16:50
+
+from django.db import migrations, models
+import django.db.models.deletion
+import wagtail.core.fields
+import wagtail.tests.testapp.models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('wagtailcore', '0062_comment_models_and_pagesubscription'),
+        ('tests', '0058_blockcountsstreammodel_minmaxcountstreammodel'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='DeadlyStreamPage',
+            fields=[
+                ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')),
+                ('body', wagtail.core.fields.StreamField([('title', wagtail.tests.testapp.models.DeadlyCharBlock())])),
+            ],
+            options={
+                'abstract': False,
+            },
+            bases=('wagtailcore.page',),
+        ),
+    ]
diff --git a/wagtail/tests/testapp/models.py b/wagtail/tests/testapp/models.py
--- a/wagtail/tests/testapp/models.py
+++ b/wagtail/tests/testapp/models.py
@@ -31,7 +31,8 @@
 from wagtail.contrib.settings.models import BaseSetting, register_setting
 from wagtail.contrib.sitemaps import Sitemap
 from wagtail.contrib.table_block.blocks import TableBlock
-from wagtail.core.blocks import CharBlock, RawHTMLBlock, RichTextBlock, StreamBlock, StructBlock
+from wagtail.core.blocks import (
+    CharBlock, FieldBlock, RawHTMLBlock, RichTextBlock, StreamBlock, StructBlock)
 from wagtail.core.fields import RichTextField, StreamField
 from wagtail.core.models import Orderable, Page, PageManager, PageQuerySet, Task, TranslatableMixin
 from wagtail.documents.edit_handlers import DocumentChooserPanel
@@ -1537,3 +1538,28 @@ class TaggedRestaurant(ItemBase):
 
 class SimpleTask(Task):
     pass
+
+
+# StreamField media definitions must not be evaluated at startup (e.g. during system checks) -
+# these may fail if e.g. ManifestStaticFilesStorage is in use and collectstatic has not been run.
+# Check this with a media definition that deliberately errors; if media handling is not set up
+# correctly, then the mere presence of this model definition will cause startup to fail.
+class DeadlyTextInput(forms.TextInput):
+    @property
+    def media(self):
+        raise Exception("BOOM! Attempted to evaluate DeadlyTextInput.media")
+
+
+class DeadlyCharBlock(FieldBlock):
+    def __init__(self, *args, **kwargs):
+        self.field = forms.CharField(widget=DeadlyTextInput())
+        super().__init__(*args, **kwargs)
+
+
+class DeadlyStreamPage(Page):
+    body = StreamField([
+        ('title', DeadlyCharBlock()),
+    ])
+    content_panels = Page.content_panels + [
+        StreamFieldPanel('body'),
+    ]