Связь "многие ко многим" с дополнительными полями с использованием WTForms, SQLAlchemy и Flask.

Для представления отношения «многие ко многим» между таблицами sites и channels существует третья, site_channels, с дополнительным полем url. Это модели и форма:

site.py:

#!/usr/bin/python
# -*- coding: utf-8 -*-
from datetime import datetime
from slugify import slugify
from sqlalchemy import event
from sqlalchemy.ext.associationproxy import association_proxy

from common import db, slug_unique, UTCDateTime
from site_channel import _SiteChannel


class Site(db.Model):
    __tablename__ = "sites"

    id = db.Column(db.Integer, primary_key=True)
    slug = db.Column(db.String(200), nullable=False, index=True)
    name = db.Column(
        db.String(200), nullable=False, index=True,
        info={'label': 'Nome', 'validators': slug_unique}
    )
    title = db.Column(
        db.String(200), nullable=False, info={'label': u'Título'}
    )

    channels_relationship = db.relationship(
        _SiteChannel,
        back_populates="site",
        primaryjoin="and_(_SiteChannel.site_id == Site.id)",
        cascade='all, delete, delete-orphan'
    )
    channels = association_proxy(
        'channels_relationship', 'channel', creator=lambda x: _SiteChannel(channel=x)
    )

    updated_at = db.Column(UTCDateTime, nullable=False)

    @property
    def identifier(self):
        return "%s/%s" % (self.owner.slug, self.slug)

    @classmethod
    def validate_site_slug(cls, target, value, oldvalue, initiator):
        if not target.slug and value:
            target.slug = slugify(value)
        return value

    def to_dict(self):
        return {
            'name': self.name,
            'title': self.title,
            'slug': self.slug,
        }

    @classmethod
    def update_timestamp(cls, mapper, connection, target):
        target.updated_at = datetime.utcnow()

        return target

    def channels_list(self):
        return [chan_rel.to_dict() for chan_rel in self.channels_relationship]


event.listen(Site, 'before_insert', Site.update_timestamp, retval=False)
event.listen(Site, 'before_update', Site.update_timestamp, retval=False)
event.listen(Site.name, 'set', Site.validate_site_slug, retval=True)

channel.py:

#!/usr/bin/python
# -*- coding: utf-8 -*-
from common import db


class _SiteChannel(db.Model):
    __tablename__ = "site_channels"

    site_id = db.Column(db.Integer, db.ForeignKey('sites.id'), primary_key=True)
    channel_id = db.Column(db.Integer, db.ForeignKey('channels.id'), primary_key=True)

    url = db.Column(db.String(200), nullable=False, info={'label': 'URL'})

    site = db.relationship("Site")
    channel = db.relationship("Channel")

    def to_dict(self):
        return {
            'id': self.channel.id,
            'url_part': self.channel.url_part,
            'name': self.channel.name,
            'url': self.url,
            'site_id': self.site.id,
            'site_name': self.site.name,
            'site_title': self.site.title,
            'site_slug': self.site.slug,
        }

site_channel.py:

#!/usr/bin/python
# -*- coding: utf-8 -*-
from sqlalchemy.ext.associationproxy import association_proxy

from site_channel import _SiteChannel
from common import db


class Channel(db.Model):
    __tablename__ = "channels"

    id = db.Column(db.Integer, primary_key=True)
    url_part = db.Column(db.String(200), nullable=False, info={'label': 'Label'})
    name = db.Column(db.String(200), nullable=False, info={'label': 'Nome'})

    sites_relationship = db.relationship(
        '_SiteChannel',
        back_populates="channel",
        primaryjoin="and_(_SiteChannel.channel_id == Channel.id)",
        cascade='all, delete, delete-orphan'
    )
    sites = association_proxy(
        'sites_relationship', 'site', creator=lambda x: _SiteChannel(site=x)
    )

    def to_dict(self):
        return {
            'id': self.id,
            'url_part': self.url_part,
            'name': self.name,
        }

    @classmethod
    def search_by_name(cls, name, exclude_ids=[]):
        query = cls.query.filter(cls.name.like("%" + name + "%"))
        if exclude_ids:
            query = query.filter(~cls.id.in_(exclude_ids))
        return query

forms.py:

#!/usr/bin/python
# -*- coding:utf-8 -*-

from flask.ext.wtf import Form
from wtforms_alchemy import model_form_factory
from wtforms.ext.sqlalchemy.fields import (
    QuerySelectMultipleField
)

from common import db
from site import Site
from channel import Channel

BaseModelForm = model_form_factory(Form)


class ModelForm(BaseModelForm):
    @classmethod
    def get_session(self):
        return db.session


class SiteForm(ModelForm):

    channels = QuerySelectMultipleField(
        query_factory=lambda: Channel.query.all(),
        get_label='name'
    )

    class Meta:
        model = Site
        exclude = ['slug', 'updated_at']

При попытке отправить форму, как и ожидалось, возникает это исключение:

