Exemple #1
0
class Product(models.Model):
    """
    Product model
    """
    shop = models.ForeignKey('shop.Shop', on_delete=models.CASCADE)
    name = models.CharField(gettext_lazy('name'), max_length=100)
    description = models.TextField(gettext_lazy('description'))
    price = models.DecimalField(gettext_lazy('price'), max_digits=10, decimal_places=2)
    offer_price = models.DecimalField(gettext_lazy('offer price'), max_digits=10, decimal_places=2, blank=True, null=True)
    image = JPEGField(
        gettext_lazy('image'),
        upload_to='images/%Y/%m/%d/',
        variations={'full': (600, 400, True)},
    )

    active = models.BooleanField(gettext_lazy('active'), default=True)
    delivery_days = models.PositiveIntegerField(gettext_lazy('delivery days'), blank=True, null=True)
    start_datetime = models.DateTimeField(gettext_lazy('start datetime'), blank=True, null=True)
    end_datetime = models.DateTimeField(gettext_lazy('end datetime'), blank=True, null=True)

    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)

    objects = models.Manager()
    actives = ActiveManager()
    inactives = InactiveManager()

    def __str__(self):
        return self.name

    def get_price(self):
        return self.offer_price if self.offer_price else self.price
class Post(models.Model):
    created = models.DateTimeField(auto_now_add=True)
    modified = models.DateTimeField(auto_now=True)

    title = models.CharField(max_length=1024)
    slug = AutoSlugField(populate_from="title", max_length=1024)
    description = models.TextField()
    content = models.TextField()

    authors = models.ManyToManyField(to=get_user_model(),
                                     related_name="blog_authors")

    logo = JPEGField(
        upload_to=get_logo_path,
        storage=public_s3_storage,
        variations=settings.STDIMAGE_SOCIAL_VARIATIONS,
    )

    published = models.BooleanField(default=False)

    history = HistoricalRecords()

    class Meta:
        ordering = ("-created", )

    def __str__(self):
        return self.title

    def get_absolute_url(self):
        return reverse("blogs:detail", kwargs={"slug": self.slug})

    @property
    def public(self):
        return self.published
Exemple #3
0
class JPEGModel(models.Model):
    """creates a thumbnail resized to maximum size to fit a 100x75 area"""
    image = JPEGField(
        upload_to=upload_to,
        blank=True,
        variations={
            'full': (float('inf'), float('inf')),
            'thumbnail': (100, 75, True),
        },
        delete_orphans=True,
    )
Exemple #4
0
class Photograph(DeepZoom):
    """
    Model for a photograph.
    """

    content_type = models.ForeignKey(ContentType,
                                     on_delete=models.CASCADE,
                                     null=True)
    object_id = models.PositiveIntegerField(default=0)
    profile = GenericForeignKey("content_type", "object_id")
    profile_position = models.PositiveSmallIntegerField(null=True, blank=True)

    img = JPEGField(
        upload_to="photograph/",
        width_field="img_original_width",
        height_field="img_original_height",
        variations={
            "large": (1200, None),
            "medium": (900, None),
            "small": (600, None),
            "thumbnail": (100, 100, True),
        },
        db_index=True,
        delete_orphans=True,
    )
    img_original_width = models.PositiveSmallIntegerField(
        editable=False, verbose_name="img width")
    img_original_height = models.PositiveSmallIntegerField(
        editable=False, verbose_name="img height")
    img_original_scale = models.FloatField(
        verbose_name="scale",
        null=True,
        blank=True,
    )

    img_alt = models.CharField(max_length=200)
    description = models.TextField(default="",
                                   blank=True,
                                   verbose_name="description (Markdown)")
    audio = models.FileField(upload_to="audio/", null=True, blank=True)
    audio_duration = models.FloatField(null=True, editable=False)

    date = models.DateField(null=True,
                            blank=True,
                            help_text="Datum der Lichtbildaufnahme")
    author = models.CharField(max_length=100, default="", blank=True)
    license = models.CharField(max_length=100, default="", blank=True)

    def __str__(self):
        return str(self.img)

    class Meta:
        image_field_name = "img"
class JPEGModel(models.Model):
    """creates a thumbnail resized to maximum size to fit a 100x75 area"""

    image = JPEGField(
        upload_to=upload_to,
        blank=True,
        variations={
            "full": (None, None),
            "thumbnail": (100, 75, True),
        },
        delete_orphans=True,
    )
Exemple #6
0
class Profile(models.Model):
    # Dependencies
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    auctions = models.ManyToManyField(Auction)
    bid_on = models.ManyToManyField(Item)

    # Member Variables
    name = models.CharField(max_length=128)
    email = models.EmailField()
    phone_number = PhoneNumberField(blank=True)
    image = JPEGField(
        blank=True,
        upload_to='images/',
        variations={'thumbnail': {
            "width": 300,
            "height": 300,
            "crop": True
        }},
        delete_orphans=True)

    @receiver(post_save, sender=User)
    def create_user_profile(sender, instance, created, **kwargs):
        if created:
            Profile.objects.create(user=instance)

    @receiver(post_save, sender=User)
    def save_user_profile(sender, instance, **kwargs):
        instance.profile.save()

    def getImageThumbnail(self):
        if self.image:
            return self.image.thumbnail.url
        else:
            return settings.MEDIA_URL + "/images/defaultProfilePicture.jpg"

    def set_bid_on(self, item):
        if not self.bid_on.filter(pk=item.pk).exists():
            self.bid_on.add(item)
        return

    # def setWon(self, item):
    #     if not self.items_won.filter(pk=item.pk).exists():
    #         self.items_won.add(item)
    #     return

    def get_name_and_pk(self):
        return '%s - %s' % (self.pk, self.username)

    User.add_to_class("__str__", get_name_and_pk)

    def __str__(self):
        return '%s - %s' % (self.name, self.pk)
Exemple #7
0
class Sketch(models.Model):
    # image = models.ImageField(upload_to=upload_to)
    image = JPEGField(
        upload_to=upload_to,
        variations={
            'large': (750, 450),
            'thumbnail': (125, 75)
        },
    )
    assignment = models.ForeignKey('Assignment',
                                   related_name="sketches",
                                   on_delete=models.SET_NULL,
                                   null=True,
                                   blank=True)
    user = models.ForeignKey('User',
                             related_name="sketches",
                             on_delete=models.CASCADE)
    datetime = models.DateTimeField(default=datetime.datetime.now)
    time_spent = models.CharField(max_length=255, blank=True, null=True)
    exchange = models.ForeignKey('Exchange',
                                 related_name="sketches",
                                 related_query_name="sketches",
                                 on_delete=models.SET_NULL,
                                 null=True,
                                 blank=True)

    def __str__(self):
        return str(self.assignment)

    def rotate(self):
        im = Image.open(self.image.file)
        angle = 90
        im = im.rotate(angle, expand=True)
        im.save(self.image.file.name)

    # https://stackoverflow.com/questions/23945494/use-html5-to-resize-an-image-before-upload
    # https://stackoverflow.com/questions/623698/resize-image-on-save
    def resize(self):
        basewidth = 800
        im = Image.open(self.image.file)
        if im.size[0] < basewidth: basewidth = 800
        wpercent = (basewidth / float(im.size[0]))
        hsize = int((float(im.size[1]) * float(wpercent)))
        im = im.resize((basewidth, hsize), Image.ANTIALIAS)
        im = im.convert('RGB')
        name = os.path.splitext(self.image.file.name)[0] + "_resize.jpg"
        im.save(name)
        name = name.replace(settings.MEDIA_ROOT, "")
        if name[0] == "/": name = name[1:]
        self.image = name
        self.save()
Exemple #8
0
class ItemImage(models.Model):
    # Dependencies
    item = models.ForeignKey(Item, on_delete=models.CASCADE)
    image = JPEGField(
        upload_to='images/',
        variations={'thumbnail': {
            "width": 300,
            "height": 300,
            "crop": True
        }},
        delete_orphans=True)

    def getImageThumbnail(self):
        return self.image.thumbnail.url
Exemple #9
0
class Employee(models.Model):
    firstname = models.CharField(max_length=100)
    lastname = models.CharField(max_length=100)
    slug = models.SlugField(unique=True)
    jobtitle = models.CharField(max_length=100)
    description = models.TextField()
    teaser = models.CharField(max_length=100, null=True)
    linkedin = models.URLField(blank=True, null=True)
    cv = models.FileField(
        upload_to='cv/',
        validators=[FileExtensionValidator(allowed_extensions=['pdf'])],
        null=True)
    image = JPEGField(upload_to='employee/',
                      variations={
                          'full': {
                              "width": None,
                              "height": None
                          },
                          'thumbnail': {
                              "width": 100,
                              "height": 100,
                              "crop": True
                          },
                          'medium': {
                              "width": 300,
                              "height": 300,
                              "crop": True
                          }
                      })
    technologies = models.ManyToManyField(Technology,
                                          through='TechnologyLevel',
                                          related_name='consultants')
    tags = models.ManyToManyField(Tag)

    class Meta:
        ordering = ['firstname', 'lastname']

    def get_full_name(self):
        return f"{self.firstname} {self.lastname}"

    def __str__(self):
        return self.get_full_name()
Exemple #10
0
class Client(models.Model):
    name = models.CharField(max_length=100)
    slug = models.SlugField(unique=True)
    description = models.TextField()
    url = models.URLField(blank=True, null=True)
    image = JPEGField(upload_to='client',
                      variations={
                          'full': {
                              "width": None,
                              "height": None
                          },
                          'thumbnail': {
                              "width": 100,
                              "height": 100,
                              "crop": True
                          }
                      })

    class Meta:
        ordering = ['name']

    def __str__(self):
        return self.name
Exemple #11
0
class ElementImages(models.Model):
    element = models.ForeignKey(Element,on_delete=models.CASCADE)
    title = models.CharField(max_length=255)
    cover = JPEGField(upload_to='images/',variations={'custom': {'width': 550, 'height': 750, "crop": True}})
    base_cover_name = models.CharField(max_length=100, default='')
    base_cover_ext = models.CharField(max_length=5, default='')

    def __str__(self):
        return self.title

    def save(self, *args, **kwargs):

        (root, ext) = os.path.splitext(self.cover.path)

        print(settings.MEDIA_ROOT)
        print(root)

        if(self.base_cover_name == ""):
            root = root.replace(settings.MEDIA_ROOT+"\\","images/")        
            self.base_cover_name = root

        self.base_cover_ext = ext

        super(ElementImages,self).save(*args, **kwargs)
