figoyouwei / taipy_success

This is a sample code that tests the deployment on heroku
2 stars 2 forks source link

Multiple-Selector with Django #17

Open figoyouwei opened 1 month ago

figoyouwei commented 1 month ago

Hi,

I need some help with this code which runs multiple selector in a Django questionnaire context. https://github.com/figoyouwei/taipy_success/blob/main/littlegiant/questionnaire/app.py

First, when running this app.py, it returns a warning, could it be solved?

WARNING:root:
--- 1 warning(s) were found for page '/' in variable 'gui_core_sc_error' ---
 - Warning 1: Cannot run adapter for choice_adapter:
'str' object has no attribute 'symbol'
-----------------------------------------------------------------------------

More importantly, how do I get the content from this multiple selectors? The page looks like this:

Screenshot 2024-09-12 at 17 12 44
figoyouwei commented 1 month ago

Please focus on this issue today, thanks.

AlexandreSajus commented 1 month ago

The warning is triggered because choice_adapter receives a string object:

Then choice_adapter tries to print cho.symbol, which fails because cho is a string.

You can avoid the warning by preventing choice_adapter from dealing with non-Choice inputs:

def choice_adapter(cho: Choice):
    if type(cho) is Choice:
        print(cho.symbol)
        choice_text = "{}: {}".format(cho.symbol, cho.text)
        return (cho.symbol_no, choice_text)
    else:
        return None
AlexandreSajus commented 1 month ago

To get the content of all selectors, my solution is a bit convoluted as we did not expect people to generate selectors this way. I will discuss R&D to make this simpler in the future. Here is a solution that works:

When I select the second answer and then the first answer and press the "Submit" button: image

The console prints state.answers with value ['2', '1']

Here is the full code:

import os
import django

# Set up Django environment
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "questionnaire.settings")
django.setup()

from questions.models import Question
from questions.models import Choice

# Fetch the questions and choices
Fragen = Question.objects.all()[0:2]
Choices = Fragen

selected_choice = Choice()

def choice_adapter(cho: Choice):
    if type(cho) is Choice:
        choice_text = "{}: {}".format(cho.symbol, cho.text)
        return (cho.symbol_no, choice_text)
    else:
        return None

answers = [""] * len(Fragen)

def get_selected(state, var_name, value):
    index = int(var_name[10:])
    state.answers[index] = value
    state.answers = state.answers

def submit(state):
    print(state.answers)

# Create the questionnaire page with Taipy GUI Builder (TGB)
from taipy.gui import Gui
import taipy.gui.builder as tgb

# Use Taipy GUI Builder (TGB) to display all questions and choices
with tgb.Page() as page:
    # Title and Theme Toggle
    tgb.toggle(theme=True)
    tgb.text("### Taipy Questionnaire", mode="md", class_name="text-center pb1")

    # Loop through all questions and their corresponding choices
    with tgb.layout(columns="1 2 1", class_name="text-center"):
        with tgb.part():
            tgb.text(f"", mode="md")

        with tgb.part(class_name="text-left"):
            # Loop through all questions using enumerate and display them dynamically
            for i, frage in enumerate(Fragen):
                # Display the question text
                tgb.text(f"#### {frage.text}", mode="md")

                exec(f"selection_{i} = ''")

                # Display selector for each question's choices
                tgb.selector(
                    value="{selection_" + str(i) + "}",
                    lov=frage.get_choices(),  # Get the choices for each question
                    type=Choice,
                    adapter=choice_adapter,
                    on_change=get_selected,  # Callback for when the selection changes
                )

            # Submit button
            tgb.button(label="Submit", class_name="mt2", on_action=submit)

        with tgb.part():
            tgb.text(f"Score", mode="md")

# ------------------------------
# Main app
# ------------------------------

if __name__ == "__main__":
    print("Starting Questionnaire app...")

    # Initialize and run the GUI
    gui = Gui(page)
    gui.run()
AlexandreSajus commented 1 month ago

Actually, here is a more elegant solution that does not use exec:

image

When the user selects something, it prints:

Getting selected items
What country do you like most?
Selected: 3
Which city do you like most?
Selected: 1

Here is the code:

import os
import django

# Set up Django environment
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "questionnaire.settings")
django.setup()

from questions.models import Question
from questions.models import Choice

