Store MozReview flags in a separate entity. (bug 1274371) r=glob draft
authorPiotr Zalewa <pzalewa@mozilla.com>
Mon, 10 Oct 2016 11:41:42 +0200
changeset 10456 fb962dbe85c697823d372d1ec3f731f6d08fbc31
parent 10129 9bf82bd1671653d6dbcbfdb52fe6c90b01ff59e4
child 10457 75284e964037eab1b6a7145f390558623e785706
push id1550
push userbmo:pzalewa@mozilla.com
push dateMon, 06 Mar 2017 20:29:47 +0000
reviewersglob
bugs1274371
Store MozReview flags in a separate entity. (bug 1274371) r=glob MozReviewFlag and MozReviewFlagType models are created. Objects are created in SignalHooks. Adding a reviewer happens in on_review_request_published and generates a 'r?' flag for each reviewer with no link to a review. Review flags are added on_review_publishing. It is possible to add more flag types. Review type is currently hardwritten into MozReviewFlagManager and is created on first call to method MozReviewFlagType::get_review_type. 1. Every requestee in ReviewRequest's target_people has to have a flag 2. There is a MozReviewFlag element for each Review with a flag set 3. When requestee is removed from target_people: * flag is deleted from database if it's not related to review. * If a flag related to deleted requestee is related to the review, then requestee is removed from the flag 4. When requestee is setting a flag on Review his flag is updated (review added if needed, status and timestamp changed). 5. There are different sets of possible statuses displayed in the drop-down: * Requestee from target_people: ['r?', 'r+', 'r-']. * For impromptu reviews: ['', 'r+', 'r-'], where '' implies no flag is gonna be created. 6. Flags are reset after a review request with a diffset is published MozReview-Commit-ID: 3VUAQP5xIiS
hgext/reviewboard/tests/test-commits-added.t
hgext/reviewboard/tests/test-commits-deleted-no-obsolescence.t
hgext/reviewboard/tests/test-commits-deleted-obsolescence.t
hgext/reviewboard/tests/test-operation-prevention.t
hgext/reviewboard/tests/test-push-http.t
hgext/reviewboard/tests/test-review-request-approval.t
hgext/reviewboard/tests/test-review-request-closed-discarded.t
hgext/reviewboard/tests/test-review-request-closed-submitted.t
hgext/reviewboard/tests/test-review-request-delegation.t
hgext/reviewboard/tests/test-review-request-delete-draft.t
hgext/reviewboard/tests/test-reviewer-flags-in-db.t
hgext/reviewboard/tests/test-reviewer-flags.t
hgext/reviewboard/tests/test-specify-reviewers.t
hgext/reviewboard/tests/test-unicode.t
pylib/mozreview/mozreview/extension.py
pylib/mozreview/mozreview/managers.py
pylib/mozreview/mozreview/models.py
pylib/mozreview/mozreview/resources/flag.py
pylib/mozreview/mozreview/review_helpers.py
pylib/mozreview/mozreview/signal_handlers.py
pylib/mozreview/mozreview/static/mozreview/js/review_flag.js
pylib/mozreview/mozreview/templates/mozreview/user-data.html
pylib/mozreview/mozreview/templatetags/mozreview.py
testing/vcttesting/reviewboard/mach_commands.py
--- a/hgext/reviewboard/tests/test-commits-added.t
+++ b/hgext/reviewboard/tests/test-commits-added.t
@@ -95,16 +95,17 @@ The parent review request should be upda
   extra_data: {}
   commit_extra_data:
     p2rb: true
     p2rb.discard_on_publish_rids: '[]'
     p2rb.first_public_ancestor: 7c5bdf0cec4a90edb36300f8f3679857f46db829
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: true
     p2rb.unpublished_rids: '[2, 3, 4]'
+  flags: []
   diffs: []
   approved: false
   approval_failure: The review request is not public.
   draft:
     bugs:
     - '1'
     commit: bz://1/mynick
     summary: bz://1/mynick
--- a/hgext/reviewboard/tests/test-commits-deleted-no-obsolescence.t
+++ b/hgext/reviewboard/tests/test-commits-deleted-no-obsolescence.t
@@ -129,16 +129,17 @@ Review request 6 should be added to the 
     p2rb.commits: '[["0b3e14fe3ff19019110705e72dcf563c0ef551f6", 2], ["bce658a3f6d6aa04bf5c449e0e779e839de4690e",
       3], ["713878e22d952d478e88bfdef897fdfc73060351", 4], ["4d0f846364eb509a1b6ae3294f05439101f6e7d3",
       5], ["4e50148c492dde95397cd666f2d4e4ad4fd2176f", 6]]'
     p2rb.discard_on_publish_rids: '[6]'
     p2rb.first_public_ancestor: 93d9429b41ecf0d2ad8c62b6ea26686dd20330f4
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: true
     p2rb.unpublished_rids: '[]'
+  flags: []
   diffs:
   - id: 1
     revision: 1
     base_commit_id: 93d9429b41ecf0d2ad8c62b6ea26686dd20330f4
     name: diff
     extra: {}
     patch:
     - diff --git a/foo1 b/foo1
@@ -250,16 +251,17 @@ Review 6 should be marked as discarded
     calculated_trophies: true
   commit_extra_data:
     p2rb: true
     p2rb.author: test
     p2rb.commit_id: 4e50148c492dde95397cd666f2d4e4ad4fd2176f
     p2rb.first_public_ancestor: 93d9429b41ecf0d2ad8c62b6ea26686dd20330f4
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: false
+  flags: []
   diffs:
   - id: 6
     revision: 1
     base_commit_id: 4d0f846364eb509a1b6ae3294f05439101f6e7d3
     name: diff
     extra: {}
     patch:
     - diff --git a/foo5 b/foo5
@@ -325,16 +327,17 @@ The review request corresponding to the 
     calculated_trophies: true
   commit_extra_data:
     p2rb: true
     p2rb.author: test
     p2rb.commit_id: 0b3e14fe3ff19019110705e72dcf563c0ef551f6
     p2rb.first_public_ancestor: 93d9429b41ecf0d2ad8c62b6ea26686dd20330f4
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: false
+  flags: []
   diffs:
   - id: 2
     revision: 1
     base_commit_id: 93d9429b41ecf0d2ad8c62b6ea26686dd20330f4
     name: diff
     extra: {}
     patch:
     - diff --git a/foo1 b/foo1
@@ -369,16 +372,17 @@ Review request 2 should be marked as dis
     p2rb.commits: '[["0b3e14fe3ff19019110705e72dcf563c0ef551f6", 2], ["bce658a3f6d6aa04bf5c449e0e779e839de4690e",
       3], ["713878e22d952d478e88bfdef897fdfc73060351", 4], ["4d0f846364eb509a1b6ae3294f05439101f6e7d3",
       5]]'
     p2rb.discard_on_publish_rids: '[2]'
     p2rb.first_public_ancestor: 93d9429b41ecf0d2ad8c62b6ea26686dd20330f4
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: true
     p2rb.unpublished_rids: '[]'
+  flags: []
   diffs:
   - id: 1
     revision: 1
     base_commit_id: 93d9429b41ecf0d2ad8c62b6ea26686dd20330f4
     name: diff
     extra: {}
     patch:
     - diff --git a/foo1 b/foo1
@@ -547,16 +551,17 @@ The parent review should have been updat
     p2rb.base_commit: 93d9429b41ecf0d2ad8c62b6ea26686dd20330f4
     p2rb.commits: '[["eeb6d49dcb0950d771959358f662cf2e5ddc9dc1", 3], ["607f375f35c0866a8e08bc1d6aaecc6ad259ed6e",
       4], ["81ee86efd38ff60717aeeeff153292e84e58be0b", 5]]'
     p2rb.discard_on_publish_rids: '[4]'
     p2rb.first_public_ancestor: 93d9429b41ecf0d2ad8c62b6ea26686dd20330f4
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: true
     p2rb.unpublished_rids: '[]'
+  flags: []
   diffs:
   - id: 1
     revision: 1
     base_commit_id: 93d9429b41ecf0d2ad8c62b6ea26686dd20330f4
     name: diff
     extra: {}
     patch:
     - diff --git a/foo1 b/foo1
@@ -741,16 +746,17 @@ recycling behavior when commit IDs are p
     p2rb.base_commit: 93d9429b41ecf0d2ad8c62b6ea26686dd20330f4
     p2rb.commits: '[["eeb6d49dcb0950d771959358f662cf2e5ddc9dc1", 3], ["607f375f35c0866a8e08bc1d6aaecc6ad259ed6e",
       4], ["81ee86efd38ff60717aeeeff153292e84e58be0b", 5]]'
     p2rb.discard_on_publish_rids: '[5]'
     p2rb.first_public_ancestor: 93d9429b41ecf0d2ad8c62b6ea26686dd20330f4
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: true
     p2rb.unpublished_rids: '[]'
+  flags: []
   diffs:
   - id: 1
     revision: 1
     base_commit_id: 93d9429b41ecf0d2ad8c62b6ea26686dd20330f4
     name: diff
     extra: {}
     patch:
     - diff --git a/foo1 b/foo1
--- a/hgext/reviewboard/tests/test-commits-deleted-obsolescence.t
+++ b/hgext/reviewboard/tests/test-commits-deleted-obsolescence.t
@@ -135,16 +135,17 @@ on publish.
     p2rb.commits: '[["0b3e14fe3ff19019110705e72dcf563c0ef551f6", 2], ["bce658a3f6d6aa04bf5c449e0e779e839de4690e",
       3], ["713878e22d952d478e88bfdef897fdfc73060351", 4], ["4d0f846364eb509a1b6ae3294f05439101f6e7d3",
       5], ["4e50148c492dde95397cd666f2d4e4ad4fd2176f", 6]]'
     p2rb.discard_on_publish_rids: '[6]'
     p2rb.first_public_ancestor: 93d9429b41ecf0d2ad8c62b6ea26686dd20330f4
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: true
     p2rb.unpublished_rids: '[]'
+  flags: []
   diffs:
   - id: 1
     revision: 1
     base_commit_id: 93d9429b41ecf0d2ad8c62b6ea26686dd20330f4
     name: diff
     extra: {}
     patch:
     - diff --git a/foo1 b/foo1
@@ -258,16 +259,17 @@ The parent review should have dropped th
     p2rb.commits: '[["0b3e14fe3ff19019110705e72dcf563c0ef551f6", 2], ["bce658a3f6d6aa04bf5c449e0e779e839de4690e",
       3], ["713878e22d952d478e88bfdef897fdfc73060351", 4], ["4d0f846364eb509a1b6ae3294f05439101f6e7d3",
       5]]'
     p2rb.discard_on_publish_rids: '[]'
     p2rb.first_public_ancestor: 93d9429b41ecf0d2ad8c62b6ea26686dd20330f4
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: true
     p2rb.unpublished_rids: '[]'
+  flags: []
   diffs:
   - id: 1
     revision: 1
     base_commit_id: 93d9429b41ecf0d2ad8c62b6ea26686dd20330f4
     name: diff
     extra: {}
     patch:
     - diff --git a/foo1 b/foo1
@@ -355,16 +357,17 @@ Review 6 should be marked as discarded
     calculated_trophies: true
   commit_extra_data:
     p2rb: true
     p2rb.author: test
     p2rb.commit_id: 4e50148c492dde95397cd666f2d4e4ad4fd2176f
     p2rb.first_public_ancestor: 93d9429b41ecf0d2ad8c62b6ea26686dd20330f4
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: false
+  flags: []
   diffs:
   - id: 6
     revision: 1
     base_commit_id: 4d0f846364eb509a1b6ae3294f05439101f6e7d3
     name: diff
     extra: {}
     patch:
     - diff --git a/foo5 b/foo5
@@ -432,16 +435,17 @@ on publish.
     p2rb.commits: '[["0b3e14fe3ff19019110705e72dcf563c0ef551f6", 2], ["bce658a3f6d6aa04bf5c449e0e779e839de4690e",
       3], ["713878e22d952d478e88bfdef897fdfc73060351", 4], ["4d0f846364eb509a1b6ae3294f05439101f6e7d3",
       5]]'
     p2rb.discard_on_publish_rids: '[2]'
     p2rb.first_public_ancestor: 93d9429b41ecf0d2ad8c62b6ea26686dd20330f4
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: true
     p2rb.unpublished_rids: '[]'
+  flags: []
   diffs:
   - id: 1
     revision: 1
     base_commit_id: 93d9429b41ecf0d2ad8c62b6ea26686dd20330f4
     name: diff
     extra: {}
     patch:
     - diff --git a/foo1 b/foo1
@@ -577,16 +581,17 @@ The dropped commit should now be discard
     calculated_trophies: true
   commit_extra_data:
     p2rb: true
     p2rb.author: test
     p2rb.commit_id: 0b3e14fe3ff19019110705e72dcf563c0ef551f6
     p2rb.first_public_ancestor: 93d9429b41ecf0d2ad8c62b6ea26686dd20330f4
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: false
+  flags: []
   diffs:
   - id: 2
     revision: 1
     base_commit_id: 93d9429b41ecf0d2ad8c62b6ea26686dd20330f4
     name: diff
     extra: {}
     patch:
     - diff --git a/foo1 b/foo1
@@ -649,16 +654,17 @@ The parent review should have been updat
     p2rb.base_commit: 93d9429b41ecf0d2ad8c62b6ea26686dd20330f4
     p2rb.commits: '[["eeb6d49dcb0950d771959358f662cf2e5ddc9dc1", 3], ["a27a94c54524d4331dec2f92f647067bfd6dfbd4",
       5]]'
     p2rb.discard_on_publish_rids: '[]'
     p2rb.first_public_ancestor: 93d9429b41ecf0d2ad8c62b6ea26686dd20330f4
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: true
     p2rb.unpublished_rids: '[]'
+  flags: []
   diffs:
   - id: 1
     revision: 1
     base_commit_id: 93d9429b41ecf0d2ad8c62b6ea26686dd20330f4
     name: diff
     extra: {}
     patch:
     - diff --git a/foo1 b/foo1
@@ -820,16 +826,17 @@ because the new commit is logically diff
     p2rb.base_commit: 93d9429b41ecf0d2ad8c62b6ea26686dd20330f4
     p2rb.commits: '[["eeb6d49dcb0950d771959358f662cf2e5ddc9dc1", 3], ["a27a94c54524d4331dec2f92f647067bfd6dfbd4",
       5]]'
     p2rb.discard_on_publish_rids: '[5]'
     p2rb.first_public_ancestor: 93d9429b41ecf0d2ad8c62b6ea26686dd20330f4
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: true
     p2rb.unpublished_rids: '[7]'
+  flags: []
   diffs:
   - id: 1
     revision: 1
     base_commit_id: 93d9429b41ecf0d2ad8c62b6ea26686dd20330f4
     name: diff
     extra: {}
     patch:
     - diff --git a/foo1 b/foo1
@@ -1001,16 +1008,17 @@ Review request 5 (whose commit was delet
     calculated_trophies: true
   commit_extra_data:
     p2rb: true
     p2rb.author: test
     p2rb.commit_id: a27a94c54524d4331dec2f92f647067bfd6dfbd4
     p2rb.first_public_ancestor: 93d9429b41ecf0d2ad8c62b6ea26686dd20330f4
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: false
+  flags: []
   diffs:
   - id: 5
     revision: 1
     base_commit_id: 713878e22d952d478e88bfdef897fdfc73060351
     name: diff
     extra: {}
     patch:
     - diff --git a/foo4 b/foo4
@@ -1059,16 +1067,17 @@ Review request 5 (whose commit was delet
   summary: ''
   description: ''
   target_people: []
   extra_data: {}
   commit_extra_data:
     p2rb: true
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: false
+  flags: []
   diffs: []
   approved: false
   approval_failure: The review request is not public.
   draft:
     bugs:
     - '1'
     commit: null
     summary: Bug 1 - Foo 6
--- a/hgext/reviewboard/tests/test-operation-prevention.t
+++ b/hgext/reviewboard/tests/test-operation-prevention.t
@@ -50,16 +50,17 @@ Publishing the parent should succeed.
     p2rb: true
     p2rb.base_commit: 3a9f6899ef84c99841f546030b036d0124a863cf
     p2rb.commits: '[["4f4c73d9c6594a0a800a82758ceb6fb12a6b9f83", 2]]'
     p2rb.discard_on_publish_rids: '[]'
     p2rb.first_public_ancestor: 3a9f6899ef84c99841f546030b036d0124a863cf
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: true
     p2rb.unpublished_rids: '[]'
+  flags: []
   diffs:
   - id: 1
     revision: 1
     base_commit_id: 3a9f6899ef84c99841f546030b036d0124a863cf
     name: diff
     extra: {}
     patch:
     - diff --git a/foo b/foo
@@ -90,16 +91,17 @@ Publishing the parent should succeed.
     calculated_trophies: true
   commit_extra_data:
     p2rb: true
     p2rb.author: test
     p2rb.commit_id: 4f4c73d9c6594a0a800a82758ceb6fb12a6b9f83
     p2rb.first_public_ancestor: 3a9f6899ef84c99841f546030b036d0124a863cf
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: false
+  flags: []
   diffs:
   - id: 2
     revision: 1
     base_commit_id: 3a9f6899ef84c99841f546030b036d0124a863cf
     name: diff
     extra: {}
     patch:
     - diff --git a/foo b/foo
--- a/hgext/reviewboard/tests/test-push-http.t
+++ b/hgext/reviewboard/tests/test-push-http.t
@@ -375,16 +375,17 @@ Test creating a review via HTTP
     p2rb.base_commit: 8d7f5c4152d8f67d67500d3b92903e365c0122f1
     p2rb.commits: '[["9d326020e0dcd3e421680e4b78bf80c9e30df0e6", 2], ["c6548fe145857055779b23d94ef3f911e8d261b0",
       3]]'
     p2rb.discard_on_publish_rids: '[]'
     p2rb.first_public_ancestor: 8d7f5c4152d8f67d67500d3b92903e365c0122f1
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: true
     p2rb.unpublished_rids: '[]'
+  flags: []
   diffs:
   - id: 1
     revision: 1
     base_commit_id: 8d7f5c4152d8f67d67500d3b92903e365c0122f1
     name: diff
     extra: {}
     patch:
     - diff --git a/foo b/foo
--- a/hgext/reviewboard/tests/test-review-request-approval.t
+++ b/hgext/reviewboard/tests/test-review-request-approval.t
@@ -60,16 +60,37 @@ Create a review request from an L1 user
     calculated_trophies: true
   commit_extra_data:
     p2rb: true
     p2rb.author: test
     p2rb.commit_id: 4f4c73d9c6594a0a800a82758ceb6fb12a6b9f83
     p2rb.first_public_ancestor: 3a9f6899ef84c99841f546030b036d0124a863cf
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: false
+  flags:
+  - id: 1
+    type: review
+    value: '?'
+    review_request: 2
+    setter: level1a
+    requestee: level3
+    review: ''
+    review_extra_data_flag: ''
+    change_description: ''
+    timestamp: '*' (glob)
+  - id: 2
+    type: review
+    value: '?'
+    review_request: 2
+    setter: level1a
+    requestee: level1b
+    review: ''
+    review_extra_data_flag: ''
+    change_description: ''
+    timestamp: '*' (glob)
   diffs:
   - id: 2
     revision: 1
     base_commit_id: 3a9f6899ef84c99841f546030b036d0124a863cf
     name: diff
     extra: {}
     patch:
     - diff --git a/foo b/foo
@@ -107,16 +128,37 @@ Have an L1 user provide a r+ review whic
     calculated_trophies: true
   commit_extra_data:
     p2rb: true
     p2rb.author: test
     p2rb.commit_id: 4f4c73d9c6594a0a800a82758ceb6fb12a6b9f83
     p2rb.first_public_ancestor: 3a9f6899ef84c99841f546030b036d0124a863cf
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: false
+  flags:
+  - id: 1
+    type: review
+    value: '?'
+    review_request: 2
+    setter: level1a
+    requestee: level3
+    review: ''
+    review_extra_data_flag: ''
+    change_description: ''
+    timestamp: '*' (glob)
+  - id: 2
+    type: review
+    value: +
+    review_request: 2
+    setter: level1b
+    requestee: level1b
+    review: 1
+    review_extra_data_flag: r+
+    change_description: ''
+    timestamp: '*' (glob)
   diffs:
   - id: 2
     revision: 1
     base_commit_id: 3a9f6899ef84c99841f546030b036d0124a863cf
     name: diff
     extra: {}
     patch:
     - diff --git a/foo b/foo
@@ -164,16 +206,37 @@ Have an L3 user provide a r+ review whic
     calculated_trophies: true
   commit_extra_data:
     p2rb: true
     p2rb.author: test
     p2rb.commit_id: 4f4c73d9c6594a0a800a82758ceb6fb12a6b9f83
     p2rb.first_public_ancestor: 3a9f6899ef84c99841f546030b036d0124a863cf
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: false
+  flags:
+  - id: 1
+    type: review
+    value: +
+    review_request: 2
+    setter: level3
+    requestee: level3
+    review: 2
+    review_extra_data_flag: r+
+    change_description: ''
+    timestamp: '*' (glob)
+  - id: 2
+    type: review
+    value: +
+    review_request: 2
+    setter: level1b
+    requestee: level1b
+    review: 1
+    review_extra_data_flag: r+
+    change_description: ''
+    timestamp: '*' (glob)
   diffs:
   - id: 2
     revision: 1
     base_commit_id: 3a9f6899ef84c99841f546030b036d0124a863cf
     name: diff
     extra: {}
     patch:
     - diff --git a/foo b/foo
@@ -227,16 +290,37 @@ Posting a new review without r+ should c
     calculated_trophies: true
   commit_extra_data:
     p2rb: true
     p2rb.author: test
     p2rb.commit_id: 4f4c73d9c6594a0a800a82758ceb6fb12a6b9f83
     p2rb.first_public_ancestor: 3a9f6899ef84c99841f546030b036d0124a863cf
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: false
+  flags:
+  - id: 1
+    type: review
+    value: '-'
+    review_request: 2
+    setter: level3
+    requestee: level3
+    review: 3
+    review_extra_data_flag: r-
+    change_description: ''
+    timestamp: '*' (glob)
+  - id: 2
+    type: review
+    value: +
+    review_request: 2
+    setter: level1b
+    requestee: level1b
+    review: 1
+    review_extra_data_flag: r+
+    change_description: ''
+    timestamp: '*' (glob)
   diffs:
   - id: 2
     revision: 1
     base_commit_id: 3a9f6899ef84c99841f546030b036d0124a863cf
     name: diff
     extra: {}
     patch:
     - diff --git a/foo b/foo
@@ -299,16 +383,37 @@ One more r+ should switch it back to app
     calculated_trophies: true
   commit_extra_data:
     p2rb: true
     p2rb.author: test
     p2rb.commit_id: 4f4c73d9c6594a0a800a82758ceb6fb12a6b9f83
     p2rb.first_public_ancestor: 3a9f6899ef84c99841f546030b036d0124a863cf
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: false
+  flags:
+  - id: 1
+    type: review
+    value: +
+    review_request: 2
+    setter: level3
+    requestee: level3
+    review: 4
+    review_extra_data_flag: r+
+    change_description: ''
+    timestamp: '*' (glob)
+  - id: 2
+    type: review
+    value: +
+    review_request: 2
+    setter: level1b
+    requestee: level1b
+    review: 1
+    review_extra_data_flag: r+
+    change_description: ''
+    timestamp: '*' (glob)
   diffs:
   - id: 2
     revision: 1
     base_commit_id: 3a9f6899ef84c99841f546030b036d0124a863cf
     name: diff
     extra: {}
     patch:
     - diff --git a/foo b/foo
@@ -381,16 +486,37 @@ Even though the author is L1, adding a n
     calculated_trophies: true
   commit_extra_data:
     p2rb: true
     p2rb.author: test
     p2rb.commit_id: f867b363f9fd58135c77672e3c34f222f16ff677
     p2rb.first_public_ancestor: 3a9f6899ef84c99841f546030b036d0124a863cf
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: false
+  flags:
+  - id: 1
+    type: review
+    value: '?'
+    review_request: 2
+    setter: level1a
+    requestee: level3
+    review: ''
+    review_extra_data_flag: ''
+    change_description: ''
+    timestamp: '*' (glob)
+  - id: 2
+    type: review
+    value: '?'
+    review_request: 2
+    setter: level1a
+    requestee: level1b
+    review: ''
+    review_extra_data_flag: ''
+    change_description: ''
+    timestamp: '*' (glob)
   diffs:
   - id: 2
     revision: 1
     base_commit_id: 3a9f6899ef84c99841f546030b036d0124a863cf
     name: diff
     extra: {}
     patch:
     - diff --git a/foo b/foo
@@ -474,16 +600,37 @@ A new r+ from L3 should give approval
     calculated_trophies: true
   commit_extra_data:
     p2rb: true
     p2rb.author: test
     p2rb.commit_id: f867b363f9fd58135c77672e3c34f222f16ff677
     p2rb.first_public_ancestor: 3a9f6899ef84c99841f546030b036d0124a863cf
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: false
+  flags:
+  - id: 1
+    type: review
+    value: +
+    review_request: 2
+    setter: level3
+    requestee: level3
+    review: 5
+    review_extra_data_flag: r+
+    change_description: ''
+    timestamp: '*' (glob)
+  - id: 2
+    type: review
+    value: '?'
+    review_request: 2
+    setter: level1a
+    requestee: level1b
+    review: ''
+    review_extra_data_flag: ''
+    change_description: ''
+    timestamp: '*' (glob)
   diffs:
   - id: 2
     revision: 1
     base_commit_id: 3a9f6899ef84c99841f546030b036d0124a863cf
     name: diff
     extra: {}
     patch:
     - diff --git a/foo b/foo