Exemple #12
0
class ElementImages(models.Model):
    element = models.ForeignKey(Element, on_delete=models.CASCADE)
    title = models.CharField(max_length=255)

    # cover sin stdimage
    # cover = models.ImageField(upload_to='images/')

    # cover con stdimage
    # cover = StdImageField(upload_to='images/',variations={'thumbnail': {'width': 300, 'height': 300, "crop": True}})

    # cover con JPEGField
    cover = JPEGField(
        upload_to='images/',
        variations={'custom': {
            'width': 360,
            'height': 487,
            "crop": True
        }})

    # para obtener el nombre de la imagen original
    base_cover_name = models.CharField(max_length=100, default='')
    # para obtener la extension
    base_cover_ext = models.CharField(max_length=5, default='')

    def __str__(self):
        return self.title

    # guardar campos de manera personalizada
    def save(self, *args, **kwargs):
        """
            Obtener la referencia del nombre original de la imagen como de su extensión
        """
        (root, ext) = os.path.splitext(self.cover.path)
        """
            Quitar path absoluto del archivo, para solo dejar la carpeta donde este
            se guardará (images), el nombre de la imagen y su extension.

            Importamos y llamamos a settings para quitar path absoluto 
            que yace en la variable root y solo dejar la carpeta donde se subirá(images).
            Esto es para almacenar el nombre y carpeta de nuestras imagenes en la base de datos
            elementimages -> base_cover_name que es lo mismo que -> cover, solo que esté sera llamado y el cover no

            Condiciones:
                1. Si el base_cover_name está vacío porque no hay imagen subida,
                   la variable root elimina el path absoluto y agrega la carpeta donde esta se sube (images)
                2. Si el base_cover_name no está vacío, quiere decir que se está actualizando
                   ya sea la imagen o solo su titulo, en este caso hay dos posibilidades, asi que
                   necesitaremos primero comprobar si la variable root contiene la carpeta de subida (images), luego:
                   2.1. Si hayamos que la variable "palabra" se encuentra en la variable "root" quiere decir que
                        estamos actualizando solo el titulo de la imagen, por lo cual tendremos que quitar
                        todo el path absoluto y agregar "images/" nuevamente, de otro modo se nos duplicará en la BBDD,
                        asi que reemplazamos la palabra "images\" y luego eliminamos el path absoluto para que no se duplique
                   2.2 Sino encuentran la palabra en el root significa que estamos actualizando por otra imagen, entonces solo
                        procedemos a eliminar el path absoluto y agregar el nombre de la carpeta de subida "images/" y asignarla
                        nuevamente a base_cover_name
        """
        if (self.base_cover_name == ""):
            root = root.replace(settings.MEDIA_ROOT + "\\", "images/")
            self.base_cover_name = root
        elif (self.base_cover_name != ""):
            # probar si root contene la palabra images\ para actualizar solo el titulo
            palabra = "images\\"
            if palabra in root.lower():
                root = root.replace('images\\', '')
                root = root.replace(settings.MEDIA_ROOT + "\\", "images/")
                self.base_cover_name = root
            else:
                root = root.replace(settings.MEDIA_ROOT + "\\", "images/")
                self.base_cover_name = root

        # guardar extensión de la imagen
        self.base_cover_ext = ext

        # llamamos a la super clase para salvar datos
        super(ElementImages, self).save(*args, **kwargs)
Exemple #13
0
class Algorithm(UUIDModel, TitleSlugDescriptionModel, ViewContentMixin):
    editors_group = models.OneToOneField(
        Group,
        on_delete=models.PROTECT,
        editable=False,
        related_name="editors_of_algorithm",
    )
    users_group = models.OneToOneField(
        Group,
        on_delete=models.PROTECT,
        editable=False,
        related_name="users_of_algorithm",
    )
    logo = JPEGField(
        upload_to=get_logo_path,
        storage=public_s3_storage,
        variations=settings.STDIMAGE_LOGO_VARIATIONS,
    )
    social_image = JPEGField(
        upload_to=get_social_image_path,
        storage=public_s3_storage,
        blank=True,
        help_text="An image for this algorithm which is displayed when you post the link for this algorithm on social media. Should have a resolution of 640x320 px (1280x640 px for best display).",
        variations=settings.STDIMAGE_SOCIAL_VARIATIONS,
    )
    workstation = models.ForeignKey(
        "workstations.Workstation", on_delete=models.PROTECT
    )
    workstation_config = models.ForeignKey(
        "workstation_configs.WorkstationConfig",
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
    )
    hanging_protocol = models.ForeignKey(
        "hanging_protocols.HangingProtocol",
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
    )
    public = models.BooleanField(
        default=False,
        help_text=(
            "Should this algorithm be visible to all users on the algorithm "
            "overview page? This does not grant all users permission to use "
            "this algorithm. Users will still need to be added to the "
            "algorithm users group in order to do that."
        ),
    )
    access_request_handling = models.CharField(
        max_length=25,
        choices=AccessRequestHandlingOptions.choices,
        default=AccessRequestHandlingOptions.MANUAL_REVIEW,
        help_text=("How would you like to handle access requests?"),
    )
    detail_page_markdown = models.TextField(blank=True)
    job_create_page_markdown = models.TextField(blank=True)
    additional_terms_markdown = models.TextField(
        blank=True,
        help_text=(
            "By using this algorithm, users agree to the site wide "
            "terms of service. If your algorithm has any additional "
            "terms of usage, define them here."
        ),
    )
    result_template = models.TextField(
        blank=True,
        default="<pre>{{ results|tojson(indent=2) }}</pre>",
        help_text=(
            "Define the jinja template to render the content of the "
            "results.json to html. For example, the following template will "
            "print out all the keys and values of the result.json. "
            "Use results to access the json root. "
            "{% for key, value in results.metrics.items() -%}"
            "{{ key }}  {{ value }}"
            "{% endfor %}"
        ),
    )
    inputs = models.ManyToManyField(
        to=ComponentInterface, related_name="algorithm_inputs", blank=False
    )
    outputs = models.ManyToManyField(
        to=ComponentInterface, related_name="algorithm_outputs", blank=False
    )
    publications = models.ManyToManyField(
        Publication,
        blank=True,
        help_text="The publications associated with this algorithm",
    )
    modalities = models.ManyToManyField(
        ImagingModality,
        blank=True,
        help_text="The imaging modalities supported by this algorithm",
    )
    structures = models.ManyToManyField(
        BodyStructure,
        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=(
            "The number of credits that are required for each execution of this algorithm."
        ),
    )
    average_duration = models.DurationField(
        null=True,
        default=None,
        editable=False,
        help_text="The average duration of successful jobs.",
    )
    use_flexible_inputs = deprecate_field(models.BooleanField(default=True))
    repo_name = models.CharField(blank=True, max_length=512)
    image_requires_gpu = models.BooleanField(default=True)
    image_requires_memory_gb = models.PositiveIntegerField(default=15)
    recurse_submodules = models.BooleanField(
        default=False,
        help_text="Do a recursive git pull when a GitHub repo is linked to this algorithm.",
    )
    highlight = models.BooleanField(
        default=False,
        help_text="Should this algorithm be advertised on the home page?",
    )
    contact_email = models.EmailField(
        blank=True,
        help_text="This email will be listed as the contact email for the algorithm and will be visible to all users of Grand Challenge.",
    )
    display_editors = models.BooleanField(
        null=True,
        blank=True,
        help_text="Should the editors of this algorithm be listed on the information page?",
    )
    summary = models.TextField(
        blank=True,
        help_text="Briefly describe your algorithm and how it was developed.",
    )
    mechanism = models.TextField(
        blank=True,
        help_text="Provide a short technical description of your algorithm.",
    )
    validation_and_performance = models.TextField(
        blank=True,
        help_text="If you have performance metrics about your algorithm, you can report them here.",
    )
    uses_and_directions = models.TextField(
        blank=True,
        default="This algorithm was developed for research purposes only.",
        help_text="Describe what your algorithm can be used for, but also what it should not be used for.",
    )
    warnings = models.TextField(
        blank=True,
        help_text="Describe potential risks and inappropriate settings for using the algorithm.",
    )
    common_error_messages = models.TextField(
        blank=True,
        help_text="Describe common error messages a user might encounter when trying out your algorithm and provide solutions for them.",
    )

    class Meta(UUIDModel.Meta, TitleSlugDescriptionModel.Meta):
        ordering = ("created",)
        permissions = [("execute_algorithm", "Can execute algorithm")]
        constraints = [
            models.UniqueConstraint(
                fields=["repo_name"],
                name="unique_repo_name",
                condition=~Q(repo_name=""),
            )
        ]

    def __str__(self):
        return f"{self.title}"

    def get_absolute_url(self):
        return reverse("algorithms:detail", kwargs={"slug": self.slug})

    @property
    def api_url(self):
        return reverse("api:algorithm-detail", kwargs={"pk": self.pk})

    @property
    def supports_batch_upload(self):
        inputs = {inpt.slug for inpt in self.inputs.all()}
        return inputs == {"generic-medical-image"}

    def save(self, *args, **kwargs):
        adding = self._state.adding

        if adding:
            self.create_groups()
            self.workstation_id = (
                self.workstation_id or self.default_workstation.pk
            )

        super().save(*args, **kwargs)

        if adding:
            self.set_default_interfaces()

        self.assign_permissions()
        self.assign_workstation_permissions()

    def delete(self, *args, **kwargs):
        ct = ContentType.objects.filter(
            app_label=self._meta.app_label, model=self._meta.model_name
        ).get()
        Follow.objects.filter(object_id=self.pk, content_type=ct).delete()
        super().delete(*args, **kwargs)

    def create_groups(self):
        self.editors_group = Group.objects.create(
            name=f"{self._meta.app_label}_{self._meta.model_name}_{self.pk}_editors"
        )
        self.users_group = Group.objects.create(
            name=f"{self._meta.app_label}_{self._meta.model_name}_{self.pk}_users"
        )

    def set_default_interfaces(self):
        if not self.inputs.exists():
            self.inputs.set(
                [
                    ComponentInterface.objects.get(
                        slug=DEFAULT_INPUT_INTERFACE_SLUG
                    )
                ]
            )
        if not self.outputs.exists():
            self.outputs.set(
                [
                    ComponentInterface.objects.get(slug="results-json-file"),
                    ComponentInterface.objects.get(
                        slug=DEFAULT_OUTPUT_INTERFACE_SLUG
                    ),
                ]
            )

    def assign_permissions(self):
        # Editors and users can view this algorithm
        assign_perm(f"view_{self._meta.model_name}", self.editors_group, self)
        assign_perm(f"view_{self._meta.model_name}", self.users_group, self)
        # Editors and users can execute this algorithm
        assign_perm(
            f"execute_{self._meta.model_name}", self.editors_group, self
        )
        assign_perm(f"execute_{self._meta.model_name}", self.users_group, self)
        # Editors can change this algorithm
        assign_perm(
            f"change_{self._meta.model_name}", self.editors_group, self
        )

        reg_and_anon = Group.objects.get(
            name=settings.REGISTERED_AND_ANON_USERS_GROUP_NAME
        )

        if self.public:
            assign_perm(f"view_{self._meta.model_name}", reg_and_anon, self)
        else:
            remove_perm(f"view_{self._meta.model_name}", reg_and_anon, self)

    def assign_workstation_permissions(self):
        """Allow the editors and users group to view the workstation."""
        perm = "workstations.view_workstation"

        for group in [self.users_group, self.editors_group]:
            workstations = get_objects_for_group(
                group=group, perms=perm, accept_global_perms=False
            )

            if (
                self.workstation not in workstations
            ) or workstations.count() > 1:
                remove_perm(perm=perm, user_or_group=group, obj=workstations)
                assign_perm(
                    perm=perm, user_or_group=group, obj=self.workstation
                )

    @cached_property
    def latest_ready_image(self):
        """
        Returns
        -------
            The most recent container image for this algorithm
        """
        return (
            self.algorithm_container_images.filter(ready=True)
            .order_by("-created")
            .first()
        )

    @cached_property
    def default_workstation(self):
        """
        Returns the default workstation, creating it if it does not already
        exist.
        """
        w, created = Workstation.objects.get_or_create(
            slug=settings.DEFAULT_WORKSTATION_SLUG
        )

        if created:
            w.title = settings.DEFAULT_WORKSTATION_SLUG
            w.save()

        return w

    def update_average_duration(self):
        """Store the duration of successful jobs for this algorithm"""
        self.average_duration = Job.objects.filter(
            algorithm_image__algorithm=self, status=Job.SUCCESS
        ).average_duration()
        self.save(update_fields=("average_duration",))

    def is_editor(self, user):
        return user.groups.filter(pk=self.editors_group.pk).exists()

    def add_editor(self, user):
        return user.groups.add(self.editors_group)

    def remove_editor(self, user):
        return user.groups.remove(self.editors_group)

    def is_user(self, user):
        return user.groups.filter(pk=self.users_group.pk).exists()

    def add_user(self, user):
        return user.groups.add(self.users_group)

    def remove_user(self, user):
        return user.groups.remove(self.users_group)
