Отправка формы с использованием SQLAlchemy throws 'list' object не имеет атрибута - PullRequest
1 голос
/ 14 марта 2019

Я новичок и во Flask, и в SQLAlchemy, и в кодировании, поэтому наберитесь терпения, пожалуйста.

Я пытаюсь отправить данные через форму в базу данных.
Работал нормально, пока ятребуется еще две таблицы с отношением один ко многим, так как в растении может накапливаться много элементов, и растение может иметь много свойств , а ошибка при вызове колбы AttributeError: объект списка не имеет атрибута'_sa_instance_state' .
Форма:

from flask_wtf import FlaskForm
from wtforms.ext.sqlalchemy.fields import  QuerySelectMultipleField
from wtforms import StringField, PasswordField, SubmitField,BooleanField, TextAreaField

#Query for Dynamic Nutrient Accumulator Model
def enabled_dna():
    return DNA.query.all()
#Query for Nitrogen Fixers Nursing Model

def enabled_nfn():
    return NFN.query.all()

class NewPlantForm(FlaskForm):
    common_name = StringField('Common Name', render_kw={"placeholder": "Common name"},
                          validators=[DataRequired(), Length(min=2, max=40)])
    botanical_name = StringField('Botanical Name', render_kw={"placeholder": "Botanical name"},
                             validators=[DataRequired(), Length(min=2, max=80)])
    short_description = TextAreaField('Short Description', render_kw={"placeholder": "Please add a short description"},
                                  validators=[DataRequired()])
    medicinal = TextAreaField('Medicinal Use', render_kw={"placeholder": "Medicinal use"},
                        validators=[DataRequired()])
    dna = QuerySelectMultipleField('Select Element',query_factory=enabled_dna,allow_blank=True)
    nfn = QuerySelectMultipleField('Select Property',query_factory=enabled_nfn,allow_blank=True)
    submit = SubmitField('Add plant')

. Файл models.py выглядит следующим образом:

