Skip to content

Instantly share code, notes, and snippets.

@rfong
Last active May 17, 2021 04:36
Show Gist options
  • Save rfong/d4fa5df918ad71c2fac466b795617844 to your computer and use it in GitHub Desktop.
Save rfong/d4fa5df918ad71c2fac466b795617844 to your computer and use it in GitHub Desktop.
Run a simple multi-prize raffle, intaking cross-platform submissions from attendees, with the help of some manual data entry

Cross-platform event raffle

Context

Run a simple online giveaway raffle, where raffle tickets are awarded for completing challenges across a variety of different platforms:

  • Facebook tagged submissions
  • Instagram tagged submissions
  • Private uploads
  • Manual tracking (e.g. volunteer shifts, performers, teachers, other staff)

Other points:

  • We have multiple raffle prizes and thus multiple raffle jars to choose from.
  • Only people with an Eventbrite registration are eligible to win the raffle.
  • Not all raffle submitters are guaranteed to have a valid Eventbrite registration.
  • Not all raffle qualifiers are guaranteed to use the raffle jar, or to have a valid Eventbrite registration.
  • Organizer team personally knows a good chunk of attendees, but not all of them.
  • We're a very small volunteer organizer team with a minimal budget.

Our goals

  • Minimize staff overhead. We want to do as little manual checking and cross-platform export as possible, and we want to batch our work as much as possible.
  • Minimize communication overhead in general.
  • Privacy: Do not expose attendee registration information to non-organizers.
  • Confident matching. Able to map all earned raffle tickets back to some canonical eligible ID.

Out of the box solutions?

  • Shortstack - $99/mo
  • Rafflecopter - Has free & inexpensive plans but doesn't provide the platform integrations we'd need to automatically check video submissions
  • Gleam.io - Minimum $49 for monthly-only, $149 for video uploads

All options are out of budget or lack functionality for our event. Clearly I should build an app for filthy lucre.

Implementation

Design decisions

Canonical raffle ID = Eventbrite email

  • Platform-agnostic contact information by which to contact the winners
  • Easy to ask for email authentication on a Google Form
  • Easy to link Eventbrite emails to manually awarded tickets (volunteers & staff)
  • Automatically gates to eligible winners

Each attendee atomically applies all their tickets toward their favorite prize.

  • If attendees were allowed to apply tickets toward multiple prizes, they would need an easy way to see how many tickets they have. This cannot satisfy our privacy requirements without either a centralized, authenticated, custom tracking system, or a server-side lookup into our spreadsheet, which is out of scope for my bandwidth for this event.
  • In order to check # of tickets, canonical ID matching would need to occur before the raffle submission. This would mean significantly more communication overhead on our end when awarding the raffle tickets.
  • With single jar selections, we get to postpone canonical-ID mapping until the point of raffle submission, sidestepping a lot of communication overhead.
  • Bonus: simplifies the form.

Outcomes

  • Organizers do not need to pre-verify identities. We only need to check in the case of an attempted scam, which will be discovered at the raffle step. This saves time and allows us to batch data entry at our leisure.
  • Attendees have no functional need to check how many tickets they possess. This saves staff overhead and infrastructure.

Procedure

  • After the cutoff date, qualifying social media posts will be checked manually in a single batch and entered into a spreadsheet. Video uniqueness will be verified manually. Uniqueness per day will be verified manually.
  • Qualifying staff roles will be entered manually.
  • Attendees will be directed to a form which collects their (authenticated) canonical email, social media handles, and raffle jar selection.
  • Then just export two CSVs and run the raffle script.

TODO

  • Programmatically scrape public instagram submissions? (may or may not be worth it, considering rate limiting and inability to scrape private submissions)
  • Programmatically gather FB submissions (requires 3rd party app permissions)
  • Make a page where attendees can see how many tickets are in each prize jar. I guess I have to put this script on a server or AWS Lambda and live feed it the google spreadsheets in that case, at which point I might as well implement multi-jar selection and ticket lookup.
