From d57901962edb92c5078e2172a191c9400d4d6315 Mon Sep 17 00:00:00 2001
From: Arne Schauf <git@asw.io>
Date: Tue, 29 Apr 2025 14:58:05 +0200
Subject: [PATCH] Add AboutUsPage model and template to core app

This commit introduces the AboutUsPage model for managing content like company mission, vision, history, team, and additional content using Wagtail StreamFields. A corresponding template is provided for rendering the page. Required dependencies, including crispy-bootstrap3 and django-recaptcha, are also added to the project.
---
 core/migrations/0012_aboutuspage.py    |  37 +++++++++
 core/models.py                         |  71 ++++++++++++++++
 core/templates/core/about_us_page.html | 108 +++++++++++++++++++++++++
 feo_homepage/settings/base.py          |   3 +
 requirements.in                        |   1 +
 requirements.txt                       |   7 +-
 6 files changed, 226 insertions(+), 1 deletion(-)
 create mode 100644 core/migrations/0012_aboutuspage.py
 create mode 100644 core/templates/core/about_us_page.html

diff --git a/core/migrations/0012_aboutuspage.py b/core/migrations/0012_aboutuspage.py
new file mode 100644
index 0000000..2c1adf4
--- /dev/null
+++ b/core/migrations/0012_aboutuspage.py
@@ -0,0 +1,37 @@
+# Generated by Django 5.2 on 2025-04-29 12:32
+
+import django.db.models.deletion
+import wagtail.fields
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0011_alter_contactformfield_choices_and_more'),
+        ('wagtailcore', '0094_alter_page_locale'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='AboutUsPage',
+            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')),
+                ('company_intro', wagtail.fields.RichTextField(blank=True, help_text='Introductory text about the company')),
+                ('mission_title', models.CharField(blank=True, max_length=255)),
+                ('mission_text', wagtail.fields.RichTextField(blank=True)),
+                ('vision_title', models.CharField(blank=True, max_length=255)),
+                ('vision_text', wagtail.fields.RichTextField(blank=True)),
+                ('history_title', models.CharField(blank=True, max_length=255)),
+                ('history_text', wagtail.fields.RichTextField(blank=True)),
+                ('team_title', models.CharField(blank=True, max_length=255)),
+                ('team_intro', wagtail.fields.RichTextField(blank=True)),
+                ('team_members', wagtail.fields.StreamField([('team_member', 5)], blank=True, block_lookup={0: ('wagtail.blocks.CharBlock', (), {'max_length': 255}), 1: ('wagtail.images.blocks.ImageChooserBlock', (), {'required': False}), 2: ('wagtail.blocks.RichTextBlock', (), {'required': False}), 3: ('wagtail.blocks.EmailBlock', (), {'required': False}), 4: ('wagtail.blocks.URLBlock', (), {'required': False}), 5: ('wagtail.blocks.StructBlock', [[('name', 0), ('position', 0), ('photo', 1), ('bio', 2), ('email', 3), ('linkedin', 4), ('xing', 4)]], {})}, null=True)),
+                ('additional_content', wagtail.fields.StreamField([('heading', 0), ('paragraph', 1), ('image', 2), ('quote', 5)], blank=True, block_lookup={0: ('wagtail.blocks.CharBlock', (), {'form_classname': 'full title'}), 1: ('wagtail.blocks.RichTextBlock', (), {}), 2: ('wagtail.images.blocks.ImageChooserBlock', (), {}), 3: ('wagtail.blocks.TextBlock', (), {}), 4: ('wagtail.blocks.CharBlock', (), {'required': False}), 5: ('wagtail.blocks.StructBlock', [[('quote', 3), ('attribution', 4)]], {})}, null=True)),
+            ],
+            options={
+                'abstract': False,
+            },
+            bases=('wagtailcore.page',),
+        ),
+    ]
diff --git a/core/models.py b/core/models.py
index 8876290..8cb96c3 100644
--- a/core/models.py
+++ b/core/models.py
@@ -519,3 +519,74 @@ class UploadedScript(TimeStampedModel):
         out = super().delete(*args, **kwargs)
         storage.delete(path)
         return out
