We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Site.hostname should be lowercase to prevent duplicates
### Issue Summary

Wagtail `Site.hostname` accepts duplicate hostnames. 
It possible to create `foo.com:80` and `Foo.com:80` but these are technically the same thus duplicates.

Site.hostname is case sensitive field. Hostnames and domain names are a case insensitive. https://tools.ietf.org/html/rfc4343 

`foo.com` and `Foo.com` should be treated as the same value and raise a validation error.

### Steps to Reproduce

1. Start a new project with `wagtail start myproject`
2. Go to Settings > Sites
3. Add site "foo.com" port 80
4. Add site "Foo.com" port 80

I expect a validation error: `Site with this Hostname and Port already exists.`

I added a clean method on wagtail.core.models.Site that  makes the hostname lowercase on save.
```
    def clean(self):
        self.hostname = self.hostname.lower()
```

The form raises an error now, but the error isn't displayed in the templates! 🐞
wagtail/admin/templates/wagtailadmin/generic/create.html
wagtail/admin/templates/wagtailadmin/generic/edit.html

These templates need `{{ form.non_field_errors }}`.

<img width="750" alt="Screenshot 2020-05-29 at 01 34 54" src="https://user-images.githubusercontent.com/1969342/83204661-a7060f00-a14c-11ea-8152-8568c0acef83.png">

