diff --git a/djblets/extensions/base.py b/djblets/extensions/base.py
--- a/djblets/extensions/base.py
+++ b/djblets/extensions/base.py
@@ -1,12 +1,19 @@
+import logging
 import os
+import pkg_resources
 import shutil
 import sys
 
 from django.conf import settings
 from django.conf.urls.defaults import patterns, include
 from django.core.exceptions import ImproperlyConfigured
+from django.core.management import call_command
 from django.core.urlresolvers import get_resolver, get_mod_func
+from django.db.models import loading
 
+from djblets.extensions.errors import DisablingExtensionError, \
+                                      EnablingExtensionError, \
+                                      InvalidExtensionError
 from djblets.extensions.models import RegisteredExtension
 
 
@@ -54,6 +61,7 @@ class Settings(dict):
 
 class Extension(object):
     is_configurable = False
+    requirements = []
 
     def __init__(self):
         self.hooks = set()
@@ -98,6 +106,7 @@ class ExtensionInfo(object):
         self.author_email = metadata.get('Author-email')
         self.license = metadata.get('License')
         self.url = metadata.get('Home-page')
+        self.app_name = '.'.join(ext_class.__module__.split('.')[:-1])
         self.enabled = False
         self.htdocs_path = os.path.join(settings.EXTENSIONS_MEDIA_ROOT,
                                         self.name)
@@ -158,29 +167,61 @@ class ExtensionManager(object):
     def get_installed_extensions(self):
         return self._extension_classes.values()
 
+    def get_installed_extension(self, extension_id):
+        if extension_id not in self._extension_classes:
+            raise InvalidExtensionError(extension_id)
+
+        return self._extension_classes[extension_id]
+
+    def get_dependent_extensions(self, dependency_extension_id):
+        if dependency_extension_id not in self._extension_instances:
+            raise InvalidExtensionError(dependency_extension_id)
+
+        dependency = self.get_installed_extension(dependency_extension_id)
+        result = []
+
+        for extension_id, extension in self._extension_classes.iteritems():
+            if extension_id == dependency_extension_id:
+                continue
+
+            for ext_requirement in extension.info.requirements:
+                if ext_requirement == dependency:
+                    result.append(extension_id)
+
+        return result
+
     def enable_extension(self, extension_id):
         if extension_id in self._extension_instances:
             # It's already enabled.
             return
 
         if extension_id not in self._extension_classes:
-            # Invalid class.
-            # TODO: Raise an exception
-            return
+            raise InvalidExtensionError(extension_id)
 
         ext_class = self._extension_classes[extension_id]
+
+        # Enable extension dependencies
+        for requirement_id in ext_class.requirements:
+            self.enable_extension(requirement_id)
+
+        self.__install_extension(ext_class)
         ext_class.registration.enabled = True
         ext_class.registration.save()
-        self.__install_extension(ext_class)
         return self.__init_extension(ext_class)
 
     def disable_extension(self, extension_id):
         if extension_id not in self._extension_instances:
-            # Invalid extension.
-            # TODO: Raise an exception
+            # It's not enabled.
             return
 
+        if extension_id not in self._extension_classes:
+            raise InvalidExtensionError(extension_id)
+
         extension = self._extension_instances[extension_id]
+
+        for dependent_id in self.get_dependent_extensions(extension_id):
+            self.disable_extension(dependent_id)
+
         extension.registration.enabled = False
         extension.registration.save()
         self.__uninstall_extension(extension)
@@ -257,12 +298,20 @@ class ExtensionManager(object):
         # At this point, if we're reloading, it's possible that the user
         # has removed some extensions. Go through and remove any that we
         # can no longer find.
-        for class_name in self._extension_classes.keys():
+        #
+        # While we're at it, since we're at a point where we've seen all
+        # extensions, we can set the ExtensionInfo.requirements for
+        # each extension
+        for class_name, ext_class in self._extension_classes.iteritems():
             if class_name not in found_extensions:
                 if class_name in self._extension_instances:
                     self.disable_extension(class_name)
 
                 del self._extension_classes[class_name]
+            else:
+                ext_class.info.requirements = \
+                    [self.get_installed_extension(requirement_id)
+                     for requirement_id in ext_class.requirements]
 
     def __init_extension(self, ext_class):
         assert ext_class.id not in self._extension_instances
@@ -285,6 +334,7 @@ class ExtensionManager(object):
                 self._admin_ext_resolver.url_patterns.remove(urlpattern)
 
         extension.info.enabled = False
