Conflict between dataset and catalog - python

I am trying to add some tags inside the dataset definition on my XML. According to the European Data Portal de structure I must follow for the tag
<foaf:Agent rdf:about="URI/of/the/publisher">
<foaf:name xml:lang="es"> Name of the publisher</foaf:name>
</foaf:Agent>
<dct:publisher rdf:resource=”URI/of/the/publisher”>
Right now inside my dcat:Dataset I can only add the dct:publisher, missing the two other tags.
In my code I have asigned the name as a literal and for the URI an URIref since it must be values for an RDF.
g = self.g
g.add((dataset_ref, RDF.type, DCAT.Dataset))
publishers = dataset_dict.get(dhc.EXPORT_AVAILABLE_PUBLISHERS, {})
organization_id = dataset_dict.get('publisher')
if organization_id in publishers:
publisher = publishers.get(organization_id)
else:
publisher = []
org = h.get_organization(organization_id, False)
publisher = [org.get('title'), None, None]
if org and org['extras']:
for extra in org.get('extras'):
if extra and 'key' in extra and extra['key'] == dhc.ORG_ID_:
notation = extra.get('value')
publisher[1] = dhc.PUBLISHER_PREFIX + notation
publisher[2] = notation
publishers[organization_id] = publisher
dataset_dict[dhc.EXPORT_AVAILABLE_PUBLISHERS] = publishers
if publisher:
# self._add_resource_list_triple(dataset_ref, DCT.publisher, publisher[1], publisher[0], None, None, publisher[2])
EDP_publisher = URIRef(publisher[1])
g.add((dataset_ref, DCT.publisher, EDP_publisher))
g.add((dataset_ref, FOAF.Agent, EDP_publisher))
g.add((EDP_publisher, FOAF.name, Literal(publisher[0])))
So everytime I use the g.add with FOAF.name and FOAF.Agent, it sends my tag out of the dataset tag (dcat:Dataset) and transforms it from about to a resource definition.
I am feeling it could be a conflict of code. Where could I be making the wrong defition?
Updated:
After giving it more tries, I've found that the issued for the structure to not get into the "main tag" is a conflict because of the use of "RDF.type" definition since the main structure is g.add((dataset_ref, RDF.type, DCAT.Dataset)) and when I try to create it a parent and child tag, the parent get duplicate with the main tag since it gets the same values, since I found in the documentation that if the child structure doesn't find on the parent a "RDF.type", it will generate it automatically by calling it "rdf:Description"
I only managed to get the followed structured even though, it's not what I need to get.
<dcat:Dataset rdf:about="URI-ref">
<dct:description xml:lang="es"> Description of the dataset </dct:description>
<dct:title xml:lang="en">Title of dataset</dct:title>
<dct:publisher rdf:resource="URI-ref"/>
</dcat:Dataset>
<foaf:Agent rdf:about="URI-ref">
<foaf:name>Name of the publisher</foaf:name>
</foaf:Agent>
I can't manage to add the Foaf:Agent and foaf:name inside the dcat:Dataset.

Related

simple-salesforce - How to create Knowledge Page Layout programatically?