+
+
+class AboutUsPage(Page):
+    """
+    A page for displaying company information, team members, mission statements, etc.
+    """
+    # Company information
+    company_intro = RichTextField(
+        blank=True,
+        help_text="Introductory text about the company"
+    )
+
+    # Mission and vision
+    mission_title = models.CharField(max_length=255, blank=True)
+    mission_text = RichTextField(blank=True)
+    vision_title = models.CharField(max_length=255, blank=True)
+    vision_text = RichTextField(blank=True)
+
+    # Company history
+    history_title = models.CharField(max_length=255, blank=True)
+    history_text = RichTextField(blank=True)
+
+    # Team members section
+    team_title = models.CharField(max_length=255, blank=True)
+    team_intro = RichTextField(blank=True)
+
+    # Team members as a StreamField to allow adding multiple team members
+    team_members = StreamField([
+        ('team_member', blocks.StructBlock([
+            ('name', blocks.CharBlock(max_length=255)),
+            ('position', blocks.CharBlock(max_length=255)),
+            ('photo', ImageChooserBlock(required=False)),
+            ('bio', blocks.RichTextBlock(required=False)),
+            ('email', blocks.EmailBlock(required=False)),
+            ('linkedin', blocks.URLBlock(required=False)),
+            ('xing', blocks.URLBlock(required=False)),
+        ])),
+    ], null=True, blank=True, use_json_field=True)
+
+    # Additional content
+    additional_content = StreamField([
+        ('heading', blocks.CharBlock(form_classname="full title")),
+        ('paragraph', blocks.RichTextBlock()),
+        ('image', ImageChooserBlock()),
+        ('quote', blocks.StructBlock([
+            ('quote', blocks.TextBlock()),
+            ('attribution', blocks.CharBlock(required=False)),
+        ])),
+    ], null=True, blank=True, use_json_field=True)
+
+
+AboutUsPage.content_panels = [
+    FieldPanel('title', classname="full title"),
+    FieldPanel('company_intro', classname="full"),
+    MultiFieldPanel([
+        FieldPanel('mission_title', classname="full"),
+        FieldPanel('mission_text', classname="full"),
+        FieldPanel('vision_title', classname="full"),
+        FieldPanel('vision_text', classname="full"),
+    ], heading="Mission and Vision", classname="collapsible"),
+    MultiFieldPanel([
+        FieldPanel('history_title', classname="full"),
+        FieldPanel('history_text', classname="full"),
+    ], heading="Company History", classname="collapsible"),
+    MultiFieldPanel([
+        FieldPanel('team_title', classname="full"),
+        FieldPanel('team_intro', classname="full"),
+        FieldPanel('team_members'),
+    ], heading="Team Members", classname="collapsible"),
+    FieldPanel('additional_content', classname="full"),
+]
diff --git a/core/templates/core/about_us_page.html b/core/templates/core/about_us_page.html
new file mode 100644
index 0000000..4821dbe
--- /dev/null
+++ b/core/templates/core/about_us_page.html
@@ -0,0 +1,108 @@
+{% extends "core/base.html" %}
+{% load core_tags menu_tags static wagtailuserbar wagtailcore_tags wagtailimages_tags %}
+
+{% block content %}
+    <div class="container content">
+        <!-- Company Introduction -->
+        <div class="row margin-bottom-30">
+            <div class="col-md-12">
+                {{ self.company_intro|richtext }}
+            </div>
+        </div>
+
+        <!-- Mission and Vision -->
+        {% if self.mission_title or self.vision_title %}
+        <div class="row margin-bottom-30">
+            {% if self.mission_title %}
+            <div class="col-md-6 md-margin-bottom-30">
+                <div class="headline"><h2>{{ self.mission_title }}</h2></div>
+                {{ self.mission_text|richtext }}
+            </div>
+            {% endif %}
+            
+            {% if self.vision_title %}
+            <div class="col-md-6">
+                <div class="headline"><h2>{{ self.vision_title }}</h2></div>
+                {{ self.vision_text|richtext }}
+            </div>
+            {% endif %}
+        </div>
+        {% endif %}
+
+        <!-- Company History -->
+        {% if self.history_title %}
+        <div class="row margin-bottom-30">
+            <div class="col-md-12">
+                <div class="headline"><h2>{{ self.history_title }}</h2></div>
+                {{ self.history_text|richtext }}
+            </div>
+        </div>
+        {% endif %}
+
+        <!-- Team Members -->
+        {% if self.team_title %}
+        <div class="row margin-bottom-30">
+            <div class="col-md-12">
+                <div class="headline"><h2>{{ self.team_title }}</h2></div>
+                {{ self.team_intro|richtext }}
+            </div>
+        </div>
+
+        <div class="row team-v1 margin-bottom-40">
+            {% for block in self.team_members %}
+                {% if block.block_type == 'team_member' %}
+                <div class="col-md-4 md-margin-bottom-30">
+                    <div class="team-img">
+                        {% if block.value.photo %}
+                            {% image block.value.photo width-400 as team_photo %}
+                            <img class="img-responsive" src="{{ team_photo.url }}" alt="{{ block.value.name }}">
+                        {% endif %}
+                        <ul>
+                            {% if block.value.email %}
+                            <li><a href="mailto:{{ block.value.email }}"><i class="fa fa-envelope"></i></a></li>
+                            {% endif %}
+                            {% if block.value.linkedin %}
+                            <li><a href="{{ block.value.linkedin }}" target="_blank"><i class="fa fa-linkedin"></i></a></li>
+                            {% endif %}
+                            {% if block.value.xing %}
+                            <li><a href="{{ block.value.xing }}" target="_blank"><i class="fa fa-xing"></i></a></li>
+                            {% endif %}
+                        </ul>
+                    </div>
+                    <h3>{{ block.value.name }}</h3>
+                    <h4>{{ block.value.position }}</h4>
+                    {% if block.value.bio %}
+                    <p>{{ block.value.bio|richtext }}</p>
+                    {% endif %}
+                </div>
+                {% endif %}
+            {% endfor %}
+        </div>
+        {% endif %}
+
+        <!-- Additional Content -->
+        {% if self.additional_content %}
+        <div class="row margin-bottom-30">
+            <div class="col-md-12">
+                {% for block in self.additional_content %}
+                    {% if block.block_type == 'heading' %}
+                        <div class="headline"><h2>{{ block.value }}</h2></div>
+                    {% elif block.block_type == 'paragraph' %}
+                        {{ block.value|richtext }}
+                    {% elif block.block_type == 'image' %}
+                        {% image block.value width-800 as content_image %}
+                        <img class="img-responsive margin-bottom-20" src="{{ content_image.url }}" alt="">
+                    {% elif block.block_type == 'quote' %}
+                        <blockquote>
+                            <p>{{ block.value.quote }}</p>
+                            {% if block.value.attribution %}
+                            <small>{{ block.value.attribution }}</small>
+                            {% endif %}
+                        </blockquote>
+                    {% endif %}
+                {% endfor %}
+            </div>
+        </div>
+        {% endif %}
+    </div>
+{% endblock content %}
diff --git a/feo_homepage/settings/base.py b/feo_homepage/settings/base.py
index 052fdb6..f9399dc 100644
--- a/feo_homepage/settings/base.py
+++ b/feo_homepage/settings/base.py
@@ -57,6 +57,8 @@ INSTALLED_APPS = [
     'taggit',
     'django_extensions',
     'crispy_forms',
+    'crispy_bootstrap3',
+    'django_recaptcha',
 
     'core',
 ]
