Skip to content

Commit

Permalink
Merge pull request #59 from Kandeel4411/support-multiple-assigned-groups
Browse files Browse the repository at this point in the history
Add support for multiple groups and users
  • Loading branch information
lociii authored Jan 22, 2024
2 parents b77a261 + b29958c commit 42127ba
Show file tree
Hide file tree
Showing 13 changed files with 413 additions and 44 deletions.
14 changes: 11 additions & 3 deletions django_datawatch/admin.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
from django.contrib import admin

from django_datawatch.models import Result, CheckExecution, ResultStatusHistory
from django_datawatch.models import Result, CheckExecution, ResultAssignedGroup, ResultAssignedUser, ResultStatusHistory


class ResultAssignedGroupInline(admin.TabularInline):
model = ResultAssignedGroup
extra = 0

class ResultAssignedUserInline(admin.TabularInline):
model = ResultAssignedUser
extra = 0

@admin.register(Result)
class CheckAdmin(admin.ModelAdmin):
list_display = ('slug', 'identifier', 'status')
readonly_fields = ('created', 'modified')
search_fields = ('slug', 'identifier', 'payload_description')
list_filter = ('status', 'slug', 'assigned_to_group')

list_filter = ('status', 'slug', 'assigned_groups')
inlines = [ResultAssignedGroupInline, ResultAssignedUserInline]

@admin.register(CheckExecution)
class CheckExecutionAdmin(admin.ModelAdmin):
Expand Down
39 changes: 26 additions & 13 deletions django_datawatch/base.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import logging
from contextlib import contextmanager
from typing import Optional, List

from django import forms
from django.contrib.auth.models import AbstractUser, Group
from django.db import transaction
from django.utils import timezone

from django_datawatch.models import Result, CheckExecution, ResultStatusHistory
Expand All @@ -24,7 +27,6 @@ def track_status_history(slug, identifier, new_status):
class DatawatchCheckSkipException(Exception):
pass