* I have confirmed that this issue can be reproduced as described on a fresh Wagtail project: yes

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `wagtail/models/sites.py`
Content:
```
1 from collections import namedtuple
2 
3 from django.apps import apps
4 from django.conf import settings
5 from django.core.cache import cache
6 from django.core.exceptions import ValidationError
7 from django.db import models
8 from django.db.models import Case, IntegerField, Q, When
9 from django.db.models.functions import Lower
10 from django.http.request import split_domain_port
11 from django.utils.translation import gettext_lazy as _
12 
13 MATCH_HOSTNAME_PORT = 0
14 MATCH_HOSTNAME_DEFAULT = 1
15 MATCH_DEFAULT = 2
16 MATCH_HOSTNAME = 3
17 
18 
19 def get_site_for_hostname(hostname, port):
20     """Return the wagtailcore.Site object for the given hostname and port."""
21     Site = apps.get_model("wagtailcore.Site")
22 
23     sites = list(
24         Site.objects.annotate(
25             match=Case(
26                 # annotate the results by best choice descending
27                 # put exact hostname+port match first
28                 When(hostname=hostname, port=port, then=MATCH_HOSTNAME_PORT),
29                 # then put hostname+default (better than just hostname or just default)
30                 When(
31                     hostname=hostname, is_default_site=True, then=MATCH_HOSTNAME_DEFAULT
32                 ),
33                 # then match default with different hostname. there is only ever
34                 # one default, so order it above (possibly multiple) hostname
35                 # matches so we can use sites[0] below to access it
36                 When(is_default_site=True, then=MATCH_DEFAULT),
37                 # because of the filter below, if it's not default then its a hostname match
38                 default=MATCH_HOSTNAME,
39                 output_field=IntegerField(),
40             )
41         )
42         .filter(Q(hostname=hostname) | Q(is_default_site=True))
43         .order_by("match")
44         .select_related("root_page")
45     )
46 
47     if sites:
48         # if there's a unique match or hostname (with port or default) match
49         if len(sites) == 1 or sites[0].match in (
50             MATCH_HOSTNAME_PORT,
51             MATCH_HOSTNAME_DEFAULT,
52         ):
53             return sites[0]
54 
55         # if there is a default match with a different hostname, see if
56         # there are many hostname matches. if only 1 then use that instead
57         # otherwise we use the default
58         if sites[0].match == MATCH_DEFAULT:
59             return sites[len(sites) == 2]
60 
61     raise Site.DoesNotExist()
62 
63 
64 class SiteManager(models.Manager):
65     def get_queryset(self):
66         return super(SiteManager, self).get_queryset().order_by(Lower("hostname"))
67 
68     def get_by_natural_key(self, hostname, port):
69         return self.get(hostname=hostname, port=port)
70 
71 
72 SiteRootPath = namedtuple("SiteRootPath", "site_id root_path root_url language_code")
73 
74 SITE_ROOT_PATHS_CACHE_KEY = "wagtail_site_root_paths"
75 # Increase the cache version whenever the structure SiteRootPath tuple changes
76 SITE_ROOT_PATHS_CACHE_VERSION = 2
77 
78 
79 class Site(models.Model):
80     hostname = models.CharField(
81         verbose_name=_("hostname"), max_length=255, db_index=True
82     )
83     port = models.IntegerField(
84         verbose_name=_("port"),
85         default=80,
86         help_text=_(
87             "Set this to something other than 80 if you need a specific port number to appear in URLs"
88             " (e.g. development on port 8000). Does not affect request handling (so port forwarding still works)."
89         ),
90     )
91     site_name = models.CharField(
92         verbose_name=_("site name"),
93         max_length=255,
94         blank=True,
95         help_text=_("Human-readable name for the site."),
96     )
97     root_page = models.ForeignKey(
98         "Page",
99         verbose_name=_("root page"),
100         related_name="sites_rooted_here",
101         on_delete=models.CASCADE,
102     )
103     is_default_site = models.BooleanField(
104         verbose_name=_("is default site"),
105         default=False,
106         help_text=_(
107             "If true, this site will handle requests for all other hostnames that do not have a site entry of their own"
108         ),
109     )
110 
111     objects = SiteManager()
112 
113     class Meta:
114         unique_together = ("hostname", "port")
115         verbose_name = _("site")
116         verbose_name_plural = _("sites")
117 
118     def natural_key(self):
119         return (self.hostname, self.port)
120 
121     def __str__(self):
122         default_suffix = " [{}]".format(_("default"))
123         if self.site_name:
124             return self.site_name + (default_suffix if self.is_default_site else "")
125         else:
126             return (
127                 self.hostname
128                 + ("" if self.port == 80 else (":%d" % self.port))
129                 + (default_suffix if self.is_default_site else "")
130             )
131 
132     @staticmethod
133     def find_for_request(request):
134         """
135         Find the site object responsible for responding to this HTTP
136         request object. Try:
137 
138         * unique hostname first
139         * then hostname and port
140         * if there is no matching hostname at all, or no matching
141           hostname:port combination, fall back to the unique default site,
142           or raise an exception
143 
144         NB this means that high-numbered ports on an extant hostname may
145         still be routed to a different hostname which is set as the default
146 
147         The site will be cached via request._wagtail_site
148         """
149 
150         if request is None:
151             return None
152 
153         if not hasattr(request, "_wagtail_site"):
154             site = Site._find_for_request(request)
155             setattr(request, "_wagtail_site", site)
156         return request._wagtail_site
157 
158     @staticmethod
159     def _find_for_request(request):
160         hostname = split_domain_port(request.get_host())[0]
161         port = request.get_port()
162         site = None
163         try:
164             site = get_site_for_hostname(hostname, port)
165         except Site.DoesNotExist:
166             pass
167             # copy old SiteMiddleware behaviour
168         return site
169 
170     @property
171     def root_url(self):
172         if self.port == 80:
173             return "http://%s" % self.hostname
174         elif self.port == 443:
175             return "https://%s" % self.hostname
176         else:
177             return "http://%s:%d" % (self.hostname, self.port)
178 
179     def clean_fields(self, exclude=None):
180         super().clean_fields(exclude)
181         # Only one site can have the is_default_site flag set
182         try:
183             default = Site.objects.get(is_default_site=True)
184         except Site.DoesNotExist:
185             pass
186         except Site.MultipleObjectsReturned:
187             raise
188         else:
189             if self.is_default_site and self.pk != default.pk:
190                 raise ValidationError(
191                     {
192                         "is_default_site": [
193                             _(
194                                 "%(hostname)s is already configured as the default site."
195                                 " You must unset that before you can save this site as default."
196                             )
197                             % {"hostname": default.hostname}
198                         ]
199                     }
200                 )
201 
202     @staticmethod
203     def get_site_root_paths():
204         """
205         Return a list of `SiteRootPath` instances, most specific path
206         first - used to translate url_paths into actual URLs with hostnames
207 
208         Each root path is an instance of the `SiteRootPath` named tuple,
209         and have the following attributes:
210 
211         - `site_id` - The ID of the Site record
212         - `root_path` - The internal URL path of the site's home page (for example '/home/')
213         - `root_url` - The scheme/domain name of the site (for example 'https://www.example.com/')
214         - `language_code` - The language code of the site (for example 'en')
215         """
216         result = cache.get(
217             SITE_ROOT_PATHS_CACHE_KEY, version=SITE_ROOT_PATHS_CACHE_VERSION
218         )
219 
220         if result is None:
221             result = []
222 
223             for site in Site.objects.select_related(
224                 "root_page", "root_page__locale"
225             ).order_by("-root_page__url_path", "-is_default_site", "hostname"):
226                 if getattr(settings, "WAGTAIL_I18N_ENABLED", False):
227                     result.extend(
228                         [
229                             SiteRootPath(
230                                 site.id,
231                                 root_page.url_path,
232                                 site.root_url,
233                                 root_page.locale.language_code,
234                             )
235                             for root_page in site.root_page.get_translations(
236                                 inclusive=True
237                             ).select_related("locale")
238                         ]
239                     )
240                 else:
241                     result.append(
242                         SiteRootPath(
243                             site.id,
244                             site.root_page.url_path,
245                             site.root_url,
246                             site.root_page.locale.language_code,
247                         )
248                     )
249 
250             cache.set(
251                 SITE_ROOT_PATHS_CACHE_KEY,
252                 result,
253                 3600,
254                 version=SITE_ROOT_PATHS_CACHE_VERSION,
255             )
256 
257         else:
258             # Convert the cache result to a list of SiteRootPath tuples, as some
259             # cache backends (e.g. Redis) don't support named tuples.
260             result = [SiteRootPath(*result) for result in result]
261 
262         return result
263 
264     @staticmethod
265     def clear_site_root_paths_cache():
266         cache.delete(SITE_ROOT_PATHS_CACHE_KEY, version=SITE_ROOT_PATHS_CACHE_VERSION)
267
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/wagtail/models/sites.py b/wagtail/models/sites.py
--- a/wagtail/models/sites.py
+++ b/wagtail/models/sites.py
@@ -129,6 +129,9 @@
                 + (default_suffix if self.is_default_site else "")
             )
 
+    def clean(self):
+        self.hostname = self.hostname.lower()
+
     @staticmethod
     def find_for_request(request):
         """

{"golden_diff": "diff --git a/wagtail/models/sites.py b/wagtail/models/sites.py\n--- a/wagtail/models/sites.py\n+++ b/wagtail/models/sites.py\n@@ -129,6 +129,9 @@\n                 + (default_suffix if self.is_default_site else \"\")\n             )\n \n+    def clean(self):\n+        self.hostname = self.hostname.lower()\n+\n     @staticmethod\n     def find_for_request(request):\n         \"\"\"\n", "issue": "Site.hostname should be lowercase to prevent duplicates\n### Issue Summary\r\n\r\nWagtail `Site.hostname` accepts duplicate hostnames. \r\nIt possible to create `foo.com:80` and `Foo.com:80` but these are technically the same thus duplicates.\r\n\r\nSite.hostname is case sensitive field. Hostnames and domain names are a case insensitive. https://tools.ietf.org/html/rfc4343 \r\n\r\n`foo.com` and `Foo.com` should be treated as the same value and raise a validation error.\r\n\r\n### Steps to Reproduce\r\n\r\n1. Start a new project with `wagtail start myproject`\r\n2. Go to Settings > Sites\r\n3. Add site \"foo.com\" port 80\r\n4. Add site \"Foo.com\" port 80\r\n\r\nI expect a validation error: `Site with this Hostname and Port already exists.`\r\n\r\nI added a clean method on wagtail.core.models.Site that  makes the hostname lowercase on save.\r\n```\r\n    def clean(self):\r\n        self.hostname = self.hostname.lower()\r\n```\r\n\r\nThe form raises an error now, but the error isn't displayed in the templates! \ud83d\udc1e\r\nwagtail/admin/templates/wagtailadmin/generic/create.html\r\nwagtail/admin/templates/wagtailadmin/generic/edit.html\r\n\r\nThese templates need `{{ form.non_field_errors }}`.\r\n\r\n<img width=\"750\" alt=\"Screenshot 2020-05-29 at 01 34 54\" src=\"https://user-images.githubusercontent.com/1969342/83204661-a7060f00-a14c-11ea-8152-8568c0acef83.png\">\r\n\r\n* I have confirmed that this issue can be reproduced as described on a fresh Wagtail project: yes\n", "before_files": [{"content": "from collections import namedtuple\n\nfrom django.apps import apps\nfrom django.conf import settings\nfrom django.core.cache import cache\nfrom django.core.exceptions import ValidationError\nfrom django.db import models\nfrom django.db.models import Case, IntegerField, Q, When\nfrom django.db.models.functions import Lower\nfrom django.http.request import split_domain_port\nfrom django.utils.translation import gettext_lazy as _\n\nMATCH_HOSTNAME_PORT = 0\nMATCH_HOSTNAME_DEFAULT = 1\nMATCH_DEFAULT = 2\nMATCH_HOSTNAME = 3\n\n\ndef get_site_for_hostname(hostname, port):\n    \"\"\"Return the wagtailcore.Site object for the given hostname and port.\"\"\"\n    Site = apps.get_model(\"wagtailcore.Site\")\n\n    sites = list(\n        Site.objects.annotate(\n            match=Case(\n                # annotate the results by best choice descending\n                # put exact hostname+port match first\n                When(hostname=hostname, port=port, then=MATCH_HOSTNAME_PORT),\n                # then put hostname+default (better than just hostname or just default)\n                When(\n                    hostname=hostname, is_default_site=True, then=MATCH_HOSTNAME_DEFAULT\n                ),\n                # then match default with different hostname. there is only ever\n                # one default, so order it above (possibly multiple) hostname\n                # matches so we can use sites[0] below to access it\n                When(is_default_site=True, then=MATCH_DEFAULT),\n                # because of the filter below, if it's not default then its a hostname match\n                default=MATCH_HOSTNAME,\n                output_field=IntegerField(),\n            )\n        )\n        .filter(Q(hostname=hostname) | Q(is_default_site=True))\n        .order_by(\"match\")\n        .select_related(\"root_page\")\n    )\n\n    if sites:\n        # if there's a unique match or hostname (with port or default) match\n        if len(sites) == 1 or sites[0].match in (\n            MATCH_HOSTNAME_PORT,\n            MATCH_HOSTNAME_DEFAULT,\n        ):\n            return sites[0]\n\n        # if there is a default match with a different hostname, see if\n        # there are many hostname matches. if only 1 then use that instead\n        # otherwise we use the default\n        if sites[0].match == MATCH_DEFAULT:\n            return sites[len(sites) == 2]\n\n    raise Site.DoesNotExist()\n\n\nclass SiteManager(models.Manager):\n    def get_queryset(self):\n        return super(SiteManager, self).get_queryset().order_by(Lower(\"hostname\"))\n\n    def get_by_natural_key(self, hostname, port):\n        return self.get(hostname=hostname, port=port)\n\n\nSiteRootPath = namedtuple(\"SiteRootPath\", \"site_id root_path root_url language_code\")\n\nSITE_ROOT_PATHS_CACHE_KEY = \"wagtail_site_root_paths\"\n# Increase the cache version whenever the structure SiteRootPath tuple changes\nSITE_ROOT_PATHS_CACHE_VERSION = 2\n\n\nclass Site(models.Model):\n    hostname = models.CharField(\n        verbose_name=_(\"hostname\"), max_length=255, db_index=True\n    )\n    port = models.IntegerField(\n        verbose_name=_(\"port\"),\n        default=80,\n        help_text=_(\n            \"Set this to something other than 80 if you need a specific port number to appear in URLs\"\n            \" (e.g. development on port 8000). Does not affect request handling (so port forwarding still works).\"\n        ),\n    )\n    site_name = models.CharField(\n        verbose_name=_(\"site name\"),\n        max_length=255,\n        blank=True,\n        help_text=_(\"Human-readable name for the site.\"),\n    )\n    root_page = models.ForeignKey(\n        \"Page\",\n        verbose_name=_(\"root page\"),\n        related_name=\"sites_rooted_here\",\n        on_delete=models.CASCADE,\n    )\n    is_default_site = models.BooleanField(\n        verbose_name=_(\"is default site\"),\n        default=False,\n        help_text=_(\n            \"If true, this site will handle requests for all other hostnames that do not have a site entry of their own\"\n        ),\n    )\n\n    objects = SiteManager()\n\n    class Meta:\n        unique_together = (\"hostname\", \"port\")\n        verbose_name = _(\"site\")\n        verbose_name_plural = _(\"sites\")\n\n    def natural_key(self):\n        return (self.hostname, self.port)\n\n    def __str__(self):\n        default_suffix = \" [{}]\".format(_(\"default\"))\n        if self.site_name:\n            return self.site_name + (default_suffix if self.is_default_site else \"\")\n        else:\n            return (\n                self.hostname\n                + (\"\" if self.port == 80 else (\":%d\" % self.port))\n                + (default_suffix if self.is_default_site else \"\")\n            )\n\n    @staticmethod\n    def find_for_request(request):\n        \"\"\"\n        Find the site object responsible for responding to this HTTP\n        request object. Try:\n\n        * unique hostname first\n        * then hostname and port\n        * if there is no matching hostname at all, or no matching\n          hostname:port combination, fall back to the unique default site,\n          or raise an exception\n\n        NB this means that high-numbered ports on an extant hostname may\n        still be routed to a different hostname which is set as the default\n\n        The site will be cached via request._wagtail_site\n        \"\"\"\n\n        if request is None:\n            return None\n\n        if not hasattr(request, \"_wagtail_site\"):\n            site = Site._find_for_request(request)\n            setattr(request, \"_wagtail_site\", site)\n        return request._wagtail_site\n\n    @staticmethod\n    def _find_for_request(request):\n        hostname = split_domain_port(request.get_host())[0]\n        port = request.get_port()\n        site = None\n        try:\n            site = get_site_for_hostname(hostname, port)\n        except Site.DoesNotExist:\n            pass\n            # copy old SiteMiddleware behaviour\n        return site\n\n    @property\n    def root_url(self):\n        if self.port == 80:\n            return \"http://%s\" % self.hostname\n        elif self.port == 443:\n            return \"https://%s\" % self.hostname\n        else:\n            return \"http://%s:%d\" % (self.hostname, self.port)\n\n    def clean_fields(self, exclude=None):\n        super().clean_fields(exclude)\n        # Only one site can have the is_default_site flag set\n        try:\n            default = Site.objects.get(is_default_site=True)\n        except Site.DoesNotExist:\n            pass\n        except Site.MultipleObjectsReturned:\n            raise\n        else:\n            if self.is_default_site and self.pk != default.pk:\n                raise ValidationError(\n                    {\n                        \"is_default_site\": [\n                            _(\n                                \"%(hostname)s is already configured as the default site.\"\n                                \" You must unset that before you can save this site as default.\"\n                            )\n                            % {\"hostname\": default.hostname}\n                        ]\n                    }\n                )\n\n    @staticmethod\n    def get_site_root_paths():\n        \"\"\"\n        Return a list of `SiteRootPath` instances, most specific path\n        first - used to translate url_paths into actual URLs with hostnames\n\n        Each root path is an instance of the `SiteRootPath` named tuple,\n        and have the following attributes:\n\n        - `site_id` - The ID of the Site record\n        - `root_path` - The internal URL path of the site's home page (for example '/home/')\n        - `root_url` - The scheme/domain name of the site (for example 'https://www.example.com/')\n        - `language_code` - The language code of the site (for example 'en')\n        \"\"\"\n        result = cache.get(\n            SITE_ROOT_PATHS_CACHE_KEY, version=SITE_ROOT_PATHS_CACHE_VERSION\n        )\n\n        if result is None:\n            result = []\n\n            for site in Site.objects.select_related(\n                \"root_page\", \"root_page__locale\"\n            ).order_by(\"-root_page__url_path\", \"-is_default_site\", \"hostname\"):\n                if getattr(settings, \"WAGTAIL_I18N_ENABLED\", False):\n                    result.extend(\n                        [\n                            SiteRootPath(\n                                site.id,\n                                root_page.url_path,\n                                site.root_url,\n                                root_page.locale.language_code,\n                            )\n                            for root_page in site.root_page.get_translations(\n                                inclusive=True\n                            ).select_related(\"locale\")\n                        ]\n                    )\n                else:\n                    result.append(\n                        SiteRootPath(\n                            site.id,\n                            site.root_page.url_path,\n                            site.root_url,\n                            site.root_page.locale.language_code,\n                        )\n                    )\n\n            cache.set(\n                SITE_ROOT_PATHS_CACHE_KEY,\n                result,\n                3600,\n                version=SITE_ROOT_PATHS_CACHE_VERSION,\n            )\n\n        else:\n            # Convert the cache result to a list of SiteRootPath tuples, as some\n            # cache backends (e.g. Redis) don't support named tuples.\n            result = [SiteRootPath(*result) for result in result]\n\n        return result\n\n    @staticmethod\n    def clear_site_root_paths_cache():\n        cache.delete(SITE_ROOT_PATHS_CACHE_KEY, version=SITE_ROOT_PATHS_CACHE_VERSION)\n", "path": "wagtail/models/sites.py"}], "after_files": [{"content": "from collections import namedtuple\n\nfrom django.apps import apps\nfrom django.conf import settings\nfrom django.core.cache import cache\nfrom django.core.exceptions import ValidationError\nfrom django.db import models\nfrom django.db.models import Case, IntegerField, Q, When\nfrom django.db.models.functions import Lower\nfrom django.http.request import split_domain_port\nfrom django.utils.translation import gettext_lazy as _\n\nMATCH_HOSTNAME_PORT = 0\nMATCH_HOSTNAME_DEFAULT = 1\nMATCH_DEFAULT = 2\nMATCH_HOSTNAME = 3\n\n\ndef get_site_for_hostname(hostname, port):\n    \"\"\"Return the wagtailcore.Site object for the given hostname and port.\"\"\"\n    Site = apps.get_model(\"wagtailcore.Site\")\n\n    sites = list(\n        Site.objects.annotate(\n            match=Case(\n                # annotate the results by best choice descending\n                # put exact hostname+port match first\n                When(hostname=hostname, port=port, then=MATCH_HOSTNAME_PORT),\n                # then put hostname+default (better than just hostname or just default)\n                When(\n                    hostname=hostname, is_default_site=True, then=MATCH_HOSTNAME_DEFAULT\n                ),\n                # then match default with different hostname. there is only ever\n                # one default, so order it above (possibly multiple) hostname\n                # matches so we can use sites[0] below to access it\n                When(is_default_site=True, then=MATCH_DEFAULT),\n                # because of the filter below, if it's not default then its a hostname match\n                default=MATCH_HOSTNAME,\n                output_field=IntegerField(),\n            )\n        )\n        .filter(Q(hostname=hostname) | Q(is_default_site=True))\n        .order_by(\"match\")\n        .select_related(\"root_page\")\n    )\n\n    if sites:\n        # if there's a unique match or hostname (with port or default) match\n        if len(sites) == 1 or sites[0].match in (\n            MATCH_HOSTNAME_PORT,\n            MATCH_HOSTNAME_DEFAULT,\n        ):\n            return sites[0]\n\n        # if there is a default match with a different hostname, see if\n        # there are many hostname matches. if only 1 then use that instead\n        # otherwise we use the default\n        if sites[0].match == MATCH_DEFAULT:\n            return sites[len(sites) == 2]\n\n    raise Site.DoesNotExist()\n\n\nclass SiteManager(models.Manager):\n    def get_queryset(self):\n        return super(SiteManager, self).get_queryset().order_by(Lower(\"hostname\"))\n\n    def get_by_natural_key(self, hostname, port):\n        return self.get(hostname=hostname, port=port)\n\n\nSiteRootPath = namedtuple(\"SiteRootPath\", \"site_id root_path root_url language_code\")\n\nSITE_ROOT_PATHS_CACHE_KEY = \"wagtail_site_root_paths\"\n# Increase the cache version whenever the structure SiteRootPath tuple changes\nSITE_ROOT_PATHS_CACHE_VERSION = 2\n\n\nclass Site(models.Model):\n    hostname = models.CharField(\n        verbose_name=_(\"hostname\"), max_length=255, db_index=True\n    )\n    port = models.IntegerField(\n        verbose_name=_(\"port\"),\n        default=80,\n        help_text=_(\n            \"Set this to something other than 80 if you need a specific port number to appear in URLs\"\n            \" (e.g. development on port 8000). Does not affect request handling (so port forwarding still works).\"\n        ),\n    )\n    site_name = models.CharField(\n        verbose_name=_(\"site name\"),\n        max_length=255,\n        blank=True,\n        help_text=_(\"Human-readable name for the site.\"),\n    )\n    root_page = models.ForeignKey(\n        \"Page\",\n        verbose_name=_(\"root page\"),\n        related_name=\"sites_rooted_here\",\n        on_delete=models.CASCADE,\n    )\n    is_default_site = models.BooleanField(\n        verbose_name=_(\"is default site\"),\n        default=False,\n        help_text=_(\n            \"If true, this site will handle requests for all other hostnames that do not have a site entry of their own\"\n        ),\n    )\n\n    objects = SiteManager()\n\n    class Meta:\n        unique_together = (\"hostname\", \"port\")\n        verbose_name = _(\"site\")\n        verbose_name_plural = _(\"sites\")\n\n    def natural_key(self):\n        return (self.hostname, self.port)\n\n    def __str__(self):\n        default_suffix = \" [{}]\".format(_(\"default\"))\n        if self.site_name:\n            return self.site_name + (default_suffix if self.is_default_site else \"\")\n        else:\n            return (\n                self.hostname\n                + (\"\" if self.port == 80 else (\":%d\" % self.port))\n                + (default_suffix if self.is_default_site else \"\")\n            )\n\n    def clean(self):\n        self.hostname = self.hostname.lower()\n\n    @staticmethod\n    def find_for_request(request):\n        \"\"\"\n        Find the site object responsible for responding to this HTTP\n        request object. Try:\n\n        * unique hostname first\n        * then hostname and port\n        * if there is no matching hostname at all, or no matching\n          hostname:port combination, fall back to the unique default site,\n          or raise an exception\n\n        NB this means that high-numbered ports on an extant hostname may\n        still be routed to a different hostname which is set as the default\n\n        The site will be cached via request._wagtail_site\n        \"\"\"\n\n        if request is None:\n            return None\n\n        if not hasattr(request, \"_wagtail_site\"):\n            site = Site._find_for_request(request)\n            setattr(request, \"_wagtail_site\", site)\n        return request._wagtail_site\n\n    @staticmethod\n    def _find_for_request(request):\n        hostname = split_domain_port(request.get_host())[0]\n        port = request.get_port()\n        site = None\n        try:\n            site = get_site_for_hostname(hostname, port)\n        except Site.DoesNotExist:\n            pass\n            # copy old SiteMiddleware behaviour\n        return site\n\n    @property\n    def root_url(self):\n        if self.port == 80:\n            return \"http://%s\" % self.hostname\n        elif self.port == 443:\n            return \"https://%s\" % self.hostname\n        else:\n            return \"http://%s:%d\" % (self.hostname, self.port)\n\n    def clean_fields(self, exclude=None):\n        super().clean_fields(exclude)\n        # Only one site can have the is_default_site flag set\n        try:\n            default = Site.objects.get(is_default_site=True)\n        except Site.DoesNotExist:\n            pass\n        except Site.MultipleObjectsReturned:\n            raise\n        else:\n            if self.is_default_site and self.pk != default.pk:\n                raise ValidationError(\n                    {\n                        \"is_default_site\": [\n                            _(\n                                \"%(hostname)s is already configured as the default site.\"\n                                \" You must unset that before you can save this site as default.\"\n                            )\n                            % {\"hostname\": default.hostname}\n                        ]\n                    }\n                )\n\n    @staticmethod\n    def get_site_root_paths():\n        \"\"\"\n        Return a list of `SiteRootPath` instances, most specific path\n        first - used to translate url_paths into actual URLs with hostnames\n\n        Each root path is an instance of the `SiteRootPath` named tuple,\n        and have the following attributes:\n\n        - `site_id` - The ID of the Site record\n        - `root_path` - The internal URL path of the site's home page (for example '/home/')\n        - `root_url` - The scheme/domain name of the site (for example 'https://www.example.com/')\n        - `language_code` - The language code of the site (for example 'en')\n        \"\"\"\n        result = cache.get(\n            SITE_ROOT_PATHS_CACHE_KEY, version=SITE_ROOT_PATHS_CACHE_VERSION\n        )\n\n        if result is None:\n            result = []\n\n            for site in Site.objects.select_related(\n                \"root_page\", \"root_page__locale\"\n            ).order_by(\"-root_page__url_path\", \"-is_default_site\", \"hostname\"):\n                if getattr(settings, \"WAGTAIL_I18N_ENABLED\", False):\n                    result.extend(\n                        [\n                            SiteRootPath(\n                                site.id,\n                                root_page.url_path,\n                                site.root_url,\n                                root_page.locale.language_code,\n                            )\n                            for root_page in site.root_page.get_translations(\n                                inclusive=True\n                            ).select_related(\"locale\")\n                        ]\n                    )\n                else:\n                    result.append(\n                        SiteRootPath(\n                            site.id,\n                            site.root_page.url_path,\n                            site.root_url,\n                            site.root_page.locale.language_code,\n                        )\n                    )\n\n            cache.set(\n                SITE_ROOT_PATHS_CACHE_KEY,\n                result,\n                3600,\n                version=SITE_ROOT_PATHS_CACHE_VERSION,\n            )\n\n        else:\n            # Convert the cache result to a list of SiteRootPath tuples, as some\n            # cache backends (e.g. Redis) don't support named tuples.\n            result = [SiteRootPath(*result) for result in result]\n\n        return result\n\n    @staticmethod\n    def clear_site_root_paths_cache():\n        cache.delete(SITE_ROOT_PATHS_CACHE_KEY, version=SITE_ROOT_PATHS_CACHE_VERSION)\n", "path": "wagtail/models/sites.py"}]}