Skip to content

Commit

Permalink
Adds orgnaisation relations, filtering and permissions (#1658)
Browse files Browse the repository at this point in the history
* Adds links and filters to organizations

* Adds activity overview to an organization

* Adds filtering tests

* Assigns view permissions to the organisations groups to related objects
  • Loading branch information
jmsmkn authored Dec 17, 2020
1 parent 51a2392 commit 57f803f
Show file tree
Hide file tree
Showing 21 changed files with 482 additions and 12 deletions.
2 changes: 2 additions & 0 deletions app/grandchallenge/algorithms/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class Meta:
"publications",
"modalities",
"structures",
"organizations",
"logo",
"public",
"workstation",
Expand All @@ -54,6 +55,7 @@ class Meta:
"publications": Select2MultipleWidget,
"modalities": Select2MultipleWidget,
"structures": Select2MultipleWidget,
"organizations": Select2MultipleWidget,
}
help_texts = {
"workstation_config": format_lazy(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 3.1.1 on 2020-12-17 06:53

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("organizations", "0001_initial"),
("algorithms", "0002_auto_20201214_0939"),
]

operations = [
migrations.AddField(
model_name="algorithm",
name="organizations",
field=models.ManyToManyField(
blank=True,
help_text="The organizations associated with this algorithm",
related_name="algorithms",
to="organizations.Organization",
),
),
]
7 changes: 7 additions & 0 deletions app/grandchallenge/algorithms/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
from grandchallenge.core.templatetags.bleach import md2html
from grandchallenge.evaluation.utils import get
from grandchallenge.modalities.models import ImagingModality
from grandchallenge.organizations.models import Organization
from grandchallenge.publications.models import Publication
from grandchallenge.subdomains.utils import reverse
from grandchallenge.workstations.models import Workstation
Expand Down Expand Up @@ -130,6 +131,12 @@ class Algorithm(UUIDModel, TitleSlugDescriptionModel):
blank=True,
help_text="The structures supported by this algorithm",
)
organizations = models.ManyToManyField(
Organization,
blank=True,
help_text="The organizations associated with this algorithm",
related_name="algorithms",
)
credits_per_job = models.PositiveIntegerField(
default=0,
help_text=(
Expand Down
2 changes: 2 additions & 0 deletions app/grandchallenge/archives/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class Meta:
"publications",
"modalities",
"structures",
"organizations",
"logo",
"workstation",
"workstation_config",
Expand All @@ -62,6 +63,7 @@ class Meta:
"publications": Select2MultipleWidget,
"modalities": Select2MultipleWidget,
"structures": Select2MultipleWidget,
"organizations": Select2MultipleWidget,
}
help_texts = {
"workstation_config": format_lazy(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 3.1.1 on 2020-12-17 06:53

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("organizations", "0001_initial"),
("archives", "0003_auto_20201215_0931"),
]

operations = [
migrations.AddField(
model_name="archive",
name="organizations",
field=models.ManyToManyField(
blank=True,
help_text="The organizations associated with this archive",
related_name="archives",
to="organizations.Organization",
),
),
]
7 changes: 7 additions & 0 deletions app/grandchallenge/archives/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from grandchallenge.core.models import RequestBase, UUIDModel
from grandchallenge.core.storage import get_logo_path, public_s3_storage
from grandchallenge.modalities.models import ImagingModality
from grandchallenge.organizations.models import Organization
from grandchallenge.patients.models import Patient
from grandchallenge.publications.models import Publication
from grandchallenge.studies.models import Study
Expand Down Expand Up @@ -76,6 +77,12 @@ class Archive(UUIDModel, TitleSlugDescriptionModel):
blank=True,
help_text="The structures contained in this archive",
)
organizations = models.ManyToManyField(
Organization,
blank=True,
help_text="The organizations associated with this archive",
related_name="archives",
)

class Meta(UUIDModel.Meta, TitleSlugDescriptionModel.Meta):
ordering = ("created",)
Expand Down
3 changes: 3 additions & 0 deletions app/grandchallenge/challenges/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class Meta:
"task_types",
"modalities",
"structures",
"organizations",
"series",
"publications",
"hidden",
Expand Down Expand Up @@ -79,6 +80,7 @@ class Meta:
"task_types": Select2MultipleWidget,
"modalities": Select2MultipleWidget,
"structures": Select2MultipleWidget,
"organizations": Select2MultipleWidget,
"series": Select2MultipleWidget,
"publications": Select2MultipleWidget,
"registration_page_text": SummernoteInplaceWidget(),
Expand Down Expand Up @@ -128,6 +130,7 @@ class Meta:
"task_types": Select2MultipleWidget,
"modalities": Select2MultipleWidget,
"structures": Select2MultipleWidget,
"organizations": Select2MultipleWidget,
"series": Select2MultipleWidget,
"publications": Select2MultipleWidget,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Generated by Django 3.1.1 on 2020-12-17 06:53

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("organizations", "0001_initial"),
("challenges", "0001_initial"),
]