+
         del self._extension_instances[extension.id]
 
     def __install_extension(self, ext_class):
@@ -293,7 +343,6 @@ class ExtensionManager(object):
         This will install the contents of htdocs into the
         EXTENSIONS_MEDIA_ROOT directory.
         """
-        import pkg_resources
 
         ext_path = ext_class.info.htdocs_path
         ext_path_exists = os.path.exists(ext_path)
@@ -310,6 +359,10 @@ class ExtensionManager(object):
 
             shutil.copytree(extracted_path, ext_path, symlinks=True)
 
+        # Mark the extension as installed
+        ext_class.registration.installed = True
+        ext_class.registration.save()
+
     def __uninstall_extension(self, extension):
         """
         Performs any uninstallation necessary for an extension.
diff --git a/djblets/extensions/errors.py b/djblets/extensions/errors.py
--- /dev/null
+++ b/djblets/extensions/errors.py
@@ -0,0 +1,15 @@
+class EnablingExtensionError(Exception):
+    """An error indicating that an extension could not be enabled"""
+    pass
+
+
+class DisablingExtensionError(Exception):
+    """An error indicating that an extension could not be disabled"""
+    pass
+
+
+class InvalidExtensionError(Exception):
+    """An error indicating that an extension does not exist"""
+    def __init__(self, extension_id):
+        super(InvalidExtensionError, self).__init__()
+        self.message = "Cannot find extension with id %s" % extension_id
diff --git a/djblets/extensions/models.py b/djblets/extensions/models.py
--- a/djblets/extensions/models.py
+++ b/djblets/extensions/models.py
@@ -14,6 +14,7 @@ class RegisteredExtension(models.Model):
     class_name = models.CharField(max_length=128, unique=True)
     name = models.CharField(max_length=32)
     enabled = models.BooleanField(default=False)
+    installed = models.BooleanField(default=False)
     settings = JSONField()
 
     def __unicode__(self):
diff --git a/djblets/extensions/templates/extensions/extension_dlgs.html b/djblets/extensions/templates/extensions/extension_dlgs.html
--- /dev/null
+++ b/djblets/extensions/templates/extensions/extension_dlgs.html
@@ -0,0 +1,26 @@
+<script type="text/javascript">
+
+var SITE_ROOT = "{{SITE_ROOT}}";
+
+$(document).ready(function() {
+    /*
+     * Extension IDs have periods in them.  In order to select them
+     * with jQuery, the periods must be escaped with double slashes.
+     */
+
+{%  for extension in extensions %}
+    $('#' + ('{{extension.id}}'.replace(/\./g, '\\.')) + '-enable')
+        .click(function() {
+            send_extension_webapi_request('{{extension.id}}', {enabled: "True"});
+            return false;
+        }
+    );
+    $('#' + ('{{extension.id}}'.replace(/\./g, '\\.')) + '-disable')
+        .click(function() {
+            send_extension_webapi_request('{{extension.id}}', {enabled: "False"});
+            return false;
+        }
+    );
+{%  endfor %}
+});
+</script>
diff --git a/djblets/extensions/templates/extensions/extension_list.html b/djblets/extensions/templates/extensions/extension_list.html
--- a/djblets/extensions/templates/extensions/extension_list.html
+++ b/djblets/extensions/templates/extensions/extension_list.html
@@ -1,15 +1,24 @@
 {% extends "admin/base_site.html" %}
-{% load adminmedia admin_list i18n %}
+{% load adminmedia admin_list djblets_extensions i18n %}
 
 {% block title %}{% trans "Manage Extensions" %} {{block.super}}{% endblock %}
 
 {% block extrahead %}
 <link rel="stylesheet" type="text/css" href="{% admin_media_prefix %}css/forms.css" />
 <link rel="stylesheet" type="text/css" href="{{MEDIA_URL}}djblets/css/admin.css" />
+<link rel="stylesheet" type="text/css" href="{{MEDIA_URL}}djblets/css/extensions.css?{{MEDIA_SERIAL}}" />
+<script type="text/javascript" src="{{MEDIA_URL}}djblets/js/jquery-1.3.2.min.js"></script>
+<script type="text/javascript" src="{{MEDIA_URL}}djblets/js/jquery-ui-1.6rc5.min.js"></script>
+<script type="text/javascript" src="{{MEDIA_URL}}djblets/js/jquery.gravy.js?{{MEDIA_SERIAL}}"></script>
+<script type="text/javascript" src="{{MEDIA_URL}}djblets/js/extensions.js?{{MEDIA_SERIAL}}"></script>
+
 {{block.super}}
 {% endblock %}
 
 {% block content %}
