snapshot: init models (#86634)

This commit is contained in:
Lauréline Guérin 2024-02-16 11:21:20 +01:00
parent f6a0b58167
commit 3f8146c092
No known key found for this signature in database
GPG Key ID: 1FAB9B9B4F93D473
8 changed files with 549 additions and 15 deletions

View File

@ -0,0 +1,118 @@
import django.db.models.deletion
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('snapshot', '0002_snapshot_models'),
('agendas', '0170_alter_agenda_events_type'),
]
operations = [
migrations.AddField(
model_name='agenda',
name='created_at',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
preserve_default=False,
),
migrations.AddField(
model_name='agenda',
name='snapshot',
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name='temporary_instance',
to='snapshot.agendasnapshot',
),
),
migrations.AddField(
model_name='agenda',
name='updated_at',
field=models.DateTimeField(auto_now=True),
),
migrations.AddField(
model_name='category',
name='created_at',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
preserve_default=False,
),
migrations.AddField(
model_name='category',
name='snapshot',
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name='temporary_instance',
to='snapshot.categorysnapshot',
),
),
migrations.AddField(
model_name='category',
name='updated_at',
field=models.DateTimeField(auto_now=True),
),
migrations.AddField(
model_name='eventstype',
name='created_at',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
preserve_default=False,
),
migrations.AddField(
model_name='eventstype',
name='snapshot',
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name='temporary_instance',
to='snapshot.eventstypesnapshot',
),
),
migrations.AddField(
model_name='eventstype',
name='updated_at',
field=models.DateTimeField(auto_now=True),
),
migrations.AddField(
model_name='resource',
name='created_at',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
preserve_default=False,
),
migrations.AddField(
model_name='resource',
name='snapshot',
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name='temporary_instance',
to='snapshot.resourcesnapshot',
),
),
migrations.AddField(
model_name='resource',
name='updated_at',
field=models.DateTimeField(auto_now=True),
),
migrations.AddField(
model_name='unavailabilitycalendar',
name='created_at',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
preserve_default=False,
),
migrations.AddField(
model_name='unavailabilitycalendar',
name='snapshot',
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name='temporary_instance',
to='snapshot.unavailabilitycalendarsnapshot',
),
),
migrations.AddField(
model_name='unavailabilitycalendar',
name='updated_at',
field=models.DateTimeField(auto_now=True),
),
]

View File

@ -77,6 +77,15 @@ from django.utils.translation import gettext_lazy as _
from django.utils.translation import ngettext, pgettext_lazy
from chrono.apps.export_import.models import WithApplicationMixin
from chrono.apps.snapshot.models import (
AgendaSnapshot,
CategorySnapshot,
EventsTypeSnapshot,
ResourceSnapshot,
UnavailabilityCalendarSnapshot,
WithSnapshotManager,
WithSnapshotMixin,
)
from chrono.utils.date import get_weekday_index
from chrono.utils.db import ArraySubquery, SumCardinality
from chrono.utils.interval import Interval, IntervalSet
@ -173,7 +182,12 @@ TimeSlot = collections.namedtuple(
)
class Agenda(WithApplicationMixin, models.Model):
class Agenda(WithSnapshotMixin, WithApplicationMixin, models.Model):
# mark temporarily restored snapshots
snapshot = models.ForeignKey(
AgendaSnapshot, on_delete=models.CASCADE, null=True, related_name='temporary_instance'
)
label = models.CharField(_('Label'), max_length=150)
slug = models.SlugField(_('Identifier'), max_length=160, unique=True)
kind = models.CharField(_('Kind'), max_length=20, choices=AGENDA_KINDS, default='events')
@ -309,10 +323,16 @@ class Agenda(WithApplicationMixin, models.Model):
validators=[MaxValueValidator(59)],
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
application_component_type = 'agendas'
application_label_singular = _('Agenda')
application_label_plural = _('Agendas')
objects = WithSnapshotManager()
snapshots = WithSnapshotManager(snapshots=True)
class Meta:
ordering = ['label']
@ -502,7 +522,7 @@ class Agenda(WithApplicationMixin, models.Model):
return agenda
@classmethod
def import_json(cls, data, overwrite=False):
def import_json(cls, data, overwrite=False, snapshot=None):
data = copy.deepcopy(data)
permissions = data.pop('permissions') or {}
reminder_settings = data.pop('reminder_settings', None)
@ -536,7 +556,13 @@ class Agenda(WithApplicationMixin, models.Model):
data['events_type'] = EventsType.objects.get(slug=data['events_type'])
except EventsType.DoesNotExist:
raise AgendaImportError(_('Missing "%s" events type') % data['events_type'])
agenda, created = cls.objects.update_or_create(slug=data['slug'], defaults=data)
slug = data.pop('slug')
qs_kwargs = {}
if snapshot:
qs_kwargs = {'snapshot': snapshot}
else:
qs_kwargs = {'slug': slug}
agenda, created = cls.objects.update_or_create(defaults=data, **qs_kwargs)
if overwrite:
AgendaReminderSettings.objects.filter(agenda=agenda).delete()
if reminder_settings:
@ -2817,15 +2843,26 @@ class Event(models.Model):
return custom_fields
class EventsType(WithApplicationMixin, models.Model):
class EventsType(WithSnapshotMixin, WithApplicationMixin, models.Model):
# mark temporarily restored snapshots
snapshot = models.ForeignKey(
EventsTypeSnapshot, on_delete=models.CASCADE, null=True, related_name='temporary_instance'
)
slug = models.SlugField(_('Identifier'), max_length=160, unique=True)
label = models.CharField(_('Label'), max_length=150)
custom_fields = models.JSONField(blank=True, default=list)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
application_component_type = 'events_types'
application_label_singular = _('Events type')
application_label_plural = _('Events types')
objects = WithSnapshotManager()
snapshots = WithSnapshotManager(snapshots=True)
def __str__(self):
return self.label
@ -2861,10 +2898,15 @@ class EventsType(WithApplicationMixin, models.Model):
return []
@classmethod
def import_json(cls, data, overwrite=False):
def import_json(cls, data, overwrite=False, snapshot=None):
data = clean_import_data(cls, data)
slug = data.pop('slug')
events_type, created = cls.objects.update_or_create(slug=slug, defaults=data)
qs_kwargs = {}
if snapshot:
qs_kwargs = {'snapshot': snapshot}
else:
qs_kwargs = {'slug': slug}
events_type, created = cls.objects.update_or_create(defaults=data, **qs_kwargs)
return created, events_type
def export_json(self):
@ -3432,15 +3474,26 @@ class Desk(models.Model):
).delete() # source was not in settings anymore
class Resource(WithApplicationMixin, models.Model):
class Resource(WithSnapshotMixin, WithApplicationMixin, models.Model):
# mark temporarily restored snapshots
snapshot = models.ForeignKey(
ResourceSnapshot, on_delete=models.CASCADE, null=True, related_name='temporary_instance'
)
slug = models.SlugField(_('Identifier'), max_length=160, unique=True)
label = models.CharField(_('Label'), max_length=150)
description = models.TextField(_('Description'), blank=True, help_text=_('Optional description.'))
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
application_component_type = 'resources'
application_label_singular = _('Resource')
application_label_plural = _('Resources')
objects = WithSnapshotManager()
snapshots = WithSnapshotManager(snapshots=True)
def __str__(self):
return self.label
@ -3466,10 +3519,15 @@ class Resource(WithApplicationMixin, models.Model):
return []
@classmethod
def import_json(cls, data, overwrite=False):
def import_json(cls, data, overwrite=False, snapshot=None):
data = clean_import_data(cls, data)
slug = data.pop('slug')
resource, created = cls.objects.update_or_create(slug=slug, defaults=data)
qs_kwargs = {}
if snapshot:
qs_kwargs = {'snapshot': snapshot}
else:
qs_kwargs = {'slug': slug}
resource, created = cls.objects.update_or_create(defaults=data, **qs_kwargs)
return created, resource
def export_json(self):
@ -3480,14 +3538,25 @@ class Resource(WithApplicationMixin, models.Model):
}
class Category(WithApplicationMixin, models.Model):
class Category(WithSnapshotMixin, WithApplicationMixin, models.Model):
# mark temporarily restored snapshots
snapshot = models.ForeignKey(
CategorySnapshot, on_delete=models.CASCADE, null=True, related_name='temporary_instance'
)
slug = models.SlugField(_('Identifier'), max_length=160, unique=True)
label = models.CharField(_('Label'), max_length=150)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
application_component_type = 'agendas_categories'
application_label_singular = _('Category (agendas)')
application_label_plural = _('Categories (agendas)')
objects = WithSnapshotManager()
snapshots = WithSnapshotManager(snapshots=True)
def __str__(self):
return self.label
@ -3507,10 +3576,15 @@ class Category(WithApplicationMixin, models.Model):
return []
@classmethod
def import_json(cls, data, overwrite=False):
def import_json(cls, data, overwrite=False, snapshot=None):
data = clean_import_data(cls, data)
slug = data.pop('slug')
category, created = cls.objects.update_or_create(slug=slug, defaults=data)
qs_kwargs = {}
if snapshot:
qs_kwargs = {'snapshot': snapshot}
else:
qs_kwargs = {'slug': slug}
category, created = cls.objects.update_or_create(defaults=data, **qs_kwargs)
return created, category
def export_json(self):
@ -3796,7 +3870,12 @@ class TimePeriodExceptionSource(models.Model):
}
class UnavailabilityCalendar(WithApplicationMixin, models.Model):
class UnavailabilityCalendar(WithSnapshotMixin, WithApplicationMixin, models.Model):
# mark temporarily restored snapshots
snapshot = models.ForeignKey(
UnavailabilityCalendarSnapshot, on_delete=models.CASCADE, null=True, related_name='temporary_instance'
)
label = models.CharField(_('Label'), max_length=150)
slug = models.SlugField(_('Identifier'), max_length=160, unique=True)
desks = models.ManyToManyField(Desk, related_name='unavailability_calendars')
@ -3819,10 +3898,16 @@ class UnavailabilityCalendar(WithApplicationMixin, models.Model):
on_delete=models.SET_NULL,
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
application_component_type = 'unavailability_calendars'
application_label_singular = _('Unavailability calendar')
application_label_plural = _('Unavailability calendars')
objects = WithSnapshotManager()
snapshots = WithSnapshotManager(snapshots=True)
class Meta:
ordering = ['label']
@ -3870,7 +3955,7 @@ class UnavailabilityCalendar(WithApplicationMixin, models.Model):
return unavailability_calendar
@classmethod
def import_json(cls, data, overwrite=False):
def import_json(cls, data, overwrite=False, snapshot=None):
data = data.copy()
permissions = data.pop('permissions', {})
exceptions = data.pop('exceptions', [])
@ -3878,7 +3963,13 @@ class UnavailabilityCalendar(WithApplicationMixin, models.Model):
if permissions.get(permission):
data[permission + '_role'] = Group.objects.get(name=permissions[permission])
data = clean_import_data(cls, data)
unavailability_calendar, created = cls.objects.update_or_create(slug=data['slug'], defaults=data)
slug = data.pop('slug')
qs_kwargs = {}
if snapshot:
qs_kwargs = {'snapshot': snapshot}
else:
qs_kwargs = {'slug': slug}
unavailability_calendar, created = cls.objects.update_or_create(defaults=data, **qs_kwargs)
if overwrite:
TimePeriodException.objects.filter(unavailability_calendar=unavailability_calendar).delete()
for exception in exceptions:

View File

View File

@ -0,0 +1,7 @@
from django.db import migrations
class Migration(migrations.Migration):
dependencies = []
operations = []

View File

@ -0,0 +1,186 @@
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('agendas', '0170_alter_agenda_events_type'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('snapshot', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='UnavailabilityCalendarSnapshot',
fields=[
(
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
('timestamp', models.DateTimeField(auto_now_add=True)),
('comment', models.TextField(blank=True, null=True)),
('serialization', models.JSONField(blank=True, default=dict)),
('label', models.CharField(blank=True, max_length=150, verbose_name='Label')),
('application_slug', models.CharField(max_length=100, null=True)),
('application_version', models.CharField(max_length=100, null=True)),
(
'instance',
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to='agendas.unavailabilitycalendar',
related_name='instance_snapshots',
),
),
(
'user',
models.ForeignKey(
null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
),
),
],
options={
'ordering': ('-timestamp',),
'abstract': False,
},
),
migrations.CreateModel(
name='ResourceSnapshot',
fields=[
(
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
('timestamp', models.DateTimeField(auto_now_add=True)),
('comment', models.TextField(blank=True, null=True)),
('serialization', models.JSONField(blank=True, default=dict)),
('label', models.CharField(blank=True, max_length=150, verbose_name='Label')),
('application_slug', models.CharField(max_length=100, null=True)),
('application_version', models.CharField(max_length=100, null=True)),
(
'instance',
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to='agendas.resource',
related_name='instance_snapshots',
),
),
(
'user',
models.ForeignKey(
null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
),
),
],
options={
'ordering': ('-timestamp',),
'abstract': False,
},
),
migrations.CreateModel(
name='EventsTypeSnapshot',
fields=[
(
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
('timestamp', models.DateTimeField(auto_now_add=True)),
('comment', models.TextField(blank=True, null=True)),
('serialization', models.JSONField(blank=True, default=dict)),
('label', models.CharField(blank=True, max_length=150, verbose_name='Label')),
('application_slug', models.CharField(max_length=100, null=True)),
('application_version', models.CharField(max_length=100, null=True)),
(
'instance',
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to='agendas.eventstype',
related_name='instance_snapshots',
),
),
(
'user',
models.ForeignKey(
null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
),
),
],
options={
'ordering': ('-timestamp',),
'abstract': False,
},
),
migrations.CreateModel(
name='CategorySnapshot',
fields=[
(
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
('timestamp', models.DateTimeField(auto_now_add=True)),
('comment', models.TextField(blank=True, null=True)),
('serialization', models.JSONField(blank=True, default=dict)),
('label', models.CharField(blank=True, max_length=150, verbose_name='Label')),
('application_slug', models.CharField(max_length=100, null=True)),
('application_version', models.CharField(max_length=100, null=True)),
(
'instance',
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to='agendas.category',
related_name='instance_snapshots',
),
),
(
'user',
models.ForeignKey(
null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
),
),
],
options={
'ordering': ('-timestamp',),
'abstract': False,
},
),
migrations.CreateModel(
name='AgendaSnapshot',
fields=[
(
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
('timestamp', models.DateTimeField(auto_now_add=True)),
('comment', models.TextField(blank=True, null=True)),
('serialization', models.JSONField(blank=True, default=dict)),
('label', models.CharField(blank=True, max_length=150, verbose_name='Label')),
('application_slug', models.CharField(max_length=100, null=True)),
('application_version', models.CharField(max_length=100, null=True)),
(
'instance',
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to='agendas.agenda',
related_name='instance_snapshots',
),
),
(
'user',
models.ForeignKey(
null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
),
),
],
options={
'ordering': ('-timestamp',),
'abstract': False,
},
),
]