operations = [
migrations.AddField(
model_name="challenge",
name="organizations",
field=models.ManyToManyField(
blank=True,
help_text="The organizations associated with this challenge",
related_name="challenges",
to="organizations.Organization",
),
),
migrations.AddField(
model_name="externalchallenge",
name="organizations",
field=models.ManyToManyField(
blank=True,
help_text="The organizations associated with this challenge",
related_name="externalchallenges",
to="organizations.Organization",
),
),
]
7 changes: 7 additions & 0 deletions app/grandchallenge/challenges/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
)
from grandchallenge.evaluation.tasks import assign_evaluation_permissions
from grandchallenge.modalities.models import ImagingModality
from grandchallenge.organizations.models import Organization
from grandchallenge.pages.models import Page
from grandchallenge.publications.models import Publication
from grandchallenge.subdomains.utils import reverse
Expand Down Expand Up @@ -181,6 +182,12 @@ class ChallengeBase(models.Model):
blank=True,
help_text="Which challenge series is this associated with?",
)
organizations = models.ManyToManyField(
Organization,
blank=True,
help_text="The organizations associated with this challenge",
related_name="%(class)ss",
)

number_of_training_cases = models.IntegerField(blank=True, null=True)
number_of_test_cases = models.IntegerField(blank=True, null=True)
Expand Down
9 changes: 9 additions & 0 deletions app/grandchallenge/core/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from grandchallenge.anatomy.models import BodyRegion, BodyStructure
from grandchallenge.modalities.models import ImagingModality
from grandchallenge.organizations.models import Organization


class FilterForm(Form):
Expand Down Expand Up @@ -37,13 +38,21 @@ class TitleDescriptionModalityStructureFilter(FilterSet):
widget=Select2MultipleWidget,
label="Anatomical Region",
)
organizations = ModelMultipleChoiceFilter(
queryset=Organization.objects.all(),
widget=Select2MultipleWidget,
label="Organization",
field_name="organizations__slug",
to_field_name="slug",
)

class Meta:
fields = (
"search",
"modalities",
"structures",
"structures__region",
"organizations",
)
form = FilterForm
search_fields = ("title", "description")
Expand Down
1 change: 1 addition & 0 deletions app/grandchallenge/organizations/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
default_app_config = "grandchallenge.organizations.apps.OrganizationsConfig"
9 changes: 9 additions & 0 deletions app/grandchallenge/organizations/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from django.apps import AppConfig


class OrganizationsConfig(AppConfig):
name = "grandchallenge.organizations"

def ready(self):
# noinspection PyUnresolvedReferences
import grandchallenge.organizations.signals # noqa: F401
81 changes: 81 additions & 0 deletions app/grandchallenge/organizations/signals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
from django.db.models.signals import m2m_changed
from django.dispatch import receiver
from guardian.shortcuts import assign_perm, remove_perm

from grandchallenge.algorithms.models import Algorithm
from grandchallenge.archives.models import Archive
from grandchallenge.challenges.models import Challenge, ExternalChallenge
from grandchallenge.reader_studies.models import ReaderStudy


@receiver(m2m_changed, sender=Algorithm.organizations.through)
@receiver(m2m_changed, sender=Archive.organizations.through)
@receiver(m2m_changed, sender=Challenge.organizations.through)
@receiver(m2m_changed, sender=ExternalChallenge.organizations.through)
@receiver(m2m_changed, sender=ReaderStudy.organizations.through)
def update_related_permissions(
sender, instance, action, reverse, model, pk_set, **_
):
if action not in ["post_add", "post_remove", "pre_clear"]:
# nothing to do for the other actions
return