+
+{% include "extensions/extension_dlgs.html" %}
+
 <h1>{% trans "Manage Extensions" %}</h1>
 
 <div id="content-main">
@@ -24,14 +33,26 @@
    </div>
    <ul class="object-tools">
 {%   if extension.info.enabled %}
-    <li><a href="{% url disable_extension extension.id %}" class="disablelink">Disable</a></li>
+    <li><a id="{{extension.id}}-disable" href="#" class="disablelink">Disable</a></li>
 {%    if extension.is_configurable %}
     <li><a href="{{extension.id}}/config/" class="changelink">Configure</a></li>
 {%    endif %}
 {%   else %}
-    <li><a href="{% url enable_extension extension.id %}" class="enablelink">Enable</a></li>
+    <li><a id="{{extension.id}}-enable" href="#" class="enablelink">Enable</a></li>
 {%   endif %}
    </ul>
+{%   if not extension.info.enabled %}
+{%     if extension|has_disabled_requirements %}
+   <h4>{% trans "Enabling this will also enable the following extension(s):" %}</h4>
+   <ul>
+{%       for requirement in extension.info.requirements %}
+{%         if not requirement.info.enabled %}
+     <li>{{requirement.info.name}}</li>
+{%         endif %}
+{%       endfor %}
+   </ul>
+{%     endif %}
+{%   endif %}
   </li>
 {%  endfor %}
  </ul>
diff --git a/djblets/extensions/templatetags/djblets_extensions.py b/djblets/extensions/templatetags/djblets_extensions.py
--- a/djblets/extensions/templatetags/djblets_extensions.py
+++ b/djblets/extensions/templatetags/djblets_extensions.py
@@ -1,8 +1,9 @@
 from django import template
 from django.template.loader import render_to_string
 
+from djblets.extensions.base import ExtensionManager
 from djblets.extensions.hooks import TemplateHook
-from djblets.util.decorators import basictag
+from djblets.util.decorators import basictag, blocktag
 
 
 register = template.Library()
@@ -20,3 +21,16 @@ def template_hook_point(context, name):
         s += render_to_string(hook.template_name, context)
 
     return s
+
+
+@register.filter
+def has_disabled_requirements(extension):
+    """
+    Returns whether or not an extension has one or more disabled
+    requirements.
+    """
+    for requirement in extension.info.requirements:
+        if not requirement.info.enabled:
+            return True
+
+    return False
diff --git a/djblets/extensions/urls.py b/djblets/extensions/urls.py
--- a/djblets/extensions/urls.py
+++ b/djblets/extensions/urls.py
@@ -4,12 +4,5 @@ from django.conf.urls.defaults import patterns, include, url, \
 
 
 urlpatterns = patterns('djblets.extensions.views',
-    (r'^$', 'extension_list'),
-
-    url(r'^(?P<ext_class>[A-Za-z0-9_.]+)/enable/$', 'set_extension_enabled',
-        {'enabled': True},
-        name="enable_extension"),
-    url(r'^(?P<ext_class>[A-Za-z0-9_.]+)/disable/$', 'set_extension_enabled',
-        {'enabled': False},
-        name="disable_extension"),
+    (r'^$', 'extension_list')
 )
