Skip to content

Commit

Permalink
Merge pull request #1218 from rdmorganiser/feat-sync-project-views-an…
Browse files Browse the repository at this point in the history
…d-tasks

signal handlers for syncing of project views and tasks 
Related issues: #966, #1198, #345, #431
  • Loading branch information
MyPyDavid authored Feb 20, 2025
2 parents 5dff860 + 7fc4740 commit 7ada4e1
Show file tree
Hide file tree
Showing 30 changed files with 846 additions and 197 deletions.
7 changes: 5 additions & 2 deletions rdmo/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,9 @@
'PROJECT_IMPORTS',
'PROJECT_IMPORTS_LIST',
'PROJECT_SEND_ISSUE',
'NESTED_PROJECTS'
'NESTED_PROJECTS',
'PROJECT_VIEWS_SYNC',
'PROJECT_TASKS_SYNC'
]

SETTINGS_API = [
Expand Down Expand Up @@ -341,7 +343,8 @@

PROJECT_SEND_INVITE = True

PROJECT_REMOVE_VIEWS = True
PROJECT_VIEWS_SYNC = False
PROJECT_TASKS_SYNC = False

PROJECT_CREATE_RESTRICTED = False
PROJECT_CREATE_GROUPS = []
Expand Down
6 changes: 4 additions & 2 deletions rdmo/projects/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,7 @@ class ProjectsConfig(AppConfig):
def ready(self):
from . import rules # noqa: F401

if settings.PROJECT_REMOVE_VIEWS:
from . import handlers # noqa: F401
if settings.PROJECT_VIEWS_SYNC:
from .handlers import m2m_changed_views, project_save_views # noqa: F401
if settings.PROJECT_TASKS_SYNC:
from .handlers import m2m_changed_tasks, project_save_tasks # noqa: F401
22 changes: 22 additions & 0 deletions rdmo/projects/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,12 +160,21 @@ class Meta:
'catalog': forms.RadioSelect()
}

def save(self, *args, **kwargs):
# if the catalog is the same, do nothing
if self.instance.catalog.id == self.cleaned_data.get('catalog'):
return self.instance
return super().save(*args, **kwargs)


class ProjectUpdateTasksForm(forms.ModelForm):

use_required_attribute = False

def __init__(self, *args, **kwargs):
if settings.PROJECT_TASKS_SYNC:
raise ValidationError(_("Editing tasks is disabled."))

tasks = kwargs.pop('tasks')
super().__init__(*args, **kwargs)
self.fields['tasks'].queryset = tasks
Expand All @@ -180,12 +189,20 @@ class Meta:
'tasks': forms.CheckboxSelectMultiple()
}

def save(self, *args, **kwargs):
if settings.PROJECT_TASKS_SYNC:
raise ValidationError(_("Editing tasks is disabled."))
super().save(*args, **kwargs)


class ProjectUpdateViewsForm(forms.ModelForm):

use_required_attribute = False

def __init__(self, *args, **kwargs):
if settings.PROJECT_VIEWS_SYNC:
raise ValidationError(_("Editing views is disabled."))

views = kwargs.pop('views')
super().__init__(*args, **kwargs)
self.fields['views'].queryset = views
Expand All @@ -200,6 +217,11 @@ class Meta:
'views': forms.CheckboxSelectMultiple()
}

def save(self, *args, **kwargs):
if settings.PROJECT_VIEWS_SYNC:
raise ValidationError(_("Editing views is disabled."))
super().save(*args, **kwargs)


class ProjectUpdateParentForm(forms.ModelForm):

Expand Down
60 changes: 0 additions & 60 deletions rdmo/projects/handlers.py

This file was deleted.

Empty file.
110 changes: 110 additions & 0 deletions rdmo/projects/handlers/generic_handlers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
from django.contrib.auth.models import Group, User
from django.contrib.sites.models import Site

from rdmo.projects.models import Membership, Project
from rdmo.questions.models import Catalog


def m2m_catalogs_changed_projects_sync_signal_handler(instance, action, pk_set, project_field):