@@ -580,16 +727,37 @@ Opening issues, even from an L1 user, sh
     calculated_trophies: true
   commit_extra_data:
     p2rb: true
     p2rb.author: test
     p2rb.commit_id: f867b363f9fd58135c77672e3c34f222f16ff677
     p2rb.first_public_ancestor: 3a9f6899ef84c99841f546030b036d0124a863cf
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: false
+  flags:
+  - id: 1
+    type: review
+    value: +
+    review_request: 2
+    setter: level3
+    requestee: level3
+    review: 5
+    review_extra_data_flag: r+
+    change_description: ''
+    timestamp: '*' (glob)
+  - id: 2
+    type: review
+    value: '-'
+    review_request: 2
+    setter: level1b
+    requestee: level1b
+    review: 6
+    review_extra_data_flag: r-
+    change_description: ''
+    timestamp: '*' (glob)
   diffs:
   - id: 2
     revision: 1
     base_commit_id: 3a9f6899ef84c99841f546030b036d0124a863cf
     name: diff
     extra: {}
     patch:
     - diff --git a/foo b/foo
@@ -701,16 +869,37 @@ Fixing the issue should restore approval
     calculated_trophies: true
   commit_extra_data:
     p2rb: true
     p2rb.author: test
     p2rb.commit_id: f867b363f9fd58135c77672e3c34f222f16ff677
     p2rb.first_public_ancestor: 3a9f6899ef84c99841f546030b036d0124a863cf
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: false
+  flags:
+  - id: 1
+    type: review
+    value: +
+    review_request: 2
+    setter: level3
+    requestee: level3
+    review: 5
+    review_extra_data_flag: r+
+    change_description: ''
+    timestamp: '*' (glob)
+  - id: 2
+    type: review
+    value: '-'
+    review_request: 2
+    setter: level1b
+    requestee: level1b
+    review: 6
+    review_extra_data_flag: r-
+    change_description: ''
+    timestamp: '*' (glob)
   diffs:
   - id: 2
     revision: 1
     base_commit_id: 3a9f6899ef84c99841f546030b036d0124a863cf
     name: diff
     extra: {}
     patch:
     - diff --git a/foo b/foo
@@ -830,16 +1019,37 @@ Review requests created by L3 users
     calculated_trophies: true
   commit_extra_data:
     p2rb: true
     p2rb.author: test
     p2rb.commit_id: b366ef9913208b4030857319aa20520f229a74f3
     p2rb.first_public_ancestor: 3a9f6899ef84c99841f546030b036d0124a863cf
     p2rb.identifier: bz://2/mynick
     p2rb.is_squashed: false
+  flags:
+  - id: 3
+    type: review
+    value: '?'
+    review_request: 4
+    setter: level3
+    requestee: level1a
+    review: ''
+    review_extra_data_flag: ''
+    change_description: ''
+    timestamp: '*' (glob)
+  - id: 4
+    type: review
+    value: '?'
+    review_request: 4
+    setter: level3
+    requestee: level1b
+    review: ''
+    review_extra_data_flag: ''
+    change_description: ''
+    timestamp: '*' (glob)
   diffs:
   - id: 6
     revision: 1
     base_commit_id: 3a9f6899ef84c99841f546030b036d0124a863cf
     name: diff
     extra: {}
     patch:
     - diff --git a/foo b/foo
@@ -877,16 +1087,37 @@ Even a ship-it from an L1 user will give
     calculated_trophies: true
   commit_extra_data:
     p2rb: true
     p2rb.author: test
     p2rb.commit_id: b366ef9913208b4030857319aa20520f229a74f3
     p2rb.first_public_ancestor: 3a9f6899ef84c99841f546030b036d0124a863cf
     p2rb.identifier: bz://2/mynick
     p2rb.is_squashed: false
+  flags:
+  - id: 3
+    type: review
+    value: +
+    review_request: 4
+    setter: level1a
+    requestee: level1a
+    review: 7
+    review_extra_data_flag: r+
+    change_description: ''
+    timestamp: '*' (glob)
+  - id: 4
+    type: review
+    value: '?'
+    review_request: 4
+    setter: level3
+    requestee: level1b
+    review: ''
+    review_extra_data_flag: ''
+    change_description: ''
+    timestamp: '*' (glob)
   diffs:
   - id: 6
     revision: 1
     base_commit_id: 3a9f6899ef84c99841f546030b036d0124a863cf
     name: diff
     extra: {}
     patch:
     - diff --git a/foo b/foo
@@ -937,16 +1168,37 @@ ship-its. Posting a new diff should not 
     calculated_trophies: true
   commit_extra_data:
     p2rb: true
     p2rb.author: test
     p2rb.commit_id: bedcf57f515ad540f582962e37ecd424d82424fd
     p2rb.first_public_ancestor: 3a9f6899ef84c99841f546030b036d0124a863cf
     p2rb.identifier: bz://2/mynick
     p2rb.is_squashed: false
+  flags:
+  - id: 3
+    type: review
+    value: '?'
+    review_request: 4
+    setter: level3
+    requestee: level1a
+    review: ''
+    review_extra_data_flag: ''
+    change_description: ''
+    timestamp: '*' (glob)
+  - id: 4
+    type: review
+    value: '?'
+    review_request: 4
+    setter: level3
+    requestee: level1b
+    review: ''
+    review_extra_data_flag: ''
+    change_description: ''
+    timestamp: '*' (glob)
   diffs:
   - id: 6
     revision: 1
     base_commit_id: 3a9f6899ef84c99841f546030b036d0124a863cf
     name: diff
     extra: {}
     patch:
     - diff --git a/foo b/foo
--- a/hgext/reviewboard/tests/test-review-request-closed-discarded.t
+++ b/hgext/reviewboard/tests/test-review-request-closed-discarded.t
@@ -133,16 +133,17 @@ no Commit ID set.
     p2rb.base_commit: 7c5bdf0cec4a90edb36300f8f3679857f46db829
     p2rb.commits: '[["98467d80785ec84dd871f213c167ed704a6d974d", 2], ["3a446ae4382006c43cdfa5aa33c494f582736f35",
       3]]'
     p2rb.discard_on_publish_rids: '[]'
     p2rb.first_public_ancestor: 7c5bdf0cec4a90edb36300f8f3679857f46db829
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: true
     p2rb.unpublished_rids: '[]'
+  flags: []
   diffs:
   - id: 1
     revision: 1
     base_commit_id: 7c5bdf0cec4a90edb36300f8f3679857f46db829
     name: diff
     extra: {}
     patch:
     - diff --git a/foo b/foo
@@ -175,16 +176,17 @@ Child review request with ID 2 should be
     calculated_trophies: true
   commit_extra_data:
     p2rb: true
     p2rb.author: test
     p2rb.commit_id: 98467d80785ec84dd871f213c167ed704a6d974d
     p2rb.first_public_ancestor: 7c5bdf0cec4a90edb36300f8f3679857f46db829
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: false
+  flags: []
   diffs:
   - id: 2
     revision: 1
     base_commit_id: 7c5bdf0cec4a90edb36300f8f3679857f46db829
     name: diff
     extra: {}
     patch:
     - diff --git a/foo b/foo
@@ -217,16 +219,17 @@ Child review request with ID 3 should be
     calculated_trophies: true
   commit_extra_data:
     p2rb: true
     p2rb.author: test
     p2rb.commit_id: 3a446ae4382006c43cdfa5aa33c494f582736f35
     p2rb.first_public_ancestor: 7c5bdf0cec4a90edb36300f8f3679857f46db829
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: false
+  flags: []
   diffs:
   - id: 3
     revision: 1
     base_commit_id: 98467d80785ec84dd871f213c167ed704a6d974d
     name: diff
     extra: {}
     patch:
     - diff --git a/foo b/foo
@@ -326,16 +329,17 @@ Commit ID re-instated.
     p2rb.base_commit: 7c5bdf0cec4a90edb36300f8f3679857f46db829
     p2rb.commits: '[["98467d80785ec84dd871f213c167ed704a6d974d", 2], ["3a446ae4382006c43cdfa5aa33c494f582736f35",
       3]]'
     p2rb.discard_on_publish_rids: '[]'
     p2rb.first_public_ancestor: 7c5bdf0cec4a90edb36300f8f3679857f46db829
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: true
     p2rb.unpublished_rids: '[]'
+  flags: []
   diffs:
   - id: 1
     revision: 1
     base_commit_id: 7c5bdf0cec4a90edb36300f8f3679857f46db829
     name: diff
     extra: {}
     patch:
     - diff --git a/foo b/foo
@@ -389,16 +393,17 @@ Child review request with ID 2 should be
     calculated_trophies: true
   commit_extra_data:
     p2rb: true
     p2rb.author: test
     p2rb.commit_id: 98467d80785ec84dd871f213c167ed704a6d974d
     p2rb.first_public_ancestor: 7c5bdf0cec4a90edb36300f8f3679857f46db829
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: false
+  flags: []
   diffs:
   - id: 2
     revision: 1
     base_commit_id: 7c5bdf0cec4a90edb36300f8f3679857f46db829
     name: diff
     extra: {}
     patch:
     - diff --git a/foo b/foo
@@ -451,16 +456,17 @@ Child review request with ID 3 should be
     calculated_trophies: true
   commit_extra_data:
     p2rb: true
     p2rb.author: test
     p2rb.commit_id: 3a446ae4382006c43cdfa5aa33c494f582736f35
     p2rb.first_public_ancestor: 7c5bdf0cec4a90edb36300f8f3679857f46db829
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: false
+  flags: []
   diffs:
   - id: 3
     revision: 1
     base_commit_id: 98467d80785ec84dd871f213c167ed704a6d974d
     name: diff
     extra: {}
     patch:
     - diff --git a/foo b/foo
@@ -579,16 +585,17 @@ Squashed review request should be publis
     p2rb.base_commit: 7c5bdf0cec4a90edb36300f8f3679857f46db829
     p2rb.commits: '[["98467d80785ec84dd871f213c167ed704a6d974d", 2], ["3a446ae4382006c43cdfa5aa33c494f582736f35",
       3]]'
     p2rb.discard_on_publish_rids: '[]'
     p2rb.first_public_ancestor: 7c5bdf0cec4a90edb36300f8f3679857f46db829
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: true
     p2rb.unpublished_rids: '[]'
+  flags: []
   diffs:
   - id: 1
     revision: 1
     base_commit_id: 7c5bdf0cec4a90edb36300f8f3679857f46db829
     name: diff
     extra: {}
     patch:
     - diff --git a/foo b/foo
@@ -621,16 +628,17 @@ Child review request with ID 2 should be
     calculated_trophies: true
   commit_extra_data:
     p2rb: true
     p2rb.author: test
     p2rb.commit_id: 98467d80785ec84dd871f213c167ed704a6d974d
     p2rb.first_public_ancestor: 7c5bdf0cec4a90edb36300f8f3679857f46db829
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: false
+  flags: []
   diffs:
   - id: 2
     revision: 1
     base_commit_id: 7c5bdf0cec4a90edb36300f8f3679857f46db829
     name: diff
     extra: {}
     patch:
     - diff --git a/foo b/foo
@@ -663,16 +671,17 @@ Child review request with ID 3 should be
     calculated_trophies: true
   commit_extra_data:
     p2rb: true
     p2rb.author: test
     p2rb.commit_id: 3a446ae4382006c43cdfa5aa33c494f582736f35
     p2rb.first_public_ancestor: 7c5bdf0cec4a90edb36300f8f3679857f46db829
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: false
+  flags: []
   diffs:
   - id: 3
     revision: 1
     base_commit_id: 98467d80785ec84dd871f213c167ed704a6d974d
     name: diff
     extra: {}
     patch:
     - diff --git a/foo b/foo
@@ -800,16 +809,17 @@ Pushing to a discarded review series wil
     p2rb.base_commit: 7c5bdf0cec4a90edb36300f8f3679857f46db829
     p2rb.commits: '[["98467d80785ec84dd871f213c167ed704a6d974d", 2], ["3a446ae4382006c43cdfa5aa33c494f582736f35",
       3]]'
     p2rb.discard_on_publish_rids: '[]'
     p2rb.first_public_ancestor: 7c5bdf0cec4a90edb36300f8f3679857f46db829
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: true
     p2rb.unpublished_rids: '[]'
+  flags: []
   diffs:
   - id: 1
     revision: 1
     base_commit_id: 7c5bdf0cec4a90edb36300f8f3679857f46db829
     name: diff
     extra: {}
     patch:
     - diff --git a/foo b/foo