Exemple #14
0
class UserProfile(models.Model):
    user = models.OneToOneField(
        get_user_model(),
        unique=True,
        verbose_name=_("user"),
        related_name="user_profile",
        on_delete=models.CASCADE,
    )

    mugshot = JPEGField(
        _("mugshot"),
        blank=True,
        upload_to=get_mugshot_path,
        help_text=_("A personal image displayed in your profile."),
        variations=settings.STDIMAGE_LOGO_VARIATIONS,
    )

    institution = models.CharField(max_length=100)
    department = models.CharField(max_length=100)
    country = CountryField()
    website = models.CharField(max_length=150, blank=True)
    display_organizations = models.BooleanField(
        default=True,
        help_text=
        "Display the organizations that you are a member of in your profile.",
    )

    receive_notification_emails = models.BooleanField(
        default=True,
        help_text="Whether to receive email updates about notifications",
    )
    notification_email_last_sent_at = models.DateTimeField(default=None,
                                                           null=True,
                                                           editable=False)
    receive_newsletter = models.BooleanField(
        null=True,
        blank=True,
        help_text=
        "Would you like to be put on our mailing list and receive newsletters about Grand Challenge updates?",
    )

    def save(self, *args, **kwargs):
        adding = self._state.adding

        super().save(*args, **kwargs)

        if adding:
            self.assign_permissions()

    def assign_permissions(self):
        if self.user != get_anonymous_user():
            assign_perm("change_userprofile", self.user, self)

    def get_absolute_url(self):
        return reverse("profile-detail",
                       kwargs={"username": self.user.username})

    def get_mugshot_url(self):
        if self.mugshot:
            return self.mugshot.x02.url
        else:
            gravatar_url = (
                "https://www.gravatar.com/avatar/" +
                md5(self.user.email.lower().encode("utf-8")).hexdigest() + "?")
            gravatar_url += urlencode({"d": "identicon", "s": "64"})
            return gravatar_url

    @property
    def has_unread_notifications(self):
        return self.unread_notifications.exists()

    @property
    def unread_notifications(self):
        return self.user.notification_set.filter(read=False)
class Organization(TitleSlugDescriptionModel, UUIDModel):
    logo = JPEGField(
        upload_to=get_logo_path,
        storage=public_s3_storage,
        variations=settings.STDIMAGE_LOGO_VARIATIONS,
    )
    location = CountryField()
    website = models.URLField()

    detail_page_markdown = models.TextField(blank=True)

    editors_group = models.OneToOneField(
        Group,
        on_delete=models.PROTECT,
        editable=False,
        related_name="editors_of_organization",
    )
    members_group = models.OneToOneField(
        Group,
        on_delete=models.PROTECT,
        editable=False,
        related_name="members_of_organization",
    )

    class Meta(TitleSlugDescriptionModel.Meta, UUIDModel.Meta):
        ordering = ("created", )

    def __str__(self):
        return f"{self.title}"

    def get_absolute_url(self):
        return reverse("organizations:detail", kwargs={"slug": self.slug})

    def save(self, *args, **kwargs):
        adding = self._state.adding

        if adding:
            self._create_groups()

        super().save(*args, **kwargs)

        if adding:
            self._assign_permissions()

    def _create_groups(self):
        self.editors_group = Group.objects.create(
            name=
            f"{self._meta.app_label}_{self._meta.model_name}_{self.pk}_editors"
        )
        self.members_group = Group.objects.create(
            name=
            f"{self._meta.app_label}_{self._meta.model_name}_{self.pk}_members"
        )

    def _assign_permissions(self):
        assign_perm(f"change_{self._meta.model_name}", self.editors_group,
                    self)

    def is_editor(self, user):
        return user.groups.filter(pk=self.editors_group.pk).exists()

    def add_editor(self, user):
        return user.groups.add(self.editors_group)

    def remove_editor(self, user):
        return user.groups.remove(self.editors_group)

    def is_member(self, user):
        return user.groups.filter(pk=self.members_group.pk).exists()

    def add_member(self, user):
        return user.groups.add(self.members_group)

    def remove_member(self, user):
        return user.groups.remove(self.members_group)
class ChallengeBase(models.Model):
    CHALLENGE_ACTIVE = "challenge_active"
    CHALLENGE_INACTIVE = "challenge_inactive"
    DATA_PUB = "data_pub"

    creator = models.ForeignKey(settings.AUTH_USER_MODEL,
                                null=True,
                                on_delete=models.SET_NULL)
    created = models.DateTimeField(auto_now_add=True)
    modified = models.DateTimeField(auto_now=True)
    short_name = CICharField(
        max_length=50,
        blank=False,
        help_text=("short name used in url, specific css, files etc. "
                   "No spaces allowed"),
        validators=[
            validate_nounderscores,
            validate_slug,
            validate_short_name,
        ],
        unique=True,
    )
    description = models.CharField(
        max_length=1024,
        default="",
        blank=True,
        help_text="Short summary of this project, max 1024 characters.",
    )
    title = models.CharField(
        max_length=64,
        blank=True,
        default="",
        help_text=(
            "The name of the challenge that is displayed on the All Challenges"
            " page. If this is blank the short name of the challenge will be "
            "used."),
    )
    logo = JPEGField(
        upload_to=get_logo_path,
        storage=public_s3_storage,
        blank=True,
        help_text=
        "A logo for this challenge. Should be square with a resolution of 640x640 px or higher.",
        variations=settings.STDIMAGE_LOGO_VARIATIONS,
    )
    social_image = JPEGField(
        upload_to=get_social_image_path,
        storage=public_s3_storage,
        blank=True,
        help_text=
        "An image for this challenge which is displayed when you post the link on social media. Should have a resolution of 640x320 px (1280x640 px for best display).",
        variations=settings.STDIMAGE_SOCIAL_VARIATIONS,
    )
    hidden = models.BooleanField(
        default=True,
        help_text="Do not display this Project in any public overview",
    )
    educational = models.BooleanField(
        default=False, help_text="It is an educational challange")
    workshop_date = models.DateField(
        null=True,
        blank=True,
        help_text=(
            "Date on which the workshop belonging to this project will be held"
        ),
    )
    event_name = models.CharField(
        max_length=1024,
        default="",
        blank=True,
        null=True,
        help_text="The name of the event the workshop will be held at",
    )
    event_url = models.URLField(
        blank=True,
        null=True,
        help_text="Website of the event which will host the workshop",
    )
    publications = models.ManyToManyField(
        Publication,
        blank=True,
        help_text="Which publications are associated with this challenge?",
    )
    data_license_agreement = models.TextField(
        blank=True,
        help_text="What is the data license agreement for this challenge?",
    )

    task_types = models.ManyToManyField(
        TaskType, blank=True, help_text="What type of task is this challenge?")
    modalities = models.ManyToManyField(
        ImagingModality,
        blank=True,
        help_text="What imaging modalities are used in this challenge?",
    )
    structures = models.ManyToManyField(
        BodyStructure,
        blank=True,
        help_text="What structures are used in this challenge?",
    )
    series = models.ManyToManyField(
        ChallengeSeries,
        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)
    filter_classes = ArrayField(CICharField(max_length=32),
                                default=list,
                                editable=False)

    objects = ChallengeManager()

    def __str__(self):
        return self.short_name

    @property
    def public(self):
        """Helper property for consistency with other objects"""
        return not self.hidden

    def get_absolute_url(self):
        raise NotImplementedError

    @property
    def is_self_hosted(self):
        return True

    @property
    def year(self):
        if self.workshop_date:
            return self.workshop_date.year
        else:
            return self.created.year

    @property
    def upcoming_workshop_date(self):
        if self.workshop_date and self.workshop_date > datetime.date.today():
            return self.workshop_date

    @property
    def registered_domain(self):
        """
        Copied from grandchallenge_tags

        Try to find out what framework this challenge is hosted on, return
        a string which can also be an id or class in HTML
        """
        return extract(self.get_absolute_url()).registered_domain

    class Meta:
        abstract = True
        ordering = ("pk", )
class Challenge(ChallengeBase):
    banner = JPEGField(
        upload_to=get_banner_path,
        storage=public_s3_storage,
        blank=True,
        help_text=("Image that gets displayed at the top of each page. "
                   "Recommended resolution 2200x440 px."),
        variations=settings.STDIMAGE_BANNER_VARIATIONS,
    )
    disclaimer = models.CharField(
        max_length=2048,
        default="",
        blank=True,
        null=True,
        help_text=("Optional text to show on each page in the project. "
                   "For showing 'under construction' type messages"),
    )
    require_participant_review = models.BooleanField(
        default=False,
        help_text=(
            "If ticked, new participants need to be approved by project "
            "admins before they can access restricted pages. If not ticked, "
            "new users are allowed access immediately"),
    )
    use_registration_page = models.BooleanField(
        default=True,
        help_text="If true, show a registration page on the challenge site.",
    )
    registration_page_text = models.TextField(
        default="",
        blank=True,
        help_text=(
            "The text to use on the registration page, you could include "
            "a data usage agreement here. You can use HTML markup here."),
    )
    use_workspaces = models.BooleanField(default=False)
    use_evaluation = models.BooleanField(
        default=True,
        help_text=(
            "If true, use the automated evaluation system. See the evaluation "
            "page created in the Challenge site."),
    )
    use_teams = models.BooleanField(
        default=False,
        help_text=("If true, users are able to form teams to participate in "
                   "this challenge together."),
    )
    admins_group = models.OneToOneField(
        Group,
        editable=False,
        on_delete=models.CASCADE,
        related_name="admins_of_challenge",
    )
    participants_group = models.OneToOneField(
        Group,
        editable=False,
        on_delete=models.CASCADE,
        related_name="participants_of_challenge",
    )
    forum = models.OneToOneField(Forum,
                                 editable=False,
                                 on_delete=models.CASCADE)
    display_forum_link = models.BooleanField(
        default=False,
        help_text="Display a link to the challenge forum in the nav bar.",
    )

    cached_num_participants = models.PositiveIntegerField(editable=False,
                                                          default=0)
    cached_num_results = models.PositiveIntegerField(editable=False, default=0)
    cached_latest_result = models.DateTimeField(editable=False,
                                                blank=True,
                                                null=True)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._hidden_orig = self.hidden

    def save(self, *args, **kwargs):
        adding = self._state.adding

        if adding:
            self.create_groups()
            self.create_forum()

            if self.creator:
                self.add_admin(user=self.creator)

        super().save(*args, **kwargs)

        if adding:
            self.update_permissions()
            self.create_forum_permissions()
            self.create_default_pages()
            self.create_default_phases()
            send_challenge_created_email(self)

        if adding or self.hidden != self._hidden_orig:
            on_commit(lambda: assign_evaluation_permissions.apply_async(
                kwargs={"challenge_pk": self.pk}))
            self.update_user_forum_permissions()

    def update_permissions(self):
        assign_perm("change_challenge", self.admins_group, self)

    def create_forum_permissions(self):
        participant_group_perms = {
            "can_see_forum",
            "can_read_forum",
            "can_start_new_topics",
            "can_reply_to_topics",
            "can_delete_own_posts",
            "can_edit_own_posts",
            "can_post_without_approval",
            "can_create_polls",
            "can_vote_in_polls",
        }
        admin_group_perms = {
            "can_lock_topics",
            "can_edit_posts",
            "can_delete_posts",
            "can_approve_posts",
            "can_reply_to_locked_topics",
            "can_post_announcements",
            "can_post_stickies",
            *participant_group_perms,
        }

        permissions = ForumPermission.objects.filter(
            codename__in=admin_group_perms).values_list("codename", "pk")
        permissions = {codename: pk for codename, pk in permissions}

        GroupForumPermission.objects.bulk_create(
            chain(
                (GroupForumPermission(
                    permission_id=permissions[codename],
                    group=self.participants_group,
                    forum=self.forum,
                    has_perm=True,
                ) for codename in participant_group_perms),
                (GroupForumPermission(
                    permission_id=permissions[codename],
                    group=self.admins_group,
                    forum=self.forum,
                    has_perm=True,
                ) for codename in admin_group_perms),
            ))

        UserForumPermission.objects.bulk_create(
            UserForumPermission(
                permission_id=permissions[codename],
                **{user: True},
                forum=self.forum,
                has_perm=not self.hidden,
            ) for codename, user in product(
                ["can_see_forum", "can_read_forum"],
                ["anonymous_user", "authenticated_user"],
            ))

    def update_user_forum_permissions(self):
        perms = UserForumPermission.objects.filter(
            permission__codename__in=["can_see_forum", "can_read_forum"],
            forum=self.forum,
        )

        for p in perms:
            p.has_perm = not self.hidden

        UserForumPermission.objects.bulk_update(perms, ["has_perm"])

    def create_groups(self):
        # Create the groups only on first save
        admins_group = Group.objects.create(name=f"{self.short_name}_admins")
        participants_group = Group.objects.create(
            name=f"{self.short_name}_participants")
        self.admins_group = admins_group
        self.participants_group = participants_group

    def create_forum(self):
        f, created = Forum.objects.get_or_create(
            name=settings.FORUMS_CHALLENGE_CATEGORY_NAME,
            type=Forum.FORUM_CAT,
        )

        if created:
            UserForumPermission.objects.bulk_create(
                UserForumPermission(
                    permission_id=perm_id,
                    **{user: True},
                    forum=f,
                    has_perm=True,
                ) for perm_id, user in product(
                    ForumPermission.objects.filter(
                        codename__in=["can_see_forum", "can_read_forum"
                                      ]).values_list("pk", flat=True),
                    ["anonymous_user", "authenticated_user"],
                ))

        self.forum = Forum.objects.create(
            name=self.title if self.title else self.short_name,
            parent=f,
            type=Forum.FORUM_POST,
        )

    def create_default_pages(self):
        Page.objects.create(
            title=self.short_name,
            html=render_to_string("pages/defaults/home.html",
                                  {"challenge": self}),
            challenge=self,
            permission_level=Page.ALL,
        )
        Page.objects.create(
            title="Contact",
            html=render_to_string("pages/defaults/contact.html",
                                  {"challenge": self}),
            challenge=self,
            permission_level=Page.REGISTERED_ONLY,
        )

    def create_default_phases(self):
        self.phase_set.create(challenge=self)

    def is_admin(self, user) -> bool:
        """Determines if this user is an admin of this challenge."""
        return (user.is_superuser
                or user.groups.filter(pk=self.admins_group.pk).exists())

    def is_participant(self, user) -> bool:
        """Determines if this user is a participant of this challenge."""
        return (user.is_superuser
                or user.groups.filter(pk=self.participants_group.pk).exists())

    def get_admins(self):
        """Return all admins of this challenge."""
        return self.admins_group.user_set.all()

    def get_participants(self):
        """Return all participants of this challenge."""
        return self.participants_group.user_set.all()

    def get_absolute_url(self):
        return reverse(
            "pages:home",
            kwargs={"challenge_short_name": self.short_name},
        )

    def add_participant(self, user):
        if user != get_anonymous_user():
            user.groups.add(self.participants_group)
            follow(user=user,
                   obj=self.forum,
                   actor_only=False,
                   send_action=False)
        else:
            raise ValueError("You cannot add the anonymous user to this group")

    def remove_participant(self, user):
        user.groups.remove(self.participants_group)
        unfollow(user=user, obj=self.forum, send_action=False)

    def add_admin(self, user):
        if user != get_anonymous_user():
            user.groups.add(self.admins_group)
            follow(user=user,
                   obj=self.forum,
                   actor_only=False,
                   send_action=False)
        else:
            raise ValueError("You cannot add the anonymous user to this group")

    def remove_admin(self, user):
        user.groups.remove(self.admins_group)
        unfollow(user=user, obj=self.forum, send_action=False)

    class Meta(ChallengeBase.Meta):
        verbose_name = "challenge"
        verbose_name_plural = "challenges"