#Plants Table
class Plants(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    common_name = db.Column(db.String(40), nullable=False)
    botanical_name = db.Column(db.String(80), unique=True, nullable=False)
    short_description = db.Column(db.Text, nullable=False)
    medicinal = db.Column(db.Text, nullable=False)
    image_file = db.Column(db.String(20), default='default_plant_pic.jpg')
    date_added = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    dna_id = db.Column(db.Integer, db.ForeignKey('DNA.id'))
    dna = db.relationship('DNA', backref=db.backref('plant_dna', lazy='dynamic'))  # Dynamic_Nutrient_Accumulated
    nfn_id = db.Column(db.Integer, db.ForeignKey('NFN.id'))
    nfn = db.relationship('NFN', backref=db.backref('plant_nfn', lazy='dynamic'))  # Nitrogen_Fixers_Nursing

    def __repr__(self):
        return f"Plants('{self.common_name}', '{self.botanical_name}', '{self.short_description}'," \
        f" '{self.medicinal}', '{self.dna}', '{self.nfn}' )"

#Dynamic_Nutrient_Accumulated
class DNA(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    element = db.Column(db.String(15))

    def __repr__(self):
        return '[ {}]'.format(self.element)
#Nitrogen_Fixers_Nursing
class NFN(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    plant_extra = db.Column(db.String(40))

    def __repr__(self):
        return '[ {}]'.format(self.plant_extra)

Маршрут и форма работали нормально сформа, содержащая поля только для одной таблицы.Однако теперь это не работает, пока я добавил второе и третье поля, содержащие данные из других таблиц (form.dna.data и form.nfn.data).

Мой маршрут для New Plant:

@app.route("/plants/new/", methods=['GET', 'POST'])
@login_required# User must be logged in to create a new plant
def new_plant():
    form = NewPlantForm()

    if form.validate_on_submit():
        new_plant = Plants(common_name=form.common_name.data,
                           botanical_name=form.botanical_name.data,
                           short_description=form.short_description.data,
                           medicinal=form.medicinal.data,
                           dna=form.dna.data,
                           nfn=form.nfn.data,
                           author=current_user)
        db.session.add(new_plant)
        db.session.commit()
        flash('Thank you ! You have successfully added a plant '
              'to the database!', 'success')

        return redirect(url_for('plants'))
    image_file = url_for('static', filename='img/plants/default_plant_pic.jpg')

    return render_template('new_plant.html', title='Add new plant',
                       image_file=image_file, form=form)

И маршрут для отображения информации о растениях:

@app.route("/plants")
def plants():
    plants = Plants.query.all()
    return render_template('plants.html', title= 'Plants Database', plants=plants)

Я пытался работать с этим локально из терминала, и это работает, но я не знаю, что мне не хватаетили если модель отношений неверна, чтобы заставить ее работать из приложения фляги.

Заранее спасибо за терпение и помощь.

Обновление

После проб и ошибок, теперь этоКажется, все это работает (добавьте завод с выбранными полями в БД, правильно отобразите данные завода в шаблон, завод будет корректно добавлен в просмотр БД с помощью браузера БД для SQLite) после того, как я изменил QuerySelectMultipleField до QuerySelectField .Однако я хотел иметь возможность выбирать и отображать несколько вариантов.

Еще одна вещь, которую я заметил, заключается в том, что при использовании QuerySelectField шаблон правильно отображаетвыпадающий, но при попытке использовать QuerySelectMultipleField , он отображает только список с элементами, без выпадающего списка.

Вот небольшая часть шаблона с полем выбора form.dna и form.nfn :

<div class="form-group">
  {{ form.dna(class="form-control form-control-sm") }}
</div>
<div class="form-group">
      {{ form.nfn(class="form-control form-control-sm")}}
</div>

Я использую Bootstrap.Может ли это быть связано с неправильным написанием шаблонов для множественного выбора?Спасибо.

Update2

Мне удалось заставить QuerySelectMultipleField работать, просматривая данные формы следующим образом:

 @app.route("/plants/new/", methods=['GET', 'POST'])
 @login_required# User must be logged in to create a new plant
 def new_plant():
     form = NewPlantForm()

    if form.validate_on_submit():
       new_plant = Plants(common_name = form.common_name.data, botanical_name = form.botanical_name.data,
                       short_description = form.short_description.data, medicinal=form.medicinal.data,
                       author=current_user)
      **for dna_element in form.dna.data:
          new_plant.dna = dna_element
      for nfn_element in form.nfn.data:
          new_plant.nfn = nfn_element**

    db.session.add(new_plant)
    db.session.commit()
    flash(f'Thank you ! You have successfully added a plant to the database!', 'success')
    return redirect(url_for('plants'))
image_file = url_for('static', filename='img/plants/default_plant_pic.jpg')
return render_template('new_plant.html', title='Add new plant',
                       image_file=image_file, form=form)

Я больше не получаю сообщение об ошибке AttributeError: у объекта 'list' нет атрибута '_sa_instance_state' и установка успешно добавлена ​​в базу данных, однако, когда я смотрю в базу данных, я могуубедитесь, что был выбран только один вариант, а не несколько вариантов.Из того, что я прочитал здесь: Приложение Flask Используя WTForms с SelectMultipleField , я должен был использовать form.something.data , чтобы получить список элементов, которые я сделал, но до сих пор нетне работает, и я получаю только один предмет.Пожалуйста помоги.Спасибо!

Обновление 3 и решение проблемы

После реализации ответа sleblanc у меня теперь есть следующий код, который работает с формой и правильно отображает: ** модели.py: **

plants_dna_table = db.Table(
'plants_dna',
db.Column('plants_id', db.Integer, db.ForeignKey('plants.id'), nullable=False),
db.Column('dna_id', db.Integer, db.ForeignKey('DNA.id'), nullable=False),
db.UniqueConstraint('plants_id', 'dna_id'))

plants_nfn_table = db.Table(
'plants_nfn',
db.Column('plants_id', db.Integer, db.ForeignKey('plants.id'), nullable=False),
db.Column('nfn_id', db.Integer, db.ForeignKey('NFN.id'), nullable=False),
db.UniqueConstraint('plants_id', 'nfn_id'))

#Plants Table
class Plants(db.Model):
id = db.Column(db.Integer, primary_key=True)
common_name = db.Column(db.String(40), nullable=False)
botanical_name = db.Column(db.String(80), unique=True, nullable=False)
short_description = db.Column(db.Text, nullable=False)
medicinal = db.Column(db.Text, nullable=False)
image_file = db.Column(db.String(20), default='default_plant_pic.jpg')
date_added = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
dna = db.relationship('DNA', secondary = plants_dna_table)  # Dynamic_Nutrient_Accumulated
nfn = db.relationship('NFN', secondary = plants_nfn_table)  # Nitrogen_Fixers_Nursing

def __repr__(self):
    return f"Plants('{self.common_name}', '{self.botanical_name}', '{self.short_description}'," \
        f" '{self.medicinal}', '{self.dna}', '{self.nfn}' )"

#Dynamic_Nutrient_Accumulated
class DNA(db.Model):
   id = db.Column(db.Integer, primary_key=True)
   element = db.Column(db.String(15))

   def __repr__(self):
       return '{}'.format(self.element)
#Nitrogen_Fixers_Nursing
class NFN(db.Model):
   id = db.Column(db.Integer, primary_key=True)
   plant_extra = db.Column(db.String(40))

   def __repr__(self):
   return '{}'.format(self.plant_extra)

db.ForeignKey ('DNA.id') , отображаемое заглавными буквами, выполняет работу и не получит ошибку, так как не найдет ДНК таблицы.

** rout.py: **

#Route for users to add a plant to the database
@app.route("/plants/new/", methods=['GET', 'POST'])
@login_required# User must be logged in to create a new plant
def new_plant():
form = NewPlantForm()

if form.validate_on_submit():
    new_plant = Plants(common_name = form.common_name.data, botanical_name = form.botanical_name.data,
                       short_description = form.short_description.data, medicinal=form.medicinal.data,
                       author=current_user)
    for dna_element in form.dna.data:
        new_plant.dna.append(dna_element)

    for nfn_element in form.nfn.data:
        new_plant.nfn.append(nfn_element)

    print(new_plant)
    db.session.add(new_plant)
    db.session.commit()
    flash(f'Thank you ! You have successfully added a plant to the database!', 'success')
    return redirect(url_for('plants'))
image_file = url_for('static', filename='img/plants/default_plant_pic.jpg')
return render_template('new_plant.html', title='Add new plant',
                       image_file=image_file, form=form)

Спасибо @sleblanc!

1 Ответ

0 голосов
/ 30 марта 2019

Вы смоделировали свои отношения в обратном порядке.

У вас есть отношение «один ко многим» от ваших Растений к вашей ДНК и NFN, если я не ошибаюсь, это означает, что один объект «Растения» будет иметь несколько ДНК и несколько NFN. Как определено, ваша модель «Растения» имеет только одно поле для ДНК и одно поле для NFN, что означает, что нет способа сохранить ассоциацию в базе данных.

Вам нужно будет изменить модель Plants, чтобы представить отношения. Сначала вы должны определить, могут ли «ДНК» или «NFN» совместно использоваться несколькими «растениями», или они уникальны для каждого экземпляра «растений», поскольку в нем используется другая схема.

class Plants(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    ...

# second option, exclusive relationship
class DNA(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    ...
    plants_id = db.Column(db.ForeignKey(Plants.id), nullable=False)
    ...

class NFN(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    ...
    plants_id = db.Column(db.ForeignKey(Plants.id), nullable=False)
    ...


# first option, non-exclusive relationship
class Plants(db.Model):
    ...
    dna = relationship("DNA", secondary=plants_dna_table)
    nfn = relationship("NFN", secondary=plants_nfn_table) 


plants_dna_table = db.Table(
    'plants_dna',
    db.Column('plants_id', db.ForeignKey('plants.id'), nullable=False),
    db.Column('dna_id', db.ForeignKey('dna.id'), nullable=False),
    db.UniqueConstraint('plants_id', 'dna_id'),
    )

plants_nfn_table = db.Table(
    'plants_nfn',
    db.Column('plants_id', db.ForeignKey('plants.id'), nullable=False),
    db.Column('nfn_id', db.ForeignKey('nfn.id'), nullable=False),
    db.UniqueConstraint('plants_id', 'nfn_id'),
    )

После изменения модели Plants и установки отношений между ними следует помнить, что атрибуты .dna и .nfn будут иметь тип списка. Вы можете добавить отношение к экземплярам, ​​append в списке.

Я настоятельно рекомендую вам использовать FlaskForm.populate_obj:

@app.route("/plants/new/", methods=['GET', 'POST'])
@login_required# User must be logged in to create a new plant
def new_plant():
obj = Plants()
form = NewPlantForm()

if form.validate_on_submit():
    form.populate_obj(obj)

    print(obj) # debugging
    db.session.add(obj)
    db.session.commit()
    flash('Thank you ! You have successfully added '
          'a plant to the database!', 'success')
    return redirect(url_for('plants'))

image_file = url_for('static', filename='img/plants/default_plant_pic.jpg')
return render_template('new_plant.html', title='Add new plant',
                   image_file=image_file, form=form)

Кроме того, он позволяет комбинировать вид будущего обновления установки, добавив еще несколько строк:

@app.route('/plant/<id>/edit', methods=('GET', 'POST',))
@app.route('/plant/new', methods=('GET', 'POST'))
def create_plant(id=None):
    if id is not None:
        obj = Plant.query.get(id)
    else:
        obj = Plant()

    form = PlantForm(request.form, obj=obj)

    if form.validate_on_submit():
        form.populate_obj(obj)

        db.session.add(obj)
        db.session.commit()
        flash('Thank you ! You have successfully added '
              'a plant to the database!', 'success')
        return redirect(url_for('get_plant', id=obj.id))

    else:
        image_file = url_for(
            'static', 
            filename='img/plants/default_plant_pic.jpg')
        return render_template('plant.html', title='Add new plant',
                               image_file=image_file,
                               form=form, obj=obj)
...