if action == 'post_remove' and pk_set:
projects_to_change = (
Project.objects
.filter_catalogs(catalogs=Catalog.objects.filter(pk__in=pk_set))
.filter(**{project_field: instance})
)
for project in projects_to_change: # remove instance from project
getattr(project, project_field).remove(instance)

elif action == 'post_clear':
projects_to_change = Project.objects.filter(**{project_field: instance})
for project in projects_to_change: # remove instance from project
getattr(project, project_field).remove(instance)

elif action == 'post_add' and pk_set:
projects_to_change = (
Project.objects
.filter_catalogs(catalogs=Catalog.objects.filter(pk__in=pk_set))
.exclude(**{project_field: instance})
)
for project in projects_to_change: # add instance to project
getattr(project, project_field).add(instance)


def m2m_sites_changed_projects_sync_signal_handler(instance, action, pk_set, project_field):

if action == 'post_remove' and pk_set:
projects_to_change = (
Project.objects
.filter_catalogs(catalogs=instance.catalogs.all())
.filter(site__in=Site.objects.filter(pk__in=pk_set))
.filter(**{project_field: instance})
)
for project in projects_to_change: # remove instance from project
getattr(project, project_field).remove(instance)

elif action == 'post_clear':
projects_to_change = (
Project.objects
.filter_catalogs()
.filter(**{project_field: instance})
)
for project in projects_to_change: # remove instance from project
getattr(project, project_field).remove(instance)

elif action == 'post_add' and pk_set:
projects_to_change = (
Project.objects
.filter_catalogs(catalogs=instance.catalogs.all())
.filter(site__in=Site.objects.filter(pk__in=pk_set))
.exclude(**{project_field: instance})
)
for project in projects_to_change: # add instance to project
getattr(project, project_field).add(instance)


def m2m_groups_changed_projects_sync_signal_handler(instance, action, pk_set, project_field):

if action == 'post_remove' and pk_set:
related_groups = Group.objects.filter(pk__in=pk_set)
users = User.objects.filter(groups__in=related_groups)
memberships = (
Membership.objects
.filter(role='owner', user__in=users)
.values_list('id', flat=True)
)
projects_to_change = (
Project.objects
.filter_catalogs(catalogs=instance.catalogs.all())
.filter(memberships__in=memberships)
.filter(**{project_field: instance})
)
for project in projects_to_change: # remove instance from project
getattr(project, project_field).remove(instance)

elif action == 'post_clear':
# Remove all linked projects regardless of catalogs
projects_to_change = (
Project.objects
.filter_catalogs()
.filter(**{project_field: instance})
)
for project in projects_to_change: # remove instance from project
getattr(project, project_field).remove(instance)

elif action == 'post_add' and pk_set:
related_groups = Group.objects.filter(pk__in=pk_set)
users = User.objects.filter(groups__in=related_groups)
memberships = (
Membership.objects
.filter(role='owner', user__in=users)
.values_list('id', flat=True)
)
projects_to_change = (
Project.objects
.filter_catalogs(catalogs=instance.catalogs.all())
.filter(memberships__in=memberships)
.exclude(**{project_field: instance})
)
for project in projects_to_change: # add instance to project
getattr(project, project_field).add(instance)
25 changes: 25 additions & 0 deletions rdmo/projects/handlers/m2m_changed_tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from django.db.models.signals import m2m_changed
from django.dispatch import receiver

from rdmo.tasks.models import Task

from .generic_handlers import (
m2m_catalogs_changed_projects_sync_signal_handler,
m2m_groups_changed_projects_sync_signal_handler,
m2m_sites_changed_projects_sync_signal_handler,
)


@receiver(m2m_changed, sender=Task.catalogs.through)
def m2m_changed_task_catalog_signal(sender, instance, action, pk_set, **kwargs):
m2m_catalogs_changed_projects_sync_signal_handler(instance, action, pk_set, 'tasks')