View File

@ -0,0 +1,131 @@
# chrono - agendas system
# Copyright (C) 2016-2024 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.conf import settings
from django.db import models
from django.utils.translation import gettext_lazy as _
class WithSnapshotManager(models.Manager):
snapshots = False
def __init__(self, *args, **kwargs):
self.snapshots = kwargs.pop('snapshots', False)
super().__init__(*args, **kwargs)
def get_queryset(self):
queryset = super().get_queryset()
if self.snapshots:
return queryset.filter(snapshot__isnull=False)
else:
return queryset.filter(snapshot__isnull=True)
class WithSnapshotMixin:
@classmethod
def get_snapshot_model(cls):
return cls._meta.get_field('snapshot').related_model
def take_snapshot(self, *args, **kwargs):
self.get_snapshot_model().take(self, *args, **kwargs)
class AbstractSnapshot(models.Model):
timestamp = models.DateTimeField(auto_now_add=True)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True)
comment = models.TextField(blank=True, null=True)
serialization = models.JSONField(blank=True, default=dict)
label = models.CharField(_('Label'), max_length=150, blank=True)
application_slug = models.CharField(max_length=100, null=True)
application_version = models.CharField(max_length=100, null=True)
class Meta:
abstract = True
ordering = ('-timestamp',)
@classmethod
def get_instance_model(cls):
return cls._meta.get_field('instance').related_model
@classmethod
def take(cls, instance, request=None, comment=None, deletion=False, label=None, application=None):
snapshot = cls(instance=instance, comment=comment, label=label or '')
if request and not request.user.is_anonymous:
snapshot.user = request.user
if not deletion:
snapshot.serialization = instance.export_json()
else:
snapshot.serialization = {}
snapshot.comment = comment or _('deletion')
if application:
snapshot.application_slug = application.slug
snapshot.application_version = application.version_number
snapshot.save()
def get_instance(self):
try:
# try reusing existing instance
return self.get_instance_model().snapshots.get(snapshot=self)
except self.get_instance_model().DoesNotExist:
return self.load_instance(self.serialization, snapshot=self)
def load_instance(self, json_instance, snapshot=None):
return self.get_instance_model().import_json(json_instance, snapshot=snapshot)[1]
class AgendaSnapshot(AbstractSnapshot):
instance = models.ForeignKey(
'agendas.Agenda',
on_delete=models.SET_NULL,
null=True,
related_name='instance_snapshots',
)
class CategorySnapshot(AbstractSnapshot):
instance = models.ForeignKey(
'agendas.Category',
on_delete=models.SET_NULL,
null=True,
related_name='instance_snapshots',
)
class EventsTypeSnapshot(AbstractSnapshot):
instance = models.ForeignKey(
'agendas.EventsType',
on_delete=models.SET_NULL,
null=True,
related_name='instance_snapshots',
)
class ResourceSnapshot(AbstractSnapshot):
instance = models.ForeignKey(
'agendas.Resource',
on_delete=models.SET_NULL,
null=True,
related_name='instance_snapshots',
)
class UnavailabilityCalendarSnapshot(AbstractSnapshot):
instance = models.ForeignKey(
'agendas.UnavailabilityCalendar',
on_delete=models.SET_NULL,
null=True,
related_name='instance_snapshots',
)

View File

@ -62,6 +62,7 @@ INSTALLED_APPS = (
'chrono.manager',
'chrono.apps.ants_hub',
'chrono.apps.export_import',
'chrono.apps.snapshot',
)
MIDDLEWARE = (