{"action":"created","comment":{"url":"https://api.github.com/repos/18F/calc/pulls/comments/196896816","pull_request_review_id":130533717,"id":196896816,"node_id":"MDI0OlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudDE5Njg5NjgxNg==","diff_hunk":"@@ -510,3 +512,60 @@ def update_normalized_labor_category(self):\n     def save(self, *args, **kwargs):\n         self.update_normalized_labor_category()\n         super().save(*args, **kwargs)\n+\n+\n+class ScheduleMetadata(models.Model):\n+    '''\n+    This model represents metadata about a schedule, containing details\n+    such as the schedule's SIN number and description.\n+    '''\n+\n+    class Meta:\n+        ordering = ['sin', 'name']\n+\n+    # This column corresponds to the \"schedule\" column of the Contract model\n+    # and can essentially be viewed as an optional foreign key.\n+    schedule = models.CharField(\n+        help_text=(\n+            'Text that appears in the \"schedule\" column of Contract data '\n+            'to identify a labor rate as being in this schedule (e.g. \"MOBIS\").'\n+        ),\n+        unique=True,\n+        db_index=True,\n+        max_length=128\n+    )\n+\n+    sin = models.CharField(\n+        help_text=(\n+            'The SIN number for the schedule (e.g. \"874\").'\n+        ),\n+        blank=True,\n+        max_length=128\n+    )\n+\n+    name = models.CharField(","path":"contracts/models.py","position":44,"original_position":44,"commit_id":"dce52559dbdb5558caa6a0a878805954dc1b807d","original_commit_id":"dce52559dbdb5558caa6a0a878805954dc1b807d","user":{"login":"tbaxter-18f","id":32077682,"node_id":"MDQ6VXNlcjMyMDc3Njgy","avatar_url":"https://avatars2.githubusercontent.com/u/32077682?v=4","gravatar_id":"","url":"https://api.github.com/users/tbaxter-18f","html_url":"https://github.com/tbaxter-18f","followers_url":"https://api.github.com/users/tbaxter-18f/followers","following_url":"https://api.github.com/users/tbaxter-18f/following{/other_user}","gists_url":"https://api.github.com/users/tbaxter-18f/gists{/gist_id}","starred_url":"https://api.github.com/users/tbaxter-18f/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/tbaxter-18f/subscriptions","organizations_url":"https://api.github.com/users/tbaxter-18f/orgs","repos_url":"https://api.github.com/users/tbaxter-18f/repos","events_url":"https://api.github.com/users/tbaxter-18f/events{/privacy}","received_events_url":"https://api.github.com/users/tbaxter-18f/received_events","type":"User","site_admin":false},"body":"I was thinking most of the fields here _felt_ like FKs, but I see your point about not knowing the other objects (or even if they exist).","created_at":"2018-06-20T18:28:12Z","updated_at":"2018-06-20T18:28:13Z","html_url":"https://github.com/18F/calc/pull/2018#discussion_r196896816","pull_request_url":"https://api.github.com/repos/18F/calc/pulls/2018","author_association":"MEMBER","_links":{"self":{"href":"https://api.github.com/repos/18F/calc/pulls/comments/196896816"},"html":{"href":"https://github.com/18F/calc/pull/2018#discussion_r196896816"},"pull_request":{"href":"https://api.github.com/repos/18F/calc/pulls/2018"}},"in_reply_to_id":196862596},"pull_request":{"url":"https://api.github.com/repos/18F/calc/pulls/2018","id":196187208,"node_id":"MDExOlB1bGxSZXF1ZXN0MTk2MTg3MjA4","html_url":"https://github.com/18F/calc/pull/2018","diff_url":"https://github.com/18F/calc/pull/2018.diff","patch_url":"https://github.com/18F/calc/pull/2018.patch","issue_url":"https://api.github.com/repos/18F/calc/issues/2018","number":2018,"state":"open","locked":false,"title":"[WIP] Centralize schedule metadata and make it editable by Data Administrators","user":{"login":"toolness","id":124687,"node_id":"MDQ6VXNlcjEyNDY4Nw==","avatar_url":"https://avatars2.githubusercontent.com/u/124687?v=4","gravatar_id":"","url":"https://api.github.com/users/toolness","html_url":"https://github.com/toolness","followers_url":"https://api.github.com/users/toolness/followers","following_url":"https://api.github.com/users/toolness/following{/other_user}","gists_url":"https://api.github.com/users/toolness/gists{/gist_id}","starred_url":"https://api.github.com/users/toolness/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/toolness/subscriptions","organizations_url":"https://api.github.com/users/toolness/orgs","repos_url":"https://api.github.com/users/toolness/repos","events_url":"https://api.github.com/users/toolness/events{/privacy}","received_events_url":"https://api.github.com/users/toolness/received_events","type":"User","site_admin":false},"body":"Future improvements to CALC point to a need for Data Administrators to be able to add new schedule data to CALC without requiring changes to the codebase.\r\n\r\nFor example, Kelly would like to augment his Region 10 bulk upload with rows containing data about Region 6 schedules. For his part, this would just require setting the `schedule` column on his bulk upload spreadsheet for the relevant price rate rows. But we also want to make sure that CALC starts listing the new schedule(s) as being available in all the places where it lists schedules.\r\n\r\nCurrently, the list of schedules available in CALC is surfaced in the following places:\r\n\r\n* A list of the schedules and their descriptions is shown in CALC's about page.\r\n* The \"SIN / Schedule\" drop-down filter in the Data Explorer lists the schedules and their SIN numbers.\r\n* The developer API documentation lists the possible values that the `schedule` field may have.\r\n\r\nCurrently these locations have hard-coded lists, and changing the schedule list requires visiting each list and making the relevant changes.\r\n\r\nThis PR changes the list to be stored in the database and be editable in the admin UI by Data Administrators, with an initial data migration that pre-populates the list to its current settings.\r\n\r\n## Examples\r\n\r\nData Administrators will see this in the admin UI:\r\n\r\n> <img width=\"780\" alt=\"screen shot 2018-06-20 at 11 53 03 am\" src=\"https://user-images.githubusercontent.com/124687/41669763-919ed908-7480-11e8-9552-9519ec250631.png\">\r\n\r\nOnce they click on it, they will see a list of schedules:\r\n\r\n> <img width=\"831\" alt=\"screen shot 2018-06-20 at 11 53 21 am\" src=\"https://user-images.githubusercontent.com/124687/41669764-91b9c8da-7480-11e8-9f64-b751edfb402f.png\">\r\n\r\nIf they click on one of them, they can edit it:\r\n\r\n> <img width=\"837\" alt=\"screen shot 2018-06-20 at 11 53 36 am\" src=\"https://user-images.githubusercontent.com/124687/41669765-91ea5662-7480-11e8-842d-6266f094bd84.png\">\r\n\r\nThe \"Schedule\" field above may be a bit confusing: put more concretely, it's the value the `schedule` column in Kelly's bulk upload has that corresponds to the name of the schedule.  So for our Region 6 example, this might be \"Region 6\", if that's the `schedule` column value Kelly gave to all the rows in his bulk upload that were for Region 6.  But the \"name\" might be \"Schedule 03FAC\" since that might be a more famliar name for end-users to see in CALC.\r\n\r\n## To do\r\n\r\n- [x] Ensure that only Data Administrators and superusers can modify the schedule metadata.\r\n- [ ] Make the schedule/SIN list on the Data Explorer front-end be dynamically generated.\r\n- [ ] Make the schedule list in the API documentation be dynamically generated (or alternatively, if this is hard, link to a separate page or API endpoint that lists them).\r\n- [ ] Consider using `bleach` or a similar HTML sanitizer to ensure that the description field can't be abused to trigger XSS.\r\n","created_at":"2018-06-20T15:46:36Z","updated_at":"2018-06-20T18:28:13Z","closed_at":null,"merged_at":null,"merge_commit_sha":"27ec6ff6ff5c0713eb63af94119956c115b14338","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"commits_url":"https://api.github.com/repos/18F/calc/pulls/2018/commits","review_comments_url":"https://api.github.com/repos/18F/calc/pulls/2018/comments","review_comment_url":"https://api.github.com/repos/18F/calc/pulls/comments{/number}","comments_url":"https://api.github.com/repos/18F/calc/issues/2018/comments","statuses_url":"https://api.github.com/repos/18F/calc/statuses/dce52559dbdb5558caa6a0a878805954dc1b807d","head":{"label":"18F:dynamic-schedule-list-2","ref":"dynamic-schedule-list-2","sha":"dce52559dbdb5558caa6a0a878805954dc1b807d","user":{"login":"18F","id":6233994,"node_id":"MDEyOk9yZ2FuaXphdGlvbjYyMzM5OTQ=","avatar_url":"https://avatars0.githubusercontent.com/u/6233994?v=4","gravatar_id":"","url":"https://api.github.com/users/18F","html_url":"https://github.com/18F","followers_url":"https://api.github.com/users/18F/followers","following_url":"https://api.github.com/users/18F/following{/other_user}","gists_url":"https://api.github.com/users/18F/gists{/gist_id}","starred_url":"https://api.github.com/users/18F/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/18F/subscriptions","organizations_url":"https://api.github.com/users/18F/orgs","repos_url":"https://api.github.com/users/18F/repos","events_url":"https://api.github.com/users/18F/events{/privacy}","received_events_url":"https://api.github.com/users/18F/received_events","type":"Organization","site_admin":false},"repo":{"id":26967517,"node_id":"MDEwOlJlcG9zaXRvcnkyNjk2NzUxNw==","name":"calc","full_name":"18F/calc","owner":{"login":"18F","id":6233994,"node_id":"MDEyOk9yZ2FuaXphdGlvbjYyMzM5OTQ=","avatar_url":"https://avatars0.githubusercontent.com/u/6233994?v=4","gravatar_id":"","url":"https://api.github.com/users/18F","html_url":"https://github.com/18F","followers_url":"https://api.github.com/users/18F/followers","following_url":"https://api.github.com/users/18F/following{/other_user}","gists_url":"https://api.github.com/users/18F/gists{/gist_id}","starred_url":"https://api.github.com/users/18F/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/18F/subscriptions","organizations_url":"https://api.github.com/users/18F/orgs","repos_url":"https://api.github.com/users/18F/repos","events_url":"https://api.github.com/users/18F/events{/privacy}","received_events_url":"https://api.github.com/users/18F/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/18F/calc","description":"An estimator for hourly rates on professional services contracts","fork":false,"url":"https://api.github.com/repos/18F/calc","forks_url":"https://api.github.com/repos/18F/calc/forks","keys_url":"https://api.github.com/repos/18F/calc/keys{/key_id}","collaborators_url":"https://api.github.com/repos/18F/calc/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/18F/calc/teams","hooks_url":"https://api.github.com/repos/18F/calc/hooks","issue_events_url":"https://api.github.com/repos/18F/calc/issues/events{/number}","events_url":"https://api.github.com/repos/18F/calc/events","assignees_url":"https://api.github.com/repos/18F/calc/assignees{/user}","branches_url":"https://api.github.com/repos/18F/calc/branches{/branch}","tags_url":"https://api.github.com/repos/18F/calc/tags","blobs_url":"https://api.github.com/repos/18F/calc/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/18F/calc/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/18F/calc/git/refs{/sha}","trees_url":"https://api.github.com/repos/18F/calc/git/trees{/sha}","statuses_url":"https://api.github.com/repos/18F/calc/statuses/{sha}","languages_url":"https://api.github.com/repos/18F/calc/languages","stargazers_url":"https://api.github.com/repos/18F/calc/stargazers","contributors_url":"https://api.github.com/repos/18F/calc/contributors","subscribers_url":"https://api.github.com/repos/18F/calc/subscribers","subscription_url":"https://api.github.com/repos/18F/calc/subscription","commits_url":"https://api.github.com/repos/18F/calc/commits{/sha}","git_commits_url":"https://api.github.com/repos/18F/calc/git/commits{/sha}","comments_url":"https://api.github.com/repos/18F/calc/comments{/number}","issue_comment_url":"https://api.github.com/repos/18F/calc/issues/comments{/number}","contents_url":"https://api.github.com/repos/18F/calc/contents/{+path}","compare_url":"https://api.github.com/repos/18F/calc/compare/{base}...{head}","merges_url":"https://api.github.com/repos/18F/calc/merges","archive_url":"https://api.github.com/repos/18F/calc/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/18F/calc/downloads","issues_url":"https://api.github.com/repos/18F/calc/issues{/number}","pulls_url":"https://api.github.com/repos/18F/calc/pulls{/number}","milestones_url":"https://api.github.com/repos/18F/calc/milestones{/number}","notifications_url":"https://api.github.com/repos/18F/calc/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/18F/calc/labels{/name}","releases_url":"https://api.github.com/repos/18F/calc/releases{/id}","deployments_url":"https://api.github.com/repos/18F/calc/deployments","created_at":"2014-11-21T16:22:24Z","updated_at":"2018-06-19T21:40:45Z","pushed_at":"2018-06-20T16:13:35Z","git_url":"git://github.com/18F/calc.git","ssh_url":"git@github.com:18F/calc.git","clone_url":"https://github.com/18F/calc.git","svn_url":"https://github.com/18F/calc","homepage":"https://calc.gsa.gov/","size":37616,"stargazers_count":101,"watchers_count":101,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":51,"mirror_url":null,"archived":false,"open_issues_count":176,"license":{"key":"other","name":"Other","spdx_id":null,"url":null,"node_id":"MDc6TGljZW5zZTA="},"forks":51,"open_issues":176,"watchers":101,"default_branch":"develop"}},"base":{"label":"18F:develop","ref":"develop","sha":"6a442856fa42dcb7dc1fd4407d00da7dab61431e","user":{"login":"18F","id":6233994,"node_id":"MDEyOk9yZ2FuaXphdGlvbjYyMzM5OTQ=","avatar_url":"https://avatars0.githubusercontent.com/u/6233994?v=4","gravatar_id":"","url":"https://api.github.com/users/18F","html_url":"https://github.com/18F","followers_url":"https://api.github.com/users/18F/followers","following_url":"https://api.github.com/users/18F/following{/other_user}","gists_url":"https://api.github.com/users/18F/gists{/gist_id}","starred_url":"https://api.github.com/users/18F/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/18F/subscriptions","organizations_url":"https://api.github.com/users/18F/orgs","repos_url":"https://api.github.com/users/18F/repos","events_url":"https://api.github.com/users/18F/events{/privacy}","received_events_url":"https://api.github.com/users/18F/received_events","type":"Organization","site_admin":false},"repo":{"id":26967517,"node_id":"MDEwOlJlcG9zaXRvcnkyNjk2NzUxNw==","name":"calc","full_name":"18F/calc","owner":{"login":"18F","id":6233994,"node_id":"MDEyOk9yZ2FuaXphdGlvbjYyMzM5OTQ=","avatar_url":"https://avatars0.githubusercontent.com/u/6233994?v=4","gravatar_id":"","url":"https://api.github.com/users/18F","html_url":"https://github.com/18F","followers_url":"https://api.github.com/users/18F/followers","following_url":"https://api.github.com/users/18F/following{/other_user}","gists_url":"https://api.github.com/users/18F/gists{/gist_id}","starred_url":"https://api.github.com/users/18F/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/18F/subscriptions","organizations_url":"https://api.github.com/users/18F/orgs","repos_url":"https://api.github.com/users/18F/repos","events_url":"https://api.github.com/users/18F/events{/privacy}","received_events_url":"https://api.github.com/users/18F/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/18F/calc","description":"An estimator for hourly rates on professional services contracts","fork":false,"url":"https://api.github.com/repos/18F/calc","forks_url":"https://api.github.com/repos/18F/calc/forks","keys_url":"https://api.github.com/repos/18F/calc/keys{/key_id}","collaborators_url":"https://api.github.com/repos/18F/calc/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/18F/calc/teams","hooks_url":"https://api.github.com/repos/18F/calc/hooks","issue_events_url":"https://api.github.com/repos/18F/calc/issues/events{/number}","events_url":"https://api.github.com/repos/18F/calc/events","assignees_url":"https://api.github.com/repos/18F/calc/assignees{/user}","branches_url":"https://api.github.com/repos/18F/calc/branches{/branch}","tags_url":"https://api.github.com/repos/18F/calc/tags","blobs_url":"https://api.github.com/repos/18F/calc/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/18F/calc/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/18F/calc/git/refs{/sha}","trees_url":"https://api.github.com/repos/18F/calc/git/trees{/sha}","statuses_url":"https://api.github.com/repos/18F/calc/statuses/{sha}","languages_url":"https://api.github.com/repos/18F/calc/languages","stargazers_url":"https://api.github.com/repos/18F/calc/stargazers","contributors_url":"https://api.github.com/repos/18F/calc/contributors","subscribers_url":"https://api.github.com/repos/18F/calc/subscribers","subscription_url":"https://api.github.com/repos/18F/calc/subscription","commits_url":"https://api.github.com/repos/18F/calc/commits{/sha}","git_commits_url":"https://api.github.com/repos/18F/calc/git/commits{/sha}","comments_url":"https://api.github.com/repos/18F/calc/comments{/number}","issue_comment_url":"https://api.github.com/repos/18F/calc/issues/comments{/number}","contents_url":"https://api.github.com/repos/18F/calc/contents/{+path}","compare_url":"https://api.github.com/repos/18F/calc/compare/{base}...{head}","merges_url":"https://api.github.com/repos/18F/calc/merges","archive_url":"https://api.github.com/repos/18F/calc/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/18F/calc/downloads","issues_url":"https://api.github.com/repos/18F/calc/issues{/number}","pulls_url":"https://api.github.com/repos/18F/calc/pulls{/number}","milestones_url":"https://api.github.com/repos/18F/calc/milestones{/number}","notifications_url":"https://api.github.com/repos/18F/calc/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/18F/calc/labels{/name}","releases_url":"https://api.github.com/repos/18F/calc/releases{/id}","deployments_url":"https://api.github.com/repos/18F/calc/deployments","created_at":"2014-11-21T16:22:24Z","updated_at":"2018-06-19T21:40:45Z","pushed_at":"2018-06-20T16:13:35Z","git_url":"git://github.com/18F/calc.git","ssh_url":"git@github.com:18F/calc.git","clone_url":"https://github.com/18F/calc.git","svn_url":"https://github.com/18F/calc","homepage":"https://calc.gsa.gov/","size":37616,"stargazers_count":101,"watchers_count":101,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":51,"mirror_url":null,"archived":false,"open_issues_count":176,"license":{"key":"other","name":"Other","spdx_id":null,"url":null,"node_id":"MDc6TGljZW5zZTA="},"forks":51,"open_issues":176,"watchers":101,"default_branch":"develop"}},"_links":{"self":{"href":"https://api.github.com/repos/18F/calc/pulls/2018"},"html":{"href":"https://github.com/18F/calc/pull/2018"},"issue":{"href":"https://api.github.com/repos/18F/calc/issues/2018"},"comments":{"href":"https://api.github.com/repos/18F/calc/issues/2018/comments"},"review_comments":{"href":"https://api.github.com/repos/18F/calc/pulls/2018/comments"},"review_comment":{"href":"https://api.github.com/repos/18F/calc/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/18F/calc/pulls/2018/commits"},"statuses":{"href":"https://api.github.com/repos/18F/calc/statuses/dce52559dbdb5558caa6a0a878805954dc1b807d"}},"author_association":"MEMBER"}}
{"actor":{"display_login":"tbaxter-18f"}}