diff --git a/dbtemplates/admin.py b/dbtemplates/admin.py index f6d65af..1d87362 100644 --- a/dbtemplates/admin.py +++ b/dbtemplates/admin.py @@ -1,13 +1,14 @@ import posixpath from django import forms from django.contrib import admin +from django.contrib.admin.widgets import FilteredSelectMultiple from django.core.exceptions import ImproperlyConfigured from django.utils.translation import gettext_lazy as _ from django.utils.translation import ngettext from django.utils.safestring import mark_safe from dbtemplates.conf import settings -from dbtemplates.models import Template, add_template_to_cache, remove_cached_template +from dbtemplates.models import Template, TemplateSite, add_template_to_cache, remove_cached_template from dbtemplates.utils.template import check_template_syntax # Check if either django-reversion-compare or django-reversion is installed and @@ -80,6 +81,10 @@ elif settings.DBTEMPLATES_USE_REDACTOR: TemplateContentTextArea = RedactorEditor +class TemplateSiteInlineAdmin(admin.TabularInline): + model = TemplateSite + + class TemplateAdminForm(forms.ModelForm): """ @@ -89,11 +94,37 @@ class TemplateAdminForm(forms.ModelForm): widget=TemplateContentTextArea(attrs={'rows': '24'}), help_text=content_help_text, required=False) + # https://stackoverflow.com/a/11658199 + sites = forms.ModelMultipleChoiceField( + queryset=TemplateSite.objects.all(), + required=False, + widget=FilteredSelectMultiple( + verbose_name=_("sites"), + is_stacked=False, + ), + ) + class Meta: model = Template fields = ('name', 'content', 'sites', 'creation_date', 'last_changed') fields = "__all__" + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if self.instance.pk: + self.fields['sites'].initial = self.instance.sites.all() + + def save(self, commit=True, **kwargs): + template = super().save(commit=False, **kwargs) + if commit: + template.save() + + if template.pk: + template.sites = self.cleaned_data['sites'] + self.save_m2m() + + return template + class TemplateAdmin(TemplateModelAdmin): form = TemplateAdminForm @@ -103,17 +134,13 @@ class TemplateAdmin(TemplateModelAdmin): 'fields': ('name', 'content'), 'classes': ('monospace',), }), - (_('Advanced'), { - 'fields': (('sites'),), - }), (_('Date/time'), { 'fields': (('creation_date', 'last_changed'),), 'classes': ('collapse',), }), ) - filter_horizontal = ('sites',) + inlines = [TemplateSiteInlineAdmin] list_display = ('name', 'creation_date', 'last_changed', 'site_list') - list_filter = ('sites',) save_as = True search_fields = ('name', 'content') actions = ['invalidate_cache', 'repopulate_cache', 'check_syntax'] diff --git a/dbtemplates/migrations/0003_templatesite_alter_template_sites_and_more.py b/dbtemplates/migrations/0003_templatesite_alter_template_sites_and_more.py new file mode 100644 index 0000000..750f76e --- /dev/null +++ b/dbtemplates/migrations/0003_templatesite_alter_template_sites_and_more.py @@ -0,0 +1,44 @@ +# Generated by Django 5.2 on 2025-05-29 20:49 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dbtemplates', '0002_alter_template_creation_date_and_more'), + ('sites', '0002_alter_domain_unique'), + ] + + operations = [ + migrations.SeparateDatabaseAndState( + database_operations=[], + state_operations=[ + migrations.CreateModel( + name='TemplateSite', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sites.site')), + ('template', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dbtemplates.template')), + ], + options={ + 'db_table': 'django_template_sites', + }, + ), + migrations.AlterField( + model_name='template', + name='sites', + field=models.ManyToManyField(blank=True, through='dbtemplates.TemplateSite', to='sites.site', verbose_name='sites'), + ), + migrations.AddConstraint( + model_name='templatesite', + constraint=models.UniqueConstraint(fields=('template', 'site'), name='django_template_sites_template_id_site_id_d00c11a4_uniq'), + ), + migrations.AddConstraint( + model_name='templatesite', + constraint=models.UniqueConstraint(fields=('template__name', 'site'), name='template_name_site_uniq'), + ), + ], + ), + ] diff --git a/dbtemplates/models.py b/dbtemplates/models.py index aa87dcb..d9dce2f 100644 --- a/dbtemplates/models.py +++ b/dbtemplates/models.py @@ -9,10 +9,36 @@ from django.contrib.sites.managers import CurrentSiteManager from django.contrib.sites.models import Site from django.db import models from django.db.models import signals +from django.db.models.constraints import UniqueConstraint from django.template import TemplateDoesNotExist from django.utils.translation import gettext_lazy as _ +class TemplateSite(models.Model): + """ + Defines an explicit through model between Template and Site models. + This explicit model allows us to add a unique_together index between + Site and Template.name + """ + template = models.ForeignKey('Template', on_delete=models.CASCADE) + site = models.ForeignKey(Site, on_delete=models.CASCADE) + + class Meta: + db_table = 'django_template_sites' + constraints = [ + UniqueConstraint( + # Preserve backwards compatibility so we don't have to drop the + # old unique index and recreate this index from scratch + name='django_template_sites_template_id_site_id_d00c11a4_uniq', + fields=['template', 'site'], + ), + UniqueConstraint( + name='template_name_site_uniq', + fields=['template__name', 'site'], + ), + ] + + class Template(models.Model): """ Defines a template model for use with the database template loader. @@ -24,7 +50,7 @@ class Template(models.Model): help_text=_("Example: 'flatpages/default.html'")) content = models.TextField(_('content'), blank=True) sites = models.ManyToManyField(Site, verbose_name=_('sites'), - blank=True) + blank=True, through=TemplateSite) creation_date = models.DateTimeField(_('creation date'), auto_now_add=True) last_changed = models.DateTimeField(_('last changed'), auto_now=True)