if sender == Algorithm.organizations.through:
related_model = Algorithm
related_name = "algorithms"
elif sender == Archive.organizations.through:
related_model = Archive
related_name = "archives"
elif sender == Challenge.organizations.through:
related_model = Challenge
related_name = "challenges"
elif sender == ExternalChallenge.organizations.through:
related_model = ExternalChallenge
related_name = "externalchallenges"
elif sender == ReaderStudy.organizations.through:
related_model = ReaderStudy
related_name = "readerstudies"
else:
raise RuntimeError(f"Unrecognised sender: {sender}")

_update_related_view_permissions(
action=action,
instance=instance,
model=model,
pk_set=pk_set,
reverse=reverse,
related_model=related_model,
related_name=related_name,
)


def _update_related_view_permissions(
*, action, instance, model, pk_set, reverse, related_model, related_name,
):
if reverse:
organizations = [instance]
if pk_set is None:
# When using a _clear action, pk_set is None
# https://docs.djangoproject.com/en/2.2/ref/signals/#m2m-changed
related_objects = getattr(instance, related_name).all()
else:
related_objects = model.objects.filter(pk__in=pk_set)
else:
related_objects = related_model.objects.filter(pk=instance.pk)
if pk_set is None:
# When using a _clear action, pk_set is None
# https://docs.djangoproject.com/en/2.2/ref/signals/#m2m-changed
organizations = instance.organizations.all()
else:
organizations = model.objects.filter(pk__in=pk_set)

op = assign_perm if "add" in action else remove_perm
perm = f"view_{related_model._meta.model_name}"

for org in organizations:
op(
perm, org.editors_group, related_objects,
)
op(
perm, org.members_group, related_objects,
)
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
{% load url %}
{% load bleach %}
{% load guardian_tags %}
{% load meta_attr %}
{% load evaluation_extras %}

{% block title %}
{{ object.title }} - {{ block.super }}
Expand Down Expand Up @@ -35,6 +37,10 @@
href="#v-pills-members" role="tab" aria-controls="v-pills-members"
aria-selected="false"><i class="fas fa-users fa-fw"></i>&nbsp;Members
</a>
<a class="nav-link"
href="{% url 'organizations:update' slug=object.slug %}">
<i class="fa fa-edit fa-fw"></i>&nbsp;Edit Organization
</a>
{% endif %}
</div>
</div>
Expand All @@ -59,14 +65,41 @@ <h2>{{ object.title }}</h2>
<dd class="col-sm-9"><a href="{{ object.website }}">{{ object.website }}</a></dd>
</dl>

{% if "change_organization" in object_perms %}
<p>
<a class="btn btn-primary"
href="{% url 'organizations:update' slug=object.slug %}">
<i class="fa fa-edit"></i> Edit Organization
</a>
</p>
{% endif %}
<h3>Activity Overview</h3>
<div class="row equal-height mx-n2">
{% for object in object_list %}
<div class="col-12 col-sm-12 col-md-6 col-lg-4 mb-3 px-2">
<div class="card">
<a class="stretched-link" href="{{ object.get_absolute_url }}"
title="View {{ object|meta_attr:'verbose_name'|title }}"></a>
<div class="embed-responsive embed-responsive-1by1">
{% if object.logo %}
<img class="card-img-top embed-responsive-item"
loading="lazy"
src="{{ object.logo.url }}"
alt="{{ object }} Logo">
{% endif %}
</div>
<div class="card-body">
<h5 class="card-title mb-0">
{% firstof object.title object.short_name %}
{% if not object.public or object.hidden %}
<i class="fas fa-lock above-stretched-link"
title="{{ object|meta_attr:'verbose_name'|title }} is private"></i>
{% endif %}
</h5>
<span class="badge badge-info above-stretched-link"
title="{{ object|meta_attr:'verbose_name'|title }} {% firstof object.title object.short_name %}">
<i class="far fa-circle fa-fw"></i>&nbsp;{{ object|meta_attr:'verbose_name'|title }}
</span>
<p class="card-text">
{{ object.description }}
</p>
</div>
</div>
</div>
{% endfor %}
</div>
</div>

{% if "change_organization" in object_perms %}
Expand Down
Loading

0 comments on commit 57f803f

Please sign in to comment.