"""
Run a raffle. See RAFFLE.md for context.
Expects two input files in the same directory.
raffle_jar.csv
Describes individuals selecting a raffle jar to put their tickets into,
& social media handles they claim to be associated with. Handles are
not guaranteed secure, and must be investigated manually in the case of
collision.
Emails are guaranteed unique and authenticated by Google Forms.
email, name, facebook_page_link, instagram_username, prize_jar
tickets_earned.csv
Describes individual tickets that have been earned
across different platforms. No guarantees. Any non-conforming or
non-matching entries will be discarded.
name, eventbrite_email, facebook_page_link, instagram_username
"""
import csv
import random
import unittest
def normalize_fb(link):
"""Normalize a human-entered FB page link as best we can"""
link = link.lower()
if "facebook.com/" in link:
link = link.split("facebook.com/")[1]
if "/?" in link:
link = link.split("/?")[0]
return link.rstrip("/")
def normalize_ig(ig):
"""Normalize an IG username"""
return ig.lower().lstrip("@")
def get_non_unique(my_list):
"""Return set of non-unique elements in a list."""
my_set = set(my_list)
nuniqs = set()
for x in my_list:
if x in my_set:
my_set.remove(x)
else:
nuniqs.add(x)
return nuniqs
def get_duplicate_usernames(rafflers):
"""
Check for duplicates. GForms guarantees no duplicate emails.
Any duplicates should be manually checked, and are either the result of
very forgetful people authenticating with multiple emails, coincidental
typos, or a scam.
"""
all_igs = list(filter(
lambda x: x is not None and len(x) > 0,
(normalize_ig(r.get("instagram_username")) for e, r in rafflers.items())))
all_fbs = list(filter(
lambda x: x is not None and len(x) > 0,
(normalize_fb(r.get("facebook_page_link")) for e, r in rafflers.items())))
non_unique_igs = get_non_unique(all_igs)
non_unique_fbs = get_non_unique(all_fbs)
if non_unique_igs:
print("STOP - check duplicate IG handles:", non_unique_igs)
if non_unique_fbs:
print("STOP - check duplicate FB handles:", non_unique_fbs)
if len(non_unique_igs) == 0 and len(non_unique_fbs) == 0:
return None
return (non_unique_igs, non_unique_fbs)
def main():
# All raffle tickets earned
with open("tickets_earned.csv", "r") as f:
csv_reader = csv.DictReader(f)
ticket_fields = csv_reader.fieldnames
tickets = list(csv_reader)
# People who used the raffle jar (`email` is a required field)
with open("raffle_jar.csv", "r") as f:
csv_reader = csv.DictReader(f)
raffler_fields = csv_reader.fieldnames
rafflers = {r["email"]: r for r in csv_reader}
# Check for duplicate usernames. If any, exit and resolve them manually.
# (May require talking to some people)
if get_duplicate_usernames(rafflers) is not None:
exit()
# Build raffler lookup tables (to canonical email)
fb_rafflers = {
normalize_fb(r["facebook_page_link"]): email
for email, r in rafflers.items() if r.get("facebook_page_link")
}
ig_rafflers = {
normalize_ig(r["instagram_username"]): email
for email, r in rafflers.items() if r.get("instagram_username")
}
print("\nFACEBOOK", fb_rafflers)
print("\nINSTAGRAM", ig_rafflers)
# Map every ticket to a raffler if match exists, else discard
print("\nall tickets:")
for ticket in tickets:
r = {}
if ticket.get("eventbrite_email"):
r = rafflers.get(ticket["eventbrite_email"], {})
print("email", ticket["eventbrite_email"])
elif ticket.get("facebook_page_link"):
print("FB", normalize_fb(ticket["facebook_page_link"]))
r = rafflers.get(fb_rafflers.get(normalize_fb(ticket["facebook_page_link"])), {})
elif ticket.get("instagram_username"):
print("IG", normalize_ig(ticket["instagram_username"]))
r = rafflers.get(ig_rafflers.get(normalize_ig(ticket["instagram_username"])), {})
r["tickets"] = r.get("tickets", []) + [ticket]
print("all valid rafflers & ticket counts")
for e, r in rafflers.items():
print(e, len(r.get("tickets", [])))
# Build the raffle jars
jars = {}
for email, r in rafflers.items():
jar_label = r.get("prize_jar")
jars[jar_label] = jars.get(jar_label, []) + [email] * len(r.get("tickets", []))
print("\n", jars)
print()
for label, emails in jars.items():
print(label, len(emails))
# Draw winners
print("\nWINNERS")
for label, jar in jars.items():
winner = random.choice(jar) # Random-enough
print("%s: %s (%s)" % (label, winner, rafflers.get(winner).get("name")))
class TestRaffle(unittest.TestCase):
"""Usage: `python -m unittest raffle.TestRaffle`"""
def test_normalize_fb(self):
test_cases = [
("facebook.com/test", "test"),
("http://facebook.com/test", "test"),
("https://facebook.com/test", "test"),
("https://facebook.com/test/", "test"),
("https://facebook.com/tEsT/", "test"),
("https://facebook.com/test/?asdfasdf=", "test"),
("test", "test"),
("tEsT", "test"),
("baddomain.com/test", "baddomain.com/test"),
]
for a, b in test_cases:
self.assertEqual(normalize_fb(a), b)
def test_normalize_ig(self):
test_cases = [
("@instagram", "instagram"),
("InStaGrAm", "instagram"),
("@InStaGrAm", "instagram"),
]
for a, b in test_cases:
self.assertEqual(normalize_ig(a), b)
def test_get_non_unique(self):
test_cases = [
([1,2,3], set()),
([1,2,3,2], set([2])),
(["a", "b", "a"], set(["a"])),
]
for my_list, res in test_cases:
self.assertEqual(get_non_unique(my_list), res)
def test_get_duplicate_usernames(self):
test_cases = [
# No dupes
({
"a": {"instagram_username": "a", "facebook_page_link": "a"},
"b": {"instagram_username": "b"},
"c": {},
}, None),
# Dupe IG only
({
"a": {"instagram_username": "a", "facebook_page_link": "a"},
"b": {"instagram_username": "a"},
}, (set(["a"]), set())),
# Dupe IG after normalization
({
"a": {"instagram_username": "a", "facebook_page_link": "a"},
"b": {"instagram_username": "@a"},
}, (set(["a"]), set())),
# Dupe FB only
({
"a": {"instagram_username": "a", "facebook_page_link": "a"},
"b": {"facebook_page_link": "a"},
}, (set(), set(["a"]))),
# Dupe FB after normalization
({
"a": {"instagram_username": "a", "facebook_page_link": "a"},
"b": {"facebook_page_link": "facebook.com/a/"},
}, (set(), set(["a"]))),
# Both duped
({
"a": {"instagram_username": "a", "facebook_page_link": "a"},
"b": {"instagram_username": "a"},
"c": {"facebook_page_link": "a"},
}, (set(["a"]), set(["a"]))),
]
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment