From fd6d11f44d2b29a9f4638364e2e12be3eba2ab36 Mon Sep 17 00:00:00 2001 From: CJ Quines Date: Sun, 23 Aug 2026 13:48:56 -0400 Subject: [PATCH] feat(tubes): make problem table sortable --- fixtures/all.json | 1327 +++++++++++++++++ fixtures/populate.py | 259 ++++ otisweb/static/table-sort.js | 32 +- .../tubes/components/proposal_table.html | 2 +- 4 files changed, 1612 insertions(+), 8 deletions(-) diff --git a/fixtures/all.json b/fixtures/all.json index bd0060f0..e7f191e0 100644 --- a/fixtures/all.json +++ b/fixtures/all.json @@ -9928,5 +9928,1332 @@ "domain": "example.com", "name": "example.com" } + }, + { + "model": "tubes.tube", + "pk": 1, + "fields": { + "display_name": "OIME testsolving", + "description": "Testsolving for the OTIS Invitational Mathematics Examination.", + "status": "TB_ACTIVE", + "main_url": "https://otis.evanchen.cc/tubes/proposals/", + "accepting_signups": true + } + }, + { + "model": "tubes.joinrecord", + "pk": 1, + "fields": { + "user": 2, + "tube": 1, + "activation_time": "2026-07-05T17:45:33.317Z", + "invite_url": "http://wood.org/" + } + }, + { + "model": "tubes.joinrecord", + "pk": 2, + "fields": { + "user": 4, + "tube": 1, + "activation_time": "2026-06-02T17:45:33.317Z", + "invite_url": "http://www.johnson-bell.com/" + } + }, + { + "model": "tubes.joinrecord", + "pk": 3, + "fields": { + "user": 5, + "tube": 1, + "activation_time": "2026-08-10T17:45:33.317Z", + "invite_url": "http://douglas.com/" + } + }, + { + "model": "tubes.joinrecord", + "pk": 4, + "fields": { + "user": 8, + "tube": 1, + "activation_time": "2026-06-05T17:45:33.317Z", + "invite_url": "https://douglas.com/" + } + }, + { + "model": "tubes.joinrecord", + "pk": 5, + "fields": { + "user": 11, + "tube": 1, + "activation_time": "2026-08-17T17:45:33.317Z", + "invite_url": "https://gates.com/" + } + }, + { + "model": "tubes.joinrecord", + "pk": 6, + "fields": { + "user": 13, + "tube": 1, + "activation_time": "2026-07-13T17:45:33.317Z", + "invite_url": "https://mccall.com/" + } + }, + { + "model": "tubes.joinrecord", + "pk": 7, + "fields": { + "user": 19, + "tube": 1, + "activation_time": "2026-08-09T17:45:33.317Z", + "invite_url": "http://www.rice.info/" + } + }, + { + "model": "tubes.joinrecord", + "pk": 8, + "fields": { + "user": 20, + "tube": 1, + "activation_time": "2026-07-11T17:45:33.317Z", + "invite_url": "https://www.phillips-kim.com/" + } + }, + { + "model": "tubes.joinrecord", + "pk": 9, + "fields": { + "user": 22, + "tube": 1, + "activation_time": "2026-07-09T17:45:33.317Z", + "invite_url": "http://garcia-gallegos.org/" + } + }, + { + "model": "tubes.joinrecord", + "pk": 10, + "fields": { + "user": 23, + "tube": 1, + "activation_time": "2026-06-13T17:45:33.317Z", + "invite_url": "http://www.warner.info/" + } + }, + { + "model": "tubes.joinrecord", + "pk": 11, + "fields": { + "user": 24, + "tube": 1, + "activation_time": "2026-06-13T17:45:33.317Z", + "invite_url": "https://www.conner-roberts.com/" + } + }, + { + "model": "tubes.joinrecord", + "pk": 12, + "fields": { + "user": 25, + "tube": 1, + "activation_time": "2026-06-19T17:45:33.317Z", + "invite_url": "http://adams.com/" + } + }, + { + "model": "tubes.joinrecord", + "pk": 13, + "fields": { + "user": 26, + "tube": 1, + "activation_time": "2026-05-25T17:45:33.317Z", + "invite_url": "http://www.hudson-sanchez.org/" + } + }, + { + "model": "tubes.oimecontributor", + "pk": 1, + "fields": { + "user": 2, + "display_name": "Natalie Jordan", + "casual_mode": false, + "ranked_cutoff": "2026-08-03T17:45:33.317Z", + "hide_from_leaderboards": false, + "hide_from_acknowledgments": false, + "revealed_proposals": [4] + } + }, + { + "model": "tubes.oimecontributor", + "pk": 2, + "fields": { + "user": 4, + "display_name": "Madison Parrish", + "casual_mode": false, + "ranked_cutoff": "2026-06-25T17:45:33.317Z", + "hide_from_leaderboards": false, + "hide_from_acknowledgments": false, + "revealed_proposals": [18] + } + }, + { + "model": "tubes.oimecontributor", + "pk": 3, + "fields": { + "user": 5, + "display_name": "Heidi Stevens", + "casual_mode": false, + "ranked_cutoff": null, + "hide_from_leaderboards": false, + "hide_from_acknowledgments": false, + "revealed_proposals": [] + } + }, + { + "model": "tubes.oimecontributor", + "pk": 4, + "fields": { + "user": 8, + "display_name": "Sharon Ingram", + "casual_mode": false, + "ranked_cutoff": "2026-06-07T17:45:33.317Z", + "hide_from_leaderboards": false, + "hide_from_acknowledgments": false, + "revealed_proposals": [18] + } + }, + { + "model": "tubes.oimecontributor", + "pk": 5, + "fields": { + "user": 11, + "display_name": "Debbie Harding", + "casual_mode": false, + "ranked_cutoff": "2026-08-11T17:45:33.317Z", + "hide_from_leaderboards": false, + "hide_from_acknowledgments": false, + "revealed_proposals": [11] + } + }, + { + "model": "tubes.oimecontributor", + "pk": 6, + "fields": { + "user": 13, + "display_name": "Tiffany Alexander", + "casual_mode": false, + "ranked_cutoff": null, + "hide_from_leaderboards": false, + "hide_from_acknowledgments": false, + "revealed_proposals": [3] + } + }, + { + "model": "tubes.oimecontributor", + "pk": 7, + "fields": { + "user": 19, + "display_name": "Shelby Mejia", + "casual_mode": false, + "ranked_cutoff": null, + "hide_from_leaderboards": false, + "hide_from_acknowledgments": false, + "revealed_proposals": [10] + } + }, + { + "model": "tubes.oimecontributor", + "pk": 8, + "fields": { + "user": 20, + "display_name": "Katrina Drake", + "casual_mode": false, + "ranked_cutoff": null, + "hide_from_leaderboards": false, + "hide_from_acknowledgments": false, + "revealed_proposals": [11] + } + }, + { + "model": "tubes.oimecontributor", + "pk": 9, + "fields": { + "user": 22, + "display_name": "Michelle Andrews", + "casual_mode": true, + "ranked_cutoff": null, + "hide_from_leaderboards": false, + "hide_from_acknowledgments": false, + "revealed_proposals": [] + } + }, + { + "model": "tubes.oimecontributor", + "pk": 10, + "fields": { + "user": 23, + "display_name": "Yesenia Gallegos", + "casual_mode": false, + "ranked_cutoff": null, + "hide_from_leaderboards": false, + "hide_from_acknowledgments": false, + "revealed_proposals": [19] + } + }, + { + "model": "tubes.oimecontributor", + "pk": 11, + "fields": { + "user": 24, + "display_name": "Erika Frost", + "casual_mode": false, + "ranked_cutoff": null, + "hide_from_leaderboards": false, + "hide_from_acknowledgments": false, + "revealed_proposals": [17, 11] + } + }, + { + "model": "tubes.oimecontributor", + "pk": 12, + "fields": { + "user": 25, + "display_name": "Adriana Gardner", + "casual_mode": false, + "ranked_cutoff": null, + "hide_from_leaderboards": false, + "hide_from_acknowledgments": false, + "revealed_proposals": [15] + } + }, + { + "model": "tubes.oimecontributor", + "pk": 13, + "fields": { + "user": 26, + "display_name": "Lydia Carrillo", + "casual_mode": false, + "ranked_cutoff": null, + "hide_from_leaderboards": false, + "hide_from_acknowledgments": false, + "revealed_proposals": [] + } + }, + { + "model": "tubes.oimeproposal", + "pk": 1, + "fields": { + "author": 12, + "credit": "", + "title": "Position technology make modern.", + "statement": "Position similar natural us always. Someone collection ask agent there.", + "answer": 173, + "solution": "Garden level exactly safe indeed music finish. Beyond maintain their already dark. On information knowledge scene forward role measure.", + "subject": "G", + "difficulty": 3, + "archived": false, + "is_draft": true, + "created_at": "2026-05-31T21:49:33.346Z", + "updated_at": "2026-08-23T17:45:33.342Z", + "upvotes": [] + } + }, + { + "model": "tubes.oimeproposal", + "pk": 2, + "fields": { + "author": 9, + "credit": "", + "title": "Bed past apply night before.", + "statement": "Chair admit society hear responsibility attack those. Degree growth window. Return start eye article defense.", + "answer": 442, + "solution": "Ready charge prevent avoid yeah just. Plant practice relationship true. Laugh me able popular. So learn value camera life.", + "subject": "G", + "difficulty": 2, + "archived": false, + "is_draft": false, + "created_at": "2026-06-07T07:29:33.346Z", + "updated_at": "2026-08-23T17:45:33.342Z", + "upvotes": [] + } + }, + { + "model": "tubes.oimeproposal", + "pk": 3, + "fields": { + "author": 10, + "credit": "", + "title": "Important structure.", + "statement": "Deal sign gun south artist option response challenge. Sometimes candidate treatment herself note. Record crime themselves night something compare economic.", + "answer": 527, + "solution": "Executive cause writer record. Ok offer card goal course very above.", + "subject": "C", + "difficulty": 3, + "archived": false, + "is_draft": false, + "created_at": "2026-06-13T04:24:33.346Z", + "updated_at": "2026-08-23T17:45:33.342Z", + "upvotes": [] + } + }, + { + "model": "tubes.oimeproposal", + "pk": 4, + "fields": { + "author": 4, + "credit": "", + "title": "At inside.", + "statement": "Feeling bring bag day study. Move plant card write perform.", + "answer": 379, + "solution": "Yeah lead house cup everyone everybody where. You research break option and now. Much today own. Source finish take rest rest civil.", + "subject": "A", + "difficulty": 2, + "archived": false, + "is_draft": false, + "created_at": "2026-06-19T02:31:33.346Z", + "updated_at": "2026-08-23T17:45:33.342Z", + "upvotes": [2] + } + }, + { + "model": "tubes.oimeproposal", + "pk": 5, + "fields": { + "author": 2, + "credit": "", + "title": "Figure boy.", + "statement": "On identify interest who scene list. Play produce leader condition like loss.", + "answer": 924, + "solution": "Personal he their wife. Light his present morning bad administration heavy.", + "subject": "N", + "difficulty": 3, + "archived": false, + "is_draft": false, + "created_at": "2026-06-25T14:22:33.346Z", + "updated_at": "2026-08-23T17:45:33.342Z", + "upvotes": [] + } + }, + { + "model": "tubes.oimeproposal", + "pk": 6, + "fields": { + "author": 12, + "credit": "", + "title": "Street effort sing nature well.", + "statement": "Until according trip want manager way family later. Almost stand drop notice.", + "answer": 247, + "solution": "Want yet south reach network. Project try now term. Feeling past hold begin may.", + "subject": "N", + "difficulty": 4, + "archived": false, + "is_draft": true, + "created_at": "2026-06-27T13:50:33.346Z", + "updated_at": "2026-08-23T17:45:33.342Z", + "upvotes": [] + } + }, + { + "model": "tubes.oimeproposal", + "pk": 7, + "fields": { + "author": 12, + "credit": "", + "title": "State act total parent site.", + "statement": "Management worker couple administration finish apply. Everybody wife upon time material source.", + "answer": 90, + "solution": "Life seem back body life any cup opportunity. Win bit edge outside last even away. Direction however everybody once tough old.", + "subject": "A", + "difficulty": 3, + "archived": false, + "is_draft": false, + "created_at": "2026-06-28T02:41:33.346Z", + "updated_at": "2026-08-23T17:45:33.342Z", + "upvotes": [] + } + }, + { + "model": "tubes.oimeproposal", + "pk": 8, + "fields": { + "author": 6, + "credit": "", + "title": "Especially future stop factor.", + "statement": "Government now visit local computer. Myself identify adult relationship alone million section. Pretty your moment consumer avoid high nice after.", + "answer": 710, + "solution": "Operation energy state. Top simply western treat. Moment firm finish hit.", + "subject": "A", + "difficulty": 5, + "archived": false, + "is_draft": true, + "created_at": "2026-07-03T15:29:33.346Z", + "updated_at": "2026-08-23T17:45:33.342Z", + "upvotes": [] + } + }, + { + "model": "tubes.oimeproposal", + "pk": 9, + "fields": { + "author": 3, + "credit": "", + "title": "Program control.", + "statement": "Break unit begin color sport. Rise since course husband house similar.", + "answer": 706, + "solution": "These boy across same glass them get in. Ball full production news discover.", + "subject": "N", + "difficulty": 3, + "archived": false, + "is_draft": false, + "created_at": "2026-07-06T13:58:33.346Z", + "updated_at": "2026-08-23T17:45:33.342Z", + "upvotes": [] + } + }, + { + "model": "tubes.oimeproposal", + "pk": 10, + "fields": { + "author": 3, + "credit": "", + "title": "Thought sense evening.", + "statement": "Other watch bad design mother range clearly. Suffer become market something office serious. Series network few phone start worry Republican.", + "answer": 939, + "solution": "Test current have according picture. Student never high few between. Whose last provide.", + "subject": "A", + "difficulty": 2, + "archived": false, + "is_draft": false, + "created_at": "2026-07-07T03:34:33.346Z", + "updated_at": "2026-08-23T17:45:33.342Z", + "upvotes": [5] + } + }, + { + "model": "tubes.oimeproposal", + "pk": 11, + "fields": { + "author": 6, + "credit": "", + "title": "Item anything so sell.", + "statement": "Time western positive chair threat check. Sort radio hear reality worry machine. Establish forward detail economy color feeling mention.", + "answer": 930, + "solution": "These sign citizen five southern. Road these line draw range without candidate. Beyond way arrive but tax.", + "subject": "G", + "difficulty": 2, + "archived": true, + "is_draft": false, + "created_at": "2026-07-11T21:15:33.346Z", + "updated_at": "2026-08-23T17:45:33.342Z", + "upvotes": [] + } + }, + { + "model": "tubes.oimeproposal", + "pk": 12, + "fields": { + "author": 3, + "credit": "", + "title": "Sense consider machine production develop.", + "statement": "Change low money. Discover single money possible chair community.", + "answer": 588, + "solution": "Respond structure memory manage. Mean central deal. Increase term card partner tend imagine reality.", + "subject": "A", + "difficulty": 4, + "archived": false, + "is_draft": true, + "created_at": "2026-07-12T03:23:33.346Z", + "updated_at": "2026-08-23T17:45:33.342Z", + "upvotes": [] + } + }, + { + "model": "tubes.oimeproposal", + "pk": 13, + "fields": { + "author": 10, + "credit": "", + "title": "Partner wonder lose party series.", + "statement": "Coach blue team president. Event none ahead report consider kid his reality. Side cause family attention process.", + "answer": 29, + "solution": "Actually build anyone new. War star beyond development shake indicate politics.", + "subject": "G", + "difficulty": 2, + "archived": false, + "is_draft": false, + "created_at": "2026-07-14T12:11:33.346Z", + "updated_at": "2026-08-23T17:45:33.342Z", + "upvotes": [] + } + }, + { + "model": "tubes.oimeproposal", + "pk": 14, + "fields": { + "author": 4, + "credit": "", + "title": "Political wrong section.", + "statement": "Make everybody laugh. Stand instead often nothing morning him. Allow any campaign reason main. Window activity of rise.", + "answer": 656, + "solution": "Southern garden ago group. Tree total road.", + "subject": "C", + "difficulty": 3, + "archived": false, + "is_draft": false, + "created_at": "2026-07-19T23:56:33.346Z", + "updated_at": "2026-08-23T17:45:33.342Z", + "upvotes": [] + } + }, + { + "model": "tubes.oimeproposal", + "pk": 15, + "fields": { + "author": 7, + "credit": "", + "title": "Still concern would there.", + "statement": "Nor deep sometimes five there dinner. Property thank cause quite personal understand off. Attack particularly task.", + "answer": 5, + "solution": "Under paper south. Machine energy page edge outside must mind. No participant shake interest.", + "subject": "C", + "difficulty": 2, + "archived": false, + "is_draft": false, + "created_at": "2026-07-20T03:34:33.346Z", + "updated_at": "2026-08-23T17:45:33.342Z", + "upvotes": [4, 8] + } + }, + { + "model": "tubes.oimeproposal", + "pk": 16, + "fields": { + "author": 11, + "credit": "", + "title": "Drive life relate action trial.", + "statement": "Small whether hour look political quite before improve. Now fear tonight least become. Itself process rock suddenly miss current build. Minute than member source market commercial.", + "answer": 267, + "solution": "Writer important enough article. Day explain head real report together.", + "subject": "C", + "difficulty": 2, + "archived": false, + "is_draft": false, + "created_at": "2026-07-21T07:11:33.346Z", + "updated_at": "2026-08-23T17:45:33.342Z", + "upvotes": [12] + } + }, + { + "model": "tubes.oimeproposal", + "pk": 17, + "fields": { + "author": 3, + "credit": "", + "title": "No turn now understand.", + "statement": "Evening quickly everybody trade standard. Pick direction stuff TV buy. Range activity detail room car just.", + "answer": 27, + "solution": "Marriage station manage. Police degree front him my.", + "subject": "C", + "difficulty": 5, + "archived": false, + "is_draft": false, + "created_at": "2026-07-24T18:14:33.346Z", + "updated_at": "2026-08-23T17:45:33.342Z", + "upvotes": [6] + } + }, + { + "model": "tubes.oimeproposal", + "pk": 18, + "fields": { + "author": 11, + "credit": "", + "title": "Theory charge according decision.", + "statement": "Well record rich scene certain. Read down themselves message herself respond.", + "answer": 209, + "solution": "Accept down recognize often area involve. Special wall food argue.", + "subject": "G", + "difficulty": 2, + "archived": false, + "is_draft": false, + "created_at": "2026-08-07T05:36:33.346Z", + "updated_at": "2026-08-23T17:45:33.342Z", + "upvotes": [] + } + }, + { + "model": "tubes.oimeproposal", + "pk": 19, + "fields": { + "author": 4, + "credit": "", + "title": "Ball our.", + "statement": "Event paper really him similar design along. Performance to hospital article whom long many. Amount create story behavior themselves however social necessary.", + "answer": 359, + "solution": "Be capital employee. Public treat message for. Young blood develop agency finish best issue Mrs.", + "subject": "N", + "difficulty": 5, + "archived": false, + "is_draft": false, + "created_at": "2026-08-08T23:54:33.346Z", + "updated_at": "2026-08-23T17:45:33.342Z", + "upvotes": [11] + } + }, + { + "model": "tubes.oimeproposal", + "pk": 20, + "fields": { + "author": 13, + "credit": "", + "title": "Way girl third across try.", + "statement": "Organization letter area toward fact mission power off.", + "answer": 392, + "solution": "Few nature rise note. That land teacher thought language. Why drop detail.", + "subject": "N", + "difficulty": 3, + "archived": false, + "is_draft": true, + "created_at": "2026-08-13T07:12:33.346Z", + "updated_at": "2026-08-23T17:45:33.342Z", + "upvotes": [] + } + }, + { + "model": "tubes.oimefight", + "pk": 1, + "fields": { + "contributor": 1, + "proposal": 2, + "started_at": "2026-08-06T03:39:47.346Z", + "submitted_at": "2026-08-06T03:52:14.346Z", + "status": "OIME_OK", + "wrong_answers": 0 + } + }, + { + "model": "tubes.oimefight", + "pk": 2, + "fields": { + "contributor": 2, + "proposal": 2, + "started_at": "2026-08-23T17:43:43.351Z", + "submitted_at": null, + "status": "OIME_TBD", + "wrong_answers": 0 + } + }, + { + "model": "tubes.oimefight", + "pk": 3, + "fields": { + "contributor": 3, + "proposal": 2, + "started_at": "2026-08-13T04:58:02.346Z", + "submitted_at": "2026-08-13T05:02:16.346Z", + "status": "OIME_ALE", + "wrong_answers": 5 + } + }, + { + "model": "tubes.oimefight", + "pk": 4, + "fields": { + "contributor": 4, + "proposal": 2, + "started_at": "2026-08-08T09:11:38.346Z", + "submitted_at": "2026-08-08T09:24:06.346Z", + "status": "OIME_OK", + "wrong_answers": 1 + } + }, + { + "model": "tubes.oimefight", + "pk": 5, + "fields": { + "contributor": 5, + "proposal": 2, + "started_at": "2026-07-02T05:33:13.346Z", + "submitted_at": "2026-07-02T05:43:25.346Z", + "status": "OIME_ALE", + "wrong_answers": 5 + } + }, + { + "model": "tubes.oimefight", + "pk": 6, + "fields": { + "contributor": 10, + "proposal": 2, + "started_at": "2026-07-15T22:31:43.346Z", + "submitted_at": "2026-07-15T22:44:36.346Z", + "status": "OIME_FAIL", + "wrong_answers": 1 + } + }, + { + "model": "tubes.oimefight", + "pk": 7, + "fields": { + "contributor": 2, + "proposal": 3, + "started_at": "2026-08-14T03:05:11.346Z", + "submitted_at": "2026-08-14T03:14:55.346Z", + "status": "OIME_OK", + "wrong_answers": 2 + } + }, + { + "model": "tubes.oimefight", + "pk": 8, + "fields": { + "contributor": 8, + "proposal": 3, + "started_at": "2026-06-23T10:18:00.346Z", + "submitted_at": "2026-06-23T10:26:32.346Z", + "status": "OIME_OK", + "wrong_answers": 0 + } + }, + { + "model": "tubes.oimefight", + "pk": 9, + "fields": { + "contributor": 9, + "proposal": 3, + "started_at": "2026-07-11T03:36:04.346Z", + "submitted_at": "2026-07-11T03:39:47.346Z", + "status": "OIME_ALE", + "wrong_answers": 5 + } + }, + { + "model": "tubes.oimefight", + "pk": 10, + "fields": { + "contributor": 12, + "proposal": 3, + "started_at": "2026-07-21T23:45:13.346Z", + "submitted_at": "2026-07-22T00:02:22.346Z", + "status": "OIME_OK", + "wrong_answers": 0 + } + }, + { + "model": "tubes.oimefight", + "pk": 11, + "fields": { + "contributor": 2, + "proposal": 4, + "started_at": "2026-07-12T13:37:20.346Z", + "submitted_at": "2026-07-12T13:51:21.346Z", + "status": "OIME_OK", + "wrong_answers": 2 + } + }, + { + "model": "tubes.oimefight", + "pk": 12, + "fields": { + "contributor": 5, + "proposal": 4, + "started_at": "2026-08-17T10:37:22.346Z", + "submitted_at": "2026-08-17T10:43:47.346Z", + "status": "OIME_OK", + "wrong_answers": 1 + } + }, + { + "model": "tubes.oimefight", + "pk": 13, + "fields": { + "contributor": 3, + "proposal": 5, + "started_at": "2026-07-28T03:30:34.346Z", + "submitted_at": "2026-07-28T03:32:12.346Z", + "status": "OIME_ALE", + "wrong_answers": 5 + } + }, + { + "model": "tubes.oimefight", + "pk": 14, + "fields": { + "contributor": 4, + "proposal": 5, + "started_at": "2026-07-09T14:41:23.346Z", + "submitted_at": "2026-07-09T14:53:59.346Z", + "status": "OIME_ALE", + "wrong_answers": 5 + } + }, + { + "model": "tubes.oimefight", + "pk": 15, + "fields": { + "contributor": 6, + "proposal": 5, + "started_at": "2026-08-15T17:10:33.346Z", + "submitted_at": "2026-08-15T17:28:14.346Z", + "status": "OIME_ALE", + "wrong_answers": 5 + } + }, + { + "model": "tubes.oimefight", + "pk": 16, + "fields": { + "contributor": 12, + "proposal": 5, + "started_at": "2026-07-17T08:00:27.346Z", + "submitted_at": "2026-07-17T08:20:27.346Z", + "status": "OIME_TLE", + "wrong_answers": 0 + } + }, + { + "model": "tubes.oimefight", + "pk": 17, + "fields": { + "contributor": 5, + "proposal": 7, + "started_at": "2026-08-23T17:42:59.351Z", + "submitted_at": null, + "status": "OIME_TBD", + "wrong_answers": 0 + } + }, + { + "model": "tubes.oimefight", + "pk": 18, + "fields": { + "contributor": 9, + "proposal": 9, + "started_at": "2026-07-26T11:10:18.346Z", + "submitted_at": "2026-07-26T11:23:37.346Z", + "status": "OIME_FAIL", + "wrong_answers": 2 + } + }, + { + "model": "tubes.oimefight", + "pk": 19, + "fields": { + "contributor": 11, + "proposal": 9, + "started_at": "2026-08-03T01:14:12.346Z", + "submitted_at": "2026-08-03T01:28:48.346Z", + "status": "OIME_OK", + "wrong_answers": 1 + } + }, + { + "model": "tubes.oimefight", + "pk": 20, + "fields": { + "contributor": 13, + "proposal": 9, + "started_at": "2026-08-06T07:36:14.346Z", + "submitted_at": "2026-08-06T07:56:14.346Z", + "status": "OIME_TLE", + "wrong_answers": 5 + } + }, + { + "model": "tubes.oimefight", + "pk": 21, + "fields": { + "contributor": 1, + "proposal": 10, + "started_at": "2026-07-28T01:06:47.346Z", + "submitted_at": "2026-07-28T01:21:33.346Z", + "status": "OIME_OK", + "wrong_answers": 4 + } + }, + { + "model": "tubes.oimefight", + "pk": 22, + "fields": { + "contributor": 5, + "proposal": 10, + "started_at": "2026-07-20T17:45:43.346Z", + "submitted_at": "2026-07-20T17:46:32.346Z", + "status": "OIME_OK", + "wrong_answers": 2 + } + }, + { + "model": "tubes.oimefight", + "pk": 23, + "fields": { + "contributor": 10, + "proposal": 10, + "started_at": "2026-07-25T02:16:50.346Z", + "submitted_at": "2026-07-25T02:31:50.346Z", + "status": "OIME_TLE", + "wrong_answers": 2 + } + }, + { + "model": "tubes.oimefight", + "pk": 24, + "fields": { + "contributor": 1, + "proposal": 13, + "started_at": "2026-08-15T14:59:47.346Z", + "submitted_at": "2026-08-15T15:14:47.346Z", + "status": "OIME_TLE", + "wrong_answers": 2 + } + }, + { + "model": "tubes.oimefight", + "pk": 25, + "fields": { + "contributor": 4, + "proposal": 13, + "started_at": "2026-07-15T00:55:01.346Z", + "submitted_at": "2026-07-15T00:57:26.346Z", + "status": "OIME_FAIL", + "wrong_answers": 1 + } + }, + { + "model": "tubes.oimefight", + "pk": 26, + "fields": { + "contributor": 7, + "proposal": 13, + "started_at": "2026-08-17T18:38:26.346Z", + "submitted_at": "2026-08-17T18:45:49.346Z", + "status": "OIME_FAIL", + "wrong_answers": 2 + } + }, + { + "model": "tubes.oimefight", + "pk": 27, + "fields": { + "contributor": 9, + "proposal": 13, + "started_at": "2026-07-16T04:40:44.346Z", + "submitted_at": "2026-07-16T04:41:34.346Z", + "status": "OIME_FAIL", + "wrong_answers": 4 + } + }, + { + "model": "tubes.oimefight", + "pk": 28, + "fields": { + "contributor": 11, + "proposal": 13, + "started_at": "2026-08-04T03:01:53.346Z", + "submitted_at": "2026-08-04T03:15:39.346Z", + "status": "OIME_ALE", + "wrong_answers": 5 + } + }, + { + "model": "tubes.oimefight", + "pk": 29, + "fields": { + "contributor": 1, + "proposal": 14, + "started_at": "2026-08-07T01:47:09.346Z", + "submitted_at": "2026-08-07T02:07:09.346Z", + "status": "OIME_TLE", + "wrong_answers": 2 + } + }, + { + "model": "tubes.oimefight", + "pk": 30, + "fields": { + "contributor": 12, + "proposal": 14, + "started_at": "2026-08-07T20:49:08.346Z", + "submitted_at": "2026-08-07T20:57:16.346Z", + "status": "OIME_FAIL", + "wrong_answers": 4 + } + }, + { + "model": "tubes.oimefight", + "pk": 31, + "fields": { + "contributor": 2, + "proposal": 15, + "started_at": "2026-08-18T18:01:41.346Z", + "submitted_at": "2026-08-18T18:03:34.346Z", + "status": "OIME_ALE", + "wrong_answers": 5 + } + }, + { + "model": "tubes.oimefight", + "pk": 32, + "fields": { + "contributor": 3, + "proposal": 15, + "started_at": "2026-07-25T18:20:47.346Z", + "submitted_at": "2026-07-25T18:25:08.346Z", + "status": "OIME_FAIL", + "wrong_answers": 2 + } + }, + { + "model": "tubes.oimefight", + "pk": 33, + "fields": { + "contributor": 4, + "proposal": 15, + "started_at": "2026-07-20T23:24:26.346Z", + "submitted_at": "2026-07-20T23:36:56.346Z", + "status": "OIME_OK", + "wrong_answers": 3 + } + }, + { + "model": "tubes.oimefight", + "pk": 34, + "fields": { + "contributor": 8, + "proposal": 15, + "started_at": "2026-08-08T21:11:04.346Z", + "submitted_at": "2026-08-08T21:20:28.346Z", + "status": "OIME_OK", + "wrong_answers": 3 + } + }, + { + "model": "tubes.oimefight", + "pk": 35, + "fields": { + "contributor": 9, + "proposal": 16, + "started_at": "2026-07-29T12:04:11.346Z", + "submitted_at": "2026-07-29T12:19:11.346Z", + "status": "OIME_TLE", + "wrong_answers": 2 + } + }, + { + "model": "tubes.oimefight", + "pk": 36, + "fields": { + "contributor": 12, + "proposal": 16, + "started_at": "2026-08-06T10:57:28.346Z", + "submitted_at": "2026-08-06T11:09:08.346Z", + "status": "OIME_OK", + "wrong_answers": 2 + } + }, + { + "model": "tubes.oimefight", + "pk": 37, + "fields": { + "contributor": 13, + "proposal": 16, + "started_at": "2026-08-23T17:38:55.351Z", + "submitted_at": null, + "status": "OIME_TBD", + "wrong_answers": 0 + } + }, + { + "model": "tubes.oimefight", + "pk": 38, + "fields": { + "contributor": 5, + "proposal": 17, + "started_at": "2026-07-28T15:41:57.346Z", + "submitted_at": "2026-07-28T16:11:57.346Z", + "status": "OIME_TLE", + "wrong_answers": 1 + } + }, + { + "model": "tubes.oimefight", + "pk": 39, + "fields": { + "contributor": 6, + "proposal": 17, + "started_at": "2026-08-04T01:05:41.346Z", + "submitted_at": "2026-08-04T01:33:42.346Z", + "status": "OIME_OK", + "wrong_answers": 0 + } + }, + { + "model": "tubes.oimefight", + "pk": 40, + "fields": { + "contributor": 8, + "proposal": 18, + "started_at": "2026-08-20T13:29:39.346Z", + "submitted_at": "2026-08-20T13:41:51.346Z", + "status": "OIME_OK", + "wrong_answers": 2 + } + }, + { + "model": "tubes.oimefight", + "pk": 41, + "fields": { + "contributor": 10, + "proposal": 18, + "started_at": "2026-08-15T00:01:58.346Z", + "submitted_at": "2026-08-15T00:06:25.346Z", + "status": "OIME_ALE", + "wrong_answers": 5 + } + }, + { + "model": "tubes.oimefight", + "pk": 42, + "fields": { + "contributor": 12, + "proposal": 18, + "started_at": "2026-08-17T20:57:47.346Z", + "submitted_at": "2026-08-17T21:01:31.346Z", + "status": "OIME_OK", + "wrong_answers": 1 + } + }, + { + "model": "tubes.oimefight", + "pk": 43, + "fields": { + "contributor": 11, + "proposal": 19, + "started_at": "2026-08-19T07:23:03.346Z", + "submitted_at": "2026-08-19T07:29:22.346Z", + "status": "OIME_OK", + "wrong_answers": 1 + } + }, + { + "model": "tubes.oimecomment", + "pk": 1, + "fields": { + "author": 4, + "proposal": 2, + "content": "Especially never type add record standard gun. Hope recent note long. Green western common several bed.", + "created_at": "2026-08-23T17:45:33.371Z", + "updated_at": "2026-08-23T17:45:33.371Z" + } + }, + { + "model": "tubes.oimecomment", + "pk": 2, + "fields": { + "author": 2, + "proposal": 3, + "content": "Exactly this attack heart property want environment. Network late win quickly.", + "created_at": "2026-08-23T17:45:33.371Z", + "updated_at": "2026-08-23T17:45:33.371Z" + } + }, + { + "model": "tubes.oimecomment", + "pk": 3, + "fields": { + "author": 8, + "proposal": 3, + "content": "Heart stay from degree somebody message much successful. Hotel movie with person.", + "created_at": "2026-08-23T17:45:33.371Z", + "updated_at": "2026-08-23T17:45:33.371Z" + } + }, + { + "model": "tubes.oimecomment", + "pk": 4, + "fields": { + "author": 12, + "proposal": 3, + "content": "Term computer employee leave assume involve detail. Market discuss perform wind. Age fire treat case guess.", + "created_at": "2026-08-23T17:45:33.371Z", + "updated_at": "2026-08-23T17:45:33.371Z" + } + }, + { + "model": "tubes.oimecomment", + "pk": 5, + "fields": { + "author": 5, + "proposal": 10, + "content": "Trouble have forget. Recently want worry interesting would anything foot.", + "created_at": "2026-08-23T17:45:33.371Z", + "updated_at": "2026-08-23T17:45:33.371Z" + } + }, + { + "model": "tubes.oimecomment", + "pk": 6, + "fields": { + "author": 9, + "proposal": 13, + "content": "Success blood gun what prove game. Total raise people. Whose election they lawyer society should police.", + "created_at": "2026-08-23T17:45:33.371Z", + "updated_at": "2026-08-23T17:45:33.371Z" + } + }, + { + "model": "tubes.oimecomment", + "pk": 7, + "fields": { + "author": 2, + "proposal": 15, + "content": "Main will best like rule trouble. Life human crime trial then television because. Manager value around then test material adult. A draw decision dark.", + "created_at": "2026-08-23T17:45:33.371Z", + "updated_at": "2026-08-23T17:45:33.371Z" + } + }, + { + "model": "tubes.oimecomment", + "pk": 8, + "fields": { + "author": 4, + "proposal": 15, + "content": "Tonight someone fill choice. Stock democratic suffer before total probably.", + "created_at": "2026-08-23T17:45:33.371Z", + "updated_at": "2026-08-23T17:45:33.371Z" + } + }, + { + "model": "tubes.oimecomment", + "pk": 9, + "fields": { + "author": 9, + "proposal": 16, + "content": "Care why every add. Throughout at force us.", + "created_at": "2026-08-23T17:45:33.371Z", + "updated_at": "2026-08-23T17:45:33.371Z" + } + }, + { + "model": "tubes.oimecomment", + "pk": 10, + "fields": { + "author": 12, + "proposal": 16, + "content": "Daughter five all indeed gun soldier performance.", + "created_at": "2026-08-23T17:45:33.371Z", + "updated_at": "2026-08-23T17:45:33.371Z" + } + }, + { + "model": "tubes.oimecomment", + "pk": 11, + "fields": { + "author": 11, + "proposal": 19, + "content": "Candidate yeah wait blood break for. Price design able box another. Trouble more floor article reflect matter apply. Appear very attorney former.", + "created_at": "2026-08-23T17:45:33.371Z", + "updated_at": "2026-08-23T17:45:33.371Z" + } } ] diff --git a/fixtures/populate.py b/fixtures/populate.py index 655177f6..47c6e103 100644 --- a/fixtures/populate.py +++ b/fixtures/populate.py @@ -59,6 +59,15 @@ ) from rpg.models import Achievement from suggestions.factories import ProblemSuggestionFactory +from tubes.factories import ( + JoinRecordFactory, + OIMECommentFactory, + OIMEContributorFactory, + OIMEFightFactory, + OIMEProposalFactory, + TubeFactory, +) +from tubes.models import OIMEContributor, OIMEFight, OIMEProposal # How many objects to create; each one is also a command-line flag. # (flag, name, default, help text) @@ -69,6 +78,7 @@ ("-e", "exam_num", 5, "number of tests and quizzes, respectively"), ("-a", "assistant_num", 2, "number of assistants"), ("-m", "market_num", 3, "number of markets"), + ("-o", "oime_num", 20, "number of OIME proposals"), ) # Dice rolls deciding who gets what; tweak to taste. @@ -84,6 +94,27 @@ MAX_STU_UNITS = 27 # largest curriculum a student can be given MIN_PSET_UNITS = 3 # units at the start of a curriculum that are never pset +# Dice rolls for the OIME testsolving tube. +P_OIME = 0.5 # chance a user signs up for OIME as a contributor +P_OIME_CASUAL = 0.15 # chance a contributor is currently in casual mode +P_OIME_RETURNED = 0.2 # chance a ranked contributor once dipped into casual mode +P_OIME_ANON = 0.1 # chance a contributor hides their name on leaderboards +P_OIME_DRAFT = 0.15 # chance a proposal is still its author's private draft +P_OIME_ARCHIVED = 0.05 # chance a proposal has been archived by staff +P_OIME_FIGHT = 0.25 # chance a contributor testsolves a given proposal +P_OIME_ACTIVE = 0.05 # chance a testsolve is still in progress right now +P_OIME_UPVOTE = 0.5 # chance a solved proposal gets an upvote from its solver +P_OIME_COMMENT = 0.25 # chance a finished testsolve leaves a comment +P_OIME_REVEAL = 0.1 # chance a contributor spoils a proposal they never fought +OIME_DAYS = 90 # how far back OIME proposals and testsolves are spread +# Weights for how a finished testsolve turned out. +OIME_OUTCOMES = ( + ("OIME_OK", 55), # solved + ("OIME_ALE", 15), # ran out of answer attempts + ("OIME_TLE", 15), # ran out of time + ("OIME_FAIL", 15), # gave up +) + ## Utils @@ -215,6 +246,233 @@ def create_sem_independent(args: argparse.Namespace, users: list[User]): HanabiContestFactory.create() +def create_oime_contributors(users: list[User]) -> list[OIMEContributor]: + """Signs a subset of users up for OIME, and records them joining the tube.""" + print("Creating the OIME tube") + tube = TubeFactory.create( + display_name="OIME testsolving", + description="Testsolving for the OTIS Invitational Mathematics Examination.", + main_url="https://otis.evanchen.cc/tubes/proposals/", + ) + + joiners = [user for user in users if random.random() < P_OIME] + now = timezone.now() + contributor_rows: list[Any] = [] + for user in joiners: + casual = random.random() < P_OIME_CASUAL + # A ranked contributor who once browsed casually keeps a cutoff, which + # leaves every older problem browse-only for them. + returned = not casual and random.random() < P_OIME_RETURNED + contributor_rows.append( + ( + user, + user.get_full_name() or user.username, + casual, + random.random() < P_OIME_ANON, + now - timedelta(days=random.randint(1, OIME_DAYS)) + if returned + else None, + ) + ) + + print(f"Creating {len(joiners)} OIME contributors") + contributors: list[OIMEContributor] = bulk_create_rows( + OIMEContributorFactory, + contributor_rows, + "user", + "display_name", + "casual_mode", + "hide_from_leaderboards", + "ranked_cutoff", + ) + print(f"Creating {len(joiners)} join records") + bulk_create_rows( + JoinRecordFactory, + [ + (user, now - timedelta(days=random.randint(1, OIME_DAYS))) + for user in joiners + ], + "user", + "activation_time", + tube=tube, + ) + return contributors + + +def create_oime_proposals( + oime_num: int, contributors: list[OIMEContributor] +) -> list[OIMEProposal]: + """Writes proposals, spread over the last few months, some drafted or archived.""" + subjects = [subject for subject, _ in OIMEProposal.SUBJECT_CHOICES] + print(f"Creating {oime_num} OIME proposals") + proposals: list[OIMEProposal] = bulk_create_rows( + OIMEProposalFactory, + [ + ( + random.choice(contributors), + random.choice(subjects), + randint_low(1, 5), + random.random() < P_OIME_DRAFT, + random.random() < P_OIME_ARCHIVED, + ) + for _ in range(oime_num) + ], + "author", + "subject", + "difficulty", + "is_draft", + "archived", + ) + + # created_at is auto_now_add, so spreading the proposals out over the term + # has to happen after the insert. Doing it matters: a contributor's + # ranked_cutoff is compared against it to decide what they may still fight. + # The stamps are sorted so that creation order matches pk order, the way it + # does in real life; otherwise the default newest-first list looks shuffled. + now = timezone.now() + stamps = sorted( + now - timedelta(minutes=random.randint(60, OIME_DAYS * 24 * 60)) + for _ in proposals + ) + for proposal, created_at in zip(proposals, stamps): + proposal.created_at = created_at + OIMEProposal.objects.bulk_update(proposals, fields=("created_at",), batch_size=50) + return proposals + + +def create_oime_fights( + contributors: list[OIMEContributor], proposals: list[OIMEProposal] +) -> list[OIMEFight]: + """Testsolves the published proposals, with a mix of outcomes.""" + now = timezone.now() + statuses = [status for status, _ in OIME_OUTCOMES] + weights = [weight for _, weight in OIME_OUTCOMES] + + rows: list[Any] = [] + started: list[datetime] = [] + # At most one active session per contributor, the invariant start_fight enforces. + busy: set[int] = set() + + for proposal in proposals: + if proposal.is_draft or proposal.archived: + continue + limit = proposal.time_limit_minutes * 60 + for contributor in contributors: + if contributor.pk == proposal.author_id: # type: ignore[attr-defined] + continue + if random.random() > P_OIME_FIGHT: + continue + + active = ( + not contributor.casual_mode + and contributor.pk not in busy + and random.random() < P_OIME_ACTIVE + ) + if active: + busy.add(contributor.pk) + # Still inside the clock, so the fight page shows a live timer. + started.append(now - timedelta(seconds=random.randint(0, limit // 2))) + rows.append((contributor, proposal, "OIME_TBD", 0, None)) + continue + + status = random.choices(statuses, weights)[0] + start = proposal.created_at + timedelta( + seconds=random.randint( + 0, int((now - proposal.created_at).total_seconds()) + ) + ) + started.append(start) + if status == "OIME_ALE": + wrong_answers = OIMEFight.ANSWER_LIMIT + elif status == "OIME_OK": + wrong_answers = randint_low(0, OIMEFight.ANSWER_LIMIT - 1) + else: + wrong_answers = randint_low(0, OIMEFight.ANSWER_LIMIT) + # A timed-out fight is closed at the buzzer; the rest end whenever. + spent = limit if status == "OIME_TLE" else random.randint(30, limit) + rows.append( + ( + contributor, + proposal, + status, + wrong_answers, + start + timedelta(seconds=spent), + ) + ) + + print(f"Creating {len(rows)} OIME testsolve attempts") + fights: list[OIMEFight] = bulk_create_rows( + OIMEFightFactory, + rows, + "contributor", + "proposal", + "status", + "wrong_answers", + "submitted_at", + ) + # started_at is auto_now_add, so it too can only be backdated after the insert. + for fight, start in zip(fights, started): + fight.started_at = start + OIMEFight.objects.bulk_update(fights, fields=("started_at",), batch_size=50) + return fights + + +def create_oime_reactions( + contributors: list[OIMEContributor], + proposals: list[OIMEProposal], + fights: list[OIMEFight], +): + """Upvotes, revealed solutions and comments left in the wake of the testsolves.""" + UpvoteThroughModel = OIMEProposal.upvotes.through + RevealedThroughModel = OIMEContributor.revealed_proposals.through + + fought = {(f.contributor_id, f.proposal_id) for f in fights} # type: ignore[attr-defined] + upvotes = [ + UpvoteThroughModel( + oimeproposal_id=fight.proposal_id, # type: ignore[attr-defined] + oimecontributor_id=fight.contributor_id, # type: ignore[attr-defined] + ) + for fight in fights + if fight.is_success and random.random() < P_OIME_UPVOTE + ] + print(f"Creating {len(upvotes)} OIME upvotes") + UpvoteThroughModel.objects.bulk_create(upvotes) + + # Reading a solution without solving is the escape hatch out of a problem; + # it only happens to problems the contributor never fought. + reveals = [ + RevealedThroughModel( + oimecontributor_id=contributor.pk, oimeproposal_id=proposal.pk + ) + for contributor in contributors + for proposal in proposals + if not proposal.is_draft + and contributor.pk != proposal.author_id # type: ignore[attr-defined] + and (contributor.pk, proposal.pk) not in fought + and random.random() < P_OIME_REVEAL + ] + print(f"Creating {len(reveals)} revealed OIME solutions") + RevealedThroughModel.objects.bulk_create(reveals) + + comments = [ + (fight.contributor, fight.proposal) + for fight in fights + if fight.is_complete and random.random() < P_OIME_COMMENT + ] + print(f"Creating {len(comments)} OIME comments") + bulk_create_rows(OIMECommentFactory, comments, "author", "proposal") + + +def create_oime(args: argparse.Namespace, users: list[User]): + """Creates the OIME tube: contributors, proposals, testsolves and discussion.""" + contributors = create_oime_contributors(users) + if not contributors: + return + proposals = create_oime_proposals(args.oime_num, contributors) + fights = create_oime_fights(contributors, proposals) + create_oime_reactions(contributors, proposals, fights) + + def assign_curriculums(students: list[Student]): """Gives each student some units, unlocking and psetting a few of them.""" units = list(Unit.objects.all()) @@ -376,6 +634,7 @@ def main(): ) create_sem_independent(args, users) + create_oime(args, users + assistant_users) current_year = timezone.now().year old_semester: Semester = SemesterFactory.create( diff --git a/otisweb/static/table-sort.js b/otisweb/static/table-sort.js index c04a6e09..cb9558d3 100644 --- a/otisweb/static/table-sort.js +++ b/otisweb/static/table-sort.js @@ -5,6 +5,10 @@ * automatically; no per-page setup is needed. A header opts out of sorting * with data-sort="none", which is what the emoji "Actions" columns use. * + * Clicking a header cycles it ascending -> descending -> back to the order the + * server rendered, which is usually meaningful (newest first, OTIS unit order, + * leaderboard rank) and is otherwise unreachable without a reload. + * * Sort state lives in aria-sort: otis.css draws the arrow from it, and screen * readers announce it. */ @@ -20,10 +24,25 @@ function otisSortValue(text) { return Number.parseFloat(match[0].replace(/,/g, "")); } -function otisSortTable(table, headers, th, index) { +function otisSortTable(table, headers, th, index, original) { const body = table.tBodies[0]; const textOf = (row) => (row.cells[index]?.textContent ?? "").trim(); + /* none -> ascending -> descending -> none. */ + const previous = th.getAttribute("aria-sort"); + const next = + previous === "none" + ? "ascending" + : previous === "ascending" + ? "descending" + : "none"; + headers.forEach((other) => other.setAttribute("aria-sort", "none")); + th.setAttribute("aria-sort", next); + if (next === "none") { + body.append(...original); + return; + } + const rows = Array.from(body.rows); const filled = rows.filter((row) => textOf(row) !== ""); const blank = rows.filter((row) => textOf(row) === ""); @@ -42,10 +61,7 @@ function otisSortTable(table, headers, th, index) { sensitivity: "base", }); - const descending = th.getAttribute("aria-sort") === "ascending"; - headers.forEach((other) => other.setAttribute("aria-sort", "none")); - th.setAttribute("aria-sort", descending ? "descending" : "ascending"); - + const descending = next === "descending"; filled.sort((a, b) => (descending ? -compare(a, b) : compare(a, b))); /* Rows with nothing in the sorted column stay at the bottom either way. */ body.append(...filled, ...blank); @@ -59,18 +75,20 @@ function otisMakeSortable(table) { const headers = Array.from(head.rows[head.rows.length - 1].cells).filter( (th) => th.dataset.sort !== "none", ); + /* Snapshot before any sorting, so the third click can put it all back. */ + const original = Array.from(table.tBodies[0].rows); headers.forEach((th) => { const index = th.cellIndex; th.tabIndex = 0; th.setAttribute("role", "button"); th.setAttribute("aria-sort", "none"); th.addEventListener("click", () => - otisSortTable(table, headers, th, index), + otisSortTable(table, headers, th, index, original), ); th.addEventListener("keydown", (event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); - otisSortTable(table, headers, th, index); + otisSortTable(table, headers, th, index, original); } }); }); diff --git a/tubes/templates/tubes/components/proposal_table.html b/tubes/templates/tubes/components/proposal_table.html index dc834c1d..278fdc6b 100644 --- a/tubes/templates/tubes/components/proposal_table.html +++ b/tubes/templates/tubes/components/proposal_table.html @@ -12,7 +12,7 @@

aria-controls="oime-table-{{ title|slugify }}">{{ title }}

- +
ID