@@ -841,16 +851,17 @@ Pushing to a discarded review series wil
     p2rb.base_commit: 7c5bdf0cec4a90edb36300f8f3679857f46db829
     p2rb.commits: '[["98467d80785ec84dd871f213c167ed704a6d974d", 5], ["3a446ae4382006c43cdfa5aa33c494f582736f35",
       6], ["1ec9946fd47ff9b5cb07e9d9c8b4d393b688e01b", 7]]'
     p2rb.discard_on_publish_rids: '[]'
     p2rb.first_public_ancestor: 7c5bdf0cec4a90edb36300f8f3679857f46db829
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: true
     p2rb.unpublished_rids: '[]'
+  flags: []
   diffs:
   - id: 4
     revision: 1
     base_commit_id: 7c5bdf0cec4a90edb36300f8f3679857f46db829
     name: diff
     extra: {}
     patch:
     - diff --git a/foo b/foo
--- a/hgext/reviewboard/tests/test-review-request-closed-submitted.t
+++ b/hgext/reviewboard/tests/test-review-request-closed-submitted.t
@@ -75,16 +75,17 @@ Squashed review request with ID 1 should
     p2rb.base_commit: 7c5bdf0cec4a90edb36300f8f3679857f46db829
     p2rb.commits: '[["98467d80785ec84dd871f213c167ed704a6d974d", 2], ["3a446ae4382006c43cdfa5aa33c494f582736f35",
       3]]'
     p2rb.discard_on_publish_rids: '[]'
     p2rb.first_public_ancestor: 7c5bdf0cec4a90edb36300f8f3679857f46db829
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: true
     p2rb.unpublished_rids: '[]'
+  flags: []
   diffs:
   - id: 1
     revision: 1
     base_commit_id: 7c5bdf0cec4a90edb36300f8f3679857f46db829
     name: diff
     extra: {}
     patch:
     - diff --git a/foo b/foo
@@ -117,16 +118,17 @@ Child review request with ID 2 should be
     calculated_trophies: true
   commit_extra_data:
     p2rb: true
     p2rb.author: test
     p2rb.commit_id: 98467d80785ec84dd871f213c167ed704a6d974d
     p2rb.first_public_ancestor: 7c5bdf0cec4a90edb36300f8f3679857f46db829
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: false
+  flags: []
   diffs:
   - id: 2
     revision: 1
     base_commit_id: 7c5bdf0cec4a90edb36300f8f3679857f46db829
     name: diff
     extra: {}
     patch:
     - diff --git a/foo b/foo
@@ -157,16 +159,17 @@ Child review request with ID 2 should be
     calculated_trophies: true
   commit_extra_data:
     p2rb: true
     p2rb.author: test
     p2rb.commit_id: 3a446ae4382006c43cdfa5aa33c494f582736f35
     p2rb.first_public_ancestor: 7c5bdf0cec4a90edb36300f8f3679857f46db829
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: false
+  flags: []
   diffs:
   - id: 3
     revision: 1
     base_commit_id: 98467d80785ec84dd871f213c167ed704a6d974d
     name: diff
     extra: {}
     patch:
     - diff --git a/foo b/foo
@@ -224,16 +227,17 @@ Squashed review request with ID 1 should
     p2rb.base_commit: 7c5bdf0cec4a90edb36300f8f3679857f46db829
     p2rb.commits: '[["98467d80785ec84dd871f213c167ed704a6d974d", 2], ["3a446ae4382006c43cdfa5aa33c494f582736f35",
       3]]'
     p2rb.discard_on_publish_rids: '[]'
     p2rb.first_public_ancestor: 7c5bdf0cec4a90edb36300f8f3679857f46db829
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: true
     p2rb.unpublished_rids: '[]'
+  flags: []
   diffs:
   - id: 1
     revision: 1
     base_commit_id: 7c5bdf0cec4a90edb36300f8f3679857f46db829
     name: diff
     extra: {}
     patch:
     - diff --git a/foo b/foo
@@ -266,16 +270,17 @@ Child review request with ID 2 should be
     calculated_trophies: true
   commit_extra_data:
     p2rb: true
     p2rb.author: test
     p2rb.commit_id: 98467d80785ec84dd871f213c167ed704a6d974d
     p2rb.first_public_ancestor: 7c5bdf0cec4a90edb36300f8f3679857f46db829
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: false
+  flags: []
   diffs:
   - id: 2
     revision: 1
     base_commit_id: 7c5bdf0cec4a90edb36300f8f3679857f46db829
     name: diff
     extra: {}
     patch:
     - diff --git a/foo b/foo
@@ -308,16 +313,17 @@ Child review request with ID 3 should be
     calculated_trophies: true
   commit_extra_data:
     p2rb: true
     p2rb.author: test
     p2rb.commit_id: 3a446ae4382006c43cdfa5aa33c494f582736f35
     p2rb.first_public_ancestor: 7c5bdf0cec4a90edb36300f8f3679857f46db829
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: false
+  flags: []
   diffs:
   - id: 3
     revision: 1
     base_commit_id: 98467d80785ec84dd871f213c167ed704a6d974d
     name: diff
     extra: {}
     patch:
     - diff --git a/foo b/foo
--- a/hgext/reviewboard/tests/test-review-request-delegation.t
+++ b/hgext/reviewboard/tests/test-review-request-delegation.t
@@ -380,8 +380,13 @@ Test ensure-drafts
     - reviewer1
     reviewers_status:
       reviewer1:
         review_flag: r?
         ship_it: false
     diff:
       delete: 0
       insert: 1
+
+Cleanup
+
+  $ mozreview stop
+  stopped 9 containers
--- a/hgext/reviewboard/tests/test-review-request-delete-draft.t
+++ b/hgext/reviewboard/tests/test-review-request-delete-draft.t
@@ -84,16 +84,17 @@ We should have a disagreement between pu
     p2rb: true
     p2rb.base_commit: 3a9f6899ef84c99841f546030b036d0124a863cf
     p2rb.commits: '[["65e5c536f9cc5816ef28ebaff6a0db47b9af0fee", 2]]'
     p2rb.discard_on_publish_rids: '[]'
     p2rb.first_public_ancestor: 3a9f6899ef84c99841f546030b036d0124a863cf
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: true
     p2rb.unpublished_rids: '[]'
+  flags: []
   diffs:
   - id: 1
     revision: 1
     base_commit_id: 3a9f6899ef84c99841f546030b036d0124a863cf
     name: diff
     extra: {}
     patch:
     - diff --git a/foo b/foo
@@ -157,16 +158,17 @@ We should have a disagreement between pu
     calculated_trophies: true
   commit_extra_data:
     p2rb: true
     p2rb.author: test
     p2rb.commit_id: 65e5c536f9cc5816ef28ebaff6a0db47b9af0fee
     p2rb.first_public_ancestor: 3a9f6899ef84c99841f546030b036d0124a863cf
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: false
+  flags: []
   diffs:
   - id: 2
     revision: 1
     base_commit_id: 3a9f6899ef84c99841f546030b036d0124a863cf
     name: diff
     extra: {}
     patch:
     - diff --git a/foo b/foo
@@ -235,16 +237,17 @@ Discarding the parent review request dra
     p2rb: true
     p2rb.base_commit: 3a9f6899ef84c99841f546030b036d0124a863cf
     p2rb.commits: '[["65e5c536f9cc5816ef28ebaff6a0db47b9af0fee", 2]]'
     p2rb.discard_on_publish_rids: '[]'
     p2rb.first_public_ancestor: 3a9f6899ef84c99841f546030b036d0124a863cf
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: true
     p2rb.unpublished_rids: '[]'
+  flags: []
   diffs:
   - id: 1
     revision: 1
     base_commit_id: 3a9f6899ef84c99841f546030b036d0124a863cf
     name: diff
     extra: {}
     patch:
     - diff --git a/foo b/foo
@@ -275,16 +278,17 @@ Discarding the parent review request dra
     calculated_trophies: true
   commit_extra_data:
     p2rb: true
     p2rb.author: test
     p2rb.commit_id: 65e5c536f9cc5816ef28ebaff6a0db47b9af0fee
     p2rb.first_public_ancestor: 3a9f6899ef84c99841f546030b036d0124a863cf
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: false
+  flags: []
   diffs:
   - id: 2
     revision: 1
     base_commit_id: 3a9f6899ef84c99841f546030b036d0124a863cf
     name: diff
     extra: {}
     patch:
     - diff --git a/foo b/foo
new file mode 100644
--- /dev/null
+++ b/hgext/reviewboard/tests/test-reviewer-flags-in-db.t
@@ -0,0 +1,569 @@
+#require mozreviewdocker
+
+  $ . $TESTDIR/hgext/reviewboard/tests/helpers.sh
+  $ commonenv
+
+Enable obsolescence so we can test code paths which use it.
+
+  $ cat >> client/.hg/hgrc << EOF
+  > [experimental]
+  > evolution = all
+  > EOF
+
+Create an initial commit.
+
+  $ cd client
+  $ echo foo > foo
+  $ hg commit -A -m 'root commit'
+  adding foo
+  $ hg phase --public -r .
+
+Add some potential reviewers.
+
+  $ mozreview create-user romulus@example.com password 'Romulus :romulus'
+  Created user 6
+  $ mozreview create-user remus@example.com password 'Remus :remus'
+  Created user 7
+
+We create a user who has decided to capitalize their ircnick.
+
+  $ mozreview create-user ryanvm@example.com password 'Ryan :RyanVM'
+  Created user 8
+
+Create few draft review requests
+
+  $ bugzilla create-bug TestProduct TestComponent 'First Bug'
+  $ echo initial > foo
+  $ hg commit -m 'Bug 1 - some stuff; r?romulus'
+  $ echo blah >> foo
+  $ hg commit -m 'Bug 1 - More stuff; r?romulus, r?remus'
+
+  $ hg push --config reviewboard.autopublish=false
+  pushing to ssh://$DOCKER_HOSTNAME:$HGPORT6/test-repo
+  (adding commit id to 2 changesets)
+  searching for changes
+  remote: adding changesets
+  remote: adding manifests
+  remote: adding file changes
+  remote: added 3 changesets with 3 changes to 1 files
+  remote: recorded push in pushlog
+  submitting 2 changesets for review
+  
+  changeset:  3:7122d7b3a455
+  summary:    Bug 1 - some stuff; r?romulus
+  review:     http://$DOCKER_HOSTNAME:$HGPORT1/r/2 (draft)
+  
+  changeset:  4:1cac575c1bc7
+  summary:    Bug 1 - More stuff; r?romulus, r?remus
+  review:     http://$DOCKER_HOSTNAME:$HGPORT1/r/3 (draft)
+  
+  review id:  bz://1/mynick
+  review url: http://$DOCKER_HOSTNAME:$HGPORT1/r/1 (draft)
+  (visit review url to publish these review requests so others can see them)
+
+Flags are not created for draft review requests
+
+  $ hg push
+  pushing to ssh://$DOCKER_HOSTNAME:$HGPORT6/test-repo
+  searching for changes
+  no changes found
+  submitting 2 changesets for review
+  
+  changeset:  3:7122d7b3a455
+  summary:    Bug 1 - some stuff; r?romulus
+  review:     http://$DOCKER_HOSTNAME:$HGPORT1/r/2 (draft)
+  
+  changeset:  4:1cac575c1bc7
+  summary:    Bug 1 - More stuff; r?romulus, r?remus
+  review:     http://$DOCKER_HOSTNAME:$HGPORT1/r/3 (draft)
+  
+  review id:  bz://1/mynick
+  review url: http://$DOCKER_HOSTNAME:$HGPORT1/r/1 (draft)
+  
+  publish these review requests now (Yn)?  y
+  (published review request 1)
+  [1]
+
+No Flag should be created for parent review request
+
+  $ reviewboard dumpflags 1
+  review_request_id: 1
+  flags: []
+
+Flags for review requests 2 and 3 are saved on publishing
+
+  $ reviewboard dumpflags 2
+  review_request_id: 2
+  flags:
+  - id: 1
+    type: review
+    value: '?'
+    review_request: 2
+    setter: default+5
+    requestee: romulus
+    review: ''
+    review_extra_data_flag: ''
+    change_description: ''
+    timestamp: '*' (glob)
+
+  $ reviewboard dumpflags 3
+  review_request_id: 3
+  flags:
+  - id: 2
+    type: review
+    value: '?'
+    review_request: 3
+    setter: default+5
+    requestee: romulus
+    review: ''
+    review_extra_data_flag: ''
+    change_description: ''
+    timestamp: '*' (glob)
+  - id: 3
+    type: review
+    value: '?'
+    review_request: 3
+    setter: default+5
+    requestee: remus
+    review: ''
+    review_extra_data_flag: ''
+    change_description: ''
+    timestamp: '*' (glob)
+
+  $ echo blah >> foo
+  $ hg commit --amend -l - << EOF
+  > Bug 1 - Even more stuff; r?romulus, r?remus
+  > 
+  > MozReview-Commit-ID: hE3OiG
+  > EOF
+  $ hg push
+  pushing to ssh://$DOCKER_HOSTNAME:$HGPORT6/test-repo
+  searching for changes
+  remote: adding changesets
+  remote: adding manifests
+  remote: adding file changes
+  remote: added 1 changesets with 1 changes to 1 files (+1 heads)
+  remote: recorded push in pushlog
+  submitting 2 changesets for review
+  
+  changeset:  3:7122d7b3a455
+  summary:    Bug 1 - some stuff; r?romulus
+  review:     http://$DOCKER_HOSTNAME:$HGPORT1/r/2
+  
+  changeset:  6:f7778a69cf9c
+  summary:    Bug 1 - Even more stuff; r?romulus, r?remus
+  review:     http://$DOCKER_HOSTNAME:$HGPORT1/r/3 (draft)
+  
+  review id:  bz://1/mynick
+  review url: http://$DOCKER_HOSTNAME:$HGPORT1/r/1 (draft)
+  
+  publish these review requests now (Yn)?  y
+  (published review request 1)
+
+These flags are already created.
+
+  $ reviewboard dumpflags 3
+  review_request_id: 3
+  flags:
+  - id: 2
+    type: review
+    value: '?'
+    review_request: 3
+    setter: default+5
+    requestee: romulus
+    review: ''
+    review_extra_data_flag: ''
+    change_description: ''
+    timestamp: '*' (glob)
+  - id: 3
+    type: review
+    value: '?'
+    review_request: 3
+    setter: default+5
+    requestee: remus
+    review: ''
+    review_extra_data_flag: ''
+    change_description: ''
+    timestamp: '*' (glob)
+
+A new flag is created when adding a reviewer
+  $ rbmanage add-reviewer 3 --user admin+1
+  3 people listed on review request
+  $ hg push
+  pushing to ssh://$DOCKER_HOSTNAME:$HGPORT6/test-repo
+  searching for changes
+  no changes found
+  submitting 2 changesets for review
+  
+  changeset:  3:7122d7b3a455
+  summary:    Bug 1 - some stuff; r?romulus
+  review:     http://$DOCKER_HOSTNAME:$HGPORT1/r/2
+  
+  changeset:  6:f7778a69cf9c
+  summary:    Bug 1 - Even more stuff; r?romulus, r?remus
+  review:     http://$DOCKER_HOSTNAME:$HGPORT1/r/3 (draft)
+  
+  review id:  bz://1/mynick
+  review url: http://$DOCKER_HOSTNAME:$HGPORT1/r/1 (draft)
+  
+  publish these review requests now (Yn)?  y
+  (published review request 1)
+  [1]
+
+  $ reviewboard dumpflags 3
+  review_request_id: 3
+  flags:
+  - id: 2
+    type: review
+    value: '?'
+    review_request: 3
+    setter: default+5
+    requestee: romulus
+    review: ''
+    review_extra_data_flag: ''
+    change_description: ''
+    timestamp: '*' (glob)
+  - id: 3
+    type: review
+    value: '?'
+    review_request: 3
+    setter: default+5
+    requestee: remus
+    review: ''
+    review_extra_data_flag: ''
+    change_description: ''
+    timestamp: '*' (glob)
+  - id: 4
+    type: review
+    value: '?'
+    review_request: 3
+    setter: default+5
+    requestee: admin+1
+    review: ''
+    review_extra_data_flag: ''
+    change_description: ''
+    timestamp: '*' (glob)
+
+Unrecognized reviewers are ignored
+
+  $ hg phase --public -r .
+  $ bugzilla create-bug TestProduct TestComponent 'Second Bug'
+  $ echo blah >> foo
+  $ hg commit -m 'Bug 2 - different stuff; r?cthulhu'
+  $ hg push
+  pushing to ssh://$DOCKER_HOSTNAME:$HGPORT6/test-repo
+  searching for changes
+  remote: adding changesets
+  remote: adding manifests
+  remote: adding file changes
+  remote: added 1 changesets with 1 changes to 1 files
+  remote: recorded push in pushlog
+  submitting 1 changesets for review
+  unrecognized reviewer: cthulhu
+  
+  changeset:  7:b5fe24c33ddf
+  summary:    Bug 2 - different stuff; r?cthulhu
+  review:     http://$DOCKER_HOSTNAME:$HGPORT1/r/5 (draft)
+  
+  review id:  bz://2/mynick
+  review url: http://$DOCKER_HOSTNAME:$HGPORT1/r/4 (draft)
+  
+  (review requests lack reviewers; visit review url to assign reviewers)
+  
+  publish these review requests now (Yn)?  y
+  (published review request 4)
+
+  $ reviewboard dumpflags 5
+  review_request_id: 5
+  flags: []
+
+
+Reviewer identification is case insensitive.
+
+  $ echo blah >> foo
+  $ hg commit -m 'Bug 2 - better stuff; r?remus r?ryanvm'
+  $ hg push
+  pushing to ssh://$DOCKER_HOSTNAME:$HGPORT6/test-repo
+  searching for changes
+  remote: adding changesets
+  remote: adding manifests
+  remote: adding file changes
+  remote: added 1 changesets with 1 changes to 1 files
+  remote: recorded push in pushlog
+  submitting 2 changesets for review
+  
+  changeset:  7:b5fe24c33ddf
+  summary:    Bug 2 - different stuff; r?cthulhu
+  review:     http://$DOCKER_HOSTNAME:$HGPORT1/r/5
+  
+  changeset:  8:c8a58a1d5f9f
+  summary:    Bug 2 - better stuff; r?remus r?ryanvm
+  review:     http://$DOCKER_HOSTNAME:$HGPORT1/r/6 (draft)
+  
+  review id:  bz://2/mynick
+  review url: http://$DOCKER_HOSTNAME:$HGPORT1/r/4 (draft)
+  
+  (review requests lack reviewers; visit review url to assign reviewers)
+  
+  publish these review requests now (Yn)?  y
+  (published review request 4)
+
+  $ reviewboard dumpflags 6
+  review_request_id: 6
+  flags:
+  - id: 5
+    type: review
+    value: '?'
+    review_request: 6
+    setter: default+5
+    requestee: remus
+    review: ''
+    review_extra_data_flag: ''
+    change_description: ''
+    timestamp: '*' (glob)
+  - id: 6
+    type: review
+    value: '?'
+    review_request: 6
+    setter: default+5
+    requestee: RyanVM
+    review: ''
+    review_extra_data_flag: ''
+    change_description: ''
+    timestamp: '*' (glob)
+
+Removing reviewer who hasn't created a review is deleting related flags
+
+  $ rbmanage remove-reviewer 6 --user remus
+  1 people listed on review request
+  $ rbmanage publish 4
+
+  $ rbmanage list-reviewers 6
+  RyanVM
+
+  $ reviewboard dumpflags 6
+  review_request_id: 6
+  flags:
+  - id: 6
+    type: review
+    value: '?'
+    review_request: 6
+    setter: default+5
+    requestee: RyanVM
+    review: ''
+    review_extra_data_flag: ''
+    change_description: ''
+    timestamp: '*' (glob)
+
+Review with r- should update the flag so setter and requestee is the same user 
+
+  $ exportbzauth ryanvm@example.com password
+  $ rbmanage create-review 6 --body-top "No way" --public --review-flag='r-'
+  created review 1
+  $ exportbzauth default@example.com password
+  $ reviewboard dumpflags 6
+  review_request_id: 6
+  flags:
+  - id: 6
+    type: review
+    value: '-'
+    review_request: 6
+    setter: RyanVM
+    requestee: RyanVM
+    review: 1
+    review_extra_data_flag: r-
+    change_description: ''
+    timestamp: '*' (glob)
+
+
+Review with r+
+
+  $ exportbzauth ryanvm@example.com password
+  $ rbmanage create-review 6 --body-top "Ship It!" --public --review-flag='r+'
+  created review 2
+  $ exportbzauth default@example.com password
+  $ reviewboard dumpflags 6
+  review_request_id: 6
+  flags:
+  - id: 6
+    type: review
+    value: +
+    review_request: 6
+    setter: RyanVM
+    requestee: RyanVM
+    review: 2
+    review_extra_data_flag: r+
+    change_description: ''
+    timestamp: '*' (glob)
+
+No flag should be created when an impropmptu review is added with empty status
+
+  $ echo blahblah > foo
+  $ hg commit -m 'Bug 2 - new commit'
+  $ hg push
+  pushing to ssh://$DOCKER_HOSTNAME:$HGPORT6/test-repo
+  searching for changes
+  remote: adding changesets
+  remote: adding manifests
+  remote: adding file changes
+  remote: added 1 changesets with 1 changes to 1 files
+  remote: recorded push in pushlog
+  submitting 3 changesets for review
+  
+  changeset:  7:b5fe24c33ddf
+  summary:    Bug 2 - different stuff; r?cthulhu
+  review:     http://$DOCKER_HOSTNAME:$HGPORT1/r/5
+  
+  changeset:  8:c8a58a1d5f9f
+  summary:    Bug 2 - better stuff; r?remus r?ryanvm
+  review:     http://$DOCKER_HOSTNAME:$HGPORT1/r/6
+  
+  changeset:  9:96e466dfc0a4
+  summary:    Bug 2 - new commit
+  review:     http://$DOCKER_HOSTNAME:$HGPORT1/r/7 (draft)
+  
+  review id:  bz://2/mynick
+  review url: http://$DOCKER_HOSTNAME:$HGPORT1/r/4 (draft)
+  
+  (review requests lack reviewers; visit review url to assign reviewers)
+  
+  publish these review requests now (Yn)?  y
+  (published review request 4)
+  $ mozreview create-user level1@example.com password 'Level 1 User :level1' --uid 2003 --scm-level 1 --bugzilla-group editbugs
+  Created user 9
+  $ rbmanage associate-ldap-user level1 level1@example.com
+  level1@example.com associated with level1
+  $ exportbzauth level1@example.com password
+  $ rbmanage create-review 7 --body-top "Just a comment" --public
+  created review 3
+  $ exportbzauth default@example.com password
+  $ reviewboard dumpflags 7
+  review_request_id: 7
+  flags: []
+  $ reviewboard dumpreview 7
+  id: 7
+  status: pending
+  public: true
+  bugs:
+  - '2'
+  commit: null
+  submitter: default+5
+  summary: Bug 2 - new commit
+  description:
+  - Bug 2 - new commit
+  - ''
+  - 'MozReview-Commit-ID: OTOPw0'
+  target_people: []
+  extra_data:
+    calculated_trophies: true
+  commit_extra_data:
+    p2rb: true
+    p2rb.author: test
+    p2rb.commit_id: 96e466dfc0a48261e5c578decfc8c0408775e935
+    p2rb.first_public_ancestor: f7778a69cf9c89cb483b97a3333a8041d6aac497
+    p2rb.identifier: bz://2/mynick
+    p2rb.is_squashed: false
+  flags: []
+  diffs:
+  - id: 13
+    revision: 1
+    base_commit_id: c8a58a1d5f9f8a17a6892504f2d3b3179954e31a
+    name: diff
+    extra: {}
+    patch:
+    - diff --git a/foo b/foo
+    - '--- a/foo'
+    - +++ b/foo
+    - '@@ -1,5 +1,1 @@'
+    - -initial
+    - -blah
+    - -blah
+    - -blah
+    - -blah
+    - +blahblah
+    - ''
+  approved: false
+  approval_failure: A suitable reviewer has not given a "Ship It!"
+  review_count: 1
+  reviews:
+  - id: 3
+    public: true
+    ship_it: false
+    extra_data:
+      p2rb.review_flag: ''
+    body_top: Just a comment
+    body_top_text_type: plain
+    diff_comments: []
+
+Impromptu review can't have an 'r?' status
+
+  $ exportbzauth level1@example.com password
+  $ rbmanage create-review 7 --body-top "Ship it please" --public --review-flag='r?'
+  created review 4
+  $ exportbzauth default@example.com password
+  $ reviewboard dumpflags 7
+  review_request_id: 7
+  flags: []
+
+A flag should be created if adding an impromptu review with a status
+
+  $ exportbzauth level1@example.com password
+  $ rbmanage create-review 7 --body-top "Ship it please" --public --review-flag='r+'
+  created review 5
+  $ exportbzauth default@example.com password
+  $ reviewboard dumpflags 7
+  review_request_id: 7
+  flags:
+  - id: 7
+    type: review
+    value: +
+    review_request: 7
+    setter: level1
+    requestee: level1
+    review: 5
+    review_extra_data_flag: r+
+    change_description: ''
+    timestamp: '*' (glob)
+
+Adding a user who created an impromptu review to target_people does not change flags
+
+  $ rbmanage add-reviewer 7 --user level1
+  1 people listed on review request
+  $ rbmanage publish 4
+  $ reviewboard dumpflags 7
+  review_request_id: 7
+  flags:
+  - id: 7
+    type: review
+    value: +
+    review_request: 7
+    setter: level1
+    requestee: level1
+    review: 5
+    review_extra_data_flag: r+
+    change_description: ''
+    timestamp: '*' (glob)
+
+Removing a user who had a review with a flag set is only clearing the requestee field 
+
+  $ rbmanage remove-reviewer 7 --user level1
+  0 people listed on review request
+  $ rbmanage publish 4
+  $ reviewboard dumpflags 7
+  review_request_id: 7
+  flags:
+  - id: 7
+    type: review
+    value: +
+    review_request: 7
+    setter: level1
+    requestee: ''
+    review: 5
+    review_extra_data_flag: r+
+    change_description: ''
+    timestamp: '*' (glob)
+
+Cleanup
+
+  $ mozreview stop
+  stopped 9 containers
--- a/hgext/reviewboard/tests/test-reviewer-flags.t
+++ b/hgext/reviewboard/tests/test-reviewer-flags.t
@@ -82,17 +82,16 @@ There are no warnings for reviewers who 
   review url: http://$DOCKER_HOSTNAME:$HGPORT1/r/1 (draft)
   
   publish these review requests now (Yn)?  y
   (published review request 1)
 
   $ rbmanage list-reviewers 2
   cthulhu
 
-
 There are warnings for reviewers who haved granted a non ship-it review when
 using r=.
 
   $ exportbzauth cthulhu@example.com password
   $ rbmanage create-review 2 --body-top "No way you should ship-it!" --public --review-flag='r-'
   created review 2
 
   $ exportbzauth default@example.com password
--- a/hgext/reviewboard/tests/test-specify-reviewers.t
+++ b/hgext/reviewboard/tests/test-specify-reviewers.t
@@ -227,16 +227,37 @@ Publishing series during push works
     calculated_trophies: true
   commit_extra_data:
     p2rb: true
     p2rb.author: test
     p2rb.commit_id: 214fce3608426755a50ae60ae8645eb9bc1f7537
     p2rb.first_public_ancestor: 3a9f6899ef84c99841f546030b036d0124a863cf
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: false
+  flags:
+  - id: 14
+    type: review
+    value: '?'
+    review_request: 10
+    setter: default+5
+    requestee: romulus
+    review: ''
+    review_extra_data_flag: ''
+    change_description: ''
+    timestamp: '*' (glob)
+  - id: 15
+    type: review
+    value: '?'
+    review_request: 10
+    setter: default+5
+    requestee: remus
+    review: ''
+    review_extra_data_flag: ''
+    change_description: ''
+    timestamp: '*' (glob)
   diffs:
   - id: 10
     revision: 1
     base_commit_id: ccfcf9b70a65731d01240f24815edf0cf6b64739
     name: diff
     extra: {}
     patch:
     - diff --git a/foo b/foo
@@ -549,17 +570,16 @@ Unrecognized reviewers should be ignored
   review url: http://$DOCKER_HOSTNAME:$HGPORT1/r/12 (draft)
   
   (review requests lack reviewers; visit review url to assign reviewers)
   
   publish these review requests now (Yn)?  y
   (published review request 12)
   $ rbmanage list-reviewers 12
   
-
 Reviewer identification should be case insensitive.
 
   $ echo blah >> foo
   $ hg commit -m 'Bug 2 - better stuff; r?ryanvm'
   $ hg push -c 28
   pushing to ssh://$DOCKER_HOSTNAME:$HGPORT6/test-repo
   searching for changes
   remote: adding changesets
@@ -605,13 +625,12 @@ Reviewer deduction can be disabled with 
   
   (review requests lack reviewers; visit review url to assign reviewers)
   
   publish these review requests now (Yn)?  y
   (published review request 12)
 
   $ rbmanage list-reviewers 15
   
-
 Cleanup
 
   $ mozreview stop
   stopped 9 containers
--- a/hgext/reviewboard/tests/test-unicode.t
+++ b/hgext/reviewboard/tests/test-unicode.t
@@ -53,16 +53,17 @@ The globbing is patching over a bug in m
     calculated_trophies: true
   commit_extra_data:
     p2rb: true
     p2rb.author: test
     p2rb.commit_id: 86ab97a5dd61e8ec7ff3c23212db732e3531af01
     p2rb.first_public_ancestor: 3a9f6899ef84c99841f546030b036d0124a863cf
     p2rb.identifier: bz://1/mynick
     p2rb.is_squashed: false
+  flags: []
   diffs:
   - id: 2
     revision: 1
     base_commit_id: 3a9f6899ef84c99841f546030b036d0124a863cf
     name: diff
     extra: {}
     patch:
     - diff --git a/foo b/foo
@@ -176,16 +177,17 @@ Put some wonky byte sequences in the dif
   summary: ''
   description: ''
   target_people: []
   extra_data: {}
   commit_extra_data:
     p2rb: true
     p2rb.identifier: bz://2/mynick
     p2rb.is_squashed: false
+  flags: []
   diffs: []
   approved: false
   approval_failure: The review request is not public.
   draft:
     bugs:
     - '2'
     commit: null
     summary: Bug 2 - base
@@ -228,16 +230,17 @@ Put some wonky byte sequences in the dif
   summary: ''
   description: ''
   target_people: []
   extra_data: {}
   commit_extra_data:
     p2rb: true
     p2rb.identifier: bz://2/mynick
     p2rb.is_squashed: false
+  flags: []
   diffs: []
   approved: false
   approval_failure: The review request is not public.
   draft:
     bugs:
     - '2'
     commit: null
     summary: Bug 2 - tip
--- a/pylib/mozreview/mozreview/extension.py
+++ b/pylib/mozreview/mozreview/extension.py
@@ -88,16 +88,19 @@ from mozreview.resources.commit_data imp
     commit_data_resource,
 )
 from mozreview.resources.commit_rewrite import (
     commit_rewrite_resource,
 )
 from mozreview.resources.ensure_drafts import (
     ensure_drafts_resource,
 )
