Day 6 of Hanukkah at Anvil

Build a web app for each night of Hanukkah, with nothing but Python!

Strong opinions

Sourcream? Applesauce? Maybe even… ketchup? Ashkenazim have been arguing over this for generations’ worth of Hanukkot, but now, with the power of Anvil, we can finally lay the question to rest. What IS the best latke topping?

In today’s app, we let you have your say on the best latke topping of all time… or at least, pick from the 10 options provided! Vote here:

https://latke-toppings-survey.anvil.app

Or get the code here:

How is the survey page made?

Just like the spellings app, we choose the ‘Hello San Francisco’ style when creating a new app and replace the header.jpg asset with a suitably wintery image.

The UI is made up of Link components, each with an Image and a Label inside. For each Link component, we write a ‘click’ event handler to define what happens when someone clicks on the image or the label.

In the event handler, we first call a server function which registers a vote for that topping, and then call open_form to take us to the results page.

def applesauce_link_click(self, **event_args):
    vote_and_open_results("applesauce")

..

def vote_and_open_results(topping):
    anvil.server.call("register_vote", topping)
    open_form('ResultsForm')

Registering votes and preventing spam

In the back end, we have a Data Table containing the information for our toppings and their current standings.

We also have the server function which registers the vote when a link is clicked:

@anvil.server.callable
def register_vote(topping):
  if not anvil.server.cookies.local.get('voted', False):
    row = app_tables.tallies.get(topping=topping)
    row['votes'] += 1
    anvil.server.cookies.local['voted'] = True

The bits using anvil.server.cookies are there to ensure that a single user can’t just vote for a given topping over and over again. Anvil’s cookie handling allows you to store data for a given user for 30 days, so unless you care enough to continuously open incognito windows or clear your cookies, it means that your vote only counts once.

Wonderful! We’ve now built something that’ll let people vote for a topping; now, we need to build a way to display the results.

in the server, we add two functions: one to send the current vote standings back to the client, and one to tell us who the current winner is.

@anvil.server.callable
def get_votes_dict():
  votes_dict = {}
  for row in app_tables.tallies.search():
    votes_dict[row['display_name']] = row['votes']
  return votes_dict


@anvil.server.callable
def get_winner():
  winning_votes = 0
  for row in app_tables.tallies.search():
    if row['votes'] > winning_votes:
      winning_votes = row['votes']
      winner = (row['topping'], row['display_name'], row['votes'])
    elif row['votes'] == winning_votes:
      winner = False
  return winner

The get_winner function actually returns False if there’s a tie. If there is a single winner, it’ll send back the information from its row in the data table, so that we can display it to the user. We’ll see how this data gets used in the client now.

Displaying the results

Earlier, we called a function called open_form in the client code. Now, we need to build a new form for it to open, called ResultsForm.

Here’s how it looks in the GUI:

That’s an XYPanel called winner_panel above, and a Plot called results_plot below. Since the way this page looks is so dependent on what gets returned from the server, we do most of the construction of it in code rather than the GUI, which is why the above image looks somewhat bare!

Here’s the code in its __init__ method:

class ResultsForm(ResultsFormTemplate):
  def __init__(self, **properties):
    # Set Form properties and Data Bindings.
    self.init_components(**properties)
    
    # Display the winner(s)
    winner = anvil.server.call('get_winner')

    if winner:
      print(winner)
      # Each of the image files is uploaded as an asset, named for the topping
      img_source = "https://latke-toppings-survey.anvil.app/_/theme/{}.png".format(winner[0])
      image = Image(source=img_source)
      name_label = Label(text=winner[1], font="Georgia", font_size=45)
      votes = winner[2]
      if votes == 1:
        votes_label = Label(text="with 1 vote", font="Georgia", font_size=45)
      else:
        votes_label = Label(text="with {} votes".format(winner[2]), font="Georgia", font_size=45)
        
      self.winner_panel.add_component(image, x=100, y=100, width=300, height=300)
      self.winner_panel.add_component(name_label, x=500, y=100)
      self.winner_panel.add_component(votes_label, x=500, y=200)
    else:
      tied_label = name_label = Label(text="Tied!", font="Georgia", font_size=60)
      self.winner_panel.add_component(tied_label, x=500, y=100)
    
    # Display the vote totals
    votes_dict = anvil.server.call('get_votes_dict')
    names = []
    votes = []
    for name, vote in votes_dict.items():
      names.append(name)
      votes.append(vote)
    self.results_plot.data = go.Bar(
      x = names,
      y = votes,
      name="Latke Topping Survey Results"
    )

That’s a lot! The first if loop handles the two possibilities: a distinct winner, or a tie. In the tie situation, we simply display a label saying ‘Tied!’ in the XYPanel, whereas if there is a clear winner, we add its image, name, and number of votes it currently has.

Then, we put the vote information into a Plotly graph.

Here’s how it looks with a tie:

And with a clear winner:

Now that we’ve added this page, we can add a button at the top of the survey page to take us right there without having to vote.

def results_button_click(self):
  open_form('ResultsForm')

Refreshing the page will take you back to the survey form, but clicking another topping won’t count towards the vote standings, because you’ve got voted = True in your cookies (unless you cheated, and opened an incognnito window…)

That’s it!

And that’s all there is to it! Now get sharing, so we can get enough data for a conclusive result ;)




Give the Gift of Python

Share this post:


Get tomorrow's app in your inbox

Don't miss a day! We'll mail you a new web app every night of Hanukkah: