diff --git a/reviewboard/accounts/managers.py b/reviewboard/accounts/managers.py
index 99d0aab708a94e1f549e20327bf8c9d9e6460f39..891cbb73add92cd22785be20d650af83deafc4c7 100644
--- a/reviewboard/accounts/managers.py
+++ b/reviewboard/accounts/managers.py
@@ -1,5 +1,7 @@
 from django.db.models import Manager
 
+from reviewboard.accounts.trophies import get_registered_trophy_types
+
 
 class ProfileManager(Manager):
     def get_or_create(self, user, *args, **kwargs):
@@ -12,3 +14,47 @@ class ProfileManager(Manager):
         user._profile = profile
 
         return profile, is_new
+
+
+class TrophyManager(Manager):
+    """Manager for trophies.
+
+    Creates new trophies, updates the database and fetches trophies from the
+    database.
+    """
+    def compute_trophies(self, review_request):
+        """Computes and returns trophies for a review request.
+
+        Computes trophies for a given review request by looping through all
+        registered trophy types and seeing if any apply to the review request.
+
+        If trophies are to be awarded, they are saved in the database and
+        returned. If no trophies are to be awarded, an empty list is returned.
+        """
+        if 'calculated_trophies' in review_request.extra_data:
+            return list(self.filter(review_request=review_request))
+
+        calculated_trophy_types = []
+
+        registered_trophy_types = get_registered_trophy_types()
+        for registered_trophy_type in registered_trophy_types.itervalues():
+            instance = registered_trophy_type()
+
+            if instance.qualifies(review_request):
+                calculated_trophy_types.append(instance)
+
+        trophies = [
+            self.model.objects.create(category=trophy_type.category,
+                                      review_request=review_request,
+                                      local_site=review_request.local_site,
+                                      user=review_request.submitter)
+            for trophy_type in calculated_trophy_types
+        ]
+
+        review_request.extra_data['calculated_trophies'] = True
+        review_request.save(update_fields=['extra_data'])
+
+        return trophies
+
+    def get_trophies(self, review_request):
+            return self.compute_trophies(review_request)
\ No newline at end of file
diff --git a/reviewboard/accounts/models.py b/reviewboard/accounts/models.py
index b34174bcde583de264766f80609d5c76eeb26928..d07fb3dd11870adf6b056f9c7f9614e441d81dee 100644
--- a/reviewboard/accounts/models.py
+++ b/reviewboard/accounts/models.py
@@ -1,5 +1,7 @@
 from django.contrib.auth.models import User
+from django.core.exceptions import ImproperlyConfigured
 from django.db import models
+from django.dispatch import receiver
 from django.utils import timezone
 from django.utils.encoding import python_2_unicode_compatible
 from django.utils.translation import ugettext_lazy as _
@@ -7,8 +9,10 @@ from djblets.util.db import ConcurrencyManager
 from djblets.util.fields import CounterField, JSONField
 from djblets.util.forms import TIMEZONE_CHOICES
 
-from reviewboard.accounts.managers import ProfileManager
+from reviewboard.accounts.managers import ProfileManager, TrophyManager
+from reviewboard.accounts.trophies import TrophyType
 from reviewboard.reviews.models import Group, ReviewRequest
+from reviewboard.reviews.signals import review_request_published
 from reviewboard.site.models import LocalSite
 
 
@@ -228,6 +232,28 @@ class LocalSiteProfile(models.Model):
         return '%s (%s)' % (self.user.username, self.local_site)
 
 
+class Trophy(models.Model):
+    """A trophy represents an achievement given to the user.
+
+    It is associated with a ReviewRequest and a User and can be associated
+    with a LocalSite.
+    """
+    category = models.CharField(max_length=100)
+    received_date = models.DateTimeField(default=timezone.now)
+    review_request = models.ForeignKey(ReviewRequest, related_name="trophies")
+    local_site = models.ForeignKey(LocalSite, null=True,
+                                   related_name="trophies")
+    user = models.ForeignKey(User, related_name="trophies")
+
+    objects = TrophyManager()
+
+    def get_trophy_type(self):
+        return TrophyType.for_category(self.category)
+
+    def get_display_text(self):
+        return self.get_trophy_type().get_display_text(self)
+
+
 #
 # The following functions are patched onto the User model.
 #
@@ -290,3 +316,8 @@ User.is_profile_visible = _is_user_profile_visible
 User.get_profile = _get_profile
 User.get_site_profile = _get_site_profile
 User._meta.ordering = ('username',)
+
+@receiver(review_request_published)
+def _call_compute_trophies(sender, review_request, **kwargs):
+    if review_request.changedescs.count() == 0 and review_request.public:
+        Trophy.objects.compute_trophies(review_request)
\ No newline at end of file
diff --git a/reviewboard/accounts/tests.py b/reviewboard/accounts/tests.py
index 14bd2b394bb02d7dbbe1c2f72932eb856ebf03ad..656219a0b884e191177b0b549662b90f78ab2a18 100644
--- a/reviewboard/accounts/tests.py
+++ b/reviewboard/accounts/tests.py
@@ -1,7 +1,8 @@
 from django.contrib.auth.models import User
 from djblets.testing.decorators import add_fixtures
 
-from reviewboard.accounts.models import LocalSiteProfile
+from reviewboard.accounts.models import LocalSiteProfile, Trophy
+
 from reviewboard.testing import TestCase
 
 
@@ -53,3 +54,65 @@ class ProfileTests(TestCase):
         self.assertFalse(review_request in
                          profile1.starred_review_requests.all())
         self.assertEqual(site_profile.starred_public_request_count, 0)
+
+
+class TrophyTests(TestCase):
+    """Testing the Trophy Case."""
+    fixtures = ['test_users']
+
+    def test_is_fish_trophy_awarded_for_new_review_request(self):
+        """Testing if a fish trophy is awarded for a new review request."""
+        user1 = User.objects.get(username='doc')
+        category = 'fish'
+        review_request = self.create_review_request(publish=True, id=3223,
+                                                    submitter=user1)
+        trophies = Trophy.objects.get_trophies(review_request)
+        self.assertEqual(trophies[0].category, category)
+        self.assertTrue(
+            trophies[0].review_request.extra_data['calculated_trophies'])
+
+    def test_is_fish_trophy_awarded_for_older_review_request(self):
+        """Testing if a fish trophy is awarded for an older review request."""
+        user1 = User.objects.get(username='doc')
+        category = 'fish'
+        review_request = self.create_review_request(publish=True, id=1001,
+                                                    submitter=user1)
+        del review_request.extra_data['calculated_trophies']
+        trophies = Trophy.objects.get_trophies(review_request)
+        self.assertEqual(trophies[0].category, category)
+        self.assertTrue(
+            trophies[0].review_request.extra_data['calculated_trophies'])
+
+    def test_is_milestone_trophy_awarded_for_new_review_request(self):
+        """Testing if a milestone trophy is awarded for a new review request.
+        """
+        user1 = User.objects.get(username='doc')
+        category = 'milestone'
+        review_request = self.create_review_request(publish=True, id=1000,
+                                                    submitter=user1)
+        trophies = Trophy.objects.compute_trophies(review_request)
+        self.assertEqual(trophies[0].category, category)
+        self.assertTrue(
+            trophies[0].review_request.extra_data['calculated_trophies'])
+
+    def test_is_milestone_trophy_awarded_for_older_review_request(self):
+        """Testing if a milestone trophy is awarded for an older review
+        request.
+        """
+        user1 = User.objects.get(username='doc')
+        category = 'milestone'
+        review_request = self.create_review_request(publish=True, id=10000,
+                                                    submitter=user1)
+        del review_request.extra_data['calculated_trophies']
+        trophies = Trophy.objects.compute_trophies(review_request)
+        self.assertEqual(trophies[0].category, category)
+        self.assertTrue(
+            trophies[0].review_request.extra_data['calculated_trophies'])
+
+    def test_is_no_trophy_awarded(self):
+        """Testing if no trophy is awarded."""
+        user1 = User.objects.get(username='doc')
+        review_request = self.create_review_request(publish=True, id=999,
+                                                    submitter=user1)
+        trophies = Trophy.objects.compute_trophies(review_request)
+        self.assertFalse(trophies)
\ No newline at end of file
diff --git a/reviewboard/accounts/trophies.py b/reviewboard/accounts/trophies.py
new file mode 100644
index 0000000000000000000000000000000000000000..1a41f76b5e1ee11bcfa9b52878c7cfa455e6a8c6
--- /dev/null
+++ b/reviewboard/accounts/trophies.py
@@ -0,0 +1,142 @@
+from __future__ import unicode_literals
+
+import logging
+import re
+
+from django.utils.translation import ugettext_lazy as _
+
+
+_trophy_types = {}
+
+
+class TrophyType(object):
+    """An abstract trophy.
+
+    Represents a type of trophy, with logic to see if it qualifies as a
+    trophy for a given review request.
+
+    Contains methods to visually display itself.
+    """
+    def __init__(self, title, image_url):
+        self.title = title
+        self.image_url = image_url
+
+    def get_display_text(self, trophy):
+        return _('%(user)s got review request #%(rid)s!') % {
+            'user': trophy.user.get_full_name(),
+            'rid': trophy.review_request.display_id
+        }
+
+    @staticmethod
+    def for_category(category):
+        if not _trophy_types:
+            _register_trophies()
+
+        return _trophy_types.get(category, UnknownTrophy)
+
+
+class MilestoneTrophy(TrophyType):
+    """A milestone trophy.
+
+    It is awarded if review request ID is greater than 1000 and is a non-zero
+    digit followed by only zeroes.
+    """
+    category = 'milestone'
+
+    def __init__(self):
+        super(MilestoneTrophy, self).__init__(
+            title=_('Milestone Trophy'),
+            image_url='/static/rb/images/trophy.png')
+
+    def qualifies(self, review_request):
+        id_str = unicode(review_request.display_id)
+        return (review_request.display_id >= 1000
+                and re.match(r'^[1-9]0+$', id_str))
+
+
+class FishTrophy(TrophyType):
+    """A fish trophy.
+
+    Give a man a fish, he'll waste hours trying to figure out why.
+    """
+    category = 'fish'
+
+    def __init__(self):
+        super(FishTrophy, self).__init__(
+            title=_('Fish Trophy'),
+            image_url='/static/rb/images/fish-trophy.png')
+
+    def qualifies(self, review_request):
+        id_str = unicode(review_request.display_id)
+        return (review_request.display_id >= 1000
+                and id_str == ''.join(reversed(id_str)))
+
+    def get_display_text(self, trophy):
+        return _('%(user)s got a fish trophy!') % {
+            'user': trophy.user.get_full_name(),
+        }
+
+
+class UnknownTrophy(TrophyType):
+    """A trophy with an unknown category.
+
+    The data for this trophy exists in the database but its category does not
+    match the category of any registered trophy types.
+    """
+    def __init__(self):
+        super(UnknownTrophy, self).__init__(
+            title=_('Unknown Trophy'),
+            image_url=None)
+
+
+def register_trophy(trophy):
+    """Registers a TrophyType subclass.
+
+    This will register a type of trophy. Review Board will use it to calculate
+    and display possible trophies.
+
+    Only TrophyType subclasses are supported.
+    """
+    _register_trophies()
+
+    if not issubclass(trophy, TrophyType):
+        raise TypeError('Only TrophyType subclasses can be registered')
+
+    if trophy in _trophy_types:
+        raise KeyError(unicode(trophy.category) +
+                       'is already a registered TrophyType subclass')
+
+        _trophy_types[trophy.category] = trophy
+
+
+def unregister_trophy(trophy):
+    """Unregisters a TrophyType subclass.
+
+    This will unregister a previously registered type of trophy.
+
+    Only TrophyType subclasses are supported. The class must have been
+    registered beforehand or a ValueError will be thrown.
+    """
+    _register_trophies()
+
+    if not issubclass(trophy, TrophyType):
+        raise TypeError('Only TrophyType subclasses can be unregistered')
+
+    try:
+        del _trophy_types[trophy.category]
+    except ValueError:
+        logging.error('Failed to unregister missing TrophyType: ' % trophy)
+        raise ValueError('This TrophyType was not previously registered')
+
+
+def get_registered_trophy_types():
+    _register_trophies()
+
+    return _trophy_types
+
+
+def _register_trophies(**kwargs):
+    """Registers all bundled TrophyTypes."""
+    if not _trophy_types:
+        _trophy_types[MilestoneTrophy.category] = MilestoneTrophy
+        _trophy_types[FishTrophy.category] = FishTrophy
\ No newline at end of file
diff --git a/reviewboard/reviews/evolutions/__init__.py b/reviewboard/reviews/evolutions/__init__.py
index 4980546f39ae125b72220a218c6cabc89a473178..ae44e7a272dba2b98c0f3bce2d2bfa52c725d4a8 100644
--- a/reviewboard/reviews/evolutions/__init__.py
+++ b/reviewboard/reviews/evolutions/__init__.py
@@ -18,4 +18,5 @@ SEQUENCE = [
     'file_attachment_comment_diff_id',
     'rich_text',
     'base_comment_extra_data',
+    'review_request_extra_data',
 ]
diff --git a/reviewboard/reviews/evolutions/review_request_extra_data.py b/reviewboard/reviews/evolutions/review_request_extra_data.py
new file mode 100644
index 0000000000000000000000000000000000000000..6397916fc6cb303852b92f33b05f6708b8b43cc5
--- /dev/null
+++ b/reviewboard/reviews/evolutions/review_request_extra_data.py
@@ -0,0 +1,9 @@
+from __future__ import unicode_literals
+
+from django_evolution.mutations import AddField
+from djblets.util.fields import JSONField
+
+
+MUTATIONS = [
+    AddField('ReviewRequest', 'extra_data', JSONField, null=True)
+]
\ No newline at end of file
diff --git a/reviewboard/reviews/models.py b/reviewboard/reviews/models.py
index 997e3b357f0cfccbeb7225930aea3000d73ac18b..b865d0053e7278eda96fe00b8f53417a1cdf2180 100644
--- a/reviewboard/reviews/models.py
+++ b/reviewboard/reviews/models.py
@@ -604,6 +604,8 @@ class ReviewRequest(BaseReviewRequestDetails):
     local_site = models.ForeignKey(LocalSite, blank=True, null=True)
     local_id = models.IntegerField('site-local ID', blank=True, null=True)
 
+    extra_data = JSONField(null=True)
+
     # Set this up with the ReviewRequestManager
     objects = ReviewRequestManager()
 
