~homeworkprod/byceps

c98d51bfcbf2508a9314278f6d01fadbd37645c2 — Jochen Kupperschmidt a month ago e8d2046
Attach email config to brand

DDL:

    ALTER TABLE email_configs ADD COLUMN brand_id text;

    -- At this point, manually set a brand for each email config.

    ALTER TABLE email_configs ALTER COLUMN brand_id SET NOT NULL;

    ALTER TABLE email_configs ADD CONSTRAINT email_configs_brand_id_fkey FOREIGN KEY (brand_id) REFERENCES brands (id);

    CREATE INDEX ix_email_configs_brand_id ON email_configs USING btree (brand_id);
M byceps/blueprints/admin/email/forms.py => byceps/blueprints/admin/email/forms.py +8 -1
@@ 6,9 6,10 @@ byceps.blueprints.admin.email.forms
:License: Revised BSD (see `LICENSE` file for details)
"""

from wtforms import StringField
from wtforms import SelectField, StringField
from wtforms.validators import InputRequired, Length, Optional

from ....services.brand import service as brand_service
from ....util.l10n import LocalizedForm




@@ 17,9 18,15 @@ class _BaseForm(LocalizedForm):
    sender_name = StringField('Absender-Name', validators=[Optional()])
    contact_address = StringField('Kontaktadresse', validators=[Optional()])

    def set_brand_choices(self):
        brands = brand_service.get_all_brands()
        brands.sort(key=lambda brand: brand.title)
        self.brand_id.choices = [(brand.id, brand.title) for brand in brands]


class CreateForm(_BaseForm):
    config_id = StringField('ID', validators=[InputRequired()])
    brand_id = SelectField('Marke', validators=[InputRequired()])


class UpdateForm(_BaseForm):

M byceps/blueprints/admin/email/templates/admin/email/create_form.html => byceps/blueprints/admin/email/templates/admin/email/create_form.html +1 -0
@@ 16,6 16,7 @@
  <form action="{{ url_for('.create') }}" method="post">
    {% call form_fieldset() %}
      {{ form_field(form.config_id, placeholder='superlan', autofocus='autofocus') }}
      {{ form_field(form.brand_id) }}
      {{ form_field(form.sender_address, placeholder='noreply@superlan.example') }}
      {{ form_field(form.sender_name, placeholder='SuperLAN') }}
      {{ form_field(form.contact_address, placeholder='info@superlan.example') }}

M byceps/blueprints/admin/email/views.py => byceps/blueprints/admin/email/views.py +4 -0
@@ 45,6 45,7 @@ def index():
def create_form(erroneous_form=None):
    """Show form to create an e-mail config."""
    form = erroneous_form if erroneous_form else CreateForm()
    form.set_brand_choices()

    return {
        'form': form,


@@ 56,17 57,20 @@ def create_form(erroneous_form=None):
def create():
    """Create an e-mail config."""
    form = CreateForm(request.form)
    form.set_brand_choices()

    if not form.validate():
        return create_form(form)

    config_id = form.config_id.data.strip()
    brand_id = form.brand_id.data
    sender_address = form.sender_address.data.strip()
    sender_name = form.sender_name.data.strip() or None
    contact_address = form.contact_address.data.strip() or None

    config = email_service.create_config(
        config_id,
        brand_id,
        sender_address,
        sender_name=sender_name,
        contact_address=contact_address,

M byceps/services/email/models.py => byceps/services/email/models.py +4 -0
@@ 9,6 9,7 @@ byceps.services.email.models
from typing import Optional

from ...database import db
from ...typing import BrandID
from ...util.instances import ReprBuilder




@@ 18,6 19,7 @@ class EmailConfig(db.Model):
    __tablename__ = 'email_configs'

    id = db.Column(db.UnicodeText, primary_key=True)
    brand_id = db.Column(db.UnicodeText, db.ForeignKey('brands.id'), index=True, nullable=False)
    sender_address = db.Column(db.UnicodeText, nullable=False)
    sender_name = db.Column(db.UnicodeText, nullable=True)
    contact_address = db.Column(db.UnicodeText, nullable=True)


@@ 25,12 27,14 @@ class EmailConfig(db.Model):
    def __init__(
        self,
        config_id: str,
        brand_id: BrandID,
        sender_address: str,
        *,
        sender_name: Optional[str] = None,
        contact_address: Optional[str] = None,
    ) -> None:
        self.id = config_id
        self.brand_id = brand_id
        self.sender_address = sender_address
        self.sender_name = sender_name
        self.contact_address = contact_address

M byceps/services/email/service.py => byceps/services/email/service.py +6 -0
@@ 12,6 12,7 @@ from sqlalchemy.exc import IntegrityError

from ...database import db, upsert
from ... import email
from ...typing import BrandID
from ...util.jobqueue import enqueue

from .models import EmailConfig as DbEmailConfig


@@ 24,6 25,7 @@ class UnknownEmailConfigId(ValueError):

def create_config(
    config_id: str,
    brand_id: BrandID,
    sender_address: str,
    *,
    sender_name: Optional[str] = None,


@@ 32,6 34,7 @@ def create_config(
    """Create a configuration."""
    config = DbEmailConfig(
        config_id,
        brand_id,
        sender_address,
        sender_name=sender_name,
        contact_address=contact_address,


@@ 111,6 114,7 @@ def get_config(config_id: str) -> EmailConfig:

def set_config(
    config_id: str,
    brand_id: BrandID,
    sender_address: str,
    *,
    sender_name: Optional[str] = None,


@@ 120,6 124,7 @@ def set_config(
    table = DbEmailConfig.__table__
    identifier = {
        'id': config_id,
        'brand_id': brand_id,
        'sender_address': sender_address,
    }
    replacement = {


@@ 168,6 173,7 @@ def _db_entity_to_config(config: DbEmailConfig) -> EmailConfig:

    return EmailConfig(
        config.id,
        config.brand_id,
        sender,
        config.contact_address,
    )

M byceps/services/email/transfer/models.py => byceps/services/email/transfer/models.py +3 -0
@@ 10,6 10,8 @@ from dataclasses import dataclass
from email.utils import formataddr
from typing import List

from ....typing import BrandID


@dataclass(frozen=True)
class Sender:


@@ 25,6 27,7 @@ class Sender:
@dataclass(frozen=True)
class EmailConfig:
    id: str
    brand_id: BrandID
    sender: Sender
    contact_address: str


M tests/conftest.py => tests/conftest.py +17 -5
@@ 20,6 20,7 @@ from byceps.services.user import (
    command_service as user_command_service,
    service as user_service,
)
from byceps.typing import BrandID

from tests.base import create_admin_app, create_site_app
from tests.database import set_up_database, tear_down_database


@@ 190,9 191,12 @@ def deleted_user(make_user):


@pytest.fixture(scope='session')
def make_email_config(admin_app):
def make_email_config(admin_app, brand):
    configs = []

    def _wrapper(
        config_id: str,
        brand_id: BrandID,
        sender_address: str,
        *,
        sender_name: Optional[str] = None,


@@ 200,20 204,28 @@ def make_email_config(admin_app):
    ):
        email_service.set_config(
            config_id,
            brand_id,
            sender_address,
            sender_name=sender_name,
            contact_address=contact_address,
        )

        return email_service.get_config(config_id)
        config = email_service.get_config(config_id)
        configs.append(config)

    return _wrapper
        return config

    yield _wrapper

    for config in configs:
        email_service.delete_config(config.id)


# Dependency on `brand` avoids error on clean up.
@pytest.fixture(scope='session')
def email_config(make_email_config):
def email_config(make_email_config, brand):
    return make_email_config(
        DEFAULT_EMAIL_CONFIG_ID, sender_address='noreply@acmecon.test'
        DEFAULT_EMAIL_CONFIG_ID, brand.id, sender_address='noreply@acmecon.test'
    )



M tests/integration/blueprints/admin/email/test_create_config.py => tests/integration/blueprints/admin/email/test_create_config.py +6 -2
@@ 6,13 6,14 @@
import byceps.services.email.service as email_service


def test_create_minimal_config(email_admin_client):
def test_create_minimal_config(email_admin_client, brand):
    config_id = 'acme-minimal'
    assert email_service.find_config(config_id) is None

    url = '/admin/email/configs'
    form_data = {
        'config_id': config_id,
        'brand_id': brand.id,
        'sender_address': 'noreply@acme.example',
    }
    response = email_admin_client.post(url, data=form_data)


@@ 20,6 21,7 @@ def test_create_minimal_config(email_admin_client):
    config = email_service.find_config(config_id)
    assert config is not None
    assert config.id == config_id
    assert config.brand_id == brand.id
    assert config.sender is not None
    assert config.sender.address == 'noreply@acme.example'
    assert config.sender.name is None


@@ 29,13 31,14 @@ def test_create_minimal_config(email_admin_client):
    email_service.delete_config(config_id)


def test_create_full_config(email_admin_client):
def test_create_full_config(email_admin_client, brand):
    config_id = 'acme-full'
    assert email_service.find_config(config_id) is None

    url = '/admin/email/configs'
    form_data = {
        'config_id': config_id,
        'brand_id': brand.id,
        'sender_address': 'noreply@acme.example',
        'sender_name': 'ACME Corp.',
        'contact_address': 'info@acme.example',


@@ 45,6 48,7 @@ def test_create_full_config(email_admin_client):
    config = email_service.find_config(config_id)
    assert config is not None
    assert config.id == config_id
    assert config.brand_id == brand.id
    assert config.sender is not None
    assert config.sender.address == 'noreply@acme.example'
    assert config.sender.name == 'ACME Corp.'

M tests/integration/blueprints/admin/email/test_delete_config.py => tests/integration/blueprints/admin/email/test_delete_config.py +4 -2
@@ 6,10 6,12 @@
import byceps.services.email.service as email_service


def test_delete_config(email_admin_client):
def test_delete_config(email_admin_client, brand):
    config_id = 'kann-weg'

    assert email_service.create_config(config_id, 'noreply@acme.example')
    assert email_service.create_config(
        config_id, brand.id, 'noreply@acme.example'
    )
    assert email_service.find_config(config_id) is not None

    url = f'/admin/email/configs/{config_id}'

M tests/integration/blueprints/site/user_message/test_send.py => tests/integration/blueprints/site/user_message/test_send.py +8 -0
@@ 16,9 16,11 @@ from tests.helpers import create_site, http_client, login_user
def site1(brand, make_email_config):
    email_config = make_email_config(
        'acme-noreply',
        brand.id,
        sender_address='noreply@acmecon.test',
        sender_name='ACME Entertainment Convention',
    )

    site = create_site(
        'acmecon-website-1',
        brand.id,


@@ 26,7 28,9 @@ def site1(brand, make_email_config):
        server_name='www1.acmecon.test',
        email_config_id=email_config.id,
    )

    yield site

    site_service.delete_site(site.id)




@@ 34,10 38,12 @@ def site1(brand, make_email_config):
def site2(brand, make_email_config):
    email_config = make_email_config(
        'acme-noreply-with-contact-address',
        brand.id,
        sender_address='noreply@acmecon.test',
        sender_name='ACME Entertainment Convention',
        contact_address='help@acmecon.test',
    )

    site = create_site(
        'acmecon-website-2',
        brand.id,


@@ 45,7 51,9 @@ def site2(brand, make_email_config):
        server_name='www2.acmecon.test',
        email_config_id=email_config.id,
    )

    yield site

    site_service.delete_site(site.id)