A lightweight tool for collecting student handwritten solutions via photo upload. Students scan a QR code → enter a 3-digit session security code → fill in name & ID → upload a photo. You control open/close from a simple password-protected panel.
submission_app/
│
├── app.py ← Entry point. Controls routing (admin vs student).
│
├── components/
│ ├── admin_view.py ← Your control panel UI (toggle, question ID, storage link).
│ └── student_view.py ← The form students see and submit.
│
├── services/
│ ├── sheets_service.py ← Reads/writes the Google Sheet (Open/Closed + Question ID).
│ ├── drive_service.py ← Uploads files to Supabase Storage with auto-naming.
│ └── image_utils.py ← Compresses photos before upload (fast on 4G).
│
├── requirements.txt ← Python dependencies for Streamlit Cloud.
└── .streamlit/
└── secrets.toml.example ← Template for your credentials (never commit the real one).
Rule of thumb: if something isn't working, the file named after the broken thing is where to look.
- Go to console.cloud.google.com.
- Create a project (or use an existing one).
- Enable the Google Sheets API.
- Go to IAM & Admin → Service Accounts → Create Service Account.
- Give it any name. Click through to finish.
- Open the service account → Keys → Add Key → JSON. Download the file.
- Create a new Google Sheet.
- In cell A1 type
Status, in B1 typeQuestion_ID, in C1 typeOpened_At, in D1 typeSession_Code. - In cell A2 type
Closed, in B2 typeQ1, leave C2 and D2 empty. - Share the sheet with the
client_emailfrom your JSON key file → give it Editor access. - Copy the sheet's full URL.
- Go to supabase.com → Sign Up Free.
- Create a new project → give it any name.
- Go to Storage → New bucket → name it
submissions→ make it private. - Go to Settings → API → copy two values:
- Project URL (looks like
https://xxxx.supabase.co) - service_role key (the long secret key, not the anon key)
- Project URL (looks like
- Push this folder to a GitHub repository.
- Go to share.streamlit.io → New app → connect your repo.
- Set Main file path to
app.py. - Open Advanced settings → Secrets and paste your credentials using the format in
.streamlit/secrets.toml.example. - Deploy. Share the plain URL with students. You access the admin panel by adding
?role=adminto the URL.
[google_credentials]
type = "service_account"
project_id = "your-project-id"
private_key_id = "..."
private_key = "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"
client_email = "your-service-account@your-project.iam.gserviceaccount.com"
client_id = "..."
auth_uri = "https://accounts.google.com/o/oauth2/auth"
token_uri = "https://oauth2.googleapis.com/token"
[settings]
sheet_url = "https://docs.google.com/spreadsheets/d/YOUR_SHEET_ID/edit"
supabase_url = "https://xxxxxxxxxxxx.supabase.co"
supabase_key = "your-service-role-key"
supabase_bucket = "submissions"
admin_password = "YourPasswordHere"- Go to qr-code-generator.com or goqr.me
- Paste your plain Streamlit app URL (no
?role=admin) - Download the QR code image
- Display it on the projector each class — it never expires
⚠️ Avoid "dynamic QR code" options — those expire on free plans. A plain static QR code is all you need since your URL never changes.
- Go to
your-app-url?role=admin - Enter the admin password in the sidebar.
- Type the Question ID (e.g.,
Midterm_Q3). - Click OPEN submissions — a random 3-digit session code is generated automatically.
- Write the code on the board — students must enter it before the form unlocks.
- Display the student URL (or QR code) on the projector.
- Session auto-closes after 15 minutes.
💡 Updating the Question ID mid-session does not change the code. Students already in the room keep the same code.
- Click CLOSE submissions when done.
- Click the Supabase Storage link in the admin panel to see all uploaded files.
Every file is automatically saved as:
{Question_ID}_{Student_Name}_{Student_ID}.jpg
Example: Midterm_Q3_Ali_Hassan_1221321.jpg
For multiple photos: Midterm_Q3_Ali_Hassan_1221321_1.jpg, _2.jpg, etc.
| What you want to change | Where to change it |
|---|---|
| Session auto-close duration | services/sheets_service.py → AUTO_CLOSE_MINUTES |
| Admin session timeout | app.py → ADMIN_SESSION_MINUTES |
| Max photo resolution | services/image_utils.py → MAX_DIMENSION |
| JPEG quality (file size vs clarity) | services/image_utils.py → JPEG_QUALITY |
| Admin password | secrets.toml → settings.admin_password |
| Page title / icon | app.py → st.set_page_config(...) |
| Student form fields | components/student_view.py |
| Admin panel layout | components/admin_view.py |
| Symptom | Likely cause | Fix |
|---|---|---|
| "Could not connect to Google Sheets" | Service account not shared with sheet | Share the sheet with client_email as Editor |
| Upload fails | Wrong Supabase key or bucket name | Make sure you used the service_role key, not the anon key. Check bucket name matches exactly (case-sensitive) |
| Student sees "Closed" after you opened | Sheet cache (5s delay) | Student clicks 🔄 Refresh |
| Student can't resubmit | Duplicate prevention by Student ID | Intended behaviour. Clear the Submissions tab in the Google Sheet to reset |
| Photos arrive sideways | EXIF rotation | image_utils.py handles this; if it still happens, update Pillow |
| App crashes on deploy | Missing secret key | Check all fields in secrets match the template above |