diff --git a/reviewboard/reviews/templatetags/reviewtags.py b/reviewboard/reviews/templatetags/reviewtags.py
index e779b09422eda1ad645be3b611041501a74e2b23..4a56f89f62578726e989ebca660cf1e5b5717afa 100644
--- a/reviewboard/reviews/templatetags/reviewtags.py
+++ b/reviewboard/reviews/templatetags/reviewtags.py
@@ -11,7 +11,7 @@ from django.utils.translation import ugettext_lazy as _
 from djblets.util.decorators import basictag, blocktag
 from djblets.util.humanize import humanize_list
 
-from reviewboard.accounts.models import Profile
+from reviewboard.accounts.models import Profile, Trophy
 from reviewboard.reviews.models import (BaseComment, Group,
                                         ReviewRequest, ScreenshotComment,
                                         FileAttachmentComment)
@@ -21,6 +21,50 @@ register = template.Library()
 
 
 @register.tag
+def display_review_request_trophies(parser, token):
+    try:
+        tag_name, review_request = token.split_contents()
+    except ValueError:
+        raise template.TemplateSyntaxError("%r tag requires a single argument"
+                                           % token.contents.split()[0])
+    return TrophyNode(review_request)
+
+
+class TrophyNode(template.Node):
+    """A node that represents a trophy's display text."""
+    def __init__(self, review_request):
+        self.object = template.Variable(review_request)
+
+    def render(self, context):
+        review_request = self.object.resolve(context)
+        trophy_models = Trophy.objects.get_trophies(review_request)
+        lines = []
+
+        if trophy_models:
+            lines.append('<div class="box-container">')
+            lines.append(' <div class="box yay">')
+            lines.append('  <div class="box-inner">')
+
+            for trophy_model in trophy_models:
+                trophy_type = trophy_model.get_trophy_type()()
+
+                lines.append('   <div>')
+                if trophy_type.image_url:
+                    lines.append('    <img src="'+ trophy_type.image_url
+                        + '" width="32" height="48" border="0" alt="" />')
+
+                lines.append('    <h1>' + trophy_type.get_display_text(
+                    trophy_model) + '</h1>')
+
+                lines.append('   </div>')
+                lines.append('  </div>')
+                lines.append(' </div>')
+                lines.append('</div>')
+
+        return '\n'.join(lines)
+
+
+@register.tag
 @blocktag
 def ifneatnumber(context, nodelist, rid):
     """
diff --git a/reviewboard/templates/reviews/trophy_box.html b/reviewboard/templates/reviews/trophy_box.html
index 0d967a6a9e1080bf1f67757cf7cf435e5feb3b6b..ee6283843a67b5f9b0be950692846727cabedda4 100644
--- a/reviewboard/templates/reviews/trophy_box.html
+++ b/reviewboard/templates/reviews/trophy_box.html
@@ -1,14 +1,3 @@
-{% load djblets_deco %}
-{% load djblets_utils %}
-{% load i18n %}
 {% load reviewtags %}
-{% load staticfiles %}
-
-{% ifneatnumber review_request.display_id %}
-{%  box "yay" %}
-{%   definevar "trophyimg" %}rb/images/{% if milestone %}trophy{% else %}fish-trophy{% endif %}.png{% enddefinevar %}
- <img src="{% static trophyimg %}" width="32" height="48" border="0" alt="" />
- <h1>{% blocktrans with review_request.submitter|user_displayname as submitter and review_request.display_id as id %}{{submitter}} got review request #{{id}}!{% endblocktrans %}</h1>
-{%  endbox %}
-{% endifneatnumber %}
 
+{% display_review_request_trophies review_request %}
\ No newline at end of file