@@ -166,6 +168,7 @@ WAGTAILSEARCH_BACKENDS = {
 # Base URL to use when referring to full URLs within the Wagtail admin backend
 WAGTAILADMIN_BASE_URL = 'http://localhost:8000'
 
+CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap3"
 CRISPY_TEMPLATE_PACK = 'bootstrap3'
 
 RECAPTCHA_PUBLIC_KEY = '6Lfa2XAUAAAAAKaTdzFBc4zrN8YxrsW9kUpWjkom'
diff --git a/requirements.in b/requirements.in
index 9698db8..af90ab9 100644
--- a/requirements.in
+++ b/requirements.in
@@ -6,5 +6,6 @@ django-extensions
 icalendar
 psycopg[binary]
 django-crispy-forms
+crispy-bootstrap3
 django-braces
 daphne
diff --git a/requirements.txt b/requirements.txt
index b483a54..11ad859 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -28,6 +28,8 @@ charset-normalizer==3.4.1
     # via requests
 constantly==23.10.4
     # via twisted
+crispy-bootstrap3==2024.1
+    # via -r requirements.in
 cryptography==44.0.2
     # via
     #   autobahn
@@ -40,6 +42,7 @@ defusedxml==0.7.1
 django==5.2
     # via
     #   -r requirements.in
+    #   crispy-bootstrap3
     #   django-braces
     #   django-crispy-forms
     #   django-extensions
@@ -58,7 +61,9 @@ django==5.2
 django-braces==1.17.0
     # via -r requirements.in
 django-crispy-forms==2.4
-    # via -r requirements.in
+    # via
+    #   -r requirements.in
+    #   crispy-bootstrap3
 django-extensions==4.1
     # via -r requirements.in
 django-filter==25.1