class BaseCheckForm(forms.Form):
def save(self, instance):
instance.config = self.cleaned_data
Expand Down Expand Up @@ -60,8 +62,8 @@ class BaseCheck(object):
Any check should inherits from `BaseCheck` and should implements `.generate(self)`
and `.check(self, payload)` methods.
Optionally, you can implements `.get_assigned_user(self, payload)` (resp. `.get_assigned_group(self, payload)`)
to define to which user (resp. group) the system had to assign the check result.
Optionally, you can implements `.get_assigned_users(self, payload)` (resp. `.get_assigned_groups(self, payload)`)
to define to which user(s) (resp. group(s)) the system had to assign the check result.
"""

config_form = None
Expand Down Expand Up @@ -131,17 +133,28 @@ def get_form_class(self):

def save(self, payload, status, data=None, unacknowledge=False):
# build default data
defaults = dict(status=status, data=data, assigned_to_user=self.get_assigned_user(payload, status),
assigned_to_group=self.get_assigned_group(payload, status),
payload_description=self.get_payload_description(payload))
defaults = dict(status=status, data=data, payload_description=self.get_payload_description(payload))

if unacknowledge:
defaults.update(dict(acknowledged_by=None, acknowledged_at=None, acknowledged_until=None))

with track_status_history(self.slug, self.get_identifier(payload), status):
# save the check
dataset, created = Result.objects.update_or_create(
slug=self.slug, identifier=self.get_identifier(payload),
defaults=defaults)
with transaction.atomic():
with track_status_history(self.slug, self.get_identifier(payload), status):
# save the check
dataset, created = Result.objects.update_or_create(
slug=self.slug, identifier=self.get_identifier(payload),
defaults=defaults)

# set assigned users and groups
if groups := self.get_assigned_groups(payload, status):
dataset.assigned_groups.set(groups)
else:
dataset.assigned_groups.clear()

if users := self.get_assigned_users(payload, status):
dataset.assigned_users.set(users)
else:
dataset.assigned_users.clear()

return dataset

Expand Down Expand Up @@ -179,10 +192,10 @@ def get_payload_description(self, payload):
def format_result_data(self, result):
return ''

def get_assigned_user(self, payload, result):
def get_assigned_users(self, payload, result) -> Optional[List[AbstractUser]]:
return None

def get_assigned_group(self, payload, result):
def get_assigned_groups(self, payload, result) -> Optional[List[Group]]:
return None

def get_context_data(self, result):
Expand Down
Binary file modified django_datawatch/locale/de/LC_MESSAGES/django.mo
Binary file not shown.
38 changes: 31 additions & 7 deletions django_datawatch/locale/de/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-09-14 10:02+0000\n"
"POT-Creation-Date: 2024-01-16 01:07+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <[email protected]>\n"
Expand Down Expand Up @@ -72,6 +72,12 @@ msgstr "Ausgeblendet bis"
msgid "Acknowledge reason"
msgstr "Ausblendungsgrund"

msgid "Assigned users"
msgstr "Zugewiesene Benutzer"

msgid "Assigned groups"
msgstr "Zugewiesene Gruppen"

msgid "Result"
msgstr "Ergebnis"

Expand All @@ -84,6 +90,27 @@ msgstr "Zu Status"
msgid "Result status history"
msgstr "Ergebnis-Status-Historie"

msgid "Group"
msgstr "Gruppe"

msgid "Result assigned group"
msgstr "Zugewiesene Gruppe"

msgid "Result assigned groups"
msgstr "Zugewiesene Gruppen"

msgid "Group must be unique across the result"
msgstr "Gruppen müssen je Ergebnis eindeutig sein"

msgid "Result assigned user"
msgstr "Zugewiesener Benutzer"

msgid "Result assigned users"
msgstr "Zugewiesene Benutzer"

msgid "User must be unique across the result"
msgstr "Benutzer müssen je Ergebnis eindeutig sein"

msgid "Check module slug"
msgstr "Modul-Bezeichner"

Expand Down Expand Up @@ -136,6 +163,9 @@ msgstr "Nicht zugewiesen"
msgid "Config form class"
msgstr "Formular-Klasse für Konfiguration"

msgid "Config"
msgstr "Konfiguration"

msgid "Run every"
msgstr "Periodisches Ausführen"

Expand All @@ -152,12 +182,6 @@ msgstr "Maximale Ausblendezeit"
msgid "%(days)s day(s)"
msgstr "%(days)s Tag(e)"

msgid "Assigned group"
msgstr "Zugewiesene Gruppe"

msgid "Assigned user"
msgstr "Zugewiesener Benutzer"

#, python-format
msgid "Acknowledged until %(date)s by %(user)s"
msgstr "Ausgeblendet bis %(date)s durch %(user)s"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Generated by Django 4.2.9 on 2024-01-16 00:05

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('django_datawatch', '0003_resultstatushistory'),
]

operations = [
migrations.CreateModel(
name='ResultAssignedUser',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('result', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='django_datawatch.result', verbose_name='Result')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
options={
'verbose_name': 'Result assigned user',
'verbose_name_plural': 'Result assigned users',
},
),
migrations.CreateModel(
name='ResultAssignedGroup',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.group', verbose_name='Group')),
('result', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='django_datawatch.result', verbose_name='Result')),
],
options={
'verbose_name': 'Result assigned group',
'verbose_name_plural': 'Result assigned groups',
},
),
migrations.AddField(
model_name='result',
name='assigned_groups',
field=models.ManyToManyField(blank=True, related_name='assigned_groups', through='django_datawatch.ResultAssignedGroup', to='auth.group', verbose_name='Assigned groups'),
),
migrations.AddField(
model_name='result',
name='assigned_users',
field=models.ManyToManyField(blank=True, related_name='assigned_results', through='django_datawatch.ResultAssignedUser', to=settings.AUTH_USER_MODEL, verbose_name='Assigned users'),
),
migrations.AddConstraint(
model_name='resultassigneduser',
constraint=models.UniqueConstraint(fields=('result', 'user'), name='unique_result_assigned_user'),
),
migrations.AddConstraint(
model_name='resultassignedgroup',
constraint=models.UniqueConstraint(fields=('result', 'group'), name='unique_result_assigned_group'),
),
]
47 changes: 47 additions & 0 deletions django_datawatch/migrations/0005_migrate_new_assigned_fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Generated by Django 4.2.9 on 2024-01-16 00:06

from django.db import migrations

from django.core.paginator import Paginator

def migrate_new_assigned_fields(apps, schema_editor):
Result = apps.get_model('django_datawatch', 'Result')
ResultAssignedGroup = apps.get_model('django_datawatch', 'ResultAssignedGroup')
ResultAssignedUser = apps.get_model('django_datawatch', 'ResultAssignedUser')

# Paginate the queryset to avoid memory issues
paginator = Paginator(
Result.objects.order_by('pk').only('assigned_to_user', 'assigned_to_group'),
5000,
)

for page_number in paginator.page_range:
page = paginator.page(page_number)
group_instances = []
user_instances = []

for result in page.object_list:
if result.assigned_to_group:
group_instances.append(ResultAssignedGroup(
result_id=result.pk,
group_id=result.assigned_to_group.pk,
))
if result.assigned_to_user:
user_instances.append(ResultAssignedUser(
result_id=result.pk,
user_id=result.assigned_to_user.pk,
))

ResultAssignedGroup.objects.bulk_create(group_instances)
ResultAssignedUser.objects.bulk_create(user_instances)


class Migration(migrations.Migration):

dependencies = [
('django_datawatch', '0004_resultassigneduser_resultassignedgroup_and_more'),
]

operations = [
migrations.RunPython(migrate_new_assigned_fields),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Generated by Django 4.2.9 on 2024-01-16 00:07

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('django_datawatch', '0005_migrate_new_assigned_fields'),
]

operations = [
migrations.RemoveField(
model_name='result',
name='assigned_to_group',
),
migrations.RemoveField(
model_name='result',
name='assigned_to_user',
),
]
43 changes: 40 additions & 3 deletions django_datawatch/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from django.utils import timezone
from django.conf import settings
from django.db import models
from django.core.exceptions import ValidationError
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from django_extensions.db.fields.json import JSONField
Expand Down Expand Up @@ -42,9 +43,9 @@ class Result(TimeStampedModel):
acknowledged_until = models.DateTimeField(null=True, blank=True, verbose_name=_('Acknowledged until'))
acknowledged_reason = models.TextField(blank=True, verbose_name=_('Acknowledge reason'))

assigned_to_user = models.ForeignKey(to=settings.AUTH_USER_MODEL, null=True, blank=True,
related_name='assigned_to_user', on_delete=models.SET_NULL)
assigned_to_group = models.ForeignKey(to='auth.Group', null=True, blank=True, on_delete=models.SET_NULL)
assigned_users = models.ManyToManyField(to=settings.AUTH_USER_MODEL, through='ResultAssignedUser', related_name='assigned_results', blank=True, verbose_name=_('Assigned users'))
assigned_groups = models.ManyToManyField(to='auth.Group', through='ResultAssignedGroup', related_name='assigned_groups', blank=True,
verbose_name=_('Assigned groups'))

objects = ResultQuerySet.as_manager()

Expand Down Expand Up @@ -116,6 +117,42 @@ class Meta:
verbose_name_plural = _('Result status history')


class ResultAssignedGroup(models.Model):
result = models.ForeignKey(Result, on_delete=models.CASCADE, verbose_name=_('Result'))
group = models.ForeignKey(to='auth.Group', verbose_name=_('Group'), on_delete=models.CASCADE)

class Meta:
verbose_name = _('Result assigned group')
verbose_name_plural = _('Result assigned groups')
constraints = [models.UniqueConstraint(fields=['result', 'group'], name='unique_result_assigned_group')]

def validate_unique(self, exclude=None):
if (
ResultAssignedGroup.objects.filter(result_id=self.result_id, group_id=self.group_id)
.exclude(pk=self.pk)
.exists()
):
raise ValidationError({"group": _("Group must be unique across the result")})
super().validate_unique(exclude)

class ResultAssignedUser(models.Model):
result = models.ForeignKey(Result, on_delete=models.CASCADE, verbose_name=_('Result'))
user = models.ForeignKey(to=settings.AUTH_USER_MODEL, verbose_name=_('User'), on_delete=models.CASCADE)

class Meta:
verbose_name = _('Result assigned user')
verbose_name_plural = _('Result assigned users')
constraints = [models.UniqueConstraint(fields=['result', 'user'], name='unique_result_assigned_user')]

def validate_unique(self, exclude=None):
if (
ResultAssignedUser.objects.filter(result_id=self.result_id, user_id=self.user_id)
.exclude(pk=self.pk)
.exists()
):
raise ValidationError({"user": _("User must be unique across the result")})
super().validate_unique(exclude)

class CheckExecution(models.Model):
slug = models.TextField(verbose_name=_('Check module slug'), unique=True)
last_run = models.DateTimeField(verbose_name=_('Last run'))
Expand Down
8 changes: 6 additions & 2 deletions django_datawatch/querysets.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@

class ResultQuerySet(models.QuerySet):
def for_user(self, user):
return self.filter(Q(assigned_to_group__isnull=True) | Q(assigned_to_group__in=user.groups.all()),
Q(assigned_to_user__isnull=True) | Q(assigned_to_user=user))
user_groups = user.groups.all()
return self.filter(
Q(assigned_users=user)
| Q(assigned_groups__in=user_groups)
| Q(assigned_users__isnull=True, assigned_groups__isnull=True)
).distinct()

def failed(self):
return self.exclude(status__in=(self.model.STATUS.unknown, self.model.STATUS.ok))
Expand Down
Loading

0 comments on commit 42127ba

Please sign in to comment.