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
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, )
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, )
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)
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()
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
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()
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
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)
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)
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)
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})
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)
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"
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
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, }
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)
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)