class Archive(UUIDModel, TitleSlugDescriptionModel):
    """Model for archive. Contains a collection of images."""

    detail_page_markdown = models.TextField(blank=True)
    logo = JPEGField(
        upload_to=get_logo_path,
        storage=public_s3_storage,
        variations=settings.STDIMAGE_LOGO_VARIATIONS,
    )
    social_image = JPEGField(
        upload_to=get_social_image_path,
        storage=public_s3_storage,
        blank=True,
        help_text=
        "An image for this archive which is displayed when you post the link to this archive on social media. Should have a resolution of 640x320 px (1280x640 px for best display).",
        variations=settings.STDIMAGE_SOCIAL_VARIATIONS,
    )
    editors_group = models.OneToOneField(
        Group,
        on_delete=models.CASCADE,
        editable=False,
        related_name="editors_of_archive",
    )
    uploaders_group = models.OneToOneField(
        Group,
        on_delete=models.CASCADE,
        editable=False,
        related_name="uploaders_of_archive",
    )
    users_group = models.OneToOneField(
        Group,
        on_delete=models.CASCADE,
        editable=False,
        related_name="users_of_archive",
    )
    public = models.BooleanField(default=False)
    workstation = models.ForeignKey(
        "workstations.Workstation",
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
    )
    workstation_config = models.ForeignKey(
        "workstation_configs.WorkstationConfig",
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
    )
    algorithms = models.ManyToManyField(
        Algorithm,
        blank=True,
        help_text=
        "Algorithms that will be executed on all images in this archive",
    )
    publications = models.ManyToManyField(
        Publication,
        blank=True,
        help_text="The publications associated with this archive",
    )
    modalities = models.ManyToManyField(
        ImagingModality,
        blank=True,
        help_text="The imaging modalities contained in this archive",
    )
    structures = models.ManyToManyField(
        BodyStructure,
        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", )
        permissions = [
            (
                "use_archive",
                ("Can use the objects in the archive as inputs to "
                 "algorithms, reader studies and challenges."),
            ),
            ("upload_archive", "Can upload to archive"),
        ]

    def __str__(self):
        return f"<{self.__class__.__name__} {self.title}>"

    @property
    def name(self):
        # Include the read only name for legacy clients
        return self.title

    def save(self, *args, **kwargs):
        adding = self._state.adding

        if adding:
            self.create_groups()

        super().save(*args, **kwargs)

        self.assign_permissions()

    def create_groups(self):
        self.editors_group = Group.objects.create(
            name=
            f"{self._meta.app_label}_{self._meta.model_name}_{self.pk}_editors"
        )
        self.uploaders_group = Group.objects.create(
            name=
            f"{self._meta.app_label}_{self._meta.model_name}_{self.pk}_uploaders"
        )
        self.users_group = Group.objects.create(
            name=
            f"{self._meta.app_label}_{self._meta.model_name}_{self.pk}_users")

    def assign_permissions(self):
        # Allow the editors, uploaders and users groups to view this
        assign_perm(f"view_{self._meta.model_name}", self.editors_group, self)
        assign_perm(f"view_{self._meta.model_name}", self.uploaders_group,
                    self)
        assign_perm(f"view_{self._meta.model_name}", self.users_group, self)

        # Allow the editors, uploaders and users group to use the archive
        assign_perm(f"use_{self._meta.model_name}", self.editors_group, self)
        assign_perm(f"use_{self._meta.model_name}", self.uploaders_group, self)
        assign_perm(f"use_{self._meta.model_name}", self.users_group, self)

        # Allow editors and uploaders to upload to this
        assign_perm(f"upload_{self._meta.model_name}", self.editors_group,
                    self)
        assign_perm(f"upload_{self._meta.model_name}", self.uploaders_group,
                    self)
        # Allow the editors to change this
        assign_perm(f"change_{self._meta.model_name}", self.editors_group,
                    self)

        reg_and_anon = Group.objects.get(
            name=settings.REGISTERED_AND_ANON_USERS_GROUP_NAME)

        if self.public:
            assign_perm(f"view_{self._meta.model_name}", reg_and_anon, self)
        else:
            remove_perm(f"view_{self._meta.model_name}", reg_and_anon, self)

    def delete(self, *args, **kwargs):
        """
        Remove all patients, studies, images, imagefiles and annotations that
        belong exclusively to this archive.
        """
        def find_protected_studies_and_patients(images):
            """
            Returns a tuple containing a set of Study ids and a set of Patient
            ids that are "protected". Where "protected" means that these Study
            and Patient objects contain images that are not in the given list
            of images. Therefore, when deleting an archive and it's related
            objects, these Study and Patient objects should not be deleted
            since that would also delete other images, because of the cascading
            delete behavior of the many-to-one relation.

            :param images: list of image objects that are going to be removed
            :return: tuple containing a set of Study ids and a set of Patient
            ids that should not be removed
            """
            protected_study_ids = set()
            protected_patient_ids = set()
            for image in images:
                if image.study is None:
                    continue
                for other_study_image in image.study.image_set.all():
                    if other_study_image not in images_to_remove:
                        protected_study_ids.add(image.study.id)
                        protected_patient_ids.add(image.study.patient.id)
                        break

            return protected_study_ids, protected_patient_ids

        images_to_remove = (Image.objects.annotate(num_archives=Count(
            "componentinterfacevalue__archive_items__archive")).filter(
                componentinterfacevalue__archive_items__archive=self,
                num_archives=1,
            ).order_by("name"))

        (
            protected_study_ids,
            protected_patient_ids,
        ) = find_protected_studies_and_patients(images_to_remove)

        with transaction.atomic():
            Patient.objects.filter(
                study__image__in=images_to_remove).distinct().exclude(
                    pk__in=protected_patient_ids).delete(*args, **kwargs)
            Study.objects.filter(
                image__in=images_to_remove).distinct().exclude(
                    pk__in=protected_study_ids).delete(*args, **kwargs)
            images_to_remove.delete(*args, **kwargs)

            super().delete(*args, **kwargs)

    def is_editor(self, user):
        return user.groups.filter(pk=self.editors_group.pk).exists()

    def add_editor(self, user):
        return user.groups.add(self.editors_group)

    def remove_editor(self, user):
        return user.groups.remove(self.editors_group)

    def is_uploader(self, user):
        return user.groups.filter(pk=self.uploaders_group.pk).exists()

    def add_uploader(self, user):
        return user.groups.add(self.uploaders_group)

    def remove_uploader(self, user):
        return user.groups.remove(self.uploaders_group)

    def is_user(self, user):
        return user.groups.filter(pk=self.users_group.pk).exists()

    def add_user(self, user):
        return user.groups.add(self.users_group)

    def remove_user(self, user):
        return user.groups.remove(self.users_group)

    def get_absolute_url(self):
        return reverse("archives:detail", kwargs={"slug": self.slug})

    @property
    def api_url(self):
        return reverse("api:archive-detail", kwargs={"pk": self.pk})
Exemple #19
0
class Workstation(UUIDModel, TitleSlugDescriptionModel):
    """Store the title and description of a workstation."""

    logo = JPEGField(
        upload_to=get_logo_path,
        storage=public_s3_storage,
        variations=settings.STDIMAGE_LOGO_VARIATIONS,
    )
    editors_group = models.OneToOneField(
        Group,
        on_delete=models.PROTECT,
        editable=False,
        related_name="editors_of_workstation",
    )
    users_group = models.OneToOneField(
        Group,
        on_delete=models.PROTECT,
        editable=False,
        related_name="users_of_workstation",
    )
    config = models.ForeignKey(
        "workstation_configs.WorkstationConfig",
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
    )
    public = models.BooleanField(
        default=False,
        help_text=(
            "If True, all logged in users can use this workstation, "
            "otherwise, only the users group can use this workstation."
        ),
    )

    class Meta(UUIDModel.Meta, TitleSlugDescriptionModel.Meta):
        ordering = ("created", "title")

    @cached_property
    def latest_ready_image(self):
        """
        Returns
        -------
            The most recent container image for this workstation
        """
        return (
            self.workstationimage_set.filter(ready=True)
            .order_by("-created")
            .first()
        )

    def __str__(self):
        public = " (Public)" if self.public else ""
        return f"Viewer {self.title}{public}"

    def get_absolute_url(self):
        return reverse("workstations:detail", kwargs={"slug": self.slug})

    def create_groups(self):
        self.editors_group = Group.objects.create(
            name=f"{self._meta.app_label}_{self._meta.model_name}_{self.pk}_editors"
        )
        self.users_group = Group.objects.create(
            name=f"{self._meta.app_label}_{self._meta.model_name}_{self.pk}_users"
        )

    def save(self, *args, **kwargs):
        adding = self._state.adding

        if adding:
            self.create_groups()

        super().save(*args, **kwargs)

        self.assign_permissions()

    def assign_permissions(self):
        # Allow the editors and users groups to view this workstation
        assign_perm(f"view_{self._meta.model_name}", self.editors_group, self)
        assign_perm(f"view_{self._meta.model_name}", self.users_group, self)
        # Allow the editors to change this workstation
        assign_perm(
            f"change_{self._meta.model_name}", self.editors_group, self
        )

        g_reg = Group.objects.get(name=settings.REGISTERED_USERS_GROUP_NAME)

        if self.public:
            assign_perm(f"view_{self._meta.model_name}", g_reg, self)
        else:
            remove_perm(f"view_{self._meta.model_name}", g_reg, self)

    def is_editor(self, user):
        return user.groups.filter(pk=self.editors_group.pk).exists()

    def add_editor(self, user):
        return user.groups.add(self.editors_group)

    def remove_editor(self, user):
        return user.groups.remove(self.editors_group)

    def is_user(self, user):
        return user.groups.filter(pk=self.users_group.pk).exists()

    def add_user(self, user):
        return user.groups.add(self.users_group)

    def remove_user(self, user):
        return user.groups.remove(self.users_group)
Exemple #20
0
class Challenge(ChallengeBase):
    banner = JPEGField(
        upload_to=get_banner_path,
        storage=public_s3_storage,
        blank=True,
        help_text=(
            "Image that gets displayed at the top of each page. "
            "Recommended resolution 2200x440 px."
        ),
        variations=settings.STDIMAGE_BANNER_VARIATIONS,
    )
    disclaimer = models.CharField(
        max_length=2048,
        default="",
        blank=True,
        null=True,
        help_text=(
            "Optional text to show on each page in the project. "
            "For showing 'under construction' type messages"
        ),
    )
    require_participant_review = deprecate_field(
        models.BooleanField(
            default=False,
            help_text=(
                "If ticked, new participants need to be approved by project "
                "admins before they can access restricted pages. If not ticked, "
                "new users are allowed access immediately"
            ),
        )
    )
    access_request_handling = models.CharField(
        max_length=25,
        choices=AccessRequestHandlingOptions.choices,
        default=AccessRequestHandlingOptions.MANUAL_REVIEW,
        help_text=("How would you like to handle access requests?"),
    )
    use_registration_page = models.BooleanField(
        default=True,
        help_text="If true, show a registration page on the challenge site.",
    )
    registration_page_text = models.TextField(
        default="",
        blank=True,
        help_text=(
            "The text to use on the registration page, you could include "
            "a data usage agreement here. You can use HTML markup here."
        ),
    )
    use_workspaces = models.BooleanField(default=False)
    use_teams = models.BooleanField(
        default=False,
        help_text=(
            "If true, users are able to form teams to participate in "
            "this challenge together."
        ),
    )
    admins_group = models.OneToOneField(
        Group,
        editable=False,
        on_delete=models.PROTECT,
        related_name="admins_of_challenge",
    )
    participants_group = models.OneToOneField(
        Group,
        editable=False,
        on_delete=models.PROTECT,
        related_name="participants_of_challenge",
    )
    forum = models.OneToOneField(
        Forum, editable=False, on_delete=models.PROTECT
    )
    display_forum_link = models.BooleanField(
        default=False,
        help_text="Display a link to the challenge forum in the nav bar.",
    )

    cached_num_participants = models.PositiveIntegerField(
        editable=False, default=0
    )
    cached_num_results = models.PositiveIntegerField(editable=False, default=0)
    cached_latest_result = models.DateTimeField(
        editable=False, blank=True, null=True
    )
    contact_email = models.EmailField(
        blank=True,
        default="",
        help_text="This email will be listed as the contact email for the challenge and will be visible to all users of Grand Challenge.",
    )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._hidden_orig = self.hidden

    def save(self, *args, **kwargs):
        adding = self._state.adding

        if adding:
            self.create_groups()
            self.create_forum()

        super().save(*args, **kwargs)

        if adding:
            if self.creator:
                self.add_admin(user=self.creator)
            self.update_permissions()
            self.create_forum_permissions()
            self.create_default_pages()
            self.create_default_phases()

        if adding or self.hidden != self._hidden_orig:
            on_commit(
                lambda: assign_evaluation_permissions.apply_async(
                    kwargs={
                        "phase_pks": list(
                            self.phase_set.values_list("id", flat=True)
                        )
                    }
                )
            )
            self.update_user_forum_permissions()

    def update_permissions(self):
        assign_perm("change_challenge", self.admins_group, self)

    def create_forum_permissions(self):
        participant_group_perms = {
            "can_see_forum",
            "can_read_forum",
            "can_start_new_topics",
            "can_reply_to_topics",
            "can_delete_own_posts",
            "can_edit_own_posts",
            "can_post_without_approval",
            "can_create_polls",
            "can_vote_in_polls",
        }
        admin_group_perms = {
            "can_lock_topics",
            "can_edit_posts",
            "can_delete_posts",
            "can_approve_posts",
            "can_reply_to_locked_topics",
            "can_post_announcements",
            "can_post_stickies",
            *participant_group_perms,
        }

        permissions = ForumPermission.objects.filter(
            codename__in=admin_group_perms
        ).values_list("codename", "pk")
        permissions = {codename: pk for codename, pk in permissions}

        GroupForumPermission.objects.bulk_create(
            chain(
                (
                    GroupForumPermission(
                        permission_id=permissions[codename],
                        group=self.participants_group,
                        forum=self.forum,
                        has_perm=True,
                    )
                    for codename in participant_group_perms
                ),
                (
                    GroupForumPermission(
                        permission_id=permissions[codename],
                        group=self.admins_group,
                        forum=self.forum,
                        has_perm=True,
                    )
                    for codename in admin_group_perms
                ),
            )
        )

        UserForumPermission.objects.bulk_create(
            UserForumPermission(
                permission_id=permissions[codename],
                **{user: True},
                forum=self.forum,
                has_perm=not self.hidden,
            )
            for codename, user in product(
                ["can_see_forum", "can_read_forum"],
                ["anonymous_user", "authenticated_user"],
            )
        )

    def update_user_forum_permissions(self):
        perms = UserForumPermission.objects.filter(
            permission__codename__in=["can_see_forum", "can_read_forum"],
            forum=self.forum,
        )

        for p in perms:
            p.has_perm = not self.hidden

        UserForumPermission.objects.bulk_update(perms, ["has_perm"])

    def create_groups(self):
        # Create the groups only on first save
        admins_group = Group.objects.create(name=f"{self.short_name}_admins")
        participants_group = Group.objects.create(
            name=f"{self.short_name}_participants"
        )
        self.admins_group = admins_group
        self.participants_group = participants_group

    def create_forum(self):
        f, created = Forum.objects.get_or_create(
            name=settings.FORUMS_CHALLENGE_CATEGORY_NAME, type=Forum.FORUM_CAT
        )

        if created:
            UserForumPermission.objects.bulk_create(
                UserForumPermission(
                    permission_id=perm_id,
                    **{user: True},
                    forum=f,
                    has_perm=True,
                )
                for perm_id, user in product(
                    ForumPermission.objects.filter(
                        codename__in=["can_see_forum", "can_read_forum"]
                    ).values_list("pk", flat=True),
                    ["anonymous_user", "authenticated_user"],
                )
            )

        self.forum = Forum.objects.create(
            name=self.title if self.title else self.short_name,
            parent=f,
            type=Forum.FORUM_POST,
        )

    def create_default_pages(self):
        Page.objects.create(
            display_title=self.short_name,
            html=render_to_string(
                "pages/defaults/home.html", {"challenge": self}
            ),
            challenge=self,
            permission_level=Page.ALL,
        )

    def create_default_phases(self):
        self.phase_set.create(challenge=self)

    def is_admin(self, user) -> bool:
        """Determines if this user is an admin of this challenge."""
        return (
            user.is_superuser
            or user.groups.filter(pk=self.admins_group.pk).exists()
        )

    def is_participant(self, user) -> bool:
        """Determines if this user is a participant of this challenge."""
        return (
            user.is_superuser
            or user.groups.filter(pk=self.participants_group.pk).exists()
        )

    def get_admins(self):
        """Return all admins of this challenge."""
        return self.admins_group.user_set.all()

    def get_participants(self):
        """Return all participants of this challenge."""
        return self.participants_group.user_set.all()

    def get_absolute_url(self):
        return reverse(
            "pages:home", kwargs={"challenge_short_name": self.short_name}
        )

    def add_participant(self, user):
        if user != get_anonymous_user():
            user.groups.add(self.participants_group)
            follow(
                user=user, obj=self.forum, actor_only=False, send_action=False
            )
        else:
            raise ValueError("You cannot add the anonymous user to this group")

    def remove_participant(self, user):
        user.groups.remove(self.participants_group)
        unfollow(user=user, obj=self.forum, send_action=False)

    def add_admin(self, user):
        if user != get_anonymous_user():
            user.groups.add(self.admins_group)
            follow(
                user=user, obj=self.forum, actor_only=False, send_action=False
            )
        else:
            raise ValueError("You cannot add the anonymous user to this group")

    def remove_admin(self, user):
        user.groups.remove(self.admins_group)
        unfollow(user=user, obj=self.forum, send_action=False)

    @property
    def status(self):
        phase_status = {phase.status for phase in self.phase_set.all()}
        if StatusChoices.OPEN in phase_status:
            status = StatusChoices.OPEN
        elif {StatusChoices.COMPLETED} == phase_status:
            status = StatusChoices.COMPLETED
        elif StatusChoices.OPENING_SOON in phase_status:
            status = StatusChoices.OPENING_SOON
        else:
            status = StatusChoices.CLOSED
        return status

    @property
    def status_badge_string(self):
        if self.status == StatusChoices.OPEN:
            detail = [
                phase.submission_status_string
                for phase in self.phase_set.all()
                if phase.status == StatusChoices.OPEN
            ]
            if len(detail) > 1:
                # if there are multiple open phases it is unclear which
                # status to print, so stay vague
                detail = ["Accepting submissions"]
        elif self.status == StatusChoices.COMPLETED:
            detail = ["Challenge completed"]
        elif self.status == StatusChoices.CLOSED:
            detail = ["Not accepting submissions"]
        elif self.status == StatusChoices.OPENING_SOON:
            start_date = min(
                (
                    phase.submissions_open_at
                    for phase in self.phase_set.all()
                    if phase.status == StatusChoices.OPENING_SOON
                ),
                default=None,
            )
            phase = (
                self.phase_set.filter(submissions_open_at=start_date)
                .order_by("-created")
                .first()
            )
            detail = [phase.submission_status_string]
        else:
            raise NotImplementedError(f"{self.status} not handled")

        return detail[0]

    @cached_property
    def visible_phases(self):
        return self.phase_set.filter(public=True)

    class Meta(ChallengeBase.Meta):
        verbose_name = "challenge"
        verbose_name_plural = "challenges"
Exemple #21
0
class Image(models.Model):
    picture = JPEGField(upload_to='gallery/', variations={'full': (None, None), 'thumbnail': {'height': 200}, 'big_thumbnail': {'width': 360}}, null=True)
    in_gallery = models.ForeignKey(Gallery, null=True, on_delete=models.SET_NULL)

    def __str__(self):
        return self.picture.name
class UserProfile(models.Model):
    user = models.OneToOneField(
        get_user_model(),
        unique=True,
        verbose_name=_("user"),
        related_name="user_profile",
        on_delete=models.CASCADE,
    )

    mugshot = JPEGField(
        _("mugshot"),
        blank=True,
        upload_to=get_mugshot_path,
        help_text=_("A personal image displayed in your profile."),
        variations=settings.STDIMAGE_LOGO_VARIATIONS,
    )

    institution = models.CharField(max_length=100)
    department = models.CharField(max_length=100)
    country = CountryField()
    website = models.CharField(max_length=150, blank=True)
    display_organizations = models.BooleanField(
        default=True,
        help_text=
        "Display the organizations that you are a member of in your profile.",
    )

    receive_notification_emails = models.BooleanField(
        default=True,
        help_text="Whether to receive email updates about notifications",
    )
    notification_email_last_sent_at = models.DateTimeField(default=None,
                                                           null=True,
                                                           editable=False)
    notifications_last_read_at = models.DateTimeField(auto_now_add=True,
                                                      editable=False)

    def save(self, *args, **kwargs):
        adding = self._state.adding

        super().save(*args, **kwargs)

        if adding:
            self.assign_permissions()

    def assign_permissions(self):
        if self.user.username not in [
                settings.RETINA_IMPORT_USER_NAME,
                settings.ANONYMOUS_USER_NAME,
        ]:
            assign_perm("change_userprofile", self.user, self)

    def get_absolute_url(self):
        return reverse("profile-detail",
                       kwargs={"username": self.user.username})

    def get_mugshot_url(self):
        if self.mugshot:
            return self.mugshot.x02.url
        else:
            gravatar_url = (
                "https://www.gravatar.com/avatar/" +
                md5(self.user.email.lower().encode("utf-8")).hexdigest() + "?")
            gravatar_url += urlencode({"d": "identicon", "s": "64"})
            return gravatar_url

    @property
    def has_unread_notifications(self):
        return self.unread_notifications.exists()

    @property
    def unread_notifications(self):
        return self.notifications.exclude(
            timestamp__lt=self.notifications_last_read_at)

    @property
    def notifications(self):
        notifications = user_stream(obj=self.user)

        # Workaround for
        # https://github.com/justquick/django-activity-stream/issues/482
        notifications = notifications.exclude(
            actor_content_type=ContentType.objects.get_for_model(self.user),
            actor_object_id=self.user.pk,
        )

        return notifications
Exemple #23
0
class ReaderStudy(UUIDModel, TitleSlugDescriptionModel):
    """
    Reader Study model.

    A reader study is a tool that allows users to have a set of readers answer
    a set of questions on a set of images (cases).
    """

    editors_group = models.OneToOneField(
        Group,
        on_delete=models.PROTECT,
        editable=False,
        related_name="editors_of_readerstudy",
    )
    readers_group = models.OneToOneField(
        Group,
        on_delete=models.PROTECT,
        editable=False,
        related_name="readers_of_readerstudy",
    )
    images = models.ManyToManyField(
        "cases.Image", related_name="readerstudies"
    )
    workstation = models.ForeignKey(
        "workstations.Workstation", on_delete=models.PROTECT
    )
    workstation_config = models.ForeignKey(
        "workstation_configs.WorkstationConfig",
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
    )
    public = models.BooleanField(
        default=False,
        help_text=(
            "Should this reader study be visible to all users on the "
            "overview page? This does not grant all users permission to read "
            "this study. Users will still need to be added to the "
            "study's readers group in order to do that."
        ),
    )
    logo = JPEGField(
        upload_to=get_logo_path,
        storage=public_s3_storage,
        variations=settings.STDIMAGE_LOGO_VARIATIONS,
    )
    social_image = JPEGField(
        upload_to=get_social_image_path,
        storage=public_s3_storage,
        blank=True,
        help_text="An image for this reader study which is displayed when you post the link on social media. Should have a resolution of 640x320 px (1280x640 px for best display).",
        variations=settings.STDIMAGE_SOCIAL_VARIATIONS,
    )
    help_text_markdown = models.TextField(blank=True)

    # A hanging_list is a list of dictionaries where the keys are the
    # view names, and the values are the filenames to place there.
    hanging_list = models.JSONField(
        default=list,
        blank=True,
        validators=[JSONValidator(schema=HANGING_LIST_SCHEMA)],
    )
    shuffle_hanging_list = models.BooleanField(default=False)
    is_educational = models.BooleanField(
        default=False,
        help_text=(
            "If checked, readers get the option to verify their answers "
            "against the uploaded ground truth. This also means that "
            "the uploaded ground truth will be readily available to "
            "the readers."
        ),
    )
    case_text = models.JSONField(
        default=dict,
        blank=True,
        validators=[JSONValidator(schema=CASE_TEXT_SCHEMA)],
    )
    allow_answer_modification = models.BooleanField(
        default=False,
        help_text=(
            "If true, readers are allowed to modify their answers for a case "
            "by navigating back to previous cases. 'allow_case_browsing' must "
            "be checked with this as well."
        ),
    )
    allow_case_navigation = models.BooleanField(
        default=False,
        help_text=(
            "If true, readers are allowed to navigate back and forth between "
            "cases in this reader study."
        ),
    )
    allow_show_all_annotations = models.BooleanField(
        default=False,
        help_text=(
            "If true, readers are allowed to show/hide all annotations "
            "for a case."
        ),
    )
    validate_hanging_list = models.BooleanField(default=True)
    publications = models.ManyToManyField(
        Publication,
        blank=True,
        help_text="The publications associated with this reader study",
    )
    modalities = models.ManyToManyField(
        ImagingModality,
        blank=True,
        help_text="The imaging modalities contained in this reader study",
    )
    structures = models.ManyToManyField(
        BodyStructure,
        blank=True,
        help_text="The structures contained in this reader study",
    )
    organizations = models.ManyToManyField(
        Organization,
        blank=True,
        help_text="The organizations associated with this reader study",
        related_name="readerstudies",
    )

    class Meta(UUIDModel.Meta, TitleSlugDescriptionModel.Meta):
        verbose_name_plural = "reader studies"
        ordering = ("created",)
        permissions = [("read_readerstudy", "Can read reader study")]

    copy_fields = (
        "workstation",
        "workstation",
        "logo",
        "social_image",
        "help_text_markdown",
        "shuffle_hanging_list",
        "is_educational",
        "allow_answer_modification",
        "allow_case_navigation",
        "allow_show_all_annotations",
    )

    def __str__(self):
        return f"{self.title}"

    @property
    def ground_truth_file_headers(self):
        return ["images"] + [
            q.question_text for q in self.answerable_questions
        ]

    def get_ground_truth_csv_dict(self):
        if len(self.hanging_list) == 0:
            return {}
        result = []
        answers = {
            q.question_text: q.example_answer
            for q in self.answerable_questions
        }
        for images in self.image_groups:
            _answers = answers.copy()
            _answers["images"] = ";".join(images)
            result.append(_answers)
        return result

    def get_example_ground_truth_csv_text(self, limit=None):
        if len(self.hanging_list) == 0:
            return "No cases in this reader study"
        headers = self.ground_truth_file_headers
        return "\n".join(
            [
                ",".join(headers),
                "\n".join(
                    [
                        ",".join([x[header] for header in headers])
                        for x in self.get_ground_truth_csv_dict()[:limit]
                    ]
                ),
            ]
        )

    def get_absolute_url(self):
        return reverse("reader-studies:detail", kwargs={"slug": self.slug})

    @property
    def api_url(self):
        return reverse("api:reader-study-detail", kwargs={"pk": self.pk})

    def create_groups(self):
        self.editors_group = Group.objects.create(
            name=f"{self._meta.app_label}_{self._meta.model_name}_{self.pk}_editors"
        )
        self.readers_group = Group.objects.create(
            name=f"{self._meta.app_label}_{self._meta.model_name}_{self.pk}_readers"
        )

    def assign_permissions(self):
        # Allow the editors group to change this study
        assign_perm(
            f"change_{self._meta.model_name}", self.editors_group, self
        )

        # Allow the editors and readers groups to read this study
        assign_perm(f"read_{self._meta.model_name}", self.editors_group, self)
        assign_perm(f"read_{self._meta.model_name}", self.readers_group, self)

        # Allow readers and editors to add answers (globally)
        # adding them to this reader study is checked in the serializers as
        # there is no get_permission_object in django rest framework.
        assign_perm(
            f"{Answer._meta.app_label}.add_{Answer._meta.model_name}",
            self.editors_group,
        )
        assign_perm(
            f"{Answer._meta.app_label}.add_{Answer._meta.model_name}",
            self.readers_group,
        )

        # Allow the editors and readers groups to view this study
        assign_perm(f"view_{self._meta.model_name}", self.editors_group, self)
        assign_perm(f"view_{self._meta.model_name}", self.readers_group, self)

        reg_and_anon = Group.objects.get(
            name=settings.REGISTERED_AND_ANON_USERS_GROUP_NAME
        )

        if self.public:
            assign_perm(f"view_{self._meta.model_name}", reg_and_anon, self)
        else:
            remove_perm(f"view_{self._meta.model_name}", reg_and_anon, self)

    def assign_workstation_permissions(self):
        perm = "workstations.view_workstation"

        for group in (self.editors_group, self.readers_group):
            workstations = get_objects_for_group(
                group=group, perms=perm, accept_global_perms=False
            )

            if (
                self.workstation not in workstations
            ) or workstations.count() > 1:
                remove_perm(perm=perm, user_or_group=group, obj=workstations)

                # Allow readers to view the workstation used for this study
                assign_perm(
                    perm=perm, user_or_group=group, obj=self.workstation
                )

    def save(self, *args, **kwargs):
        adding = self._state.adding

        if adding:
            self.create_groups()

        super().save(*args, **kwargs)

        self.assign_permissions()
        self.assign_workstation_permissions()

    def delete(self):
        ct = ContentType.objects.filter(
            app_label=self._meta.app_label, model=self._meta.model_name
        ).get()
        Follow.objects.filter(object_id=self.pk, content_type=ct).delete()
        super().delete()

    def is_editor(self, user):
        """Checks if ``user`` is an editor for this ``ReaderStudy``."""
        return user.groups.filter(pk=self.editors_group.pk).exists()

    def add_editor(self, user):
        """Adds ``user`` as an editor for this ``ReaderStudy``."""
        return user.groups.add(self.editors_group)

    def remove_editor(self, user):
        """Removes ``user`` as an editor for this ``ReaderStudy``."""
        return user.groups.remove(self.editors_group)

    def is_reader(self, user):
        """Checks if ``user`` is a reader for this ``ReaderStudy``."""
        return user.groups.filter(pk=self.readers_group.pk).exists()

    def add_reader(self, user):
        """Adds ``user`` as a reader for this ``ReaderStudy``."""
        return user.groups.add(self.readers_group)

    def remove_reader(self, user):
        """Removes ``user`` as a reader for this ``ReaderStudy``."""
        return user.groups.remove(self.readers_group)

    @property
    def help_text(self):
        """The cleaned help text from the markdown sources"""
        return md2html(self.help_text_markdown, link_blank_target=True)

    @property
    def cleaned_case_text(self):
        study_images = {im.name: im.api_url for im in self.images.all()}
        return {
            study_images.get(k): md2html(v)
            for k, v in self.case_text.items()
            if k in study_images
        }

    @property
    def study_image_names(self):
        """Names for all images added to this ``ReaderStudy``."""
        return self.images.values_list("name", flat=True)

    @property
    def hanging_image_names(self):
        """Names for all images in the hanging list."""
        return [
            name for hanging in self.hanging_list for name in hanging.values()
        ]

    @property
    def hanging_list_valid(self):
        """
        Tests that all of the study images are included in the hanging list
        exactly once.
        """
        return not self.validate_hanging_list or sorted(
            self.study_image_names
        ) == sorted(self.hanging_image_names)

    def hanging_list_diff(self, provided=None):
        """
        Returns the diff between the images added to the study and the images
        in the hanging list.
        """
        comparison = provided or self.study_image_names
        return {
            "in_provided_list": set(comparison)
            - set(self.hanging_image_names),
            "in_hanging_list": set(self.hanging_image_names) - set(comparison),
        }

    @property
    def non_unique_study_image_names(self):
        """Returns all of the non-unique image names for this ``ReaderStudy``."""
        return [
            name
            for name, count in Counter(self.study_image_names).items()
            if count > 1
        ]

    @property
    def is_valid(self):
        """
        Returns ``True`` if the hanging list is valid and there are no
        duplicate image names in this ``ReaderStudy`` and ``False`` otherwise.
        """
        return (
            self.hanging_list_valid
            and len(self.non_unique_study_image_names) == 0
        )

    @property
    def hanging_list_images(self):
        """
        Substitutes the image name for the image detail api url for each image
        defined in the hanging list.
        """
        if not self.is_valid:
            return None

        study_images = {im.name: im.api_url for im in self.images.all()}

        hanging_list_images = [
            {view: study_images.get(name) for view, name in hanging.items()}
            for hanging in self.hanging_list
        ]

        return hanging_list_images

    @property
    def image_groups(self):
        """Names of the images as they are grouped in the hanging list."""
        return [sorted(x.values()) for x in self.hanging_list]

    @property
    def has_ground_truth(self):
        return Answer.objects.filter(
            question__reader_study_id=self.id, is_ground_truth=True
        ).exists()

    @cached_property
    def answerable_questions(self):
        """
        All questions for this ``ReaderStudy`` except those with answer type
        `heading`.
        """
        return self.questions.exclude(answer_type=Question.AnswerType.HEADING)

    @cached_property
    def answerable_question_count(self):
        """The number of answerable questions for this ``ReaderStudy``."""
        return self.answerable_questions.count()

    def add_ground_truth(self, *, data, user):  # noqa: C901
        """Add ground truth answers provided by ``data`` for this ``ReaderStudy``."""
        answers = []
        for gt in data:
            images = self.images.filter(name__in=gt["images"].split(";"))
            for key in gt.keys():
                if key == "images" or key.endswith("__explanation"):
                    continue
                question = self.questions.get(question_text=key)
                _answer = json.loads(gt[key])
                if question.answer_type == Question.AnswerType.CHOICE:
                    try:
                        option = question.options.get(title=_answer)
                        _answer = option.pk
                    except CategoricalOption.DoesNotExist:
                        raise ValidationError(
                            f"Option '{_answer}' is not valid for question {question.question_text}"
                        )
                if question.answer_type in (
                    Question.AnswerType.MULTIPLE_CHOICE,
                    Question.AnswerType.MULTIPLE_CHOICE_DROPDOWN,
                ):
                    _answer = list(
                        question.options.filter(title__in=_answer).values_list(
                            "pk", flat=True
                        )
                    )
                Answer.validate(
                    creator=user,
                    question=question,
                    images=images,
                    answer=_answer,
                    is_ground_truth=True,
                )
                try:
                    explanation = json.loads(gt.get(key + "__explanation", ""))
                except (json.JSONDecodeError, TypeError):
                    explanation = ""
                answers.append(
                    {
                        "answer_obj": Answer.objects.filter(
                            images__in=images,
                            question=question,
                            is_ground_truth=True,
                        ).first()
                        or Answer(
                            creator=user,
                            question=question,
                            is_ground_truth=True,
                            explanation="",
                        ),
                        "answer": _answer,
                        "explanation": explanation,
                        "images": images,
                    }
                )

        for answer in answers:
            answer["answer_obj"].answer = answer["answer"]
            answer["answer_obj"].explanation = answer["explanation"]
            answer["answer_obj"].save()
            answer["answer_obj"].images.set(answer["images"])
            answer["answer_obj"].save()

    def get_hanging_list_images_for_user(self, *, user):
        """
        Returns a shuffled list of the hanging list images for a particular
        user.

        The shuffle is seeded with the users pk, and using ``RandomState``
        from numpy guarantees that the ordering will be consistent across
        python/library versions. Returns the normal list if
        ``shuffle_hanging_list`` is ``False``.
        """
        hanging_list = self.hanging_list_images

        if self.shuffle_hanging_list and hanging_list is not None:
            # In place shuffle
            RandomState(seed=int(user.pk)).shuffle(hanging_list)

        return hanging_list

    def generate_hanging_list(self):
        """
        Generates a new hanging list.

        Each image in the ``ReaderStudy`` is assigned to the primary port of its
        own hanging.
        """
        image_names = self.images.values_list("name", flat=True)
        self.hanging_list = [{"main": name} for name in image_names]
        self.save()

    def get_progress_for_user(self, user):
        """Returns the percentage of completed hangings and questions for ``user``."""
        if not self.is_valid or not self.hanging_list:
            return {
                "questions": 0.0,
                "hangings": 0.0,
                "diff": 0.0,
            }

        hanging_list_count = len(self.hanging_list)

        expected = hanging_list_count * self.answerable_question_count

        answers = Answer.objects.filter(
            question__in=self.answerable_questions,
            creator_id=user.id,
            is_ground_truth=False,
        ).distinct()
        answer_count = answers.count()

        if expected == 0 or answer_count == 0:
            return {"questions": 0.0, "hangings": 0.0, "diff": 0.0}

        # Group the answers by images and filter out the images that
        # have an inadequate amount of answers
        unanswered_images = (
            answers.order_by("images__name")
            .values("images__name")
            .annotate(answer_count=Count("images__name"))
            .filter(answer_count__lt=self.answerable_question_count)
        )
        image_names = set(
            unanswered_images.values_list("images__name", flat=True)
        ).union(
            set(
                Image.objects.filter(readerstudies=self)
                .annotate(
                    answers_for_user=Count(
                        Subquery(
                            Answer.objects.filter(
                                creator=user,
                                images=OuterRef("pk"),
                                is_ground_truth=False,
                            ).values("pk")[:1]
                        )
                    )
                )
                .filter(answers_for_user=0)
                .order_by("name")
                .distinct()
                .values_list("name", flat=True)
            )
        )
        # Determine which hangings have images with unanswered questions
        hanging_list = [set(x.values()) for x in self.hanging_list]
        completed_hangings = [
            x for x in hanging_list if len(x - image_names) == len(x)
        ]
        completed_hangings = len(completed_hangings)

        hangings = completed_hangings / hanging_list_count * 100
        questions = answer_count / expected * 100
        return {
            "questions": questions,
            "hangings": hangings,
            "diff": questions - hangings,
        }

    def score_for_user(self, user):
        """Returns the average and total score for answers given by ``user``."""
        return Answer.objects.filter(
            creator=user, question__reader_study=self, is_ground_truth=False
        ).aggregate(Sum("score"), Avg("score"))

    @cached_property
    def scores_by_user(self):
        """The average and total scores for this ``ReaderStudy`` grouped by user."""
        return (
            Answer.objects.filter(
                question__reader_study=self, is_ground_truth=False
            )
            .order_by("creator_id")
            .values("creator__username")
            .annotate(Sum("score"), Avg("score"))
            .order_by("-score__sum")
        )

    @cached_property
    def leaderboard(self):
        """The leaderboard for this ``ReaderStudy``."""
        question_count = float(self.answerable_question_count) * len(
            self.hanging_list
        )
        return {
            "question_count": question_count,
            "grouped_scores": self.scores_by_user,
        }

    @cached_property
    def statistics(self):
        """Statistics per question and case based on the total / average score."""
        scores_by_question = (
            Answer.objects.filter(
                question__reader_study=self, is_ground_truth=False
            )
            .order_by("question_id")
            .values("question__question_text")
            .annotate(Sum("score"), Avg("score"))
            .order_by("-score__avg")
        )

        scores_by_case = (
            Answer.objects.filter(
                question__reader_study=self, is_ground_truth=False
            )
            .order_by("images__name")
            .values("images__name", "images__pk")
            .annotate(Sum("score"), Avg("score"),)
            .order_by("score__avg")
        )

        options = {}
        for option in CategoricalOption.objects.filter(
            question__reader_study=self
        ).values("id", "title", "question"):
            qt = option["question"]
            options[qt] = options.get(qt, {})
            options[qt].update({option["id"]: option["title"]})

        ground_truths = {}
        questions = []
        for gt in (
            Answer.objects.filter(
                question__reader_study=self, is_ground_truth=True
            )
            .values(
                "images__name",
                "answer",
                "question",
                "question__question_text",
                "question__answer_type",
            )
            .order_by("question__order", "question__created")
        ):
            questions.append(gt["question__question_text"])

            ground_truths[gt["images__name"]] = ground_truths.get(
                gt["images__name"], {}
            )

            if gt["question__answer_type"] in [
                Question.AnswerType.MULTIPLE_CHOICE,
                Question.AnswerType.MULTIPLE_CHOICE_DROPDOWN,
            ]:
                human_readable_answers = [
                    options[gt["question"]].get(a, a) for a in gt["answer"]
                ]
                human_readable_answers.sort()
                human_readable_answer = ", ".join(human_readable_answers)
            else:
                human_readable_answer = options.get(gt["question"], {}).get(
                    gt["answer"], gt["answer"]
                )

            ground_truths[gt["images__name"]][
                gt["question__question_text"]
            ] = human_readable_answer

        questions = list(dict.fromkeys(questions))

        return {
            "max_score_questions": float(len(self.hanging_list))
            * self.scores_by_user.count(),
            "scores_by_question": scores_by_question,
            "max_score_cases": float(self.answerable_question_count)
            * self.scores_by_user.count(),
            "scores_by_case": scores_by_case,
            "ground_truths": ground_truths,
            "questions": questions,
        }
Exemple #24
0
class Algorithm(UUIDModel, TitleSlugDescriptionModel):
    editors_group = models.OneToOneField(
        Group,
        on_delete=models.CASCADE,
        editable=False,
        related_name="editors_of_algorithm",
    )
    users_group = models.OneToOneField(
        Group,
        on_delete=models.CASCADE,
        editable=False,
        related_name="users_of_algorithm",
    )
    logo = JPEGField(
        upload_to=get_logo_path,
        storage=public_s3_storage,
        variations=settings.STDIMAGE_LOGO_VARIATIONS,
    )
    social_image = JPEGField(
        upload_to=get_social_image_path,
        storage=public_s3_storage,
        blank=True,
        help_text="An image for this algorithm which is displayed when you post the link for this algorithm on social media. Should have a resolution of 640x320 px (1280x640 px for best display).",
        variations=settings.STDIMAGE_SOCIAL_VARIATIONS,
    )
    workstation = models.ForeignKey(
        "workstations.Workstation", on_delete=models.CASCADE
    )
    workstation_config = models.ForeignKey(
        "workstation_configs.WorkstationConfig",
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
    )
    public = models.BooleanField(
        default=False,
        help_text=(
            "Should this algorithm be visible to all users on the algorithm "
            "overview page? This does not grant all users permission to use "
            "this algorithm. Users will still need to be added to the "
            "algorithm users group in order to do that."
        ),
    )
    detail_page_markdown = models.TextField(blank=True)
    job_create_page_markdown = models.TextField(blank=True)
    additional_terms_markdown = models.TextField(
        blank=True,
        help_text=(
            "By using this algortihm, users agree to the site wide "
            "terms of service. If your algorithm has any additional "
            "terms of usage, define them here."
        ),
    )
    result_template = models.TextField(
        blank=True,
        default="<pre>{{ results|tojson(indent=2) }}</pre>",
        help_text=(
            "Define the jinja template to render the content of the "
            "results.json to html. For example, the following template will "
            "print out all the keys and values of the result.json. "
            "Use results to access the json root. "
            "{% for key, value in results.metrics.items() -%}"
            "{{ key }}  {{ value }}"
            "{% endfor %}"
        ),
    )
    inputs = models.ManyToManyField(
        to=ComponentInterface, related_name="algorithm_inputs"
    )
    outputs = models.ManyToManyField(
        to=ComponentInterface, related_name="algorithm_outputs"
    )
    publications = models.ManyToManyField(
        Publication,
        blank=True,
        help_text="The publications associated with this algorithm",
    )
    modalities = models.ManyToManyField(
        ImagingModality,
        blank=True,
        help_text="The imaging modalities supported by this algorithm",
    )
    structures = models.ManyToManyField(
        BodyStructure,
        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=(
            "The number of credits that are required for each execution of this algorithm."
        ),
    )
    average_duration = models.DurationField(
        null=True,
        default=None,
        editable=False,
        help_text="The average duration of successful jobs.",
    )
    use_flexible_inputs = models.BooleanField(default=True)

    class Meta(UUIDModel.Meta, TitleSlugDescriptionModel.Meta):
        ordering = ("created",)
        permissions = [("execute_algorithm", "Can execute algorithm")]

    def __str__(self):
        return f"{self.title}"

    def get_absolute_url(self):
        return reverse("algorithms:detail", kwargs={"slug": self.slug})

    @property
    def api_url(self):
        return reverse("api:algorithm-detail", kwargs={"pk": self.pk})

    def save(self, *args, **kwargs):
        adding = self._state.adding

        if adding:
            self.create_groups()
            self.workstation_id = (
                self.workstation_id or self.default_workstation.pk
            )

        super().save(*args, **kwargs)

        if adding:
            self.set_default_interfaces()

        self.assign_permissions()
        self.assign_workstation_permissions()

    def create_groups(self):
        self.editors_group = Group.objects.create(
            name=f"{self._meta.app_label}_{self._meta.model_name}_{self.pk}_editors"
        )
        self.users_group = Group.objects.create(
            name=f"{self._meta.app_label}_{self._meta.model_name}_{self.pk}_users"
        )

    def set_default_interfaces(self):
        if not self.inputs.exists():
            self.inputs.set(
                [
                    ComponentInterface.objects.get(
                        slug=DEFAULT_INPUT_INTERFACE_SLUG
                    )
                ]
            )
        if not self.outputs.exists():
            self.outputs.set(
                [
                    ComponentInterface.objects.get(slug="results-json-file"),
                    ComponentInterface.objects.get(
                        slug=DEFAULT_OUTPUT_INTERFACE_SLUG
                    ),
                ]
            )

    def assign_permissions(self):
        # Editors and users can view this algorithm
        assign_perm(f"view_{self._meta.model_name}", self.editors_group, self)
        assign_perm(f"view_{self._meta.model_name}", self.users_group, self)
        # Editors and users can execute this algorithm
        assign_perm(
            f"execute_{self._meta.model_name}", self.editors_group, self
        )
        assign_perm(f"execute_{self._meta.model_name}", self.users_group, self)
        # Editors can change this algorithm
        assign_perm(
            f"change_{self._meta.model_name}", self.editors_group, self
        )

        reg_and_anon = Group.objects.get(
            name=settings.REGISTERED_AND_ANON_USERS_GROUP_NAME
        )

        if self.public:
            assign_perm(f"view_{self._meta.model_name}", reg_and_anon, self)
        else:
            remove_perm(f"view_{self._meta.model_name}", reg_and_anon, self)

    def assign_workstation_permissions(self):
        """Allow the editors and users group to view the workstation."""
        perm = f"view_{Workstation._meta.model_name}"

        for group in [self.users_group, self.editors_group]:
            workstations = get_objects_for_group(
                group=group, perms=perm, klass=Workstation
            )

            if (
                self.workstation not in workstations
            ) or workstations.count() > 1:
                remove_perm(perm=perm, user_or_group=group, obj=workstations)
                assign_perm(
                    perm=perm, user_or_group=group, obj=self.workstation
                )

    @property
    def latest_ready_image(self):
        """
        Returns
        -------
            The most recent container image for this algorithm
        """
        return (
            self.algorithm_container_images.filter(ready=True)
            .order_by("-created")
            .first()
        )

    @property
    def default_workstation(self):
        """
        Returns the default workstation, creating it if it does not already
        exist.
        """
        w, created = Workstation.objects.get_or_create(
            slug=settings.DEFAULT_WORKSTATION_SLUG
        )

        if created:
            w.title = settings.DEFAULT_WORKSTATION_SLUG
            w.save()

        return w

    def update_average_duration(self):
        """Store the duration of successful jobs for this algorithm"""
        self.average_duration = Job.objects.filter(
            algorithm_image__algorithm=self, status=Job.SUCCESS
        ).average_duration()
        self.save(update_fields=("average_duration",))

    def is_editor(self, user):
        return user.groups.filter(pk=self.editors_group.pk).exists()

    def add_editor(self, user):
        return user.groups.add(self.editors_group)

    def remove_editor(self, user):
        return user.groups.remove(self.editors_group)

    def is_user(self, user):
        return user.groups.filter(pk=self.users_group.pk).exists()

    def add_user(self, user):
        return user.groups.add(self.users_group)

    def remove_user(self, user):
        return user.groups.remove(self.users_group)
Exemple #25
0
class Archive(UUIDModel, TitleSlugDescriptionModel):
    """Model for archive. Contains a collection of images."""

    detail_page_markdown = models.TextField(blank=True)
    logo = JPEGField(
        upload_to=get_logo_path,
        storage=public_s3_storage,
        variations=settings.STDIMAGE_LOGO_VARIATIONS,
    )
    social_image = JPEGField(
        upload_to=get_social_image_path,
        storage=public_s3_storage,
        blank=True,
        help_text=
        "An image for this archive which is displayed when you post the link to this archive on social media. Should have a resolution of 640x320 px (1280x640 px for best display).",
        variations=settings.STDIMAGE_SOCIAL_VARIATIONS,
    )
    editors_group = models.OneToOneField(
        Group,
        on_delete=models.PROTECT,
        editable=False,
        related_name="editors_of_archive",
    )
    uploaders_group = models.OneToOneField(
        Group,
        on_delete=models.PROTECT,
        editable=False,
        related_name="uploaders_of_archive",
    )
    users_group = models.OneToOneField(
        Group,
        on_delete=models.PROTECT,
        editable=False,
        related_name="users_of_archive",
    )
    public = models.BooleanField(default=False)
    workstation = models.ForeignKey(
        "workstations.Workstation",
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
    )
    workstation_config = models.ForeignKey(
        "workstation_configs.WorkstationConfig",
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
    )
    algorithms = models.ManyToManyField(
        Algorithm,
        blank=True,
        help_text=
        "Algorithms that will be executed on all images in this archive",
    )
    publications = models.ManyToManyField(
        Publication,
        blank=True,
        help_text="The publications associated with this archive",
    )
    modalities = models.ManyToManyField(
        ImagingModality,
        blank=True,
        help_text="The imaging modalities contained in this archive",
    )
    structures = models.ManyToManyField(
        BodyStructure,
        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", )
        permissions = [
            (
                "use_archive",
                ("Can use the objects in the archive as inputs to "
                 "algorithms, reader studies and challenges."),
            ),
            ("upload_archive", "Can upload to archive"),
        ]

    def __str__(self):
        return f"{self.title}"

    @property
    def name(self):
        # Include the read only name for legacy clients
        return self.title

    def save(self, *args, **kwargs):
        adding = self._state.adding

        if adding:
            self.create_groups()

        super().save(*args, **kwargs)

        self.assign_permissions()

    def delete(self):
        ct = ContentType.objects.filter(app_label=self._meta.app_label,
                                        model=self._meta.model_name).get()
        Follow.objects.filter(object_id=self.pk, content_type=ct).delete()
        super().delete()

    def create_groups(self):
        self.editors_group = Group.objects.create(
            name=
            f"{self._meta.app_label}_{self._meta.model_name}_{self.pk}_editors"
        )
        self.uploaders_group = Group.objects.create(
            name=
            f"{self._meta.app_label}_{self._meta.model_name}_{self.pk}_uploaders"
        )
        self.users_group = Group.objects.create(
            name=
            f"{self._meta.app_label}_{self._meta.model_name}_{self.pk}_users")

    def assign_permissions(self):
        # Allow the editors, uploaders and users groups to view this
        assign_perm(f"view_{self._meta.model_name}", self.editors_group, self)
        assign_perm(f"view_{self._meta.model_name}", self.uploaders_group,
                    self)
        assign_perm(f"view_{self._meta.model_name}", self.users_group, self)

        # Allow the editors, uploaders and users group to use the archive
        assign_perm(f"use_{self._meta.model_name}", self.editors_group, self)
        assign_perm(f"use_{self._meta.model_name}", self.uploaders_group, self)
        assign_perm(f"use_{self._meta.model_name}", self.users_group, self)

        # Allow editors and uploaders to upload to this
        assign_perm(f"upload_{self._meta.model_name}", self.editors_group,
                    self)
        assign_perm(f"upload_{self._meta.model_name}", self.uploaders_group,
                    self)
        # Allow the editors to change this
        assign_perm(f"change_{self._meta.model_name}", self.editors_group,
                    self)

        reg_and_anon = Group.objects.get(
            name=settings.REGISTERED_AND_ANON_USERS_GROUP_NAME)

        if self.public:
            assign_perm(f"view_{self._meta.model_name}", reg_and_anon, self)
        else:
            remove_perm(f"view_{self._meta.model_name}", reg_and_anon, self)

    def is_editor(self, user):
        return user.groups.filter(pk=self.editors_group.pk).exists()

    def add_editor(self, user):
        return user.groups.add(self.editors_group)

    def remove_editor(self, user):
        return user.groups.remove(self.editors_group)

    def is_uploader(self, user):
        return user.groups.filter(pk=self.uploaders_group.pk).exists()

    def add_uploader(self, user):
        return user.groups.add(self.uploaders_group)

    def remove_uploader(self, user):
        return user.groups.remove(self.uploaders_group)

    def is_user(self, user):
        return user.groups.filter(pk=self.users_group.pk).exists()

    def add_user(self, user):
        return user.groups.add(self.users_group)

    def remove_user(self, user):
        return user.groups.remove(self.users_group)

    def get_absolute_url(self):
        return reverse("archives:detail", kwargs={"slug": self.slug})

    @property
    def api_url(self):
        return reverse("api:archive-detail", kwargs={"pk": self.pk})
class Post(models.Model):
    created = models.DateTimeField(auto_now_add=True)
    modified = models.DateTimeField(auto_now=True)

    title = models.CharField(max_length=1024)
    slug = AutoSlugField(populate_from="title", max_length=1024)
    description = models.TextField()
    content = models.TextField()

    authors = models.ManyToManyField(to=get_user_model(),
                                     related_name="blog_authors")

    logo = JPEGField(
        upload_to=get_logo_path,
        storage=public_s3_storage,
        variations=settings.STDIMAGE_SOCIAL_VARIATIONS,
    )

    tags = models.ManyToManyField(to=Tag, blank=True, related_name="posts")

    companies = models.ManyToManyField(to=Company,
                                       blank=True,
                                       related_name="posts")

    published = models.BooleanField(default=False)

    highlight = models.BooleanField(
        default=False,
        help_text=
        "If selected, this blog post will appear in first position in the news carousel on the home page.",
    )

    history = HistoricalRecords()

    class Meta:
        ordering = ("-created", )

    def __str__(self):
        return self.title

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._published_orig = self.published

    def save(self, *args, **kwargs):
        if self._published_orig is False and self.published is True:
            self.created = timezone.now()

        super().save(*args, **kwargs)

    def get_absolute_url(self):
        if self.tags.filter(slug="products").exists():
            return reverse("products:blogs-detail", kwargs={"slug": self.slug})
        else:
            return reverse("blogs:detail", kwargs={"slug": self.slug})

    @property
    def public(self):
        return self.published

    def add_author(self, user):
        self.authors.add(user)