A desktop application for uploading student submissions to Google Drive and automatically linking them in your Google Sheets gradebook. This tool streamlines the process of managing assignment submissions by organizing files in Google Drive and updating spreadsheet entries with direct links.
- Intelligent File Matching: Match submission files by student ID or first/last name from Moodle exports
- Batch Upload: Upload multiple student submission files to Google Drive
- Automatic Linking: Automatically update Google Sheets with clickable hyperlinks to uploaded files
- Link Conflict Prevention: Skip files that would overwrite existing submission links
- Dual Interface: User-friendly PyQt5 GUI and powerful command-line interface
- Smart Configuration: Persistent configuration with auto-loading and manual save functionality
- One-Click Google Sign-In: Users authorize a third-party app in the browser — no Google Cloud setup
- Enhanced Authentication: Visual status indicators and saved sessions via
token.json - Unicode Support: Handles international characters in filenames safely
- Progress Tracking: Real-time upload progress with detailed logging and emoji indicators
- Python 3.6 or higher (or the packaged
.exebuild) - Google account with access to Google Drive and Google Sheets
- Run
SubmissionUploader.exe - Click Connect Google Account and approve access in your browser
- Enter your Sheet ID, Drive folder ID, and submissions folder
- Click Save Configuration, then Start Upload
You do not need a Google Cloud project or any credential files.
- Install dependencies:
pip install -r requirements.txt - Run the application:
python app.py - Follow the workflow:
- ✅ Configuration auto-loads from
config.json(if exists) - 🔐 Click Connect Google Account and approve access in your browser
- ⚙️ Fill/modify configuration fields as needed
- 📁 Use Select Folder next to the submissions path field
- 💾 Click Save Configuration to persist changes
- 🚀 Click Start Upload to begin processing
- ✅ Configuration auto-loads from
- Copy configuration:
copy config.sample.json config.json - Edit
config.jsonwith your settings - Sign in:
python cli_auth.py setup - Upload files:
python uploader.py /path/to/submissions
pip install -r requirements.txtYou do not need to create a Google Cloud project. This app is a third-party application — you only sign in and approve access.
- Run the app (
python app.pyor the packaged executable). - Click Connect Google Account.
- Sign in with the Google account that owns your Drive folder and spreadsheet.
- On Google's consent screen, review the permissions and click Allow.
- The app saves your session locally in
token.jsonso you usually won't need to sign in again.
Permissions requested:
- Access files this app creates in Google Drive
- View and edit your Google Sheets spreadsheets
Note: Keep token.json private. It is your personal session file — do not share it or commit it to version control.
Only the person building or distributing the app needs a Google Cloud project. End users never see this.
- Go to the Google Cloud Console and create a project.
- Enable Google Drive API and Google Sheets API under APIs & Services > Library.
- Configure the OAuth consent screen (app name, support email, scopes for Drive and Sheets).
- For a Google Workspace org only: set Audience to Internal so anyone in the domain can sign in.
- For personal Google accounts / multiple orgs: set Audience to External. While in Testing mode, add each user under Test users.
- For public External distribution, submit the app for Google verification.
- Create an OAuth client ID with type Desktop app under Clients / Credentials.
- Download the client JSON and save it as
oauth_client.jsonin the project root (seeoauth_client.sample.json). - Bundle
oauth_client.jsonwhen building the executable (see Building Executables).
Direct link: Google Cloud credentials
The application now uses a config.json file that loads into the GUI on startup with manual save functionality.
- GUI (app.py): Automatically loads settings from
config.jsonon startup - Manual Save: Click "Save Configuration" button to save changes to
config.json - CLI (uploader.py): Reads configuration from
config.jsonat runtime - Unified Settings: Saved changes from GUI are immediately available for CLI usage
-
Copy the sample configuration:
copy config.sample.json config.json
-
Edit your configuration: Open
config.jsonand update the following fields:google_sheets.sheet_id: Your Google Sheet IDgoogle_sheets.sheet_name: Sheet tab name (e.g., "A2", "Submissions")google_sheets.id_column: Column containing student IDs (e.g., "A")google_sheets.first_name_column: Column containing first names (e.g., "B")google_sheets.last_name_column: Column containing last names (e.g., "C")google_sheets.link_column: Column for file links (e.g., "N")google_sheets.start_row: Data start row (usually 2 or 3)google_drive.folder_id: Target Google Drive folder IDsubmissions.folder_path: Default path to submissions folder (optional)
{
"google_sheets": {
"sheet_id": "YOUR_SHEET_ID_HERE",
"sheet_name": "Sheet1",
"id_column": "A",
"first_name_column": "B",
"last_name_column": "C",
"link_column": "N",
"start_row": 3
},
"google_drive": {
"folder_id": "YOUR_FOLDER_ID_HERE"
},
"submissions": {
"folder_path": ""
},
"authentication": {
"scopes": [
"https://www.googleapis.com/auth/drive.file",
"https://www.googleapis.com/auth/spreadsheets"
],
"token_file": "token.json"
},
"output": {
"summary_file": "upload_summary.txt"
},
"upload": {
"mime_type": "application/octet-stream",
"permissions": {
"role": "reader",
"type": "anyone"
}
}
}To support course assignments where submissions are uploaded by one group member and should be recorded under a group name, the uploader can load a local CSV that maps members to group names. Add a group_mappings block to config.json pointing to your CSV and the column names to use:
"group_mappings": {
"file": "C:\\path\\to\\groups.csv",
"member_first_name_column": "First Name",
"member_last_name_column": "Last Name",
"group_name_column": "Group Name"
}Notes:
- The CSV must be readable from the path in
file(absolute or relative to the project root). - The uploader looks for headers case-insensitively but you should provide the exact header names when possible. The expected headers are
First Name,Last Name, andGroup Name. - When grouped mode is enabled (the CSV loads successfully), the uploader will, for each filename, attempt the following in order:
- Check
matches.csv(pre-computed filename -> group matches) - Extract a student name from the filename and map the student to their group using the CSV
- Fallback name extraction strategies (simple name parts)
- Check
If a mapped group name is not present in the sheet ID column, the uploader will create a new row and write the group name into the ID column before writing the link.
python app.pyEnhanced Features:
- Auto-load Configuration: Settings automatically populate from
config.jsonon startup - Integrated Folder Selection: Select submissions folder with button positioned next to the path field
- Google Sign-In: Connect Google Account opens Google's consent screen — no credential file picker
- Status indicators: Color-coded Connected / Not connected status in the GUI
- Manual Configuration Save: Click "Save Configuration" to persist changes
- Real-time Feedback: Visual confirmation of save operations and authentication status
Use the CLI sign-in tool:
# Check if you are signed in
python cli_auth.py check
# Sign in with Google (opens browser)
python cli_auth.py setup
# Get help
python cli_auth.py helpThe GUI Connect Google Account button runs the same flow and saves token.json.
python uploader.py /path/to/submissions/folderIf you want to verify how files will be mapped before performing any uploads, use the dry-run flag. This maps filenames to groups/IDs using matches.csv and the optional group_mappings CSV, and writes a summary to upload_summary.txt without contacting Google APIs.
# Run a mapping-only dry-run and write results to upload_summary.txt
python uploader.py --dry-run
# Or provide a folder path and dry-run in one command
python uploader.py --dry-run "C:\\path\\to\\submissions"The dry-run summary includes:
- total files scanned
- files that would be mapped (filename -> group/ID and match method)
- files with no mapping (skipped)
Check upload_summary.txt in the project root after the dry-run completes.
Prerequisites for CLI:
config.jsonmust exist with your settings- Google sign-in completed (
python cli_auth.py setupor via the GUI)
- Sheet ID: The ID from your Google Sheets URL (the long string between
/d/and/edit) - Sheet Name: The name of the specific sheet tab (e.g., "Sheet1", "Gradebook")
- ID Column: Column letter containing student IDs (e.g., "A", "B", "C")
- Link Column: Column letter where file links will be placed (e.g., "D", "E", "F")
- Start Row: Row number where student data begins (usually 2 if row 1 has headers)
- Google Drive Folder ID: The ID of the destination folder (from the folder's URL)
Supported File Formats: PDF, DOCX, and other common document formats
File Matching Strategies:
- Primary: Student ID matching from Moodle export format
- Secondary: First and last name matching when student ID fails
Moodle Export Naming Format:
FirstName LastName_SubmissionID_assignsubmission_file_StudentID_COMP1600_A1.pdf
Examples:
Aadam Seenath_1835025_assignsubmission_file_816050357_COMP1600_A1.pdfAaron Charran_1835097_assignsubmission_file_816049096_ COMP1600_A1.pdfBilly Dee Williams_1834952_assignsubmission_file_816044707_COMP1600_A1.pdf
Legacy Format Support: The system also supports older naming conventions like:
816040296_A2.pdf320053318_A2.pdf400014890.A2.pdf
Smart Matching Algorithm:
- Primary: Extract student ID from filename using regex patterns
- Secondary: When ID matching fails, extract and match names:
- Exact first name + last name match
- First name + partial last name match
- Normalized comparison (handles hyphens, spaces, case differences)
- Conflict Prevention: Skip files where target cells already contain links
Spreadsheet Requirements:
- ID Column: Contains student IDs (e.g., 816050357, 320053318)
- First Name Column: Contains first names for name-based matching
- Last Name Column: Contains last names for name-based matching
- Link Column: Target column for submission file links (skipped if already populated)
-
Install PyInstaller:
pip install pyinstaller
-
Ensure all dependencies are installed:
pip install -r requirements.txt
Windows:
pyinstaller --onefile --windowed --add-data "oauth_client.json;." --name "SubmissionUploader" app.pyMac/Linux:
pyinstaller --onefile --windowed --add-data "oauth_client.json:." --name "SubmissionUploader" app.pyWindows:
pyinstaller --onefile --add-data "oauth_client.json;." --name "SubmissionUploaderCLI" uploader.pyMac/Linux:
pyinstaller --onefile --add-data "oauth_client.json:." --name "SubmissionUploaderCLI" uploader.py--onefile: Creates a single executable file--windowed: Suppresses console window (GUI only)--add-data: Bundlesoauth_client.json(the app's OAuth client) in the build--name: Sets the executable name
Create a build.spec file for more control:
# -*- mode: python ; coding: utf-8 -*-
block_cipher = None
a = Analysis(
['app.py'],
pathex=[],
binaries=[],
datas=[('oauth_client.json', '.')],
hiddenimports=[
'google.oauth2.credentials',
'google_auth_oauthlib.flow',
'googleapiclient.discovery',
'googleapiclient.http'
],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='SubmissionUploader',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon='icon.ico' # Optional: add your own icon
)Then build with:
pyinstaller build.specThe built executable will be in the dist/ folder. For distribution:
-
Include required files:
- The executable
config.json(template or pre-configured)- Installation instructions
-
Create distribution folder:
SubmissionUploader_v1.0/ ├── SubmissionUploader.exe (or SubmissionUploader on Mac/Linux) ├── config.json ├── README.txt └── USER_GUIDE.txt
Common Issues:
- Missing modules: Add to
hiddenimportsin spec file - File not found errors: Ensure all data files are included with
--add-data - Large executable size: Consider using
--exclude-modulefor unused packages - Slow startup: Use
--onedirinstead of--onefilefor faster startup
Testing the Build:
- Test the executable on a clean system without Python
- Verify all features work correctly
- Check that
oauth_client.jsonis properly bundled - Test both GUI and Google sign-in flows
- "Configuration file not found": Copy
config.sample.jsontoconfig.json - UI not loading previous settings: Check that
config.jsonis valid JSON - Settings not saving: Click "Save Configuration" button and ensure write permissions in the project directory
-
❌ "App is not configured for Google sign-in":
- End users: contact whoever provided the app — the build is missing
oauth_client.json - Developers: copy
oauth_client.sample.jsontooauth_client.jsonand add your Desktop OAuth client
- End users: contact whoever provided the app — the build is missing
-
⚠️ "Not signed in":- GUI: Click Connect Google Account
- CLI: Run
python cli_auth.py setup
-
⚠️ Session expired:- The app refreshes automatically when possible
- If refresh fails, click Connect Google Account again
-
❌ "Sign-in failed" / access blocked:
- If the app is in Google testing mode, your account must be listed as a test user
- Confirm you approved Drive and Sheets permissions on the consent screen
-
GUI status colors:
- Green: signed in
- Orange: sign-in needed
- Red: configuration or sign-in error
-
File matching problems:
- Check that filenames follow Moodle export format or contain student IDs
- Verify first/last name columns are correctly configured for name-based matching
- Review upload log for specific matching failures
-
"Link already exists, skipping...":
- This is normal behavior - prevents overwriting existing submissions
- Files are automatically skipped if the target spreadsheet cell already has content
-
Permission errors:
- Verify Google account has edit access to the target Drive folder and Sheet
- Check that OAuth scopes include both Drive and Sheets permissions
-
API quota exceeded:
- Google APIs have usage limits; wait and retry if needed
- Consider smaller batch sizes for large numbers of files
-
Configuration not loading:
- Ensure
config.jsonexists and is valid JSON - Check file permissions in the project directory
- Ensure
-
"Select Folder" button not working:
- Button is now positioned between the label and text field
- Selected folder automatically updates the submissions path configuration
-
Settings not persisting:
- Click "Save Configuration" button after making changes
- Manual save prevents accidental overwrites during form editing
- "Invalid JSON in configuration file": Validate your
config.jsonsyntax - Sign-in fails: Run
python cli_auth.py setupor use Connect Google Account in the GUI - Upload script can't find config: Run from the same directory as
config.json
To find your Google Drive folder ID:
- Open Google Drive in your browser
- Navigate to the desired folder
- Copy the folder ID from the URL:
https://drive.google.com/drive/folders/FOLDER_ID_HERE
Sub_Uploader/
├── app.py # Main GUI application with enhanced UI
├── uploader.py # CLI upload script with name matching
├── cli_auth.py # CLI authentication helper tool
├── config.json # Configuration file (auto-loads in GUI)
├── oauth_client.json # App OAuth client (developers only; bundled in releases)
├── oauth_client.sample.json
├── google_auth.py # Shared Google sign-in helpers
├── token.json # User session (auto-created after sign-in)
├── requirements.txt # Python dependencies
├── readme.md # Documentation (this file)
├── upload_summary.txt # Upload report (generated after each run)
├── app.spec # PyInstaller spec file (optional)
├── submissions/ # Example submissions folder with Moodle exports
│ ├── Aadam Seenath_1835025_assignsubmission_file_816050357_COMP1600_A1.pdf
│ ├── Aaron Charran_1835097_assignsubmission_file_816049096_ COMP1600_A1.pdf
│ └── ...
├── build/ # PyInstaller build artifacts
└── dist/ # Built executables
├── SubmissionUploader.exe (Windows)
└── SubmissionUploaderCLI.exe
- app.py: Enhanced GUI with improved authentication flow, smart folder selection, and visual feedback
- uploader.py: CLI version with intelligent file matching (student ID + name fallback)
- config.json: Unified configuration file that auto-loads in GUI and supports manual save
- oauth_client.json: Desktop OAuth client for the app (maintainers add once; bundled in
.exebuilds) - token.json: Per-user session after Google sign-in (keep secure)
- submissions/: Contains student submission files from Moodle exports
- Keep
oauth_client.jsonandtoken.jsonprivate — do not commit them to git token.jsonis each user's personal session; treat it as sensitive- Maintainers: rotate the OAuth client in Google Cloud Console if it is ever exposed