Multiselection Drop down selector default value

Hi

I am using multiselection for 8 value but the user does not need to fill all of them to save in the DB the problem is that when the selection is not complet the link does not esxist. In a previous test i was using label and it work well the issue now is i have around 150 value in total and creating 150 hidden label is probably not the best way to do it.

Exemple the multiselect can have up to 8 value but in this case user entered only 2.
image

Then my script for my querry does not work because the link does not exist.

Also is there a way to set the maximum value to exemple 8 ?

Thank You

First off, having a hard-coded number of params is a questionable way to do this. Secondly, what value do you expect these params to have when not supplied by a user? Additionally, why are you binding a driving value against the displayed text of another component? Always bind against the pure data, so use whatever value the Label is bound to instead of using the Label itself.

# This will default all of the "missing" params to `None`
selected_options = self.getSibling("DROP_ABS").props.value
abs_params = [None] * 8
params = {}
for i in range(len(selected_options)):
    abs_params[i] = selected_options[i]
for i in range(len(abs_params)):
    params["ABS_{0}".format(i + 1)] = abs_params[i]  # i + 1 to account for 1-indexed param names
params["ShiftID"] = self.getSibling("LABEL_15").props.text

# and then your query

A better way would be to use a prepared query with some indeterminate number of param values, and then use a script like this:

select_options = 
query = "select * from MyTable where name in ({0}) and ShiftID=?".format(['?' for option in select_options])
system.db.runPrepUpdate(query=query, args=selected_options + [self.getSibling("LABEL_15").props.text], database="SomeDB")

As for setting the maximum amount of options a user could select, you'd use the change script and check against the length of currentValue.value.

if currentValue and len(currentValue.value) > 8:
    # note that this doesn't stop a user from selecting a ninth value, but it immediately removes the ninth value after selection.
    self.props.value = previousValue.value

itertools can help make this a bit more pythonic:

from itertools import izip_longest as zipl

params = {
	"ABS_{}".format(i): opt
	for i, opt in zipl(xrange(1, 9), selected_options[:8], fillvalue=None)
}
params["ShiftID"] = self.getSibling("LABEL_15").props.text
2 Likes

Much better, but I think this would still require a bit more to adjust from being 0-indexed to 1-indexed.

oops, edited for 1-based indexing.
I always forget those exist :smiley:

edit:
but to be honest, we'd need to see what the query actually looks like to figure out the best way of doing this.

1 Like

Hi work well only thing is that this creat me a error in the designer when i have a no value. Probably not a problem.

if currentValue and len(currentValue.value) > 8:
# note that this doesn't stop a user from selecting a ninth value, but it immediately removes the ninth value after selection.
self.props.value = previousValue.value

I used your firts option as the second one look to be over my current skillset.

Here is the querry i just learned how to do querry probably not the best.

IF EXISTS (SELECT * FROM Employe_Machine WHERE ShiftID = :ShiftID)
UPDATE Employe_Machine
SET ABS_1 = :ABS_1, ABS_2 = :ABS_2, ABS_3 = :ABS_3, ABS_4 = :ABS_4, ABS_5 = :ABS_5, ABS_6 = :ABS_6, ABS_7 = :ABS_7, ABS_8 = :ABS_8,
AS01_1 = :AS01_1, AS01_2 = :AS01_2, AS01_3 = :AS01_3, AS01_4 = :AS01_4, MP05_1 = :MP05_1, MP05_2 = :MP05_2, MP05_3 = :MP05_3, MP05_4 = :MP05_4,
MP09_1 = :MP09_1, MP09_2 = :MP09_2, MP09_3 = :MP09_3, MP09_4 = :MP09_4, MP10_1 = :MP10_1, MP10_2 = :MP10_2, MP10_3 = :MP10_3, MP10_4 = :MP10_4,
MP14_1 = :MP14_1, MP14_2 = :MP14_2, MP14_3 = :MP14_3, MP14_4 = :MP14_4, MP15_1 = :MP15_1, MP15_2 = :MP15_2, MP15_3 = :MP15_3, MP15_4 = :MP15_4,
MP16_1 = :MP16_1, MP16_2 = :MP16_2, MP16_3 = :MP16_3, MP16_4 = :MP16_4, MP17_1 = :MP17_1, MP17_2 = :MP17_2, MP17_3 = :MP17_3, MP17_4 = :MP17_4,
MP18_1 = :MP18_1, MP18_2 = :MP18_2, MP18_3 = :MP18_3, MP18_4 = :MP18_4, MP19_1 = :MP19_1, MP19_2 = :MP19_2, MP19_3 = :MP19_3, MP19_4 = :MP19_4,
MP20_1 = :MP20_1, MP20_2 = :MP20_2, MP20_3 = :MP20_3, MP20_4 = :MP20_4, MP21_1 = :MP21_1, MP21_2 = :MP21_2, MP21_3 = :MP21_3, MP21_4 = :MP21_4,
MP22_1 = :MP22_1, MP22_2 = :MP22_2, MP22_3 = :MP22_3, MP22_4 = :MP22_4, MP23_1 = :MP23_1, MP23_2 = :MP23_2, MP23_3 = :MP23_3, MP23_4 = :MP23_4,
MP24_1 = :MP24_1, MP24_2 = :MP24_2, MP24_3 = :MP24_3, MP24_4 = :MP24_4, Relais = :Relais
WHERE ShiftID = :ShiftID
ELSE
INSERT INTO Employe_Machine (ABS_1, ABS_2, ABS_3, ABS_4, ABS_5, ABS_6, ABS_7, ABS_8, AS01_1, AS01_2, AS01_3, AS01_4, MP05_1, MP05_2, MP05_3, MP05_4, MP09_1, MP09_2, MP09_3, MP09_4, MP10_1, MP10_2, MP10_3, MP10_4, MP14_1, MP14_2, MP14_3, MP14_4, MP15_1, MP15_2, MP15_3, MP15_4, MP16_1, MP16_2, MP16_3, MP16_4, MP17_1, MP17_2, MP17_3, MP17_4, MP18_1, MP18_2, MP18_3, MP18_4, MP19_1, MP19_2, MP19_3, MP19_4, MP20_1, MP20_2, MP20_3, MP20_4, MP21_1, MP21_2, MP21_3, MP21_4, MP22_1, MP22_2, MP22_3, MP22_4, MP23_1, MP23_2, MP23_3, MP23_4, MP24_1, MP24_2, MP24_3, MP24_4, ShiftID)
VALUES (:ABS_1, :ABS_2, :ABS_3, :ABS_4, :ABS_5, :ABS_6, :ABS_7, :ABS_8, :AS01_1, :AS01_2, :AS01_3, :AS01_4, :MP05_1, :MP05_2, :MP05_3, :MP05_4, :MP09_1, :MP09_2, :MP09_3, :MP09_4, :MP10_1, :MP10_2, :MP10_3, :MP10_4, :MP14_1, :MP14_2, :MP14_3, :MP14_4, :MP15_1, :MP15_2, :MP15_3, :MP15_4, :MP16_1, :MP16_2, :MP16_3, :MP16_4, :MP17_1, :MP17_2, :MP17_3, :MP17_4, :MP18_1, :MP18_2, :MP18_3, :MP18_4, :MP19_1, :MP19_2, :MP19_3, :MP19_4, :MP20_1, :MP20_2, :MP20_3, :MP20_4, :MP21_1, :MP21_2, :MP21_3, :MP21_4, :MP22_1, :MP22_2, :MP22_3, :MP22_4, :MP23_1, :MP23_2, :MP23_3, :MP23_4, :MP24_1, :MP24_2, :MP24_3, :MP24_4, :ShiftID)