diff --git a/djblets/extensions/views.py b/djblets/extensions/views.py
--- a/djblets/extensions/views.py
+++ b/djblets/extensions/views.py
@@ -13,21 +13,11 @@ def extension_list(request, extension_manager,
     extension_manager.load()
 
     return render_to_response(template_name, RequestContext(request, {
-        'extensions': extension_manager.get_installed_extensions(),
+        'extensions': extension_manager.get_installed_extensions()
     }))
 
 
 @staff_member_required
-def set_extension_enabled(request, ext_class, enabled, extension_manager):
-    if enabled:
-        extension_manager.enable_extension(ext_class)
-    else:
-        extension_manager.disable_extension(ext_class)
-
-    return HttpResponseRedirect("../../")
-
-
-@staff_member_required
 def configure_extension(request, ext_class, form_class, extension_manager,
                         template_name='extensions/configure_extension.html'):
     context = {}
diff --git a/djblets/media/css/extensions.css b/djblets/media/css/extensions.css
--- /dev/null
+++ b/djblets/media/css/extensions.css
@@ -0,0 +1,63 @@
+#activity-indicator {
+  background-color: #fce94f;
+  background-image: url("../images/extensions/spinner.gif");
+  background-position: 0.4em 0.4em;
+  background-repeat: no-repeat;
+  border: 1px #c4a000 solid;
+  border-top: 0;
+  font-weight: bold;
+  left: 50%;
+  margin-left: -3em;
+  padding: 0.5em 0.6em 0.5em 2.2em;
+  position: fixed;
+  text-align: center;
+  top: 0;
+  width: 6em;
+  z-index: 20000;
+}
+
+/****************************************************************************
+ * Modal Boxes
+ ****************************************************************************/
+
+
+.modalbox {
+  background-color: white;
+  background-image: url('../images/extensions/box_top_bg.png');
+  background-position: top left;
+  background-repeat: repeat-x;
+  border: 1px #888A85 solid;
+  margin: 10px;
+}
+
+.modalbox-title {
+  background: #a2bedc url('../images/extensions/title_box_top_bg.png') repeat-x top left;
+  border-bottom: 1px #728eac solid;
+  font-size: 120%;
+  margin: 0;
+  padding: 5px 10px 5px 5px;
+}
+
+.modalbox-inner {
+  background-image: url('../images/extensions/box_bottom_bg.png');
+  background-position: bottom left;
+  background-repeat: repeat-x;
+  padding-bottom: 1px; /* IE wants this and it does no harm. */
+}
+
+.modalbox-contents {
+  margin: 10px;
+  position: relative; /* Makes this the offsetParent for calculations. */
+}
+
+.modalbox-buttons {
+  bottom: 0;
+  margin: 10px;
+  position: absolute;
+  right: 0;
+  text-align: right;
+}
+
+.modalbox-buttons input {
+  margin-left: 10px;
+}
diff --git a/djblets/media/images/extensions/box_bottom_bg.png b/djblets/media/images/extensions/box_bottom_bg.png
diff --git a/djblets/media/images/extensions/box_bottom_bg_trans.png b/djblets/media/images/extensions/box_bottom_bg_trans.png
diff --git a/djblets/media/images/extensions/box_top_bg.png b/djblets/media/images/extensions/box_top_bg.png
diff --git a/djblets/media/images/extensions/box_top_bg_trans.png b/djblets/media/images/extensions/box_top_bg_trans.png
diff --git a/djblets/media/images/extensions/button_bg.png b/djblets/media/images/extensions/button_bg.png
diff --git a/djblets/media/images/extensions/spinner.gif b/djblets/media/images/extensions/spinner.gif
diff --git a/djblets/media/images/extensions/title_box_top_bg.png b/djblets/media/images/extensions/title_box_top_bg.png
diff --git a/djblets/media/js/extensions.js b/djblets/media/js/extensions.js
--- /dev/null
+++ b/djblets/media/js/extensions.js
@@ -0,0 +1,37 @@
+function send_extension_webapi_request(extension_id, data) {
+    $('#activity-indicator').show();
+    $.ajax({
+        type: "PUT",
+        url: SITE_ROOT + "api/extensions/" + extension_id + "/",
+        data: data,
+        success: function(xhr) {
+            window.location = window.location;
+        },
+        complete: function(xhr) {
+            $('#activity-indicator').hide();
+        },
+        error: function(xhr) {
+            /*
+             * If something goes wrong, try to dump out the error
+             * to a modal dialog.
+             */
+            var jsonData = eval("(" + xhr.responseText + ")");
+            var dlg = $("<p/>")
+            .text(jsonData['err']['msg'])
+            .modalBox({
+                title: "Error",
+                buttons: [
+                    $('<input type="button" value="OK"/>'),
+                ]
+            });
+        }
+    });
+}
+
+
+$(document).ready(function() {
+    $('<div id="activity-indicator" />')
+        .text("Loading...")
+        .hide()
+        .appendTo("body");
+});
diff --git a/djblets/webapi/errors.py b/djblets/webapi/errors.py
--- a/djblets/webapi/errors.py
+++ b/djblets/webapi/errors.py
@@ -35,6 +35,16 @@ class WebAPIError(object):
         self.http_status = http_status
         self.headers = headers
 