# Fetch the questions and choices
Fragen = Question.objects.all()[0:2]
Choices = Fragen

selected_choice = Choice()

def choice_adapter(cho: Choice):
    if type(cho) is Choice:
        choice_text = "{}: {}".format(cho.symbol, cho.text)
        return (cho.symbol_no, choice_text)
    else:
        return None

def get_selected(state):
    print("Getting selected items")
    for i, frage in enumerate(Fragen):
        print(frage.text)
        print(f"Selected: {answers[f'frage_{i}']}")

answers = {}
for i, frage in enumerate(Fragen):
    answers[f"frage_{i}"] = ""

# Create the questionnaire page with Taipy GUI Builder (TGB)
from taipy.gui import Gui
import taipy.gui.builder as tgb

# Use Taipy GUI Builder (TGB) to display all questions and choices
with tgb.Page() as page:
    # Title and Theme Toggle
    tgb.toggle(theme=True)
    tgb.text("### Taipy Questionnaire", mode="md", class_name="text-center pb1")

    # Loop through all questions and their corresponding choices
    with tgb.layout(columns="1 2 1", class_name="text-center"):
        with tgb.part():
            tgb.text(f"", mode="md")

        with tgb.part(class_name="text-left"):
            # Loop through all questions using enumerate and display them dynamically
            for i, frage in enumerate(Fragen):
                # Display the question text
                tgb.text(f"#### {frage.text}", mode="md")

                # Display selector for each question's choices
                tgb.selector(
                    value="{answers.frage_" + str(i) + "}",
                    lov=frage.get_choices(),  # Get the choices for each question
                    type=Choice,
                    adapter=choice_adapter,
                    on_change=get_selected,  # Callback for when the selection changes
                )

            # Submit button
            tgb.button(
                label="Submit",
                class_name="mt2",
            )

        with tgb.part():
            tgb.text(f"Score", mode="md")

# ------------------------------
# Main app
# ------------------------------

if __name__ == "__main__":
    print("Starting Questionnaire app...")

    # Initialize and run the GUI
    gui = Gui(page)
    gui.run()
figoyouwei commented 1 month ago

That was elegant.

figoyouwei commented 1 month ago

selector returned more than one Choice. The code is updated and the video in a separate email, please focus on this issue, thanks. https://github.com/figoyouwei/taipy_success/blob/main/littlegiant/questionnaire/app.py

figoyouwei commented 1 month ago

Could I expect an update on this Issue?

AlexandreSajus commented 1 month ago

I found the issue:

The choices: B: Tokyo with Score: 2 and C: Shanghai with Score: 0 have the same symbol_no (cho.symbol_no) equals to 2

When you select one of the two, the choice_adapter will tell Taipy that you selected their cho.symbol_no. Since they both have the same symbol_no, it will select both of them.

Either ensure that all choices for the same question have different symbol_no in the dataset or change choice_adapter so that it does not use symbol_no as a key to identify the choice.

figoyouwei commented 1 month ago

ok, thanks, it is running fine now. Just one technical question which I found contradicting to my previous selector experience:

You see, in this code, the value returns ChatSession instance as defined in the tgb.selector.type

tgb.text("### Previous activities", mode="md", class_name="h5 mt2 mb-half")
tgb.selector(
    value="{selected_session}",
    lov="{sessions}",
    on_change=select_session,
    # NOTE: displayed text of selector item
    type=ChatSession,
    adapter=selector_adapter,
    # NOTE: css identifier
    id="past_prompts_list",
    class_name="past_prompts_list",
)

but in the questionnaire code, why the value didn't return a Choice instance as define in type?

with tgb.part(class_name="text-left"):
    # Loop through all questions using enumerate and display them dynamically
    for i, frage in enumerate(Fragen):
        # Display the question text
        tgb.text(f"#### {frage.text}", mode="md")

        # Display selector for each question's choices
        # NOTE: Difference between Django data model and Pydantic 
        tgb.selector(
            value="{selected_choices.frage_" + str(i) + "}",
            lov=frage.get_choices(),  # Get the choices for each question
            type=Choice,
            adapter=choice_adapter,
            on_change=get_selected,  # Callback for when the selection changes
        )
AlexandreSajus commented 1 month ago

From what I understand, we use "type" to understand the "lov" when it is ambiguous. "type" does not define which value will be used, the "adapter" defines which value will be chosen