[Fixed]-How do you extend the Site model in django?

9๐Ÿ‘

โœ…

I just used my own subclass of Site and created a custom admin for it.

Basically, when you subclass a model in django it creates FK pointing to parent model and allows to access parent modelโ€™s fields transparently- the same way youโ€™d access parent class attributes in pyhon.
Built in admin wonโ€™t suffer in any way, but youโ€™ll have to un-register Sites ModelAdmin and register your own ModelAdmin.

๐Ÿ‘คfest

5๐Ÿ‘

As of Django 2.2 there still no simple straight way to extend Site as can be done for User. Best way to do it now is to create new entity and put parameters there. This is the only way if you want to leverage existing sites support.

class SiteProfile(models.Model):
    title = models.TextField()
    site = models.OneToOneField(Site, on_delete=models.CASCADE)

You will have to create admin for SiteProfile. Then add some SiteProfile records with linked Site. Now you can use site.siteprofile.title anywhere where you have access to current site from model.

๐Ÿ‘คigo

4๐Ÿ‘

If you only want to change behaviour of the object, but not add any new fields, you should consider using a โ€œproxy modelโ€ (new in Django 1.1). You can add extra Python methods to existing models, and more:

This is what proxy model inheritance is for: creating a proxy for the original model. You can create, delete and update instances of the proxy model and all the data will be saved as if you were using the original (non-proxied) model. The difference is that you can change things like the default model ordering or the default manager in the proxy, without having to alter the original.

Read more in the documentation.

๐Ÿ‘คJustin Voss

3๐Ÿ‘

You can have another model like SiteProfile which has a OneToOne relation with Site.

๐Ÿ‘คantitoxic

1๐Ÿ‘

It has been a long time since the question was asked, but I think there is not yet (Django 3.1) an easy solution for it like creating a custom user model. In this case, creating a custom user model inheriting from django.contrib.auth.models.AbstractUser model and changing AUTH_USER_MODEL (in settings) to the newly created custom user model solves the issue.

However, it can be achieved for also Site model with a long solution written below:

SOLUTION

Suppose that you have an app with the name core. Use that app for all of the code below, except the settings file.

  1. Create a SiteProfile model with a site field having an OneToOne relation with the Site model. I have also changed its app_label meta so it will be seen under the Sites app in the admin.
# in core.models

...
from django.contrib.sites.models import Site
from django.db import models

class SiteProfile(models.Model):
    """SiteProfile model is OneToOne related to Site model."""
    site = models.OneToOneField(
        Site, on_delete=models.CASCADE, primary_key=True,
        related_name='profiles', verbose_name='site')

    long_name = models.CharField(
        max_length=255, blank=True, null=True)
    meta_name = models.CharField(
        max_length=255, blank=True, null=True)

    def __str__(self):
        return self.site.name

    class Meta:
        app_label = 'sites'  # make it under sites app (in admin)

...
  1. Register the model in the admin. (in core.admin)

What we did until now was good enough if you just want to create a site profile model. However, you will want the first profile to be created just after migration. Because the first site is created, but not the first profile related to it. If you donโ€™t want to create it by hand, you need the 3rd step.

  1. Write below code in core.apps.py:
# in core.apps

...
from django.conf import settings
from django.db.models.signals import post_migrate

def create_default_site_profile(sender, **kwargs):
    """after migrations"""
    from django.contrib.sites.models import Site
    from core.models import SiteProfile

    site = Site.objects.get(id=getattr(settings, 'SITE_ID', 1))

    if not SiteProfile.objects.exists():
        SiteProfile.objects.create(site=site)

class CoreConfig(AppConfig):
    name = 'core'

    def ready(self):
        post_migrate.connect(create_default_site_profile, sender=self)
        from .signals import (create_site_profile)  # now create the second signal

The function (create_default_site_profile) will automatically create the first profile related to the first site after migration, using the post_migrate signal. However, you will need another signal (post_save), the last row of the above code.

  1. If you do this step, your SiteProfile model will have a full connection with the Site model. A SiteProfile object is automatically created/updated when any Site object is created/updated. The signal is called from apps.py with the last row.
# in core.signals

from django.contrib.sites.models import Site
from django.db.models.signals import post_save, post_migrate
from django.dispatch import receiver

from .models import SiteProfile


@receiver(post_save, sender=Site)
def create_site_profile(sender, instance, **kwargs):
    """This signal creates/updates a SiteProfile object 
    after creating/updating a Site object.
    """
    siteprofile, created = SiteProfile.objects.update_or_create(
        site=instance
    )

    if not created:
        siteprofile.save()


Would you like to use it on templates? e.g.
{{ site.name }}