Holy mother of sandwiches, that's one ugly query !

It's not that complicated. Let's take it step by step, I can also explain in french if some things are unclear.
I'll simplify a few things that are not relevant to understand what's going on.

from itertools import izip_longest

zip is a built-in function that takes lists, and produces tuples containing an element from each list.
For example:

a = ['a', 'b', 'c']
b = [1, 2, 3]
zip(a, b)

Will produce [('a', 1), ('b', 2), ('c', 3)].
The function name actually reflects quite well what it does, if you think about a zipper (fermeture éclair):
The problem with zip in this case is that it stops as soon as the end of one of the lists is reached.
So we use izip_longest instead, which will go to the end of the longest list, filling the missing values with the supplied fillvalue (or None by default).
For example:

a = ['a', 'b', 'c']
b = [1, 2, 3, 4, 5]
izip_longest(a, b, fillvalue='x')

will produce [('a', 1), ('b', 2), ('c', 3), ('x', 4), ('x', 5)]

Using this, it doesn't matter how many options were selected with the dropdown.

results = [do_something(element) for element in iterable]

This is a list comprehension. It's basically a one-line loop.
What it does here is take every element from iterable, pass it to do_something, and returns a list with all the results.
It's equivalent to this:

results = []
for element in iterable:
    results.append(do_something(element))

But we don't need a list, we need a dictionary. Good thing you use the same principle to build a dictionary:

d = {key: value for key, value in iterable}

This is where zip comes in handy, as it produces tuples we can use to get our key and value.
If you have a list with keys and a list with values, zipping them together allows us to use this syntax:

d = {k: v for k, v in zip(keys, values)}

The exact same syntax works with izip_longest.

We have a our values list, it's the selected_options. But we don't have a keys list... we can generate it with a list comprehension:

keys = ["ABS{}".format(i) for i in xrange(1, 9)]

range(low, high) will generate a list of integers between low included and high excluded,
so range(1, 9) generates [1, 2, 3, 4, 5, 6, 7, 8].
We can use those values to format a string with "ABS{}".format(n).
xrange does the same thing as range, except it produces a generator instead of a list, but don't worry about this.

So, we could make a list of keys beforehand... or inline it in the dictionary comprehension:

d = {
    "ABS{}".format(i): option
    for i, option in zip(xrange(1, 9), selected_options)
}

Putting things on different lines doesn't affect the outcome, it just makes things clearer.

We're almost there, we just need to replace zip with izip_longest, and add a fillvalue in case there are less than 8 options. If we don't provide one, it will use None, which is probably what we want here anyway... but for the sake of explicitness, we'll add it anyway.
Then make sure we only use the first 8 options, which can be achieve with a simple slice: iterable[start:end] will cut the list and return the elements between start and end.
We'll also alias izip_longest to zipl when importing it. It's not necessary, it's just a habit I have.
Putting everything together:

from itertools import izip_longest as zipl

params = {
	"ABS_{}".format(i): opt
	for i, opt in zipl(xrange(1, 9), selected_options[:8], fillvalue=None)
}

All that's left to do is add any extra key:value pair you need in the dictionary, and you're done.

See, nothing complicated here !

4 Likes

Psssst! The built-in map() does this if you give it None as its first argument. That is, your

zipl(iterableA, iterableB, ....)

is the same as

map(None, iterableA, iterableB, ....)

Saves you an import and allows you to use a highly-optimized built-in.

3 Likes

I don't see myself explaining this during code review :X
Also, if you need a fill value different than None... It gets ugly.

The None is not a fill value. It is a no-op transform function. You can give map() a function or lambda that does pretty much anything you want. But with a no-op, it is equivalent to izip_longest.

Learning map() is worth your time.

1 Like

No, I know map and how it works.
But it DOES pad with None, and if you need something else as fill value, it gets ugly.