+    def with_message(self, msg):
+        """
+        Overrides the default message for a WebAPIError with something
+        more context specific.
+
+        Example:
+        return ENABLE_EXTENSION_FAILED.with_message('some error message')
+        """
+        self.msg = msg
+        return self
 
 WWW_AUTHENTICATE_HEADERS = {
     'WWW-Authenticate': 'Basic realm="Web API"',
@@ -76,3 +86,11 @@ INVALID_FORM_DATA         = WebAPIError(105,
 MISSING_ATTRIBUTE         = WebAPIError(106,
                                         "Missing value for the attribute",
                                         http_status=400)
+ENABLE_EXTENSION_FAILED   = WebAPIError(107, "There was a problem enabling "
+                                             "the extension",
+                                        http_status=500) # 500 Internal Server
+                                                         #     Error
+DISABLE_EXTENSION_FAILED  = WebAPIError(108, "There was a problem disabling "
+                                             "the extension",
+                                        http_status=500) # 500 Internal Server
+                                                         #     Error
diff --git a/djblets/webapi/resources.py b/djblets/webapi/resources.py
--- a/djblets/webapi/resources.py
+++ b/djblets/webapi/resources.py
@@ -5,15 +5,22 @@ from django.db import models
 from django.db.models.query import QuerySet
 from django.http import HttpResponseNotAllowed, HttpResponse
 
+from djblets.extensions.base import RegisteredExtension
+from djblets.extensions.errors import DisablingExtensionError, \
+                                      EnablingExtensionError, \
+                                      InvalidExtensionError
 from djblets.util.decorators import augment_method_from
 from djblets.util.misc import never_cache_patterns
 from djblets.webapi.core import WebAPIResponse, WebAPIResponseError, \
                                 WebAPIResponsePaginated
 from djblets.webapi.decorators import webapi_login_required, \
-                                      webapi_response_errors, \
-                                      webapi_request_fields
+                                      webapi_permission_required, \
+                                      webapi_request_fields, \
+                                      webapi_response_errors
 from djblets.webapi.errors import WebAPIError, DOES_NOT_EXIST, \
-                                  PERMISSION_DENIED
+                                  PERMISSION_DENIED, \
+                                  ENABLE_EXTENSION_FAILED, \
+                                  DISABLE_EXTENSION_FAILED
 
 
 _model_to_resources = {}
@@ -935,6 +942,69 @@ class GroupResource(WebAPIResource):
     allowed_methods = ('GET',)
 
 
+class ExtensionResource(WebAPIResource):
+    """A default resource for representing an Extension model."""
+    model = RegisteredExtension
+    fields = ('class_name', 'name', 'enabled', 'installed')
+    name = 'extension'
+    plural_name = 'extensions'
+    uri_object_key = 'extension_name'
+    uri_object_key_regex = '[.A-Za-z0-9_-]+'
+    model_object_key = 'class_name'
+
+    allowed_methods = ('GET', 'PUT',)
+
+    def __init__(self, extension_manager):
+        super(ExtensionResource, self).__init__()
+        self.extension_manager = extension_manager
+
+    @webapi_login_required
+    def get_list(self, request, *args, **kwargs):
+        return WebAPIResource.get_list(self, request, *args, **kwargs)
+
+    @webapi_login_required
+    @webapi_permission_required('extensions.change_registeredextension')
+    @webapi_request_fields(
+        required={
+            'enabled': {
+                'type': bool,
+                'description': 'Whether or not to make the extension active.'
+            },
+        },
+    )
+    def update(self, request, *args, **kwargs):
+        # Try to find the registered extension
+        try:
+            registered_extension = self.get_object(request, *args, **kwargs)
+        except ObjectDoesNotExist:
+            return DOES_NOT_EXIST
+
+        try:
+            ext_class = self.extension_manager.get_installed_extension(
+                registered_extension.class_name)
+        except InvalidExtensionError:
+            return DOES_NOT_EXIST
+
+        if kwargs.get('enabled'):
+            try:
+                self.extension_manager.enable_extension(ext_class.id)
+            except (EnablingExtensionError, InvalidExtensionError), e:
+                return ENABLE_EXTENSION_FAILED.with_message(e.message)
+        else:
+            try:
+                self.extension_manager.disable_extension(ext_class.id)
+            except (DisablingExtensionError, InvalidExtensionError), e:
+                return DISABLE_EXTENSION_FAILED.with_message(e.message)
+
+        # Refetch extension, since the ExtensionManager may have changed
+        # the model.
+        registered_extension = self.get_object(request, *args, **kwargs)
+
+        return 200, {
+            self.item_result_key: registered_extension
+        }
+
+
 def register_resource_for_model(model, resource):
     """Registers a resource as the official location for a model.
 
@@ -953,6 +1023,7 @@ def get_resource_for_object(obj):
 
     return resource
 
+
 def get_resource_from_name(name):
     """Returns the resource of the specified name."""
     return _name_to_resources.get(name, None)
