Ticket #15089: 15089_patch_b.diff

File 15089_patch_b.diff, 40.4 KB (added by legutierr, 9 years ago)

Patch that includes all specified functionality, tests, and documentation changes. Tests for sites and sites_framework pass, but other tests have not been run.

  • django/conf/project_template/settings.py

     
    3333# http://www.i18nguy.com/unicode/language-identifiers.html
    3434LANGUAGE_CODE = 'en-us'
    3535
     36# Specify an overriding SITE_ID to statically determine your current site.
     37# If this is not specified, the sites framwork will use the data on the request.
     38# If this is specified, the sites framework will ignore the data on the request.
    3639SITE_ID = 1
    3740
    3841# If you set this to False, Django will make some optimizations so as not
  • django/contrib/sites/managers.py

     
    1 from django.conf import settings
    21from django.db import models
    32from django.db.models.fields import FieldDoesNotExist
     3from django.contrib.sites.models import Site
    44
    55class CurrentSiteManager(models.Manager):
    66    "Use this to limit objects to those associated with the current site."
     
    3838    def get_query_set(self):
    3939        if not self.__is_validated:
    4040            self._validate_field_name()
    41         return super(CurrentSiteManager, self).get_query_set().filter(**{self.__field_name + '__id__exact': settings.SITE_ID})
     41        return super(CurrentSiteManager, self).get_query_set().filter(
     42          **{self.__field_name: Site.objects.get_current()}
     43        )
  • django/contrib/sites/middleware.py

     
     1from django.conf import settings
     2from django.utils.cache import patch_vary_headers
     3from django.contrib.sites.models import get_current_site, Site, RequestSite, SITE_CACHE
     4
     5
     6class LazySite(object):
     7    """
     8    A lazy site object that refers to either Site instance or
     9    a case insensitive RequestSite.
     10    """
     11    def __get__(self, request, obj_type=None):
     12        if not hasattr(request, '_cached_site'):
     13            request._cached_site = get_current_site(request)
     14        return request._cached_site
     15
     16
     17class SitesMiddleware(object):
     18
     19    def process_request(self, request):
     20        if not hasattr(request.__class__, 'site'):
     21            request.__class__.site = LazySite()
     22        return None
     23
     24    def process_response(self, request, response):
     25        """
     26        Forces the HTTP Vary header onto requests to avoid having responses
     27        cached from incorrect urlconfs.
     28
     29        If you'd like to disable this for some reason, set `FORCE_VARY_ON_HOST`
     30        in your Django settings file to `False`.
     31        """
     32        if getattr(settings, 'SITES_VARY_ON_HOST', True):
     33            patch_vary_headers(response, ('Host',))
     34        return response
     35
  • django/contrib/sites/__init__.py

     
     1
     2from django.contrib.sites.models import get_current_site
  • django/contrib/sites/tests.py

     
    11from django.conf import settings
    22from django.contrib.sites.models import Site, RequestSite, get_current_site
    3 from django.core.exceptions import ObjectDoesNotExist
     3from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
    44from django.http import HttpRequest
    55from django.test import TestCase
    66
     7NONE_VAL = object()
    78
     9class TestSite():
     10    def __init__(self):
     11        self.domain = "domain"
     12        self.name = "name"
     13        self.save = lambda x: x
     14        self.delete = lambda x: x
     15
    816class SitesFrameworkTests(TestCase):
    917
    1018    def setUp(self):
    11         Site(id=settings.SITE_ID, domain="example.com", name="example.com").save()
     19        self.old_site_id = settings.SITE_ID if hasattr(settings, "SITE_ID") else NONE_VAL
     20        settings.SITE_ID = 1
     21        Site(id=settings.SITE_ID, domain="staticsite.com", name="staticsite.com").save()
     22        Site(domain="dynamicsite.com", name="dynamicsite.com").save()
    1223        self.old_Site_meta_installed = Site._meta.installed
    1324        Site._meta.installed = True
    1425
    1526    def tearDown(self):
    1627        Site._meta.installed = self.old_Site_meta_installed
     28        if self.old_site_id is not NONE_VAL:
     29            settings.SITE_ID = self.old_site_id
     30        else:
     31            del settings.SITE_ID
    1732
    1833    def test_site_manager(self):
    1934        # Make sure that get_current() does not return a deleted Site object.
     
    2641        # After updating a Site object (e.g. via the admin), we shouldn't return a
    2742        # bogus value from the SITE_CACHE.
    2843        site = Site.objects.get_current()
    29         self.assertEqual(u"example.com", site.name)
     44        self.assertEqual(u"staticsite.com", site.name)
    3045        s2 = Site.objects.get(id=settings.SITE_ID)
    3146        s2.name = "Example site"
    3247        s2.save()
     
    3449        self.assertEqual(u"Example site", site.name)
    3550
    3651    def test_get_current_site(self):
     52        del settings.SITE_ID
     53
    3754        # Test that the correct Site object is returned
    3855        request = HttpRequest()
    3956        request.META = {
    40             "SERVER_NAME": "example.com",
     57            "SERVER_NAME": "dynamicsite.com",
    4158            "SERVER_PORT": "80",
    4259        }
    4360        site = get_current_site(request)
    4461        self.assert_(isinstance(site, Site))
    45         self.assertEqual(site.id, settings.SITE_ID)
     62        self.assertEqual(site.id, self.old_site_id+1)
     63        self.assertEqual(site.domain, "dynamicsite.com")
    4664
    4765        # Test that an exception is raised if the sites framework is installed
    4866        # but there is no matching Site
    4967        site.delete()
    5068        self.assertRaises(ObjectDoesNotExist, get_current_site, request)
    5169
     70        settings.SITE_ID = 1
     71
     72        # by setting settings.SITE_ID, the requset-driven lookup is overridden
     73        site = get_current_site(request)
     74        self.assert_(isinstance(site, Site))
     75        self.assertEqual(site.id, self.old_site_id)
     76        self.assertEqual(site.domain, "staticsite.com")
     77
     78        del settings.SITE_ID
     79
     80        request = HttpRequest()
     81        request.META = {
     82            "SERVER_NAME": "staticsite.com",
     83            "SERVER_PORT": "80",
     84        }
     85        site = get_current_site(request)
     86        self.assert_(isinstance(site, Site))
     87        self.assertEqual(site.id, self.old_site_id)
     88        self.assertEqual(site.domain, "staticsite.com")
     89
     90        # Test that an exception is raised if the sites framework is installed
     91        # but there is no matching Site
     92        site.delete()
     93        self.assertRaises(ObjectDoesNotExist, get_current_site, request)
     94
     95        # now, setting the SITE_ID should have no effect
     96        settings.SITE_ID = 1
     97        self.assertRaises(ObjectDoesNotExist, get_current_site, request)
     98
    5299        # A RequestSite is returned if the sites framework is not installed
    53100        Site._meta.installed = False
    54101        site = get_current_site(request)
    55102        self.assert_(isinstance(site, RequestSite))
    56         self.assertEqual(site.name, u"example.com")
     103        self.assertEqual(site.name, u"staticsite.com")
     104
     105    def test_site_callback(self):
     106        request = HttpRequest()
     107
     108        # baseline control
     109        ts = TestSite()
     110        settings.SITE_CALLBACK = lambda request, require_site_obj: ts
     111        self.assertEqual(get_current_site(request), ts)
     112
     113        # test not an ORM Site
     114        ts = TestSite()
     115        settings.SITE_CALLBACK = lambda request, require_site_obj: ts
     116        self.assertRaises(ImproperlyConfigured, get_current_site, request, True)
     117
     118        # test missing domain
     119        ts = TestSite()
     120        del ts.domain
     121        settings.SITE_CALLBACK = lambda request, require_site_obj: ts
     122        self.assertRaises(ImproperlyConfigured, get_current_site, request)
     123
     124        # test missing name
     125        ts = TestSite()
     126        del ts.name
     127        settings.SITE_CALLBACK = lambda request, require_site_obj: ts
     128        self.assertRaises(ImproperlyConfigured, get_current_site, request)
     129
     130        # test missing save
     131        ts = TestSite()
     132        del ts.save
     133        settings.SITE_CALLBACK = lambda request, require_site_obj: ts
     134        self.assertRaises(ImproperlyConfigured, get_current_site, request)
     135
     136        # test missing delete
     137        ts = TestSite()
     138        del ts.delete
     139        settings.SITE_CALLBACK = lambda request, require_site_obj: ts
     140        self.assertRaises(ImproperlyConfigured, get_current_site, request)
     141
     142        del settings.SITE_CALLBACK
  • django/contrib/sites/models.py

     
    11from django.db import models
    22from django.utils.translation import ugettext_lazy as _
     3from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
    34
    45
    56SITE_CACHE = {}
     
    78
    89class SiteManager(models.Manager):
    910
     11    def get_site_by_request(self, request):
     12        host = request.get_host().lower()
     13        if ':' in host:
     14            host, _ = host.split(':', 1)
     15        return self.get(domain__iexact=host)
     16
     17    def get_site_by_id(self, id):
     18        site = SITE_CACHE.get(id, None)
     19        if site is None:
     20            site = self.get(pk=id)
     21            SITE_CACHE[id] = site
     22        return site
     23
    1024    def get_current(self):
    1125        """
    1226        Returns the current ``Site`` based on the SITE_ID in the
    1327        project's settings. The ``Site`` object is cached the first
    1428        time it's retrieved from the database.
    1529        """
     30        from warnings import warn
     31        warn(
     32            "The SiteManager.get_current() method is deprecated in favor of the "
     33            "get_current_site() function.", DeprecationWarning
     34        )
     35
    1636        from django.conf import settings
    1737        try:
    1838            sid = settings.SITE_ID
    1939        except AttributeError:
    20             from django.core.exceptions import ImproperlyConfigured
    21             raise ImproperlyConfigured("You're using the Django \"sites framework\" without having set the SITE_ID setting. Create a site in your database and set the SITE_ID setting to fix this error.")
    22         try:
    23             current_site = SITE_CACHE[sid]
    24         except KeyError:
    25             current_site = self.get(pk=sid)
    26             SITE_CACHE[sid] = current_site
    27         return current_site
     40            raise ImproperlyConfigured(
     41                "You're using the Django \"sites framework\" without having set "
     42                "the SITE_ID setting. Create a site in your database and set "
     43                "the SITE_ID setting to fix this error."
     44            )
     45        return self.get_site_by_id(sid)
    2846
    2947    def clear_cache(self):
    3048        """Clears the ``Site`` object cache."""
     
    3452
    3553class Site(models.Model):
    3654
    37     domain = models.CharField(_('domain name'), max_length=100)
     55    domain = models.CharField(_('domain name'), max_length=100, unique=True)
    3856    name = models.CharField(_('display name'), max_length=50)
    3957    objects = SiteManager()
    4058
     
    7189    The save() and delete() methods raise NotImplementedError.
    7290    """
    7391    def __init__(self, request):
    74         self.domain = self.name = request.get_host()
     92        self.domain = self.name = request.get_host().lower()
    7593
    7694    def __unicode__(self):
    7795        return self.domain
     
    82100    def delete(self):
    83101        raise NotImplementedError('RequestSite cannot be deleted.')
    84102
     103    def __repr__(self):
     104        return "<RequestSite: %s, %s>" % (self.domain, self.name)
    85105
    86 def get_current_site(request):
     106
     107ERROR_MSG_1 = "settings.SITE_CALLBACK must return an object with a '%s' field defined. "
     108ERROR_MSG_2 = "settings.SITE_CALLBACK must return an object with a '%s' method defined. "
     109
     110def get_current_site(request, require_site_object=False):
    87111    """
    88112    Checks if contrib.sites is installed and returns either the current
    89113    ``Site`` object or a ``RequestSite`` object based on the request.
    90114    """
    91     if Site._meta.installed:
    92         current_site = Site.objects.get_current()
     115    from django.conf import settings
     116
     117    if hasattr(settings, 'SITE_CALLBACK') and settings.SITE_CALLBACK is not None:
     118        if callable(settings.SITE_CALLBACK):
     119            site = settings.SITE_CALLBACK(request, require_site_object)
     120
     121            # validate returned object to guarantee contract
     122            if site is None:
     123                pass # if the settings.SITE_CALLBACK returns None, then use other methods
     124            elif require_site_object and not site.__class__ is Site:
     125                raise ImproperlyConfigured(
     126                    'settings.SITE_CALLBACK must return a Site object when '
     127                    'require_site_object argument is True.'
     128                )
     129            elif not hasattr(site, 'domain'):
     130                raise ImproperlyConfigured(ERROR_MSG_1 % 'domain')
     131            elif not hasattr(site, 'name'):
     132                raise ImproperlyConfigured(ERROR_MSG_1 % 'name')
     133            elif not hasattr(site, 'save') or not callable(site.save):
     134                raise ImproperlyConfigured(ERROR_MSG_2 % 'save')
     135            elif not hasattr(site, 'delete') or not callable(site.delete):
     136                raise ImproperlyConfigured(ERROR_MSG_2 % 'delete')
     137            else:
     138                return site
     139        else:
     140            raise ImproperlyConfigured(
     141                'Settings contains a SITE_CALLBACK value which is not callable.'
     142            )
     143
     144    if not Site._meta.installed:
     145        if not require_site_object:
     146            return RequestSite(request)
     147        else:
     148            raise ImproperlyConfigured(
     149                'The Django "sites framework" is being used somewhere in '
     150                'your project in a manner that requires it to be added '
     151                'to INSTALLED_APPS in your settings file.'
     152            )
     153
     154    elif hasattr(settings, 'SITE_ID'):
     155        return Site.objects.get_site_by_id(settings.SITE_ID)
     156
    93157    else:
    94         current_site = RequestSite(request)
    95     return current_site
     158        return Site.objects.get_site_by_request(request)
  • django/contrib/sites/decorators.py

     
     1from django.utils.decorators import decorator_from_middleware
     2
     3from django.contrib.sites.middleware import SitesMiddleware
     4
     5site_aware = decorator_from_middleware(SitesMiddleware)
     6site_aware.__name__ = "site_aware"
     7site_aware.__doc__ = """
     8This decorator adds a LazySite instance to the request in exactly the same
     9way as the SitesMiddleware, but it can be used on a per view basis.
     10"""
     11
  • tests/regressiontests/sites_framework/tests.py

     
    11from django.conf import settings
    22from django.contrib.sites.models import Site
    33from django.test import TestCase
     4from django.test.client import Client
     5from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
    46
     7
    58from models import SyndicatedArticle, ExclusiveArticle, CustomArticle, InvalidArticle, ConfusedArticle
    69
    710class SitesFrameworkTestCase(TestCase):
    811    def setUp(self):
    912        Site.objects.get_or_create(id=settings.SITE_ID, domain="example.com", name="example.com")
    1013        Site.objects.create(id=settings.SITE_ID+1, domain="example2.com", name="example2.com")
     14        self.old_site_id = settings.SITE_ID
    1115       
     16    def tearDown(self):
     17        settings.SITE_ID = self.old_site_id
     18
    1219    def test_site_fk(self):
    1320        article = ExclusiveArticle.objects.create(title="Breaking News!", site_id=settings.SITE_ID)
    1421        self.assertEqual(ExclusiveArticle.on_site.all().get(), article)
     
    3239    def test_invalid_field_type(self):
    3340        article = ConfusedArticle.objects.create(title="More Bad News!", site=settings.SITE_ID)
    3441        self.assertRaises(TypeError, ConfusedArticle.on_site.all)
     42
     43    def test_middleware(self):
     44        #setup meta installed
     45        old_meta_installed = Site._meta.installed
     46        Site._meta.installed = False
     47
     48        #baseline control
     49        c = Client()
     50        extra = { "SERVER_NAME": "example3.com",
     51                  "SERVER_PORT": "80" }
     52
     53        result = c.get("/sites/", **extra)
     54        self.assertEquals(result.content, "")
     55
     56        #decorator, using RequestSite
     57        result = c.get("/sites/decorated/", **extra)
     58        self.assertEquals(result.content, "example3.com")
     59
     60        #set up middleware
     61        middleware_str = 'django.contrib.sites.middleware.SitesMiddleware'
     62        old_middleware_setting = settings.MIDDLEWARE_CLASSES
     63        if middleware_str not in settings.MIDDLEWARE_CLASSES:
     64            settings.MIDDLEWARE_CLASSES += (middleware_str,)
     65        old_site_id = settings.SITE_ID
     66        del settings.SITE_ID
     67        c = Client()
     68        Site._meta.installed = True
     69
     70        #retrieve by request
     71        extra["SERVER_NAME"] = "example2.com"
     72        result = c.get("/sites/", **extra)
     73        self.assertEquals(result.content, "example2.com")
     74
     75        #retrieve default, no site found
     76        extra["SERVER_NAME"] = "example3.com"
     77        result = c.get("/sites/", **extra)
     78        # this is empty, instead of raising an exception, because
     79        # ObjectDoesNotExist has a "silent_variable_failure" field
     80        # set to True, which the template engine replaces with ""
     81        self.assertEquals(result.content, "")
     82
     83        #retrieve by site id
     84        settings.SITE_ID = old_site_id
     85        extra["SERVER_NAME"] = "example2.com"
     86        result = c.get("/sites/", **extra)
     87        self.assertEquals(result.content, "example.com")
     88
     89        #retrieve default, no site found
     90        extra["SERVER_NAME"] = "example3.com"
     91        result = c.get("/sites/", **extra)
     92        self.assertEquals(result.content, "example.com")
     93
     94        #use RequestSite
     95        Site._meta.installed = False
     96        extra["SERVER_NAME"] = "example3.com"
     97        result = c.get("/sites/", **extra)
     98        self.assertEquals(result.content, "example3.com")
     99
     100        #reset to original values
     101        Site._meta.installed = old_meta_installed
     102        settings.MIDDLEWARE_CLASSES = old_middleware_setting
     103
  • tests/urls.py

     
    4141
    4242    # special headers views
    4343    (r'special_headers/', include('regressiontests.special_headers.urls')),
     44
     45    # sites middleware tests
     46    (r'sites/', include('regressiontests.sites_framework.urls')),
    4447)
  • docs/ref/contrib/sites.txt

     
    66   :synopsis: Lets you operate multiple Web sites from the same database and
    77              Django project
    88
     9.. versionchanged:: 1.3
     10
     11.. note::
     12    Prior to Django 1.3, django.contrib.sites relied heavily on settings.SITE_ID
     13    as the only manner in which to determine the "current site".  Now, with
     14    Django 1.3, settings.SITE_ID is only one possible way to determine the
     15    "current site", with the preferred method being an inspection of the incomming
     16    request.
     17
     18    For this reason, the `Site.objects.get_current()` manager method has been
     19    depricated in favor of `django.contrib.sites.get_current_site`. settings.SITE_ID,
     20    which used to be a mandatory setting for django.contrib.sites, is now optional.
     21
    922Django comes with an optional "sites" framework. It's a hook for associating
    1023objects and functionality to particular Web sites, and it's a holding place for
    1124the domain names and "verbose" names of your Django-powered sites.
    1225
    13 Use it if your single Django installation powers more than one site and you
    14 need to differentiate between those sites in some way.
     26This framework has a number of uses, but its primary use is to implement a
     27standard approach to "multitenancy", whereby a single instance or installation
     28of Django can be used to power more than one site.  In other words, the sites
     29framework will allow you to differentiate between distinct sites in your
     30application. Django iteslf uses the sites framework in a couple of ways in
     31the contrib apps, automatically via simple conventions.
    1532
    16 The whole sites framework is based on a simple model:
     33The "sites" framework centralizes control in a single function, which
     34should act as the entry point for anyone using the library:
    1735
    18 .. class:: django.contrib.sites.models.Site
     36.. :func:`~django.contrib.sites.get_current_site`
    1937
    20 This model has :attr:`~django.contrib.sites.models.Site.domain` and
    21 :attr:`~django.contrib.sites.models.Site.name` fields. The :setting:`SITE_ID`
    22 setting specifies the database ID of the
    23 :class:`~django.contrib.sites.models.Site` object associated with that
    24 particular settings file.
     38This function takes a request as a parameter, and returns an object
     39representing the current site.  The returned object may be user-defined,
     40or it may be one of two types of object that the sites framework itself
     41implements.  Regardless of what specific type of object is returned, however,
     42a `domain` field, corresponding to the domain name of the site, and a `name`
     43field, which is a verbose name that describes the site, will always be defined. 
     44Non-ORM objects that are returned are also required to implement mock `save`
     45and `delete` methods, to maintain API compatibilty with the Site model.
    2546
    26 How you use this is up to you, but Django uses it in a couple of ways
    27 automatically via simple conventions.
     47How ``get_current_site`` Works
     48----------------------------
    2849
     50.. versionchanged:: 1.3
     51
     52The return value of ``get_current_site`` can be determined by any of four
     53things (in order of precidence):
     54
     55    * the optional SITE_CALLBACK setting, which should reference a callable
     56      with the same signature and contract as ``get_current_site`` iteslf--
     57      specifically, it must specify ``request`` and ``require_site_object`` as
     58      parameters, and return an object with members ``domain``, ``name``,
     59      ``save``, and ``delete``
     60
     61    * whether or not the ``contrib.sites`` app is installed in your project,
     62      i.e. referenced inside the INSTALLED_APPS list in settings; in the
     63      event that ``contrib.sites`` is not installed, a non-ORM RequestSite
     64      object is returned
     65
     66    * the optional SITE_ID setting, which points to an overriding Site
     67      database record.
     68
     69    * the hostname associated with the inbound http request, which will
     70      be used to retrieve a Site ORM object from the database.
     71
     72The return value of SITE_CALLBACK can be any user-specified object.  However,
     73it is important to note that whenever the ``require_site_object`` argument is
     74passed in as ``True`` (the default is ``False``), the object returned must
     75be a django.contrib.sites.models.Site object.
     76
     77Similarly, whenever ``contrib.sites`` is not installed in your project, a
     78RequestSite object will be returned instead of a Site object.  However,
     79if the ``require_site_object`` argument is passed in as ``True`` then failing
     80to include the sites framework in your INSTALLED_APPS will cause an
     81ImproperlyConfigured exception to be raised.
     82
     83Note that the SITE_ID setting acts to override whatever host might be specified
     84in the request. The reason for this override behavior is explained in the section
     85below titled "Static Multitenancy".
     86
     87If the last step in the above precidence chain is reached and no Site object
     88is found for the incomming request, then the ``get_current_site()`` function
     89returns None.
     90
    2991Example usage
    3092=============
    3193
     
    74136
    75137          def article_detail(request, article_id):
    76138              try:
    77                   a = Article.objects.get(id=article_id, sites__id__exact=settings.SITE_ID)
     139                  site = get_current_site(request, require_site_object=True)
     140                  a = Article.objects.get(id=article_id, sites=site)
    78141              except Article.DoesNotExist:
    79142                  raise Http404
    80143              # ...
    81144
     145Note that because the ``Article`` model relates specifically to the ``Site``
     146model, ``get_current_site()`` is called with the require_site_object parameter
     147set to True.  If this value were not specified, there would be a risk that
     148a ``RequestSite`` or even perhaps a user-defined site object being returned,
     149which would cause a failure in the ORM when the above query were run.
     150
    82151.. _ljworld.com: http://www.ljworld.com/
    83152.. _lawrence.com: http://www.lawrence.com/
    84153
     
    100169        # ...
    101170        site = models.ForeignKey(Site)
    102171
    103 This has the same benefits as described in the last section.
     172This has the same benefits as described in the next section.
    104173
    105174.. _hooking-into-current-site-from-views:
    106175
    107176Hooking into the current site from views
    108177----------------------------------------
    109178
     179.. versionchanged:: 1.3
     180
    110181You can use the sites framework in your Django views to do
    111182particular things based on the site in which the view is being called.
    112183For example::
    113184
    114185    from django.conf import settings
     186    from django.contrib.sites import get_current_site
    115187
    116188    def my_view(request):
    117         if settings.SITE_ID == 3:
    118             # Do something.
    119         else:
    120             # Do something else.
    121 
    122 Of course, it's ugly to hard-code the site IDs like that. This sort of
    123 hard-coding is best for hackish fixes that you need done quickly. A slightly
    124 cleaner way of accomplishing the same thing is to check the current site's
    125 domain::
    126 
    127     from django.conf import settings
    128     from django.contrib.sites.models import Site
    129 
    130     def my_view(request):
    131         current_site = Site.objects.get(id=settings.SITE_ID)
     189        current_site = get_current_site(request)
    132190        if current_site.domain == 'foo.com':
    133191            # Do something
    134192        else:
    135193            # Do something else.
    136194
    137 The idiom of retrieving the :class:`~django.contrib.sites.models.Site` object
    138 for the value of :setting:`settings.SITE_ID <SITE_ID>` is quite common, so
    139 the :class:`~django.contrib.sites.models.Site` model's manager has a
    140 ``get_current()`` method. This example is equivalent to the previous one::
     195For code that relies on getting the current domain but cannot be certain
     196that the sites framework will be installed for any given project, the
     197:func:`~django.contrib.sites.get_current_site` function will return either
     198a Site instance (if the sites framework is installed) or a RequestSite
     199instance (if it is not). This allows loose coupling with the sites framework
     200and provides a usable fallback for cases where it is not installed.
    141201
    142     from django.contrib.sites.models import Site
     202Additionally, you can implement your own version of ``get_current_site()``
     203and assign it to settings.SITE_CALLBACK in order to override all ``get_current_site()``
     204behavior.  See the section "How ``get_current_site`` Works", above.
    143205
    144     def my_view(request):
    145         current_site = Site.objects.get_current()
    146         if current_site.domain == 'foo.com':
    147             # Do something
    148         else:
    149             # Do something else.
     206Getting the current domain for display
     207--------------------------------------
    150208
    151209.. versionchanged:: 1.3
    152210
    153 For code which relies on getting the current domain but cannot be certain
    154 that the sites framework will be installed for any given project, there is a
    155 utility function :func:`~django.contrib.sites.models.get_current_site` that
    156 takes a request object as an argument and returns either a Site instance (if
    157 the sites framework is installed) or a RequestSite instance (if it is not).
    158 This allows loose coupling with the sites framework and provides a usable
    159 fallback for cases where it is not installed.
    160 
    161 Getting the current domain for display
    162 --------------------------------------
    163 
    164211LJWorld.com and Lawrence.com both have e-mail alert functionality, which lets
    165212readers sign up to get notifications when news happens. It's pretty basic: A
    166213reader signs up on a Web form, and he immediately gets an e-mail saying,
     
    168215
    169216It'd be inefficient and redundant to implement this signup-processing code
    170217twice, so the sites use the same code behind the scenes. But the "thank you for
    171 signing up" notice needs to be different for each site. By using
    172 :class:`~django.contrib.sites.models.Site`
    173 objects, we can abstract the "thank you" notice to use the values of the
    174 current site's :attr:`~django.contrib.sites.models.Site.name` and
    175 :attr:`~django.contrib.sites.models.Site.domain`.
     218signing up" notice needs to be different for each site. By using site objects,
     219we can abstract the "thank you" notice to use the values of whatever object
     220is returned by :func:`~django.contrib.sites.get_current_site`, specifically
     221``name`` and ``domain``.
    176222
    177223Here's an example of what the form-handling view looks like::
    178224
    179     from django.contrib.sites.models import Site
     225    from django.contrib.sites import get_current_site
    180226    from django.core.mail import send_mail
    181227
    182228    def register_for_newsletter(request):
    183229        # Check form values, etc., and subscribe the user.
    184230        # ...
    185231
    186         current_site = Site.objects.get_current()
     232        current_site = get_current_site(request)
    187233        send_mail('Thanks for subscribing to %s alerts' % current_site.name,
    188234            'Thanks for your subscription. We appreciate it.\n\n-The %s team.' % current_site.name,
    189235            'editor@%s' % current_site.domain,
     
    195241lawrence.com alerts." On LJWorld.com, the e-mail has the subject "Thanks for
    196242subscribing to LJWorld.com alerts." Same goes for the e-mail's message body.
    197243
    198 Note that an even more flexible (but more heavyweight) way of doing this would
    199 be to use Django's template system. Assuming Lawrence.com and LJWorld.com have
    200 different template directories (:setting:`TEMPLATE_DIRS`), you could simply farm out
    201 to the template system like so::
     244It's a good idea to exploit the sites objects returned by
     245:func:`~django.contrib.sites.get_current_site` objects as much as possible,
     246to remove unneeded complexity and redundancy.
    202247
     248Getting the current domain for full URLs
     249----------------------------------------
     250
     251.. versionchanged:: 1.3
     252
     253Django's ``get_absolute_url()`` convention is nice for getting your objects'
     254URL without the domain name, but in some cases you might want to display the
     255full URL -- with ``http://`` and the domain and everything -- for an object.
     256To do this, you can use the sites framework. A simple example::
     257
     258    from django.contrib.sites import get_current_site
     259    from django.shortcuts import render
     260    from myapp.models import MyModel
     261
     262    def view_with_full_urls(request, obj_id):
     263        obj = MyModel.objects.get(id=obj_id)
     264        partial_url = obj.get_absolute_url()
     265        domain = get_current_site(request).domain         
     266
     267        context_data = { "full_url" : 'http://%s%s' % (domain, partial_url) }
     268        return render(request, "my_template.html", context_data)
     269
     270Static Multitenancy vs. Dynamic Multitenancy
     271============================================
     272
     273.. versionadded:: 1.3
     274
     275As per 'wikipedia'_:
     276
     277    "Multitenancy refers to a principle in software architecture where a single instance
     278    of the software runs on a server, serving multiple client organizations (tenants).
     279    Multitenancy is contrasted with a multi-instance architecture where separate software
     280    instances (or hardware systems) are set up for different client organizations."
     281
     282Django implements two forms of multitenancy, what is referred to in this document
     283as "static" (or partial) multitenancy and "dynamic" (or full) multitenancy.
     284
     285Static multitenancy is distinguished by its deployment architecture: a common
     286database installation is shared by N runtime instances of Django, all running off of
     287the same codebase, but each instance differing in its localized configuration.
     288Two instances operating in this fasion might differ, for example, only in the templates
     289installed in their respective template directories.  Each instance would be
     290affiliated with only a single ``Site``, configured using settings.SITE_ID,
     291and would access specific site-affiliated data only affilitated with that ``Site``. 
     292In such a configuration, to change the ``Site`` with which an instance is affiliated
     293would require restarging that instance and possibly reconfiguring the webserver.
     294
     295Revisit for a moment the case of Lawrence.com and LJWorld.com's email notification system.
     296Using Django's template system, each website can send out notifications that contain
     297unique layout and design elements, in addition to unique content.  All that a static
     298multitenant setup requires to enable this behavior is that the TEMPLATE_DIRS setting in
     299each instance differ, and that :file:`subject.txt` and :file:`message.txt` template
     300files exist in both the LJWorld.com and Lawrence.com template directories.
     301
     302The view code itself would be the same, however, in both runtime instances, and would
     303reference the same database:
     304
    203305    from django.core.mail import send_mail
    204306    from django.template import loader, Context
    205307
     
    208310        # ...
    209311
    210312        subject = loader.get_template('alerts/subject.txt').render(Context({}))
    211         message = loader.get_template('alerts/message.txt').render(Context({}))
     313        message = loader.get_template('alerts/message.html').render(Context({}))
    212314        send_mail(subject, message, 'editor@ljworld.com', [user.email])
    213315
    214316        # ...
    215317
    216 In this case, you'd have to create :file:`subject.txt` and :file:`message.txt` template
    217 files for both the LJWorld.com and Lawrence.com template directories. That
    218 gives you more flexibility, but it's also more complex.
     318Typically, a static multitenant implementation involves a small and relatively static
     319set of sites running off a given codebase.  In a dynamic multitenant system, however,
     320the set of tenant sites changes often and should be managable via the database without
     321the need for differing instance configurations.
    219322
    220 It's a good idea to exploit the :class:`~django.contrib.sites.models.Site`
    221 objects as much as possible, to remove unneeded complexity and redundancy.
     323In a dynamic multitnant system, all instances are identical. Instead of relying on a
     324global settings.SITE_ID value to determine the current site for the whole instance,
     325each request is determined to have a "current site" based on the host it specifies. 
     326The requirement to have the ``request`` on hand in every situation where the "current
     327site" is to be retrieved does place constraints on a system's design.  Furthermore,
     328dynamic multitenant systems cannot take advantage of static configuration differences
     329(such as in the above example) in order to provide differing functionality.  All
     330multitenant behavior must be handled programatically.  Even with these limitations,
     331however, dynamic multitenancy is superior to static multitenency in its flexibility,
     332and its ability to scale to a large number of tenants.
    222333
    223 Getting the current domain for full URLs
    224 ----------------------------------------
     334...note::
     335    Prior to Django version 1.3, only "static multitenancy" was officially supported,
     336    and as a result at various places in the Django source code settings.SITE_ID
     337    was used to retrieve the "current site" object without reference to the
     338    incomming request.  Prior versions of this documentation encouraged such an approach.
     339    With the introduction of `get_current_site` as the standard entry-point to the
     340    sites framework, however, direct use of settings.SITE_ID to retrieve the current
     341    site, or any attempt to retrieve the current site inside the request-response
     342    loop without reference to the incomming request, has been deprecated.
     343   
     344    Care should be taken in implementing your multitenant system to always have the
     345    recent request on hand wherever you might wish to retrieve the current site. Always
     346    use ``get_current_site`` in every such circumstance, even if you are only
     347    implementing static multitenancy, in order to ensure that your application is
     348    compatible with both forms of multitenancy should you wish to shift your
     349    implementation in the future, and to avoid lock-in.
    225350
    226 Django's ``get_absolute_url()`` convention is nice for getting your objects'
    227 URL without the domain name, but in some cases you might want to display the
    228 full URL -- with ``http://`` and the domain and everything -- for an object.
    229 To do this, you can use the sites framework. A simple example::
     351.. _wikipedia: http://en.wikipedia.org/wiki/Multitenancy
    230352
    231     >>> from django.contrib.sites.models import Site
    232     >>> obj = MyModel.objects.get(id=3)
    233     >>> obj.get_absolute_url()
    234     '/mymodel/objects/3/'
    235     >>> Site.objects.get_current().domain
    236     'example.com'
    237     >>> 'http://%s%s' % (Site.objects.get_current().domain, obj.get_absolute_url())
    238     'http://example.com/mymodel/objects/3/'
    239 
    240353Caching the current ``Site`` object
    241354===================================
    242355
     356.. versionchanged:: 1.3
     357
    243358As the current site is stored in the database, each call to
    244 ``Site.objects.get_current()`` could result in a database query. But Django is a
    245 little cleverer than that: on the first request, the current site is cached, and
    246 any subsequent call returns the cached data instead of hitting the database.
     359:func:`~django.contrib.sites.get_current_site` could result in a database query.
     360But Django helps reduce this burden when a ``Site`` object's id (such as
     361settings.SITE_ID) is being used to retrieve the Site record. On the first request,
     362the current site is cached, and any subsequent call returns the cached data instead
     363of hitting the database.
    247364
    248365If for any reason you want to force a database query, you can tell Django to
    249 clear the cache using ``Site.objects.clear_cache()``::
     366clear the cache using ``Site.objects.clear_cache()``:
    250367
    251368    # First call; current site fetched from database.
    252     current_site = Site.objects.get_current()
     369    current_site = get_current_site(request)
    253370    # ...
    254371
    255     # Second call; current site fetched from cache.
    256     current_site = Site.objects.get_current()
     372    # Second call; current site fetched from cache, as long as SITE_ID is defined.
     373    current_site = get_current_site(request)
    257374    # ...
    258375
    259376    # Force a database query for the third call.
    260377    Site.objects.clear_cache()
    261     current_site = Site.objects.get_current()
     378    current_site = get_current_site(request)
    262379
    263380The ``CurrentSiteManager``
    264381==========================
    265382
     383..note::
     384    The ``CurrentSiteManager`` is deprecated in version 1.3, due to its
     385    reliance on settings.SITE_ID as the sole manner it uses to determing
     386    the current site.  Similar functionality can be achived by using
     387    ``get_current_site`` in your views.
     388
    266389.. class:: django.contrib.sites.managers.CurrentSiteManager
    267390
    268391If :class:`~django.contrib.sites.models.Site` plays a key role in your
     
    401524:class:`~django.contrib.sites.models.RequestSite` class, which can be used as a
    402525fallback when the database-backed sites framework is not available.
    403526
    404 A :class:`~django.contrib.sites.models.RequestSite` object has a similar
    405 interface to a normal :class:`~django.contrib.sites.models.Site` object, except
    406 its :meth:`~django.contrib.sites.models.RequestSite.__init__()` method takes an
     527The :class:`~django.contrib.sites.models.RequestSite` object, referred to at
     528various places above, has a similar interface to a normal
     529:class:`~django.contrib.sites.models.Site` object, except its
     530:meth:`~django.contrib.sites.models.RequestSite.__init__()` method takes an
    407531:class:`~django.http.HttpRequest` object. It's able to deduce the
    408532:attr:`~django.contrib.sites.models.RequestSite.domain` and
    409533:attr:`~django.contrib.sites.models.RequestSite.name` by looking at the
     
    411535and :meth:`~django.contrib.sites.models.RequestSite.delete()` methods to match
    412536the interface of :class:`~django.contrib.sites.models.Site`, but the methods
    413537raise :exc:`NotImplementedError`.
     538
     539As noted above, if your application cannot handle a ``RequestSite`` object, for
     540instance because you need a ``Site`` object to use in a foreign key or many to many
     541database relation, you can call ``get_current_site`` with the ``require_site_object``
     542keyword argument set to ``True``.  In such a case, if the sites framework is
     543not installed, an exception will be raised.
Back to Top