+from mozreview.resources.flag import (
+    flag_resource,
+)
 from mozreview.resources.modify_reviewer import (
     modify_reviewer_resource,
 )
 from mozreview.resources.review_request_summary import (
     review_request_summary_resource,
 )
 from mozreview.resources.verify_reviewer import (
     verify_reviewer_resource,
@@ -192,16 +195,17 @@ class MozReviewExtension(Extension):
         batch_review_request_resource,
         batch_review_resource,
         bugzilla_api_key_login_resource,
         commit_data_resource,
         commit_rewrite_resource,
         employee_ldap_association_resource,
         ensure_drafts_resource,
         file_diff_reviewer_resource,
+        flag_resource,
         ldap_association_resource,
         modify_reviewer_resource,
         review_request_summary_resource,
         try_autoland_trigger_resource,
         verify_reviewer_resource,
     ]
 
     middleware = [
new file mode 100644
--- /dev/null
+++ b/pylib/mozreview/mozreview/managers.py
@@ -0,0 +1,35 @@
+from django.db.models import Manager, Q
+from django.core.exceptions import ObjectDoesNotExist
+
+
+class MozReviewFlagTypeManager(Manager):
+    def get_review_type(self):
+        """Get or create MozReviewFlagType for review."""
+        try:
+            return self.get(name='review')
+        except ObjectDoesNotExist:
+            return self.create(
+                name='review',
+                short_name='r',
+                description='',
+                values_json='["+", "-", "?"]')
+
+
+class MozReviewFlagManager(Manager):
+    def common_filter(self, review_request=None, requestee=None,
+                      type_name='review'):
+        """Returns flags of given type and requestee for a request"""
+        q = Q(type__name=type_name)
+
+        if review_request:
+            q = q & Q(review_request__pk=review_request.pk)
+
+        if requestee:
+            q = q & Q(requestee__pk=requestee.pk)
+
+        # make sure the review exists
+        if type_name == 'review':
+            q = q & Q(review__pk__isnull=False,
+                      review__base_reply_to__isnull=True)
+
+        return self.filter(q).order_by('-timestamp')
--- a/pylib/mozreview/mozreview/models.py
+++ b/pylib/mozreview/mozreview/models.py
@@ -1,42 +1,53 @@
 from __future__ import unicode_literals
 
+import json
+
 from django.contrib.auth.models import User
+from django.core.exceptions import ValidationError
 from django.db import models
-from reviewboard.diffviewer.models import FileDiff
+from reviewboard.changedescs.models import ChangeDescription
 
 from mozreview.autoland.models import (
     AutolandEventLogEntry,
     AutolandRequest
 )
 from mozreview.bugzilla.models import (
     BugzillaUserMap,
     get_bugzilla_api_key,
     get_or_create_bugzilla_users,
     set_bugzilla_api_key,
     UnverifiedBugzillaApiKey
 )
 from mozreview.commits.models import (
     CommitData,
     DiffSetVerification,
 )
+from mozreview.file_diff_reviewer.models import FileDiffReviewer
 from mozreview.ldap import query_scm_group
-
-from mozreview.file_diff_reviewer.models import FileDiffReviewer
+from mozreview.managers import (
+    MozReviewFlagManager,
+    MozReviewFlagTypeManager,
+)
+from reviewboard.reviews.models import (
+    ReviewRequest,
+    Review,
+)
 
 __all__ = [
     'AutolandEventLogEntry',
     'AutolandRequest',
     'BugzillaUserMap',
     'CommitData',
     'DiffSetVerification',
     'FileDiffReviewer',
     'get_bugzilla_api_key',
     'get_or_create_bugzilla_users',
+    'MozReviewFlag',
     'MozReviewUserProfile',
     'set_bugzilla_api_key',
     'UnverifiedBugzillaApiKey',
 ]
 
 
 def get_profile(user):
     """Return the MozReviewUserProfile associated with a user.
@@ -60,17 +71,120 @@ class MozReviewUserProfile(models.Model)
 
     def has_scm_ldap_group(self, group):
         """Return True if the user is a member of the provided ldap group."""
         if not self.ldap_username:
             return False
 
         return query_scm_group(self.ldap_username, group)
 
-
     class Meta:
         app_label = 'mozreview'
         permissions = (
             ("modify_ldap_association",
              "Can change ldap assocation for all users"),
             ("enable_autoland",
              "Can enable or disable autoland for a repository"),
         )
+
+
+class MozReviewFlagType(models.Model):
+    """Flag Types to be used with class::MozReviewFlag"""
+    # i.e. "review"
+    name = models.CharField(max_length=200, unique=True)
+    # short_name is used for presentation only i.e. 'r'
+    short_name = models.CharField(max_length=4)
+    description = models.TextField(default='')
+    # Returns whether the flagtype is active or disabled.
+    # JSON string reprenting a list of allowed values ex. "['+', '-', '?']"
+    values_json = models.CharField(max_length=200)
+
+    class Meta:
+        verbose_name = 'flag type'
+        app_label = 'mozreview'
+
+    objects = MozReviewFlagTypeManager()
+
+    @property
+    def values(self):
+        return json.loads(self.values_json)
+
+    def get_value_from_status(self, value_name):
+        """Returns a value of the flag if its presentation name was given.
+
+        This is useful to strip flag value from extra_data.
+        For "r?" returns "?"
+        """
+        if not value_name.startswith(self.short_name):
+            raise ValueError('Status should start with the right short_name')
+
+        value = value_name.split(self.short_name, 1)[1]
+        if value not in self.values:
+            raise ValueError('Status (%s) is not allowed' % value_name)
+
+        return value
+
+    def __str__(self):
+        return self.name
+
+
+class MozReviewFlag(models.Model):
+    """MozReview Flag representation in database."""
+    type = models.ForeignKey(MozReviewFlagType)
+    # a value from MozReviewFlagType::values_json (+|-|?)
+    value = models.CharField(max_length=10)
+    review_request = models.ForeignKey(ReviewRequest, related_name='flags')
+    review = models.ForeignKey(Review, related_name='flags',
+                               blank=True, null=True)
+    change_description = models.ForeignKey(ChangeDescription,
+                                           related_name='flags',
+                                           blank=True, null=True)
+    # user who set the flag
+    setter = models.ForeignKey(User, related_name='flags_set')
+    # user who has been requested to set the flag
+    requestee = models.ForeignKey(User, default=None,
+                                  related_name='flags_requested',
+                                  blank=True, null=True)
+    timestamp = models.DateTimeField(auto_now=True)
+
+    class Meta:
+        verbose_name = 'flag'
+        app_label = 'mozreview'
+        get_latest_by = 'timestamp'
+
+    objects = MozReviewFlagManager()
+
+    @property
+    def type_name(self):
+        """Returns name of the related type
+
+        Used primarily in API to avoid creation of the resource for
+        class::MozReviewFlagType
+        """
+        return self.type.name
+
+    @staticmethod
+    def validate_value(value, type, review=None, requestee=None,
+                       target_people=[]):
+        """Validate the value.
+
+        It has to be a class method as we validate the flag value in
+        signal_handlers before object is instantiated.
+        """
+        if value not in type.values:
+            raise ValidationError("Value '%s' is not allowed." % value)
+
+        if (review and value == '?'
+                and requestee and requestee not in target_people):
+            raise ValidationError("Value '?' is not allowed for user "
+                                  "%s." % requestee)
+
+    def full_clean(self, *args, **kwargs):
+        """Check if value is within values allowed by the type"""
+        super(MozReviewFlag, self).full_clean(*args, **kwargs)
+        MozReviewFlag.validate_value(self.value, self.type, self.review,
+                                     self.requestee,
+                                     self.review_request.target_people.all())
+
+    def save(self, *args, **kwargs):
+        """Validate and save"""
+        self.full_clean()
+        return super(MozReviewFlag, self).save(*args, **kwargs)
new file mode 100644
--- /dev/null
+++ b/pylib/mozreview/mozreview/resources/flag.py
@@ -0,0 +1,78 @@
+from djblets.webapi.errors import (
+    DOES_NOT_EXIST,
+)
+from reviewboard.reviews.models import (
+    ReviewRequest,
+)
+from reviewboard.webapi.resources import (
+    WebAPIResource,
+)
+
+from mozreview.extra_data import (
+    REVIEW_FLAG_KEY,
+)
+from mozreview.models import (
+    MozReviewFlag,
+)
+
+
+class MozReviewFlagResource(WebAPIResource):
+    """Provides read-only access to MozReviewFlag objects
+
+    This resource is primarily needed for testing to allow the flags
+    related to ReviewRequest to be inspected.
+    """
+
+    name = 'flag'
+    model = MozReviewFlag
+    model_parent_key = 'review-request'
+    allowed_methods = ('GET',)
+
+    def _serialize(self, flag):
+        return {
+            'id': flag.pk,
+            'type': flag.type_name,
+            'value': flag.value,
+            'review_request': flag.review_request.pk,
+            'setter': flag.setter.username,
+            'requestee': flag.requestee.username if flag.requestee else '',
+            'review': flag.review.pk if flag.review else '',
+            'review_extra_data_flag': flag.review.extra_data.get(
+                REVIEW_FLAG_KEY, '') if flag.review else '',
+            'change_description': (flag.change_description.pk
+                                   if flag.change_description else ''),
+            'timestamp': flag.timestamp
+        }
+
+    def get_list(self, request, *args, **kwargs):
+        """Return list of flags related to a given review_request_id
+
+        GET attributes:
+            * review-request (int) required, `ReviewRequest` pk
+        """
+        review_request_id = request.GET.get(self.model_parent_key)
+        try:
+            rr = ReviewRequest.objects.get(id=review_request_id)
+        except ReviewRequest.DoesNotExist:
+            return DOES_NOT_EXIST
+
+        if not rr.is_accessible_by(request.user):
+            return self.get_no_access_error(request, *args, **kwargs)
+
+        flags = rr.flags.all()
+
+        data = {
+            'links': self.get_links(self.list_child_resources,
+                                    request=request, *args, **kwargs),
+        }
+
+        return self.paginated_cls(
+            request,
+            queryset=flags,
+            results_key=self.list_result_key,
+            serialize_object_func=lambda obj: self._serialize(obj),
+            extra_data=data,
+            **self.build_response_args(request))
+
+
+flag_resource = MozReviewFlagResource()
--- a/pylib/mozreview/mozreview/review_helpers.py
+++ b/pylib/mozreview/mozreview/review_helpers.py
@@ -1,18 +1,19 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from __future__ import absolute_import, unicode_literals
 
 from reviewboard.reviews.models import ReviewRequestDraft
 
-from mozreview.extra_data import REVIEW_FLAG_KEY
-from mozreview.models import get_profile
+from mozreview.models import (
+    get_profile,
+)
 
 
 def gen_latest_reviews(review_request):
     """Generate a series of relevant reviews.
 
     Generates the set of reviews for a review request where there is
     only a single review for each user and it is that users most
     recent review.
@@ -94,40 +95,38 @@ def get_reviewers_status(review_request,
         reviewers = designated_reviewers
 
     # We need to grab the reviews and statuses off the non-draft.
     if isinstance(review_request, ReviewRequestDraft):
         review_request = review_request.review_request
 
     reviewers_status = dict()
 
-    for r in reviewers:
+    for reviewer in reviewers:
         # The initial state is r?
-        reviewers_status[r.username] = {
+        reviewers_status[reviewer.username] = {
             'ship_it': False,
-            'review_flag': 'r?' if r in designated_reviewers else ' ',
+            'review_flag': 'r?' if reviewer in designated_reviewers else '',
         }
 
-    for review in gen_latest_reviews(review_request):
-        review_flag = review.extra_data.get(REVIEW_FLAG_KEY)
-        user = review.user.username
+    # Find flags for each review
+    for review_flag in review_request.flags.common_filter():
+        if not review_flag.requestee:
+            break
 
+        user = review_flag.requestee.username
         if (user not in reviewers_status) and include_drive_by:
             reviewers_status[user] = {}
 
         if user in reviewers_status:
-            reviewers_status[user]['ship_it'] = review.ship_it
-            if review_flag:
-                reviewers_status[user]['review_flag'] = review_flag
-            else:
-                # For backwards compatibility.
-                if review.ship_it:
-                    reviewers_status[user]['review_flag'] = 'r+'
-                else:
-                    reviewers_status[user]['review_flag'] = ' '
+            reviewers_status[user] = {
+                'ship_it': review_flag.review.ship_it,
+                'review_flag': '%s%s' % (review_flag.type.short_name,
+                                         review_flag.value)
+            }
 
     return reviewers_status
 
 
 def has_shipit_carryforward(review_request):
     """Return whether the review request has a carried forward ship-it
 
     A ship-it is considered carried forward if the commit for which it was
--- a/pylib/mozreview/mozreview/signal_handlers.py
+++ b/pylib/mozreview/mozreview/signal_handlers.py
@@ -5,16 +5,19 @@ import json
 import logging
 
 from django.contrib.sites.models import (
     Site
 )
 from django.contrib.auth.models import (
     User,
 )
+from django.core.exceptions import (
+    ValidationError,
+)
 from django.db.models.signals import (
     post_save,
     pre_delete,
     pre_save,
 )
 
 from djblets.siteconfig.models import (
     SiteConfiguration,
@@ -81,24 +84,23 @@ from mozreview.messages import (
     AUTO_SUBMITTED_DESCRIPTION,
     NEVER_USED_DESCRIPTION,
     OBSOLETE_DESCRIPTION,
 )
 from mozreview.models import (
     CommitData,
     DiffSetVerification,
     get_bugzilla_api_key,
+    MozReviewFlag,
+    MozReviewFlagType,
 )
 from mozreview.rb_utils import (
     get_diff_url,
     get_obj_url,
 )
-from mozreview.review_helpers import (
-    get_reviewers_status,
-)
 from mozreview.signals import (
     commit_request_publishing,
 )
 
 
 logger = logging.getLogger(__name__)
 
 
@@ -210,16 +212,88 @@ def ensure_parent_draft(draft):
         parent_rr_draft = parent_rr.get_draft()
 
         if parent_rr_draft is None:
             parent_rr_draft = ReviewRequestDraft.create(parent_rr)
 
         update_parent_rr_reviewers(parent_rr_draft)
 
 
+def manage_flags_on_rr_publishing(review_request, review_request_draft,
+                                  user, has_diffset=False):
+    """Manage flags when ReviewRequest is publishing.
+
+    All reviewers flags which are or were in `ReviewRequest`'s `target_people`
+    needs to be amended.
+    Flag of a requestee removed from target_people needs to be either
+    removed or requestee removed from the flag.
+    """
+    if is_parent(review_request):
+        return
+
+    flag_type_review = MozReviewFlagType.objects.get_review_type()
+    review_flags = review_request.flags.filter(type=flag_type_review)
+
+    # Requestees before publishing
+    existing_requestee_set = set(
+        map(lambda f: f.requestee.pk,
+            list(review_flags.filter(requestee__pk__isnull=False))))
+    # Requestees to be published
+    target_requestee_set = set(
+        map(lambda r: r.pk, list(review_request_draft.target_people.all())))
+    # Requestees who created a comment and have been later removed from
+    # target_people list.
+    empty_requestee_set = set(
+        map(lambda f: f.setter.pk,
+            list(review_flags.filter(requestee__pk__isnull=True))))
+
+    if has_diffset:
+        # remove older flags and create new ones for members of
+        # target_people
+        for requestee_pk in existing_requestee_set:
+            review_flags.get(requestee__pk=requestee_pk).delete()
+
+        for requestee_pk in target_requestee_set:
+            MozReviewFlag.objects.create(
+                type=flag_type_review,
+                value='?',
+                review_request=review_request,
+                setter=user,
+                requestee_id=requestee_pk)
+    else:
+        # manage modifications of target_people
+        added_requestee_set = target_requestee_set - existing_requestee_set
+        removed_requestee_set = existing_requestee_set - target_requestee_set
+
+        for requestee_pk in added_requestee_set:
+            if requestee_pk in empty_requestee_set:
+                # readd requestee to the review flag
+                flag = review_flags.get(setter__pk=requestee_pk)
+                flag.requestee_id = requestee_pk
+                flag.save()
+            else:
+                MozReviewFlag.objects.create(
+                    type=flag_type_review,
+                    value='?',
+                    review_request=review_request,
+                    setter=user,
+                    requestee_id=requestee_pk)
+
+        for requestee_pk in removed_requestee_set:
+            try:
+                review_flags.get(requestee__pk=requestee_pk,
+                                 review__pk__isnull=True).delete()
+            except MozReviewFlag.DoesNotExist:
+                # there was a review created - remove the requestee
+                # from the flag
+                flag = review_flags.get(requestee__pk=requestee_pk)
+                flag.requestee = None
+                flag.save()
+
+
 @bugzilla_to_publish_errors
 def on_review_request_publishing(user, review_request_draft, **kwargs):
     # There have been strange cases (all local, and during development), where
     # when attempting to publish a review request, this handler will fail
     # because the draft does not exist. This is a really strange case, and not
     # one we expect to happen in production. However, since we've seen it
     # locally, we handle it here, and log.
     if not review_request_draft:
@@ -227,17 +301,19 @@ def on_review_request_publishing(user, r
                      'review request we were attempting to publish.')
         return
 
     # If the review request draft has a new DiffSet we will only allow
     # publishing if that DiffSet has been verified. It is important to
     # do this for every review request, not just pushed ones, because
     # we can't trust the storage mechanism which indicates it was pushed.
     # TODO: This will be fixed when we transition away from extra_data.
+    has_diffset = False
     if review_request_draft.diffset:
+        has_diffset = True
         try:
             DiffSetVerification.objects.get(
                 diffset=review_request_draft.diffset)
         except DiffSetVerification.DoesNotExist:
             logger.error(
                 'An attempt was made by User %s to publish an unverified '
                 'DiffSet with id %s',
                 user.id,
@@ -399,16 +475,19 @@ def on_review_request_publishing(user, r
     for key in DRAFTED_COMMIT_DATA_KEYS:
         if key in commit_data.draft_extra_data:
             commit_data.extra_data[key] = commit_data.draft_extra_data[key]
 
     commit_data.save(update_fields=['extra_data'])
 
     review_request.save()
 
+    manage_flags_on_rr_publishing(review_request, review_request_draft,
+                                  user, has_diffset)
+
 
 def on_draft_pre_delete(sender, instance, using, **kwargs):
     """ Handle draft discards.
 
     There are no handy signals built into Review Board (yet) for us to detect
     when a squashed Review Request Draft is discarded. Instead, we monitor for
     deletions of models, and handle cases where the models being deleted are
     ReviewRequestDrafts. We then do some processing to ensure that the draft
@@ -606,57 +685,105 @@ def pre_save_review(sender, *args, **kwa
     review = kwargs["instance"]
     if review.pk:
         # The review create endpoint calls save twice: the first time it
         # gets or creates the review and the second time it updates the
         # object retrieved/created. This condition let us execute the code
         # below only once.
 
         if not is_parent(review.review_request):
+            if REVIEW_FLAG_KEY not in review.extra_data:
+                try:
+                    # Find review_request flag of that user and carry
+                    # forward the status
+                    status = str(review.review_request.flags.get(
+                        requestee=review.user))
+                except MozReviewFlag.DoesNotExist:
+                    if (review.user
+                            in review.review_request.target_people.all()):
+                        status = 'r?'
+                    else:
+                        status = ''
 
-            if REVIEW_FLAG_KEY not in review.extra_data:
-                # TODO: we should use a different query than going through
-                # all the reviews, which is what get_reviewers_status does.
-                reviewers_status = get_reviewers_status(review.review_request,
-                                                        reviewers=[review.user])
-                user = review.user.username
-                flag = reviewers_status.get(user, {}).get('review_flag', ' ')
-                review.extra_data[REVIEW_FLAG_KEY] = flag
+                review.extra_data[REVIEW_FLAG_KEY] = status
 
             review.ship_it = (review.extra_data[REVIEW_FLAG_KEY] == 'r+')
 
 
+def get_flag_value(status, review, flag_type_review, user, target_people):
+    """Validate status and return flag_value, log the errors on exceptions"""
+    if status == '':
+        flag_value = None
+    else:
+        try:
+            flag_value = flag_type_review.get_value_from_status(status)
+        except ValueError:
+            logger.error('Did not publish review: %s, wrong value: %s' % (
+                review.id, status))
+            raise
+        try:
+            MozReviewFlag.validate_value(flag_value, flag_type_review, review,
+                                         user, target_people)
+        except ValidationError:
+            logger.error('Did not publish review: '
+                         '%s, invalid flag: %s' % (review.id, status))
+            raise
+
+    return flag_value
+
+
+def save_flag_on_review_publishing(flag_value, flag_type_review, user, review):
+    """Create a new one or update an existing flag"""
+    try:
+        flag = MozReviewFlag.objects.get(
+            type=flag_type_review,
+            review_request=review.review_request,
+            requestee=user)
+    except MozReviewFlag.DoesNotExist:
+        flag = MozReviewFlag(
+            type=flag_type_review,
+            review_request=review.review_request,
+            requestee=user)
+
+    flag.value = flag_value
+    flag.review = review
+    flag.setter = user
+    flag.save()
+    return flag
+
+
 @bugzilla_to_publish_errors
 def on_review_publishing(user, review, **kwargs):
     """Comment in the bug and potentially r+ or clear a review flag.
 
     Note that a reviewer *must* have editbugs to set an attachment flag on
     someone else's attachment (i.e. the standard BMO review process).
 
     TODO: Report lack-of-editbugs properly; see bug 1119065.
     """
     review_request = review.review_request
     logger.info('Publishing review for user: %s review id: %s '
                 'review request id: %s' % (user, review.id,
-                                            review_request.id))
+                                           review_request.id))
 
     # skip review requests that were not pushed
     if not is_pushed(review_request):
         logger.info('Did not publish review: %s: for user: %d: review not '
                     'pushed.' % (user, review.id))
         return
 
     site = Site.objects.get_current()
     siteconfig = SiteConfiguration.objects.get_current()
     comment = build_plaintext_review(review,
                                      get_obj_url(review, site,
                                                  siteconfig),
                                      {"user": user})
     b = Bugzilla(get_bugzilla_api_key(user))
 
+    flag_value = False
     if is_parent(review_request):
         # We only support raw comments on parent review requests to prevent
         # confusion.  If the ship-it flag or the review flag was set, throw
         # an error.
         # Otherwise, mirror the comment over, associating it with the first
         # commit.
         if review.ship_it or review.extra_data.get(REVIEW_FLAG_KEY):
             raise ParentShipItError
@@ -667,36 +794,55 @@ def on_review_publishing(user, review, *
         first_child = list(gen_child_rrs(review_request))[0]
         b.post_comment(int(first_child.get_bug_list()[0]), comment,
                        get_diff_url(first_child), False)
     else:
         diff_url = get_diff_url(review_request)
         bug_id = int(review_request.get_bug_list()[0])
 
         commented = False
-        flag = review.extra_data.get(REVIEW_FLAG_KEY)
+        status = review.extra_data.get(REVIEW_FLAG_KEY)
+
+        flag_type_review = MozReviewFlagType.objects.get_review_type()
 
-        if flag is not None:
-            commented = b.set_review_flag(bug_id, flag, review.user.email,
+        if status is not None:
+            flag_value = None
+            try:
+                flag_value = get_flag_value(status, review, flag_type_review,
+                                            user,
+                                            review_request.target_people.all())
+            except ValidationError:
+                pass
+            except ValueError:
+                pass
+
+            # Set status in Bugzilla.
+            commented = b.set_review_flag(bug_id, status, review.user.email,
                                           diff_url, comment)
         else:
             # If for some reasons we don't have the flag set in extra_data,
             # fall back to ship_it
             logger.warning('Review flag not set on review %s, '
                            'updating attachment based on ship_it' % review.id)
             if review.ship_it:
+                # setting the status to create a flag later
+                flag_value = '+'
                 commented = b.r_plus_attachment(bug_id, review.user.email,
                                                 diff_url, comment)
             else:
                 commented = b.cancel_review_request(bug_id, review.user.email,
                                                     diff_url, comment)
 
         if comment and not commented:
             b.post_comment(bug_id, comment, diff_url, False)
 
+        if flag_value:
+            save_flag_on_review_publishing(flag_value, flag_type_review,
+                                           user, review)
+
 
 def get_reply_url(reply, site=None, siteconfig=None):
     """ Get the URL for a reply to a review.
 
     Since replies can have multiple comments, we can't link to a specific
     comment, so we link to the parent review which the reply is targeted at.
     """
     return get_obj_url(reply.base_reply_to, site=site, siteconfig=siteconfig)
--- a/pylib/mozreview/mozreview/static/mozreview/js/review_flag.js
+++ b/pylib/mozreview/mozreview/static/mozreview/js/review_flag.js
@@ -1,29 +1,33 @@
 /**
  * MRReviewFlag is responsible for showing the r?, r+, r- select dropdown
  * in the review dialog. It will also do the work of setting the extraData
  * in a Review model with the value that the user selects.
  */
 MRReviewFlag = {};
 
+// Choose a set of flags to be displayed in the drop-down based on the
+// ReviewRequest::target_people list
+var is_designated_reviewer = $('#user_data').data('is-designated-reviewer');
+const REVIEW_FLAG_STATES = [['', 'r+', 'r-'], ['r?', 'r+', 'r-']][is_designated_reviewer];
 
 MRReviewFlag.View = Backbone.View.extend({
   /**
    * If we save state, it'll be to a Review's extraData using this
    * key. This is the js counterpart of mozreview.extra_data.REVIEW_FLAG_KEY
    */
   key: 'p2rb.review_flag',
 
   /**
    * These are the possible states that will be shown in the dropdown,
    * in order. These are also the values that will be stored in the
    * extraData field.
    */
-  states: [' ', 'r?', 'r+', 'r-'],
+  states: REVIEW_FLAG_STATES,
 
   template: _.template([
       '<label for="mr-review-flag" hidden>Review state:</label> ',
       '<select id="mr-review-flag">',
       '<% _(states).each(function(state) { %>',
       '  <option <% if (state === val) { %> selected <% } %> >',
       '    <%= state %>',
       '  </option>',
--- a/pylib/mozreview/mozreview/templates/mozreview/user-data.html
+++ b/pylib/mozreview/mozreview/templates/mozreview/user-data.html
@@ -1,10 +1,11 @@
 {% load mozreview %}
 <div id="user_data"
   data-scm-level="{{ request.mozreview_profile|scm_level }}"
   {% if request.user.is_authenticated and review_request %}
     data-last-reviewed-revision="{{ review_request|data_reviewed_revision:request.user }}"
     {% if review_request.submitter.id == request.user.id %}
       data-is-submitter="true"
     {% endif %}
+	data-is-designated-reviewer="{{ review_request|is_designated_reviewer:request.user }}"
   {% endif %}
 ></div>
--- a/pylib/mozreview/mozreview/templatetags/mozreview.py
+++ b/pylib/mozreview/mozreview/templatetags/mozreview.py
@@ -76,16 +76,24 @@ def scm_level(mozreview_profile):
 def data_reviewed_revision(review_request, user):
     """Return the latest diff revision a user reviewed.
 
     `0`, a revision number which will never exist, is returned
     if the user has not performed a review.
     """
     return latest_revision_reviewed(review_request, user=user) or 0
 
+@register.filter()
+def is_designated_reviewer(review_request, user):
+    """Check if registered user is in listed in review_request's target_people
+
+    Returns 0 or 1
+    """
+    return review_request.target_people.filter(pk=user.pk).count()
+
 
 @register.filter()
 def required_ldap_group(repository):
     try:
         return repository.extra_data['required_ldap_group']
     except (AttributeError, KeyError):
         return ''
 
--- a/testing/vcttesting/reviewboard/mach_commands.py
+++ b/testing/vcttesting/reviewboard/mach_commands.py
@@ -35,16 +35,46 @@ def dict_from_diff(diff):
     d['revision'] = diff.revision
     d['base_commit_id'] = diff.base_commit_id
     d['name'] = diff.name
     d['extra'] = dict(diff.extra_data.iteritems())
     d['patch'] = diff.get_patch().data.splitlines()
     return d
 
 
+def serialize_flag(flag):
+    d = OrderedDict()
+    d['id'] = flag['id']
+    d['type'] = flag['type']
+    d['value'] = flag['value']
+    d['review_request'] = flag['review_request']
+    d['setter'] = flag['setter']
+    d['requestee'] = flag['requestee']
+    d['review'] = flag['review']
+    d['review_extra_data_flag'] = flag['review_extra_data_flag']
+    d['change_description'] = flag['change_description']
+    d['timestamp'] = flag['timestamp']
+    return d
+
+
+def get_and_serialize_flags(api_client, rr):
+    flags_res = api_client.get_path(
+        '/extensions/mozreview.extension.MozReviewExtension/flags/',
+        review_request=rr.id)
+    flags = []
+    for flag in flags_res:
+        flags.append(serialize_flag(flag))
+    return flags
+
+def serialize_flags(api_client, rr):
+    d = OrderedDict()
+    d['review_request_id'] = rr.id
+    d['flags'] = get_and_serialize_flags(api_client, rr)
+    return yaml.safe_dump(d, default_flow_style=False).rstrip()
+
 def serialize_review_requests(api_client, rr):
     from rbtools.api.errors import APIError
     d = OrderedDict()
     d['id'] = rr.id
     d['status'] = rr.status
     d['public'] = rr.public
     d['bugs'] = list(rr.bugs_closed)
     d['commit'] = rr.commit_id
@@ -54,16 +84,18 @@ def serialize_review_requests(api_client
     d['target_people'] = [p.get().username for p in rr.target_people]
     d['extra_data'] = dict(rr.extra_data.iteritems())
 
     commit_data = api_client.get_path(
         '/extensions/mozreview.extension.MozReviewExtension/commit-data/%s/' %
         rr.id)
     d['commit_extra_data'] = dict(commit_data.extra_data.iteritems())
 
+    d['flags'] = get_and_serialize_flags(api_client, rr)
+
     d['diffs'] = []
     for diff in rr.get_diffs():
         d['diffs'].append(dict_from_diff(diff))
 
     d['approved'] = rr.approved
     d['approval_failure'] = rr.approval_failure
 
     review_list = rr.get_reviews(review_request_id=rr.id)
@@ -233,16 +265,25 @@ class ReviewBoardCommands(object):
         description='Print a representation of a review request.')
     @CommandArgument('rrid', help='Review request id to dump')
     def dumpreview(self, rrid):
         client = self._get_client()
         root = client.get_root()
         r = root.get_review_request(review_request_id=rrid)
         print(serialize_review_requests(client, r))
 
+    @Command('dumpflags', category='reviewboard',
+        description='Print a representation of flags for a review request')
+    @CommandArgument('rrid', help='Review request id')
+    def dumpflags(self, rrid):
+        client = self._get_client()
+        root = client.get_root()
+        r = root.get_review_request(review_request_id=rrid)
+        print serialize_flags(client, r)
+
     @Command('dump-raw-diff', category='reviewboard',
              description='Dump the raw content of a diff from the server')
     @CommandArgument('rrid', help='Review request id of diffs to dump')
     def dump_raw_diff(self, rrid):
         from rbtools.api.errors import APIError
 
         client = self._get_client()
         root = client.get_root()
@@ -436,18 +477,21 @@ class ReviewBoardCommands(object):
         if body_bottom:
             args['body_bottom'] = body_bottom
         if body_top:
             args['body_top'] = body_top
 
         try:
             r = reviews.create(**args)
         except APIError as e:
-            print('API Error: %s: %s: %s' % (e.http_status, e.error_code,
-                                             e.rsp['err']['msg']))
+            if e.rsp:
+                print('API Error: %s: %s: %s' % (e.http_status, e.error_code,
+                      e.rsp['err']['msg']))
+            else:
+                print('API Error: %s: %s' % (e.http_status, e))
             return 1
 
         print('created review %s' % r.rsp['review']['id'])
 
     @Command('publish-review', category='reviewboard',
         description='Publish a review')
     @CommandArgument('rrid', help='Review request review is attached to')
     @CommandArgument('rid', help='Review to publish')