I want to be able to create and edit the secondary table attributes (the relational table) of a many-to-many relationship during the creation or editing of either of the primary tables. So, when I edit one of the primary tables and add a relation to another model (implicitly using the secondary table), I want to be able to access / edit the attributes of that secondary relationship.
More specifically:
Models
# "Primary" table
class Paper(db.Model):
__tablename__ = 'papers'
...
chapters = db.relationship(Chapter, secondary="chapter_paper")
...
# "Primary" table
class Chapter(db.Model):
...
papers = db.relationship('Paper', secondary="chapter_paper")
...
# "Secondary" table
class ChapterPaper(db.Model):
__tablename__ = 'chapter_paper'
paper_id = db.Column(db.Integer,
db.ForeignKey('papers.id'),
primary_key=True)
chapter_id = db.Column(db.Integer,
db.ForeignKey('chapters.id'),
primary_key=True)
### WANT TO EDIT
printed = db.Column(db.Boolean, default=False)
note = db.Column(db.Text, nullable=True)
### WANT TO EDIT
paper = db.relationship('Paper',
backref=db.backref("chapter_paper_assoc",
lazy='joined'),
lazy='joined')
chapter = db.relationship(Chapter,
backref=db.backref("chapter_paper_assoc",
lazy='joined'),
lazy='joined')
So, for this example, I want to be able to edit the "printed" and "note" attribute of ChapterPaper from the create / edit forms of Paper and Chapter in flask admin.
ModelViews
# MainModelView subclasses flask_admin.contrib.sqla.ModelView
class PaperModelView(MainModelView):
...
form_columns = (
'title',
'abstract',
'doi',
'pubmed_id',
'link',
'journals',
'keywords',
'authors',
'chapters',
)
# Using form_columns allows CRUD for the many to many
# relation itself, but does not allow access to secondary attributes
...
So, I honestly have very little idea of how to do this. If I added the form fields as extras and then manually validated them...? (I don't know how to do this)
Even then, adding extra fields to the form doesn't really cover multiple models. Can anyone show me how to do this, or point me to a tutorial / even a relevant example from code that's part of some random project?
Thanks!
Alrighty, this was a lot of work and required a lot of RTFM, but it was pretty straightforward once I got going.
The way to do this without a neat API is to extend the model view and replace the create / edit form with a form of your own.
Here is my form class:
class ExtendedPaperForm(FlaskForm):
title = StringField()
abstract = TextAreaField()
doi = StringField()
pubmed_id = StringField()
link = StringField()
journals = QuerySelectMultipleField(
query_factory=_get_model(Journal),
allow_blank=False,
)
issue = StringField()
volume = StringField()
pages = StringField()
authors = QuerySelectMultipleField(
query_factory=_get_model(Author),
allow_blank=False,
)
keywords = QuerySelectMultipleField(
query_factory=_get_model(Keyword),
allow_blank=True,
)
chapters_printed = QuerySelectMultipleField(
query_factory=_get_model(Chapter),
allow_blank=True,
label="Chapters (Printed)",
)
chapters = QuerySelectMultipleField(
query_factory=_get_model(Chapter),
allow_blank=True,
label="Chapters (All)",
)
The important part for making this functionality happen is the on_model_change method, which performs an action before a model is saved.
...
def on_model_change(self, form, model, is_created):
"""
Perform some actions before a model is created or updated.
Called from create_model and update_model in the same transaction (if it has any meaning for a store backend).
By default does nothing.
Parameters:
form – Form used to create/update model
model – Model that will be created/updated
is_created – Will be set to True if model was created and to False if edited
"""
all_chapters = list(set(form.chapters.data + form.chapters_printed.data))
for chapter in all_chapters:
if chapter in form.chapters_printed.data: # if chapter in both, printed takes priority
chapter_paper = ChapterPaper.query.filter_by(chapter_id=chapter.id, paper_id=model.id).first()
if not chapter_paper:
chapter_paper = ChapterPaper(chapter_id=chapter.id, paper_id=model.id)
chapter_paper.printed = True
db.session.add(chapter_paper)
journal = None
if form.journals.data:
journal = form.journals.data[0]
if journal: # Assumes only 1 journal if there are any journals in this field
issue = form.issue.data
volume = form.volume.data
pages = form.pages.data
journal_paper = JournalPaper.query.filter_by(journal_id=journal.id, paper_id=model.id).first()
if not journal_paper:
journal_paper = JournalPaper(journal_id=journal.id, paper_id=model.id)
journal_paper.issue = issue
journal_paper.volume = volume
journal_paper.pages = pages
db.session.add(journal_paper)
...
Related
I'm trying to create new vector layer with the same fields as contained in original layer.
original_layer_fields_list = original_layer.fields().toList()
new_layer = QgsVectorLayer("Point", "new_layer", "memory")
pr = new_layer.dataProvider()
However, when I try:
for fld in original_layer_fields_list:
type_name = fld.typeName()
pr.addAttributes([QgsField(name = fld.name(), typeName = type_name)])
new_layer.updateFields()
QgsProject.instance().addMapLayer(new_layer)
I get a layer with no fields in attribute table.
If I try something like:
for fld in original_layer_fields_list:
if fld.type() == 2:
pr.addAttributes([QgsField(name = fld.name(), type = QVariant.Int)])
new_layer.updateFields()
QgsProject.instance().addMapLayer(new_layer)
... it works like charm.
Anyway ... I'd rather like the first solution to work in case if one wants to automate the process and not check for every field type and then find an appropriate code. Besides - I really am not able to find any documentation about codes for data types. I managed to find this post https://gis.stackexchange.com/questions/353975/get-only-fields-with-datatype-int-in-pyqgis where in comments Kadir pointed on this sourcecode (https://codebrowser.dev/qt5/qtbase/src/corelib/kernel/qvariant.h.html#QVariant::Type).
I'd really be thankful for any kind of direction.
# models.py
from django.db import models
from django.forms import ModelForm
question_choices = (
(1, 'yes/no'),
(2,'text'),
(3,'numberic'),
)
class Question(models.Model):
title = models.CharField(max_length=500,blank=False)
responseType = models.SmallIntegerField(choices=question_choices,blank=False
,default=2)
mandatory = models.BooleanField(default=True)
def __str__(self):
return self.title
answer_choices = (
(1, 'YES'),
(2,'NO'),
)
class Response(models.Model):
questionId = models.ForeignKey(Question,on_delete=models.CASCADE)
answerType1 = models.CharField(max_length=4,choices=answer_choices,blank=True)
answerType2 = models.TextField(max_length=500,blank=True)
answerType3 = models.IntegerField(blank=True)
def __str__(self):
return self.id
Assume two buttons on UI. 1. Create questions 2. Fill Questions.
When user will click on 1st button(Create questions), according to Question model user will be able to save some question to the DB.
When user clicks on 2nd button (Fill Questions)
If data is there in DB then , create a form according to row of Question model and send it to user such that after getting the response from user data will be saved inside Response model.
For reference i have added screenshot of Question model from admin pannel.
I wanted to create a form according to every row.
I'm using a SessionWizardView from django-formtools to construct a two-form wizard. The challenge I'm facing is that I need to reference the input from the first form to limit the available querysets on the second form.
To make it more interesting, I'm using crispy forms for layout and the queryset needs to be limited by a method on a related item.
Here's the (much simplified) gist of where I'm at:
Models
class Product(models.Model):
# pk, name, etc....
catalogitem = ForeignKey("myapp.CatalogItem")
colors = ManyToManyField("myapp.Colors")
class Colors(models.Model):
# pk, name, etc....
class CatalogItem(models.Model):
# Colors are stored within CatalogVariants, which I've left
# as a blackbox in this example, since they are retrieved as
# a queryset on this model with this method:
# pk, name, etc....
def get_colors(self):
# Returns a queryset of color objects.
Views
ProductFormWizard(SessionWizardView):
form_list = [
productFormWizard_Step1,
productFormWizard_Step2,
]
def get_context_data(self, **kwargs):
# ...
pass
def get_form_initial(self, step):
initial = {}
# ...
return self.initial_dict.get(step, initial)
def process_step(self, form):
if self.steps.step1 == 1:
pass
return self.get_form_step_data(form)
def done(self, form_list, **kwargs):
return render(self.request, 'done.html', {
'form_data': [form.cleaned_data for form in form_list],
})
Forms
productFormWizard_Step1(forms.ModelForm):
# Defines a form where the user selects a CatalogProduct.
model = Product
productFormWizard_Step2(forms.ModelForm):
"""
Defines a form where the user chooses colors based on
the CatalogProduct they selected in the previous step.
"""
model = Product
Based on research via the Googles and some SO questions (none of which were =directly= related), I'm assuming I need to set the .queryset property on the colors field, but I'm not exactly sure where to do that. Two thoughts:
I would guess it goes in .get_form_initial() somehow, but I'm at a loss as to the best way to achieve that.
Alternatively, the appropriate code might go into the productFormWizard.get_context_data() method somehow.
Within .get_form_initial(), I can do something like this:
if step == '1':
itemID = self.storage.get_step_data('0').data.get('0-pfProduct', "")
if itemID:
obj = CatalogItem.objects.get(id=itemID)
initial['colors'] = obj.get_get_colors()
However, this just selects the available related items... it doesn't limit the list.
Additional Info
Python == 3.5.3
Django == 1.10.6
django-crispy-forms == 1.6.1
django-formtools == 2.0
The solution is to override the .get_form() method on the View:
def get_form(self, step=None, data=None, files=None):
form = super(bzProductFormWizard, self).get_form(step, data, files)
if step == '1':
past_data = self.get_cleaned_data_for_step('0')
product = past_data['product']
form.fields['colors'].queryset = ... #CUSTOM QUERYSET
return form
I have a model (see code below) on which I want to execute a function after an object is inserted that will update one of the object's fields. I'm using the after_insert Mapper Event to do this.
I've confirmed that the after_insert properly calls the event_extract_audio_text() handler, and the target is getting updated with the correct audio_text value. However, once the event handler finishes executing, the text value is not set for the object in the database.
Code
# Event handler
def event_extract_audio_text(mapper, connect, target):
# Extract text from audio file
audio_text = compute_text_from_audio_file(target.filename)
# Update the 'text' field with extracted text
target.audio_text = audio_text
# Model
class SoundsRaw(db.Model):
__tablename__ = 'soundsraw'
id = db.Column(db.BigInteger(), primary_key=True, autoincrement=True)
filename = db.Column(db.String(255))
audio_text = db.Column(db.Text())
# Event listener
event.listen(SoundsRaw, 'after_insert', event_extract_audio_text)
I've also tried calling db.session.commit() to try to update the object with the text value, but then I get the following stack trace:
File "/Users/alexmarse/.virtualenvs/techmuseum/lib/python2.7/site-packages/sqlalchemy/orm/session.py", line 219, in _assert_active
raise sa_exc.ResourceClosedError(closed_msg)
ResourceClosedError: This transaction is closed
Any ideas?
Software versions
SQLAlchemy 0.9.4
Flask 0.10.1
Flask-SQLAlchemy 1.0
The thing with 'after_insert' kind of handlers is to use the connection directly. Here's how I did it:
class Link(db.Model):
"News link data."
__tablename__ = 'news_links'
id = db.Column(db.BigInteger, primary_key=True)
slug = db.Column(db.String, unique=True) #, nullable=False
url = db.Column(db.String, nullable=False, unique=True)
title = db.Column(db.String)
image_url = db.Column(db.String)
description = db.Column(db.String)
#db.event.listens_for(Link, "after_insert")
def after_insert(mapper, connection, target):
link_table = Link.__table__
if target.slug is None:
connection.execute(
link_table.update().
where(link_table.c.id==target.id).
values(slug=slugify(target.id))
)
I ended up solving this by ditching the Mapper Event approach and using Flask's Signalling Support instead.
Basically, you can register "signals" on your model, which are essentially callback functions that are called whenever a specific kind of event happens. In my case, the event is an "update" on my model.
To configure the signals, I added this method to my app.py file:
def on_models_committed(sender, changes):
"""Handler for model change signals"""
for model, change in changes:
if change == 'insert' and hasattr(model, '__commit_insert__'):
model.__commit_insert__()
if change == 'update' and hasattr(model, '__commit_update__'):
model.__commit_update__()
if change == 'delete' and hasattr(model, '__commit_delete__'):
model.__commit_delete__()
Then, on my model, I added this function to handle the update event:
# Event methods
def __commit_update__(self):
# create a new db session, which avoids the ResourceClosedError
session = create_db_session()
from techmuseum.modules.sensors.models import SoundsRaw
# Get the SoundsRaw record by uuid (self contains the object being updated,
# but we can't just update/commit self -- we'd get a ResourceClosedError)
sound = session.query(SoundsRaw).filter_by(uuid=self.uuid).first()
# Extract text from audio file
audio_text = compute_text_from_audio_file(sound)
# Update the 'text' field of the sound
sound.text = audio_text
# Commit the update to the sound
session.add(sound)
session.commit()
def create_db_session():
# create a new Session
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
psql_url = app.config['SQLALCHEMY_DATABASE_URI']
some_engine = create_engine(psql_url)
# create a configured "Session" class
session = sessionmaker(bind=some_engine)
return session
I have a simple e-commerce web application with product URL's like:
http://www.example.com/product/view/product_id/15
where "Product" is the controller and "view" is the action in the Product Controller
How do I change this URL to show up as:
http://www.example.com/product/view/product_name/iphone-4S-16-gb
where product_id "15" is the primary key in the product table and product_name has the value "iphone 4s 16 gb" without the hyphens
What is the simplest way for me to make this change.
Would really appreciate your help.
Thanks a lot.
resources.router.routes.view-article.type = "Zend_Controller_Router_Route_Regex"
resources.router.routes.view-article.route = "articles/(?!archive)([a-zA-Z\-]+)/(\d+)(?:/(.*))?"
resources.router.routes.view-article.reverse = "articles/%s/%d/%s"
resources.router.routes.view-article.defaults.module = "articles"
resources.router.routes.view-article.defaults.controller = "view"
resources.router.routes.view-article.defaults.action = "view-article"
resources.router.routes.view-article.map.1 = topicSlug
resources.router.routes.view-article.defaults.topicSlug = topicSlug
resources.router.routes.view-article.map.2 = id
resources.router.routes.view-article.defaults.id = 0
resources.router.routes.view-article.map.3 = articleSlug
resources.router.routes.view-article.defaults.articleSlug = articleSlug
links like http://example.com/articles/circus/616/4-marta-vse-za-ruletkami
http://example.com/products/category/product_id/product_name
EDIT 1
this is a setup for default router plugin. shown as articles from my blog module, but easily updates for shop.
parts - http://example.com/ is host :) articles/circus/ => module and controller mapeed.
resources.router.routes.view-article.map.1 = topicSlug is a category. for shop.
616/4-marta-vse-za-ruletkami ID and any slug. product description, for example, 'iphone-4S-16-gb
'
defaults are in config.
another example /{maps2module}/{maps2topicSlug}/{maps2id}/{maps2articleSlug}