PDF printing - SerializationError: Cannot pass XXX object to a server function

Hi,

I’m trying to print a Dashboard with plots to PDF.

This is basically the Dashboard example from the docs with a couple of enhancements, plus trying to print the results to PDF.

First, what I think I got right (of course, subject to be corrected)

  1. What you will print is not the page the user is seeing, but another Form that receives the values that the user has entered in the interactive form
  2. Then you call a server function render_form to which you pass the values entered by the user, and the name of the “printing form”.
  3. What will happen is that the server function will call the render_form which is basically the same as open_form, so you rebuild the original Form with a perhaps a more printable layout.

So far so good. I’ve tried this with simple components like dropdown lists and labels and it works.

Now, the problem:
In the application, the user creates a bunch of charts that display inside a grid panel. The charts are created as Timeplot components (which is another Form/Class in the application), and added to the gridpanel.
So, following the same logic I pass the gridpanel to the server function so it can pass it to the “printing form” on the render_form call.

And there is when I get

SerializationError: Cannot pass TimePlot object to a server function: arguments [2][1][“$_components”][0][0]

I understand that there are limitations in the type of object you can pass from client to server and vice-versa.
So it seems clear that I cannot pass (or don’t know how to pass) the gridpanel to the server.

I’ve read many posts with similar, but not equal problems, and I was a bit overwhelmed because the OO is still a bit elusive to me.

I’ve also thought about defining the Timeplot class as Portable
But for that I would need the Form to be a Module instead.
And, I’m not sure that this would solve the problem, as in another post Meredith says that despite the Portable definition, the limitations on the kind of object you can pass remains the same in terms of what kind of things you can pass.

At this point I’m a bit lost on how to procede.
Any direction I look, looks a bit confusing in that, first, for printing this I have to rebuild the Form, and now with this more complex type, I might need to pass not only the input of the main form (Dashboard) with the plots created by the user into the gridpanel, but also the input of the Timeplot form, and then write a whole new logic in the “printing form” to simulate the user input and the plot generation, and only then be able to print them.

This doesn’t seem right (too much work for what it is advertised as a very simple solution), so probably it is not, and I’m looking at it the wrong way.

So, I would really appreciate a pointer in the right direction!!
I attach the application so you can see it firsthand and match it with the explanation (in case I haven’t been clear enough)

Thank you very much!!

Generally speaking, you never want to pass a Form to the server. Instead. pass the data that constructed the form to the server. That’s what the server really cares about, not the UI widgets in the form, but what data the form used to construct those widgets.

Well, I understand what you say, but that is more or less what I was talking about, that might be simple for a simple Form, but in this case seems too complicated.

In order to print this charts that are build from the Timeplot Form inside a Gridpanel, inside the Dashboard Form, I would need to go to the deepest level and get the data used to build each of the plots in the Timeplot form, plus the addtional options/filters the user might have chosen in that form, plus the options the user chose to display the whole Dashboard Form (different from the ones chosen in the Timeplot), and then build some kind of structure with that, and pass that to the server function, and then reconstruct the building of everything from scratch, only to print it.

Is that really the way it is? I feel I’m still missing something.

You have code that rendered the form on the client based on some input, you need to provide the same input and run the same code on the client side.

A simple example of a form that shows an invoice, on the client you could do something like:

frm = InvoiceForm(invoice_id=invoice_id)
open_form(frm)

And on the server would be something like:

anvil.pdf.render_form('InvoiceForm', invoice_id=invoice_id)

@stefano.menci
Great! I understand that.

Now, please take a look at this:

This is the code I use to render the form: (the user will call this n times and generate different plots in the same form, and store them in a gridpanel)

new_plot = TimePlot()  # Call the create plot function, which is in fact a Form
self.grid_panel_1.add_component(new_plot, width_xs=6)

That calls TimePlot() that basically builds the plot based on what type of plot the user selected, and after the plot is built it puts the resulting plot in a gridpanel:

if trace_type == 'scatter':
    trace = go.Scatter(x=Data.time_data, y=Data.y_data[variable])
elif trace_type == 'bar':
    trace = go.Bar(x=Data.time_data, y=Data.y_data[variable])
elif trace_type == 'line':
    trace = go.Scatter(x=Data.time_data, y=Data.y_data[variable], mode='lines')
elif trace_type == 'box':
    trace = go.Box(x=Data.time_data, y=Data.y_data[variable])      
elif trace_type == 'area':
    trace = go.Scatter(x=Data.time_data, y=Data.y_data[variable], mode='lines', fill='tozeroy')
elif trace_type == 'mesh3d':
    trace = go.Mesh3d(x=Data.time_data, y=Data.y_data[variable])

self.plot_1.data = [trace]

This is the data it uses to build the plots: (and this is called from the same form that calls the plot creation)

global time_data
global y_data
time_data, y_data = anvil.server.call_s(‘get_weather_for_day’, dt)

And this is the printing function to which I have to pass all the above, plus the options the user chose to build and style the plots (theme, date, type of plot, the Data to plot, the variable to plot against the X-axis in each plot)

@anvil.server.callable
def create_pdf(fecha, template):
media_object = anvil.pdf.render_form(‘PaginasSostenibilidad.Dashboard_PrintPDF’, …)
return media_object

So, what do I pass to the render_from?
Do I have to gather all that infomation pieces I mentioned above, in a sort of list or dictionary and then pass that to the render_form?, and in the Dashboard_PrintPDF which is the Form called by the render_form, unpack all that information and render the plots one by one using the Data, and all the other pieces of information?, and only after doing that I will be able to have the printed version of my Form?

With a simple case, it is pretty clear, I agree with you and @jshaffstall 100%.
But in this case, translating from your example, it seems to imply iterating over each of the plot objects and getting the information from each one which will vary depending on the options the user chose to build each individual plot, and saving it into a data structure, plus the other information in the forms, and pass that to the printing function, and then rebuild all from scratch.

Is all this really what is required?
Again, I’m not very clear with OO, so maybe I’m seeing a great difficulty where there is none, and that is why I’m kindly asking for orientation for this particular situation (and not the general idea which it is indeed simple, “gather the information and pass it to the render_form”)

Yes, easy! :slight_smile:

Actually, it’s often required one more parameter printing_pdf, so you can do things like button1.visible = not printing_pdf.

I usually, even when the form has many interaction with the user, always collect all the input and store it somewhere, so I can call an update_ui function that regenerates the whole UI.

I don’t know how long it will take to your plots to generate, but, unless the user interactions include some drags, this technique allows you to keep user input and UI generation separated and much easier to test. You could create a few test json input scenarios and easily test them. You would just need to add self.input_data = {[...]} and the form would immediately render.

If the code that updates the UI is sprinkled around all the event managers, where each input updates one piece of the UI, then you are out of luck.

If each event manager updates the input data structure and calls update_ui, then everything is easy.

1 Like

OMG Bad news for me. :astonished: :laughing: :joy: :sob:

To answer your question: The first plot takes a couple of seconds to generate, and the following almost nothing because I cache the dataset. (and they all use the same dataset, just different variables to plot)

Ok. I’ll try to give a try to your suggestion, but writing code for traversing objects and managing data structures is not my thing (but it seems it will have to start being :grin: )

Would you mind sharing a small working example of your self.input_data/update_ui strategy?

Thank you!!