OperationalError: (OperationalError) (1048, "Column 'url' cannot be null") 'INSERT INTO site_channels (site_id, channel_id, url) VALUES (%s, %s, %s)' (1L, 1L, None)

Есть подсказки, как правильно писать form.py?

Спасибо!


person scorphus    schedule 20.10.2014    source источник
comment
Так что, может быть, мне что-то не хватает, но где функция просмотра, отправляющая данные в базу данных? Разве эта ошибка не возникает в функции просмотра?   -  person ACV    schedule 21.10.2014
comment
Ой, извините за то, что раскрыл это. Представление - это просто набор обращений ко всем этим полям формы: {{ form.title }}{{ form.slug }}… и т. Д. Мне удалось заставить его работать после того, как освоил SQLAlchemy и WTForms. Я отправлю его как можно скорее.   -  person scorphus    schedule 21.10.2014


Ответы (1)


вступление

Я неправильно понял, как работают relationship, backref и cascade SQLAlchemy и как WTForms взаимодействует со всем этим. С обеими документами у меня под мышкой это то, что я получил - включая комментарии:

site.py:

#!/usr/bin/python
# -*- coding: utf-8 -*-
from datetime import datetime
from sqlalchemy import event

from common import db, UTCDateTime


class Site(db.Model):
    __tablename__ = 'sites'

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(200), nullable=False, index=True)
    domain = db.Column(db.String(200), nullable=False)
    title = db.Column(db.String(200), nullable=False)

    updated_at = db.Column(UTCDateTime, nullable=False)

    def to_dict(self):
        return {
            'name': self.name,
            'domain': self.domain,
            'title': self.title,
            'channels': self.channels_list(),
        }

    @classmethod
    def update_timestamp(cls, mapper, connection, target):
        target.updated_at = datetime.utcnow()

        return target

    def channels_list(self):
        return [chan_rel.to_dict() for chan_rel in self.channels]


event.listen(Site, 'before_insert', Site.update_timestamp, retval=False)
event.listen(Site, 'before_update', Site.update_timestamp, retval=False)

channel.py:

#!/usr/bin/python
# -*- coding: utf-8 -*-
from common import db


class Channel(db.Model):
    __tablename__ = 'channels'

    id = db.Column(db.Integer, primary_key=True)
    url_part = db.Column(
        db.String(200),
        nullable=False
    )
    name = db.Column(
        db.String(200),
        nullable=False
    )

    def to_dict(self):
        return {
            'channel_id': self.id,
            'channel_url_part': self.url_part,
            'channel_name': self.name,
        }

    @classmethod
    def search_by_name(cls, name, exclude_ids=[]):
        query = cls.query.filter(cls.name.like('%' + name + '%'))
        if exclude_ids:
            query = query.filter(~cls.id.in_(exclude_ids))
        return query

site_channel.py:

#!/usr/bin/python
# -*- coding: utf-8 -*-
from sqlalchemy.orm import backref

from common import db


class _SiteChannel(db.Model):
    __tablename__ = 'site_channels'

    site_id = db.Column(
        db.Integer,
        db.ForeignKey('sites.id'),
        primary_key=True
    )
    channel_id = db.Column(
        db.Integer,
        db.ForeignKey('channels.id'),
        primary_key=True
    )

    url = db.Column(db.String(200), nullable=False, info={'label': 'URL'})

    # And now for the true magic, backref is what really does the trick:
    site = db.relationship('Site', backref=backref(
        'channels',
        cascade='save-update, merge, delete, delete-orphan'
    ))
    channel = db.relationship('Channel', backref=backref(
        'sites',
        cascade='save-update, merge, delete, delete-orphan'
    ))

    def to_dict(self):
        return {
            'channel_id': self.channel.id,
            'channel_url_part': self.channel.url_part,
            'channel_name': self.channel.name,
            'site_id': self.site.id,
            'site_name': self.site.name,
            'site_domain': self.site.domain,
            'site_title': self.site.title,
            'url': self.url,
        }

forms.py:

#!/usr/bin/python
# -*- coding:utf-8 -*-
from flask.ext.wtf import Form
from wtforms import fields
from wtforms.ext.sqlalchemy.fields import QuerySelectField
from wtforms.widgets import ListWidget
from wtforms_alchemy import model_form_factory, ModelFieldList

from common import db
from site import Site
from channel import Channel
from site_channel import _SiteChannel

BaseModelForm = model_form_factory(Form)


class ModelForm(BaseModelForm):

    @classmethod
    def get_session(self):
        return db.session


class SiteChannelForm(ModelForm):

    # Please notice here the use of WTForms-Alchemy’s ModelFieldList
    # (https://github.com/kvesteri/wtforms-alchemy)
    channels = ModelFieldList(fields.FormField(
        SiteChannelForm,
        widget=ListWidget()
    ))

    class Meta:
        model = _SiteChannel


class SiteForm(ModelForm):

    channels = ModelFieldList(fields.FormField(SiteChannelForm))

    class Meta:
        model = Site
        exclude = ['updated_at']
person scorphus    schedule 22.10.2014