@receiver(m2m_changed, sender=Task.sites.through)
def m2m_changed_task_sites_signal(sender, instance, action, pk_set, **kwargs):
m2m_sites_changed_projects_sync_signal_handler(instance, action, pk_set, 'tasks')


@receiver(m2m_changed, sender=Task.groups.through)
def m2m_changed_task_groups_signal(sender, instance, action, pk_set, **kwargs):
m2m_groups_changed_projects_sync_signal_handler(instance, action, pk_set, 'tasks')
25 changes: 25 additions & 0 deletions rdmo/projects/handlers/m2m_changed_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from django.db.models.signals import m2m_changed
from django.dispatch import receiver

from rdmo.views.models import View

from .generic_handlers import (
m2m_catalogs_changed_projects_sync_signal_handler,
m2m_groups_changed_projects_sync_signal_handler,
m2m_sites_changed_projects_sync_signal_handler,
)


@receiver(m2m_changed, sender=View.catalogs.through)
def m2m_changed_view_catalog_signal(sender, instance, action, pk_set, **kwargs):
m2m_catalogs_changed_projects_sync_signal_handler(instance, action, pk_set, 'views')


@receiver(m2m_changed, sender=View.sites.through)
def m2m_changed_view_sites_signal(sender, instance, action, pk_set, **kwargs):
m2m_sites_changed_projects_sync_signal_handler(instance, action, pk_set, 'views')


@receiver(m2m_changed, sender=View.groups.through)
def m2m_changed_view_groups_signal(sender, instance, action, pk_set, **kwargs):
m2m_groups_changed_projects_sync_signal_handler(instance, action, pk_set, 'views')
36 changes: 36 additions & 0 deletions rdmo/projects/handlers/project_save_tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from django.db.models.signals import post_save, pre_save
from django.dispatch import receiver

from rdmo.projects.models import Project
from rdmo.tasks.models import Task


@receiver(pre_save, sender=Project)
def pre_save_project_sync_tasks_from_catalog(sender, instance, raw, update_fields, **kwargs):
instance._catalog_has_changed_sync_tasks = False

if raw or (update_fields and 'catalog' not in update_fields):
return

if instance.id is not None:
# Fetch the original catalog from the database
if sender.objects.get(id=instance.id).catalog == instance.catalog:
# Do nothing if the catalog has not changed
return

# Defer synchronization of views
instance._catalog_has_changed_sync_tasks = True


@receiver(post_save, sender=Project)
def post_save_project_sync_tasks_from_catalog(sender, instance, created, raw, update_fields, **kwargs):
if raw or (update_fields and 'catalog' not in update_fields):
return

if instance._catalog_has_changed_sync_tasks or (created and not instance.tasks.exists) :
instance.tasks.set(
Task.objects
.filter_for_project(instance)
.filter(available=True)
.values_list('id', flat=True)
)
36 changes: 36 additions & 0 deletions rdmo/projects/handlers/project_save_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from django.db.models.signals import post_save, pre_save
from django.dispatch import receiver

from rdmo.projects.models import Project
from rdmo.views.models import View


@receiver(pre_save, sender=Project)
def pre_save_project_sync_views_from_catalog(sender, instance, raw, update_fields, **kwargs):
instance._catalog_has_changed_sync_views = False

if raw or (update_fields and 'catalog' not in update_fields):
return

if instance.id is not None:
# Fetch the original catalog from the database
if sender.objects.get(id=instance.id).catalog == instance.catalog:
# Do nothing if the catalog has not changed
return

# Defer synchronization of views
instance._catalog_has_changed_sync_views = True


@receiver(post_save, sender=Project)
def post_save_project_sync_views_from_catalog(sender, instance, created, raw, update_fields, **kwargs):
if raw or (update_fields and 'catalog' not in update_fields):
return

if instance._catalog_has_changed_sync_views or (created and not instance.views.exists):
instance.views.set(
View.objects
.filter_for_project(instance)
.filter(available=True)
.values_list('id', flat=True)
)
Loading

0 comments on commit 7ada4e1

Please sign in to comment.