DynamoDB and Plant Tracer¶
The Plant Tracer webapp uses AWS DynamoDB to store:
The user list
The course list
The enrollment join table (which users are in which courses)
The movie list
The per-frame trackpoint annotations
API keys and audit logs
Originally this was stored in a MySQL database. We migrated to DynamoDB for cost — most uses of Plant Tracer can fit within the DynamoDB free tier, while the cost for running MySQL is upwards of $50/month on AWS.
Each DynamoDB table is identified by an account and a table name. All table names share a common
prefix controlled by the DYNAMODB_TABLE_PREFIX environment variable (e.g. demo-). For local
development you can use the AWS DynamoDB local (downloadable version). We
recommend using the version downloaded as a JAR file.
The canonical table definitions are in src/app/schema.py. All attribute-name constants are
defined at the top of src/app/odb.py. Tables are created by odbmaint.create_tables() and
dropped by odbmaint.drop_tables() — used by make make-local-demo and the test fixtures.
Table Summary¶
All table names below are shown without the prefix. With DYNAMODB_TABLE_PREFIX=demo- the
users table is named demo-users, etc.
Table |
Purpose |
Partition Key |
Sort Key |
|---|---|---|---|
|
One record per registered user |
|
— |
|
Enforces email address uniqueness across users |
|
— |
|
One record per issued API key (a user may have several) |
|
— |
|
One record per course |
|
— |
|
Enrollment join table — which users are in which courses |
|
|
|
One record per uploaded movie |
|
— |
|
Per-frame trackpoint annotations |
|
|
|
Audit log entries |
|
— |
Table Details¶
users¶
One record per registered user.
Attribute |
Type |
Description |
|---|---|---|
|
String (PK) |
Unique identifier, prefixed |
|
String |
User’s email address (unique, enforced via |
|
String |
Display name; may be blank |
|
Integer |
Unix epoch seconds at registration |
|
Integer (0/1) |
Whether the account is active |
|
String |
The course the user registered through; used as default context |
|
String |
Denormalized name of the primary course |
|
List of strings |
All courses the user is enrolled in |
|
List of strings |
Courses for which the user has admin privileges |
api_keys¶
One record per issued API key. A user may hold multiple keys (e.g. after re-sending a login link). The key is sent as a cookie or POST parameter; the server validates it on every request.
Attribute |
Type |
Description |
|---|---|---|
|
String (PK) |
The key value, a random hex string |
|
String |
Owner of the key |
|
Integer |
Unix epoch seconds of first use (i.e. first login) |
|
Integer |
Unix epoch seconds of most recent use |
|
Integer (0/1) |
Whether the key is still valid |
GSI: user_id_idx on user_id. Used by DDBO.get_user_login_times() to aggregate
first/last login times across all of a user’s keys without a table scan.
courses¶
Attribute |
Type |
Description |
|---|---|---|
|
String (PK) |
Unique identifier for the course |
|
String |
Human-readable course name |
|
String |
Registration passphrase that students use to self-enroll |
|
List of strings |
|
|
Integer |
Maximum number of students allowed to self-register (default 50) |
course_users¶
A lightweight join table that records which users are enrolled in which courses. Each item has only
the two key attributes: course_id (partition key) and user_id (sort key).
Querying course_users by course_id returns all enrolled user IDs efficiently — this is used
by course_enrollments(course_id) in odb.py.
Note
delete_user() does not currently remove course_users rows for the deleted user,
which can leave stale enrollment records. list_users_courses() handles this defensively
by catching InvalidUser_Id and logging a warning. See issue #968 for the planned fix.
movies¶
One record per uploaded movie. Key attributes:
movie_id, title, description, user_id, course_id, published (0/1; defaults to 1 on creation),
deleted (0/1), status, total_frames, fps, width, height,
movie_data_urn (S3 URN of the MP4), movie_zipfile_urn, first_frame_urn,
last_frame_tracked, research_use (0/1/None; None = not yet answered), credit_by_name (0/1/None; None = not yet answered), attribution_name,
rotation (0/90/180/270 degrees).
See src/app/schema.py Movie class for the full schema and constraints.
movie_frames¶
Per-frame trackpoint storage. Keyed by (movie_id, frame_number).
Each record’s trackpoints attribute is a list of objects with fields
x, y, label, frame_number, status, and err (all defined in the
Trackpoint class in schema.py).
Data Consistency Notes¶
Email uniqueness is enforced by a transactional write to both
usersandunique_emailsat registration time.Course admin list (
admins_for_courseon the course record) is kept in sync withadmin_for_courseson the user record byadd_course_admin()andremove_course_admin().Enrollment (
course_usersrows) is added byregister_email()but is not removed bydelete_user(). See issue #968.Login times (
first_used_at,last_used_at) live onapi_keys, notusers.list_users_courses()aggregates them per user via theuser_id_idxGSI.
Schema and Naming Changes¶
Some naming changes were made for clarity or to avoid conflicts with DynamoDB’s reserved words.
Old name |
New name |
Reason |
|---|---|---|
|
|
|