Then you need the 5th and 6th steps.

  1. Add the below code in settings.py > TEMPLATES > OPTIONS > context_processors
    'core.context_processors.site_processor'
# in settings.py

TEMPLATES = [
    {
        # ...
        'OPTIONS': {
            'context_processors': [
                # ...

                # custom processor for getting the current site
                'core.context_processors.site_processor',
            ],
        },
    },
]
  1. Create a context_processors.py file in the core app with the code below.
    A try-catch block is needed (catch part) to make it safer. If you delete all sites from the database you will have an error both in admin and on the front end pages. Error is Site matching query does not exist. So the catch block creates one if it is empty.

This solution may not be fully qualified if you have a second site and it is deleted. This solution only creates a site with id=1.

# in core.context_processors

from django.conf import settings
from django.contrib.sites.models import Site


def site_processor(request):
    try:
        return {
            'site': Site.objects.get_current()
        }
    except:
        Site.objects.create(
            id=getattr(settings, 'SITE_ID', 1),
            domain='example.com', name='example.com')

You can now use the site name, domain, meta_name, long_name, or any field you added, in your templates.

# e.g.
{{ site.name }} 
{{ site.profiles.long_name }} 

It normally adds two DB queries, one for File.objects and one for FileProfile.objects. However, as it is mentioned in the docs,
Django is clever enough to cache the current site at the first request and it serves the cached data at the subsequent calls.

https://docs.djangoproject.com/en/3.1/ref/contrib/sites/#caching-the-current-site-object

๐Ÿ‘คPy Data Geek

0๐Ÿ‘

Apparently, you can also create a models.py file in a folder that you add to INSTALLED_APPS, with the following content:

from django.contrib.sites.models import Site as DjangoSite, SiteManager    
from django.core.exceptions import ImproperlyConfigured    
from django.db import models    
from django.http.request import split_domain_port    
    

# our site model
class Site(DjangoSite):    
    settings = models.JSONField(blank=True, default={})    
    port = models.PositiveIntegerField(null=True)    
    protocol = models.CharField(default='http', max_length=5)    
    
    @property    
    def url(self):    
        if self.port:    
            host = f'{self.domain}:{self.port}'    
        else:    
            host = self.domain    
        return f'{self.protocol}://{host}/'    
    

# patch django.contrib.sites.models.Site.objects to use our Site class
DjangoSite.objects.model = Site      
    

# optionnal: override get_current to auto create site instances 
old_get_current = SiteManager.get_current    
def get_current(self, request=None):    
    try:     
        return old_get_current(self, request)    
    except (ImproperlyConfigured, Site.DoesNotExist):    
        if not request:    
            return Site(domain='localhost', name='localhost')    
        host = request.get_host()    
        domain, port = split_domain_port(host)    
        Site.objects.create(    
            name=domain.capitalize(),    
            domain=host,    
            port=port,    
            protocol=request.META['wsgi.url_scheme'],    
        )    
    return old_get_current(self, request)    
SiteManager.get_current = get_current    
๐Ÿ‘คjpic

0๐Ÿ‘

In my opinion, the best way to doing this is by writing a model related to the site model using inheritance

First, add the site id to the Django settings file

SITE_ID = 1

now create a model in one of your apps

from django.db import models
from django.contrib.sites.models import Site

class Settings(Site):
    field_a = models.CharField(max_length=150, null=True)
    field_b = models.CharField(max_length=150, null=True)

    class Meta:
        verbose_name_plural = 'settings'
        db_table = 'core_settings' # core is name of my app

    def __str__(self) -> str:
        return 'Settings'

then edit the apps.py file of that app

from django.apps import AppConfig
from django.db.models.signals import post_migrate

def build_settings(sender, **kwargs):
    from django.contrib.sites.models import Site
    from .models import Settings
    if Settings.objects.count() < 1:
        Settings.objects.create(site_ptr=Site.objects.first())


class CoreConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'project.apps.core'

    def ready(self) -> None:
        post_migrate.connect(build_settings, sender=self)

now every time you run migrations a row will be auto-generated in core_settings that have a one to one relationship with your Site model

and now you can access your settings like this


Site.objects.get_current().settings.access_id

optional: if have only one site
unregister site model from admin site and disable deleting and creating settings model in admin site

from django.contrib import admin
from . import models
from django.contrib.sites.models import Site


admin.site.unregister(Site)

@admin.register(models.Settings)
class SettingAdminModel(admin.ModelAdmin):
    def has_delete_permission(self, request,obj=None) -> bool:
        return False

    def has_add_permission(self, request) -> bool:
        return False
๐Ÿ‘คMahdi mehrabi

Leave a comment