I've been able to create a Record Type and Custom Field for the Knowledge module of Salesforce with the metadata api implemented in simple-salesforce's python library, but when trying to create a Page Layout it throws an exception without much info on why is the creation failing (or at least I don't get it).
I'm trying to create the page layout in the following way:
def create_page_layout(self, layout_name, fields):
mdapi = self.sf.mdapi
# custom_layout_section = []
custom_layout_items = []
for field in fields:
custom_layout_items.append(mdapi.LayoutItem(
field=field
))
custom_layout_section = [mdapi.LayoutSection(
label='Information',
style='OneColumn',
layoutColumns=mdapi.LayoutColumn(
layoutItems=custom_layout_items
)
)]
custom_layout = mdapi.Layout(
fullName='Knowledge__kav.' + layout_name + '__c',
layoutSections=custom_layout_section
)
mdapi.Layout.create(custom_layout)
The param layout_name contains 'TestDefaultPL'.
The param fields contains ['Title', 'UrlName'].
The exception: Exception: Knowledge__kav.TestDefaultPL__c: (FIELD_INTEGRITY_EXCEPTION, Parent entity failed to deploy).
The docs about this object: link.
As an example, I was able to create a custom field for the Knowledge module in the following way:
def create_custom_field(self, name):
mdapi = self.sf.mdapi
# TODO: Field permissions
custom_field = mdapi.CustomField(
label=name, # Field label
fullName='Knowledge__kav.' + name + '__c', # Field name (format has to be CustomObject.FieldName)
type=mdapi.FieldType("LongTextArea"),
length=32000,
description='Default Rich Text Area',
visibleLines=25
)
return mdapi.CustomField.create(custom_field)
Could someone help me with this problem about the Page Layout creation?

Django Efficiency For Data Manipulation

I am doing some data changes in a django app with a large amount of data and would like to know if there is a way to make this more efficient. It's currently taking a really long time.
I have a model that used to look like this (simplified and changed names):
class Thing(models.Model):
... some fields...
stuff = models.JSONField(encoder=DjangoJSONEncoder, default=list, blank=True)
I need to split the list up based on a new model.
class Tag(models.Model):
name = models.CharField(max_length=200)
class Thing(models.Model):
.... some fields ...
stuff = models.JSONField(encoder=DjangoJSONEncoder, default=list, blank=True)
other_stuff = models.JSONField(encoder=DjangoJSONEncoder, default=list, blank=True)
tags = models.Many2ManyField(Tag)
What I need to do is take the list that is currently in stuff, and split it up. For items that have a tag in the Tag model, add it to the Many2Many. For things that don't have a Tag, I add it to other_stuff. Then in the end, the stuff field should contain of the items that were saved in tags.
I start by looping through the Tags to make a dict that maps the string version that would be in the stuff list to the tag object so I don't have to keep querying the Tag model.
Then I loop through the Thing model, get the stuff field, loop through that and add each Tag item to the many2many while keeping lists for each item that is or isn't in Tags. Then put those in the stuff and other stuff fields at the end.
tags = Tag.objects.all()
tag_dict = {tag.name.lower():Tag for tag in tags}
things = Thing.objects.all()
for thing in things:
stuff_list = thing.stuff
stuff_in_tags = []
stuff_not_in_tags = []
for item in stuff_list:
if item.lower() in tag_dict.keys():
stuff_in_tags.append(item)
thing.tags.add(tag_dict[item.lower()])
else:
stuff_not_in_tags.append(item)
thing.stuff = stuff_in_tags
thing.other_stuff = stuff_not_in_tags
thing.save()
(Ignore any typos. This code works in my actual code)
That seems pretty efficient to me, but its taking hours to run as our database is pretty big (about 500k+ records). Are there any other ways to make this more efficient?
Unless you move some work to the database level with bulk operations, it won't run faster. You are making at least N (500k+) UPDATE queries.
If the parsing cannot be done on the DB level, chunked bulk_update is the next option.
Also, you can use iterator() to avoid loading all the objects to memory and only() to load only relevant columns.
There is a typo in tag_dict - it should be : tag (instance) instead of : Tag (model).
EDIT: I've originally missed the thing.tags.add - this will need additional handling. You have to bulk_create m2m table rows.
chunk_size = 10000
TagsToThing = Thing.tags.through
tag_dict = {tag.name.lower():tag for tag in Tag.objects.all()}
for_update = []
tags_for_create = []
for thing in Thing.objects.only('pk', 'stuff').iterator(chunk_size):
stuff_in_tags = []
stuff_not_in_tags = []
for item in thing.stuff:
if item.lower() in tag_dict.keys():
stuff_in_tags.append(item)
tags_for_create.append(
TagsToThing(thing=thing, tag=tag_dict[item.lower()])
)
else:
stuff_not_in_tags.append(item)
thing.stuff = stuff_in_tags
thing.other_stuff = stuff_not_in_tags
for_update.append(thing)
if len(for_update) == chunk_size:
Thing.objects.bulk_update(for_update, ['stuff', 'other_stuff'], chunk_size)
TagsToThing.objects.bulk_create(tags_for_create, ignore_conflicts=True) # in case the tag is already assigned
for_update = []
tags_for_create = []
# Save remaining objects
Thing.objects.bulk_update(for_update, ['stuff', 'other_stuff'], chunk_size)
TagsToThing.objects.bulk_create(tags_for_create, ignore_conflicts=True) # in case the tag is already assigned

Django-tables2 - can't I use [A('argument')] inside the "text" parameter?

I'm trying to make this table with a clickable field which changes the boolean for the entry to its opposite value. It works, but I want an alternative text as "False" or "True" does not look nice, and the users are mainly Norwegian.
def bool_to_norwegian(boolean):
if boolean:
return "Ja"
else:
return "Nei"
class OrderTable(tables.Table):
id = tables.LinkColumn('admin_detail', args=[A('id')])
name = tables.Column()
address = tables.Column()
order = tables.Column()
order_placed_at = tables.DateTimeColumn()
order_delivery_at = tables.DateColumn()
price = tables.Column()
comment = tables.Column()
sent = tables.LinkColumn('status_sent', args=[A('id')])
paid = tables.LinkColumn('status_paid', args=[A('id')], text=[A('paid')])
class Meta:
attrs = {'class': 'order-table'}
If you look under the "paid" entry I am testing this right now, why can't I access the data with the same accessor as I do in the args? If I change the args to args=[A('paid')] and look at the link, it does indeed have the correct data on it. The model names are the same as the ones in this table, and "paid" and "sent" are BooleanFields.
This is kind of what I ultimately want:
text=bool_to_norwegian([A('paid')])
Here is what I send to the table:
orders = Order.objects.order_by("-order_delivery_at")
orders = orders.values()
table = OrderTable(orders)
RequestConfig(request).configure(table)
The text argument expects a callable that accepts a record, and returns a text value. You are passing it a list (which it will just ignore), and your function is expecting a boolean instead of a record. There is also no need for using accessors here.
Something like this should work:
def bool_to_norwegian(record):
if record.paid:
return "Ja"
else:
return "Nei"
Then in your column:
paid = tables.LinkColumn('status_paid', text=bool_to_norwegian)
(Note, it is not clear from your question where the data is coming from - is paid a boolean? You may need to adjust this to fit).
As an aside, the way you are passing args to your columns is weird (it seems the documentation also recommends this, but I don't understand why - it's very confusing). A more standard approach would be:
id = tables.LinkColumn('admin_detail', A('id'))
or using named arguments:
id = tables.LinkColumn('admin_detail', accessor=A('id'))

Create a canonical "parent" product in Django Oscar programmatically

I'm trying to use a modified version of the django-oscar import_oscar_catalogue class to import a bunch of products from a CSV, and on the first encounter of a product (defined by title), create a canonical parent product, and then for all future encounters create a child product under that parent product.
This seems to work, but the canonical product does not reflect the combined stock levels of the child product, nor display the correct attributes for that product. It does correctly list them as variations within the django dashboard though.
How can I programmatically create this child/parent relationship in products, with the correct stock records?
Relevant code:
def _create_item(self, upc, title, product_class, other_product_attributes):
product_class, __ \
= ProductClass.objects.get_or_create(name=product_class)
try:
parent = Product.objects.get(title=title)
item = Product()
item.parent = parent
except Product.DoesNotExist:
# Here is where I think it might need to be changed
# Maybe pitem = ParentProduct() or something?
pitem = Product()
pitem.upc = upc
pitem.title = title
pitem.other_product_attributes = other_product_attributes
# Here parent item is saved to db
pitem.save()
# Create item because no parent was found
item = Product()
parent = Product.objects.get(title=title)
#Set parent
item.parent = parent
# Customize child attributes
item.product_class = product_class
item.title = title
item.other_product_attributes = other_product_attributes
# Save the child item
item.save()
def _create_stockrecord(self, item, partner_name, partner_sku, price_excl_tax,
num_in_stock, stats):
# Create partner and stock record
partner, _ = Partner.objects.get_or_create(
name=partner_name)
try:
stock = StockRecord.objects.get(partner_sku=partner_sku)
except StockRecord.DoesNotExist:
stock = StockRecord()
stock.num_in_stock = 0
# General attributes
stock.product = item
stock.partner = partner
# SKU will be unique for every object
stock.partner_sku = partner_sku
stock.price_excl_tax = D(price_excl_tax)
stock.num_in_stock += int(num_in_stock)
# Save the object to database
stock.save()
The create_stockrecord() creates a record of 1 stock for each unique item variation, but these variation's stockrecords don't translate to the parent item.
EDIT:
I have updated the class with a method that explicitly calls ProductClass.objects.track_stock() against the ProductClass instance, and I'm calling it after looping through all rows of the CSV file (passing it the name of the one product class I use currently). However, when looking at the stock in dashboard, none of the child/variations stock is being counted against the parent.
def track_stock(self, class_name):
self.logger.info("ProductClass name: %s" % class_name)
product_class = ProductClass.objects.get_or_create(name=class_name)
self.logger.info("ProductClass: %s" % str(product_class))
self.logger.info("TrackStock: %s" % str(product_class[0].track_stock))
product_class[0].track_stock = True
self.logger.info("TrackStock: %s" % str(product_class[0].track_stock))
product_class[0].save()
INFO Starting catalogue import
INFO - Importing records from 'sample_inventory.csv'
INFO - Flushing product data before import
INFO Parent items: 6, child items: 10
INFO ProductClass name: ClassName
INFO ProductClass: (<ProductClass: ClassName>, False)
INFO TrackStock: True
INFO TrackStock: True
I have checked the admin page, only 1 ProductClass is created, and it has the same name as is being passed to track_stock(). Is there something else that needs to be done to enable this feature? track_stock() documentation is kind of sparse. In the output, track_stock looks like it is true in both instances. Does it have to be False while the child_objects are created, and then flipped to True?
To be able to correctly reflect the stock levels for any Product you need to have a Partner that will supply the Product and then you need to have StockRecord that links the Partner and the Products together.
First make sure that you have all that information in the database for each one of your Product variations.
Then you need to update your ProductClass and set the "track_stock" attribute as True since its None by default.
You also need to remove the ProductClass from your child products since they inherit the ProductClass from their Parent Product.
EDIT 1:
To add attributes to a Product you have to add a ProductAttribute for the ProductClass and then you can set the attributes directly on the Product like this example.
EDIT 2:
You also need to set the "net_stock_level" on the StockRecord.
To get a more in depth look into how Oscar gets the stock levels look into Selector. This class determines which pricing, tax and stock level strategies to use which you might need to customize in the future if you want to charge tax or offer different pricing based on the user.
After some research from the test factory, I solved the issue by specifying
product.stucture = 'parent'
On the parent object, and
product.structure = 'child'
on the child object. I also needed to change the custom attributes of my objects to a dict product_attributes, and then set each value on the object:
if product_attributes:
for code, value in product_attributes.items():
product_class.attributes.get_or_create(name=code, code=code)
setattr(product.attr, code, value)
It was not necessary to create a stock record for each parent object, as they track the stock records of the child objects to which they are associated. It was also not necessary to set track_stock = True, as it is set to True by default when creating a Product()
This answer was posted as an edit to the question Create a canonical "parent" product in Django Oscar programmatically by the OP Tui Popenoe under CC BY-SA 3.0.

flask-admin form: Constrain Value of Field 2 depending on Value of Field 1

One feature I have been struggling to implement in flask-admin is when the user edits a form, to constrain the value of Field 2 once Field 1 has been set.
Let me give a simplified example in words (the actual use case is more convoluted). Then I will show a full gist that implements that example, minus the "constrain" feature.
Let's say we have a database that tracks some software "recipes" to output reports in various formats. The recipe table of our sample database has two recipes: "Serious Report", "ASCII Art".
To implement each recipe, we choose one among several methods. The method table of our database has two methods: "tabulate_results", "pretty_print".
Each method has parameters. The methodarg table has two parameter names for "tabulate_results" ("rows", "display_total") and two parameters for "pretty_print" ("embellishment_character", "lines_to_jump").
Now for each of the recipes ("Serious Report", "ASCII Art") we need to provide the value of the arguments of their respective methods ("tabulate_results", "pretty_print").
For each record, the recipearg table lets us select a recipe (that's Field 1, for instance "Serious Report") and an argument name (that's Field 2). The problem is that all possible argument names are shown, whereas they need to be constrained based on the value of Field 1.
What filtering / constraining mechanism can we implement such that once we select "Serious Report", we know we will be using the "tabulate_results" method, so that only the "rows" and "display_total" arguments are available?
I'm thinking some AJAX wizardry that checks Field 1 and sets a query for Field 2 values, but have no idea how to proceed.
You can see this by playing with the gist: click on the Recipe Arg tab. In the first row ("Serious Report"), if you try to edit the "Methodarg" value by clicking on it, all four argument names are available, instead of just two.
# full gist: please run this
from flask import Flask
from flask_admin import Admin
from flask_admin.contrib import sqla
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.orm import relationship
# Create application
app = Flask(__name__)
# Create dummy secrey key so we can use sessions
app.config['SECRET_KEY'] = '123456790'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///a_sample_database.sqlite'
app.config['SQLALCHEMY_ECHO'] = True
db = SQLAlchemy(app)
# Create admin app
admin = Admin(app, name="Constrain Values", template_mode='bootstrap3')
# Flask views
#app.route('/')
def index():
return 'Click me to get to Admin!'
class Method(db.Model):
__tablename__ = 'method'
mid = Column(Integer, primary_key=True)
method = Column(String(20), nullable=False, unique=True)
methodarg = relationship('MethodArg', backref='method')
recipe = relationship('Recipe', backref='method')
def __str__(self):
return self.method
class MethodArg(db.Model):
__tablename__ = 'methodarg'
maid = Column(Integer, primary_key=True)
mid = Column(ForeignKey('method.mid', ondelete='CASCADE', onupdate='CASCADE'), nullable=False)
methodarg = Column(String(20), nullable=False, unique=True)
recipearg = relationship('RecipeArg', backref='methodarg')
inline_models = (Method,)
def __str__(self):
return self.methodarg
class Recipe(db.Model):
__tablename__ = 'recipe'
rid = Column(Integer, primary_key=True)
mid = Column(ForeignKey('method.mid', ondelete='CASCADE', onupdate='CASCADE'), nullable=False)
recipe = Column(String(20), nullable=False, index=True)
recipearg = relationship('RecipeArg', backref='recipe')
inline_models = (Method,)
def __str__(self):
return self.recipe
class RecipeArg(db.Model):
__tablename__ = 'recipearg'
raid = Column(Integer, primary_key=True)
rid = Column(ForeignKey('recipe.rid', ondelete='CASCADE', onupdate='CASCADE'), nullable=False)
maid = Column(ForeignKey('methodarg.maid', ondelete='CASCADE', onupdate='CASCADE'), nullable=False)
strvalue = Column(String(80), nullable=False)
inline_models = (Recipe, MethodArg)
def __str__(self):
return self.strvalue
class MethodArgAdmin(sqla.ModelView):
column_list = ('method', 'methodarg')
column_editable_list = column_list
class RecipeAdmin(sqla.ModelView):
column_list = ('recipe', 'method')
column_editable_list = column_list
class RecipeArgAdmin(sqla.ModelView):
column_list = ('recipe', 'methodarg', 'strvalue')
column_editable_list = column_list
admin.add_view(RecipeArgAdmin(RecipeArg, db.session))
# More submenu
admin.add_view(sqla.ModelView(Method, db.session, category='See Other Tables'))
admin.add_view(MethodArgAdmin(MethodArg, db.session, category='See Other Tables'))
admin.add_view(RecipeAdmin(Recipe, db.session, category='See Other Tables'))
if __name__ == '__main__':
db.drop_all()
db.create_all()
db.session.add(Method(mid=1, method='tabulate_results'))
db.session.add(Method(mid=2, method='pretty_print'))
db.session.commit()
db.session.add(MethodArg(maid=1, mid=1, methodarg='rows'))
db.session.add(MethodArg(maid=2, mid=1, methodarg='display_total'))
db.session.add(MethodArg(maid=3, mid=2, methodarg='embellishment_character'))
db.session.add(MethodArg(maid=4, mid=2, methodarg='lines_to_jump'))
db.session.add(Recipe(rid=1, mid=1, recipe='Serious Report'))
db.session.add(Recipe(rid=2, mid=2, recipe='ASCII Art'))
db.session.commit()
db.session.add(RecipeArg(raid=1, rid=1, maid=2, strvalue='true' ))
db.session.add(RecipeArg(raid=2, rid=1, maid=1, strvalue='12' ))
db.session.add(RecipeArg(raid=3, rid=2, maid=4, strvalue='3' ))
db.session.commit()
# Start app
app.run(debug=True)
I see two ways of tacking this problem:
1- When Flask-Admin generate the form, add data attributes with the mid of each methodArg on each option tag in the methodArg select. Then have some JS code filter the option tags based on the recipe selected.
EDIT
Here is a tentative try at putting a data-mid attribute on each option:
def monkeypatched_call(self, field, **kwargs):
kwargs.setdefault('id', field.id)
if self.multiple:
kwargs['multiple'] = True
html = ['<select %s>' % html_params(name=field.name, **kwargs)]
for (val, label, selected), (_, methodarg) in zip(field.iter_choices(), field._get_object_list()):
html.append(self.render_option(val, label, selected, **{'data-mid': methodarg.mid}))
html.append('</select>')
return HTMLString(''.join(html))
Select.__call__ = monkeypatched_call
The blocker is in the fact that those render calls are triggered from the jinja templates, so you are pretty much stuck updating a widget (Select being the most low-level one in WTForms, and is used as a base for Flask-Admin's Select2Field).
After getting those data-mid on each of your options, you can proceed with just binding an change on your recipe's select and display the methodarg's option that have a matching data-mid. Considering Flask-Admin uses select2, you might have to do some JS tweaking (easiest ugly solution would be to clean up the widget and re-create it for each change event triggered)
Overall, I find this one less robust than the second solution. I kept the monkeypatch to make it clear this should not be used in production imho. (the second solution is slightly less intrusive)
2- Use the supported ajax-completion in Flask-Admin to hack your way into getting the options that you want based on the selected recipe:
First, create a custom AjaxModelLoader that will be responsible for executing the right selection query to the DB:
class MethodArgAjaxModelLoader(sqla.ajax.QueryAjaxModelLoader):
def get_list(self, term, offset=0, limit=10):
query = self.session.query(self.model).filter_by(mid=term)
return query.offset(offset).limit(limit).all()
class RecipeArgAdmin(sqla.ModelView):
column_list = ('recipe', 'methodarg', 'strvalue')
form_ajax_refs = {
'methodarg': MethodArgAjaxModelLoader('methodarg', db.session, MethodArg, fields=['methodarg'])
}
column_editable_list = column_list
Then, update Flask-Admin's form.js to get the browser to send you the recipe information instead of the methodArg name that needs to be autocompleted. (or you could send both in query and do some arg parsing in your AjaxLoader since Flask-Admin does no parsing whatsoever on query, expecting it to be a string I suppose [0]. That way, you would keep the auto-completion)
data: function(term, page) {
return {
query: $('#recipe').val(),
offset: (page - 1) * 10,
limit: 10
};
},
This snippet is taken from Flask-Admin's form.js [1]
Obviously, this needs some tweaking and parametrising (because doing such a hacky solution would block you from using other ajax-populated select in the rest of your app admin + the update on form.js directly like that would make upgrading Flask-Admin extremely cumbersome)
Overall, I am unsatisfied with both solutions and this showcase that whenever you want to go out of the tracks of a framework / tool, you can end up in complex dead ends. This might be an interesting feature request / project for someone willing to contribute a real solution upstream to Flask-Admin though.
There is another easy solution that I made and it works
1- Create your first select option normally with data loaded on it and add a hook to it which will add js event listener when it selects change like this.
from wtforms import SelectField
form_extra_fields = {
'streetname': SelectField(
'streetname',
coerce=str,
choices=([street.streetname for street in StreetsMetadata.query.all()]),
render_kw={'onchange': "myFunction()"}
)
}
**2- Add a JavaScript URL file to the view you want to use this function in, for example.
def render(self, template, **kwargs):
#using extra js in render method allow use url_for that itself requires an app context
self.extra_js = [url_for("static", filename="admin/js/users.js")]
response = render_miror(self, template,**kwargs)
return response
3- Create a role-protected endpoint that you used for this view that will accept a GET request from JS based on the first value specified for the entry, for example this route returns a list of house numbers by querying the street name that came from the first entry
#super_admin_permission.require(http_exception=403)
#adminapp.route('/get_houses_numbers')
def gethouses():
request_data = request.args
if request_data and 'street' in request_data:
street = StreetsMetadata.query.filter(StreetsMetadata.streetname == request_data['street']).one_or_none()
street_houses = lambda:giveMeAllHousesList(street.excluded, street.min, street.max)
if street_houses:
return jsonify({'code': 200, 'houses': street_houses()})
else:
return jsonify({'code': 404, 'houses': []})
else:
return jsonify({'code': 400, 'street': []})
now python part completed time for JavaScript
4- We have to define three functions, the first of which will be called when the form build page is loaded and which do two things first,
A dummy select entry will be created using JS and append that entry to the same string input container
Make string entry read-only to improve user experience
Second, it will send a GET request to the specified route to get a list of house numbers using the specified street input value
Then get the result and create the option elements and append these options to the dummy selection, you can also select the first option while appending the options.
5- The second function "myFunction" is the hook defined in Python in this part
render_kw={'onchange': "myFunction()"}
This function will do nothing new, it will only send a GET request when the first specified input value is changed, send a GET request to get a list of new house numbers based on the given street name input value by doing a query on the database, then dump the inner HTML of the dummy selection entry , then create and append new options to it.
6- The last function is the callback function which listens for the change on the dummy select entry created with JS when the user chooses the house number which will be reflected in the main string entry, finally you can click save and you will see it working
Note that this whole idea I created is not as good as the built in flask admin, but if you are looking for the end goal and without any problems you can use it
My JS code
/*
This Function when run when a form included it will create JS select input with the
default loaded streetname and add house number on that select this select will used
to guide creator of the house number or to select the house number
*/
async function onFlaskFormLoad(){
const streetSelect = document.querySelector("#streetname");
const checkIfForm = document.querySelector("form.admin-form");
if (checkIfForm){
let checkSelect = document.querySelector("#realSelect");
if (!checkSelect){
const mySelectBox = document.createElement("select");
const houseString = document.querySelector("#housenumber");
const houseStringCont = houseString.parentElement;
mySelectBox.classList.add("form-control")
mySelectBox.id = "realSelect";
houseStringCont.appendChild(mySelectBox);
mySelectBox.addEventListener("change", customFlaskAdminUnpredefinedSelect);
houseString.setAttribute("readonly", "readonly");
const res = await fetch(`/get_houses_numbers?street=${streetSelect.value}`);
const data = await res.json();
console.log(data);
if (data.code == 200 && mySelectBox){
data.houses.forEach( (houseOption, index)=>{
if (index == 0){
houseString.value = houseOption;
}
let newHouse = document.createElement("option");
newHouse.setAttribute("value", houseOption);
newHouse.innerText = houseOption;
mySelectBox.appendChild(newHouse);
});
}
}
}
}
onFlaskFormLoad();
/*
this function will called to change the string input value to my custom js select
value and then use that string to house number which required by flask-admin
*/
function customFlaskAdminUnpredefinedSelect(){
const theSelect = document.querySelector("#realSelect");
const houseString = document.querySelector("#housenumber");
houseString.value = theSelect.value;
return true;
}
/*
flask admin hook that will listen on street input change and then it will send
get request to secured endpoint with role superadmin required and get the housenumbers
using the streetname selected and then create options and add to my select input
*/
async function myFunction(){
const streetSelect = document.querySelector("#streetname");
const houseString = document.querySelector("#housenumber");
const houseStringCont = houseString.parentElement;
const theSelect = document.querySelector("#realSelect");
const res = await fetch(`/get_houses_numbers?street=${streetSelect.value}`);
const data = await res.json();
console.log(data);
if (data.code == 200 && theSelect){
theSelect.innerHTML = "";
data.houses.forEach( (houseOption, index)=>{
if (index == 0){
houseString.value = houseOption;
}
let newHouse = document.createElement("option");
newHouse.setAttribute("value", houseOption);
newHouse.innerText = houseOption;
theSelect.appendChild(newHouse);
});
}
}
Now if I change the street name of the first specified input I will get a new list containing the numbers based on the first input value, note if you have a way to create a python field that accepts the non-predefined options then there is no need to create dummy input you can create and append the new options Directly to second select input
final result

Categories

Resources