class DashboardItem(models.Model): dashboard: models.ForeignKey = models.ForeignKey( "Dashboard", related_name="items", on_delete=models.CASCADE, null=True, blank=True ) name: models.CharField = models.CharField(max_length=400, null=True, blank=True) description: models.CharField = models.CharField(max_length=400, null=True, blank=True) team: models.ForeignKey = models.ForeignKey("Team", on_delete=models.CASCADE) filters: JSONField = JSONField(default=dict) filters_hash: models.CharField = models.CharField(max_length=400, null=True, blank=True) order: models.IntegerField = models.IntegerField(null=True, blank=True) deleted: models.BooleanField = models.BooleanField(default=False) saved: models.BooleanField = models.BooleanField(default=False) created_at: models.DateTimeField = models.DateTimeField(null=True, blank=True, auto_now_add=True) layouts: JSONField = JSONField(default=dict) color: models.CharField = models.CharField(max_length=400, null=True, blank=True) last_refresh: models.DateTimeField = models.DateTimeField(blank=True, null=True) refreshing: models.BooleanField = models.BooleanField(default=False) created_by: models.ForeignKey = models.ForeignKey("User", on_delete=models.SET_NULL, null=True, blank=True) is_sample: models.BooleanField = models.BooleanField( default=False ) # indicates if it's a sample graph generated by dashboard templates # Deprecated in favour of `display` within the Filter object type: models.CharField = deprecate_field(models.CharField(max_length=400, null=True, blank=True)) # Deprecated as we don't store funnels as a separate model any more funnel: models.ForeignKey = deprecate_field(models.IntegerField(null=True, blank=True)) def dashboard_filters(self, dashboard: Optional[Dashboard] = None): if dashboard is None: dashboard = self.dashboard if dashboard: return {**self.filters, **dashboard.filters} else: return self.filters
class EnterprisePropertyDefinition(PropertyDefinition): description: models.TextField = models.TextField(blank=True, null=True, default="") updated_at: models.DateTimeField = models.DateTimeField(auto_now=True) updated_by = models.ForeignKey("posthog.User", null=True, on_delete=models.SET_NULL, blank=True) # Deprecated in favour of app-wide tagging model. See EnterpriseTaggedItem deprecated_tags: ArrayField = deprecate_field( ArrayField(models.CharField(max_length=32), null=True, blank=True, default=list), return_instead=[], ) tags: ArrayField = deprecate_field( ArrayField(models.CharField(max_length=32), null=True, blank=True, default=None), return_instead=[], )
class EnterpriseEventDefinition(EventDefinition): owner = models.ForeignKey("posthog.User", null=True, on_delete=models.SET_NULL, related_name="event_definitions") description: models.TextField = models.TextField(blank=True, null=True, default="") updated_at: models.DateTimeField = models.DateTimeField(auto_now=True) updated_by = models.ForeignKey("posthog.User", null=True, on_delete=models.SET_NULL, blank=True) verified: models.BooleanField = models.BooleanField(default=False, blank=True) verified_at: models.DateTimeField = models.DateTimeField(null=True, blank=True) verified_by = models.ForeignKey( "posthog.User", null=True, on_delete=models.SET_NULL, blank=True, related_name="verifying_user", ) # Deprecated in favour of app-wide tagging model. See EnterpriseTaggedItem deprecated_tags: ArrayField = deprecate_field( ArrayField(models.CharField(max_length=32), null=True, blank=True, default=list), return_instead=[], ) tags: ArrayField = deprecate_field( ArrayField(models.CharField(max_length=32), null=True, blank=True, default=None), return_instead=[], )
class DashboardItem(models.Model): dashboard: models.ForeignKey = models.ForeignKey("Dashboard", related_name="items", on_delete=models.CASCADE, null=True, blank=True) name: models.CharField = models.CharField(max_length=400, null=True, blank=True) description: models.CharField = models.CharField(max_length=400, null=True, blank=True) team: models.ForeignKey = models.ForeignKey("Team", on_delete=models.CASCADE) filters: JSONField = JSONField(default=dict) filters_hash: models.CharField = models.CharField(max_length=400, null=True, blank=True) order: models.IntegerField = models.IntegerField(null=True, blank=True) type: models.CharField = models.CharField(max_length=400, null=True, blank=True) deleted: models.BooleanField = models.BooleanField(default=False) saved: models.BooleanField = models.BooleanField(default=False) created_at: models.DateTimeField = models.DateTimeField(null=True, blank=True, auto_now_add=True) layouts: JSONField = JSONField(default=dict) color: models.CharField = models.CharField(max_length=400, null=True, blank=True) last_refresh: models.DateTimeField = models.DateTimeField(blank=True, null=True) refreshing: models.BooleanField = models.BooleanField(default=False) funnel: models.ForeignKey = deprecate_field( models.IntegerField(null=True, blank=True)) created_by: models.ForeignKey = models.ForeignKey( "User", on_delete=models.SET_NULL, null=True, blank=True) is_sample: models.BooleanField = models.BooleanField( default=False ) # indicates if it's a sample graph generated by dashboard templates
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 Insight(models.Model): """ Stores saved insights along with their entire configuration options. Saved insights can be stored as standalone reports or part of a dashboard. """ dashboard: models.ForeignKey = models.ForeignKey( "Dashboard", related_name="items", on_delete=models.CASCADE, null=True, blank=True, ) name: models.CharField = models.CharField(max_length=400, null=True, blank=True) derived_name: models.CharField = models.CharField(max_length=400, null=True, blank=True) description: models.CharField = models.CharField(max_length=400, null=True, blank=True) team: models.ForeignKey = models.ForeignKey("Team", on_delete=models.CASCADE) filters: models.JSONField = models.JSONField(default=dict) filters_hash: models.CharField = models.CharField(max_length=400, null=True, blank=True) order: models.IntegerField = models.IntegerField(null=True, blank=True) deleted: models.BooleanField = models.BooleanField(default=False) saved: models.BooleanField = models.BooleanField(default=False) created_at: models.DateTimeField = models.DateTimeField(null=True, blank=True, auto_now_add=True) layouts: models.JSONField = models.JSONField(default=dict) color: models.CharField = models.CharField(max_length=400, null=True, blank=True) last_refresh: models.DateTimeField = models.DateTimeField(blank=True, null=True) refreshing: models.BooleanField = models.BooleanField(default=False) created_by: models.ForeignKey = models.ForeignKey( "User", on_delete=models.SET_NULL, null=True, blank=True) # Indicates if it's a sample graph generated by dashboard templates is_sample: models.BooleanField = models.BooleanField(default=False) # Unique ID per team for easy sharing and short links short_id: models.CharField = models.CharField( max_length=12, blank=True, default=generate_short_id, ) favorited: models.BooleanField = models.BooleanField(default=False) refresh_attempt: models.IntegerField = models.IntegerField(null=True, blank=True) last_modified_at: models.DateTimeField = models.DateTimeField( default=timezone.now) last_modified_by: models.ForeignKey = models.ForeignKey( "User", on_delete=models.SET_NULL, null=True, blank=True, related_name="modified_insights", ) # TODO: dive dashboards have never been shipped, but they still may be in the future dive_dashboard: models.ForeignKey = models.ForeignKey( "Dashboard", on_delete=models.SET_NULL, null=True, blank=True) # DEPRECATED: in practically all cases field `last_modified_at` should be used instead updated_at: models.DateTimeField = models.DateTimeField(auto_now=True) # DEPRECATED: use `display` property of the Filter object instead type: models.CharField = deprecate_field( models.CharField(max_length=400, null=True, blank=True)) # DEPRECATED: we don't store funnels as a separate model any more funnel: models.IntegerField = deprecate_field( models.IntegerField(null=True, blank=True)) # Deprecated in favour of app-wide tagging model. See EnterpriseTaggedItem deprecated_tags: ArrayField = deprecate_field( ArrayField(models.CharField(max_length=32), blank=True, default=list), return_instead=[], ) tags: ArrayField = deprecate_field( ArrayField(models.CharField(max_length=32), blank=True, default=None), return_instead=[], ) # Changing these fields materially alters the Insight, so these count for the "last_modified_*" fields MATERIAL_INSIGHT_FIELDS = {"name", "description", "filters"} class Meta: db_table = "posthog_dashboarditem" unique_together = ( "team", "short_id", ) def dashboard_filters(self, dashboard: Optional[Dashboard] = None): if dashboard is None: dashboard = self.dashboard if dashboard: return {**self.filters, **dashboard.filters} else: return self.filters @property def effective_restriction_level(self) -> Dashboard.RestrictionLevel: return (self.dashboard.effective_restriction_level if self.dashboard is not None else Dashboard.RestrictionLevel.EVERYONE_IN_PROJECT_CAN_EDIT) def get_effective_privilege_level( self, user_id: int) -> Dashboard.PrivilegeLevel: return (self.dashboard.get_effective_privilege_level(user_id) if self.dashboard is not None else Dashboard.PrivilegeLevel.CAN_EDIT)
class Dashboard(models.Model): class CreationMode(models.TextChoices): DEFAULT = "default", "Default" TEMPLATE = "template", "Template" # dashboard was created from a predefined template DUPLICATE = "duplicate", "Duplicate" # dashboard was duplicated from another dashboard class RestrictionLevel(models.IntegerChoices): """Collaboration restriction level (which is a dashboard setting). Sync with PrivilegeLevel.""" EVERYONE_IN_PROJECT_CAN_EDIT = 21, "Everyone in the project can edit" ONLY_COLLABORATORS_CAN_EDIT = 37, "Only those invited to this dashboard can edit" class PrivilegeLevel(models.IntegerChoices): """Collaboration privilege level (which is a user property). Sync with RestrictionLevel.""" CAN_VIEW = 21, "Can view dashboard" CAN_EDIT = 37, "Can edit dashboard" name: models.CharField = models.CharField(max_length=400, null=True, blank=True) description: models.TextField = models.TextField(blank=True) team: models.ForeignKey = models.ForeignKey("Team", on_delete=models.CASCADE) pinned: models.BooleanField = models.BooleanField(default=False) created_at: models.DateTimeField = models.DateTimeField(auto_now_add=True, blank=True) created_by: models.ForeignKey = models.ForeignKey( "User", on_delete=models.SET_NULL, null=True, blank=True) deleted: models.BooleanField = models.BooleanField(default=False) share_token: models.CharField = models.CharField(max_length=400, null=True, blank=True) is_shared: models.BooleanField = models.BooleanField(default=False) last_accessed_at: models.DateTimeField = models.DateTimeField(blank=True, null=True) filters: models.JSONField = models.JSONField(default=dict) creation_mode: models.CharField = models.CharField( max_length=16, default="default", choices=CreationMode.choices) restriction_level: models.PositiveSmallIntegerField = models.PositiveSmallIntegerField( default=RestrictionLevel.EVERYONE_IN_PROJECT_CAN_EDIT, choices=RestrictionLevel.choices, ) # Deprecated in favour of app-wide tagging model. See EnterpriseTaggedItem deprecated_tags: ArrayField = deprecate_field( ArrayField(models.CharField(max_length=32), blank=True, default=list), return_instead=[], ) tags: ArrayField = deprecate_field( ArrayField(models.CharField(max_length=32), blank=True, default=None), return_instead=[], ) @property def effective_restriction_level(self) -> RestrictionLevel: return (self.restriction_level if self.team.organization.is_feature_available( AvailableFeature.DASHBOARD_PERMISSIONING) else self.RestrictionLevel.EVERYONE_IN_PROJECT_CAN_EDIT) def get_effective_privilege_level(self, user_id: int) -> PrivilegeLevel: if ( # Checks can be skipped if the dashboard in on the lowest restriction level self.effective_restriction_level == self.RestrictionLevel.EVERYONE_IN_PROJECT_CAN_EDIT # Users with restriction rights can do anything or self.can_user_restrict(user_id)): # Returning the highest access level if no checks needed return self.PrivilegeLevel.CAN_EDIT from ee.models import DashboardPrivilege try: return cast( Dashboard.PrivilegeLevel, self.privileges.values_list("level", flat=True).get(user_id=user_id)) except DashboardPrivilege.DoesNotExist: # Returning the lowest access level if there's no explicit privilege for this user return self.PrivilegeLevel.CAN_VIEW def can_user_edit(self, user_id: int) -> bool: if self.effective_restriction_level < self.RestrictionLevel.ONLY_COLLABORATORS_CAN_EDIT: return True return self.get_effective_privilege_level( user_id) >= self.PrivilegeLevel.CAN_EDIT def can_user_restrict(self, user_id: int) -> bool: # Sync conditions with frontend hasInherentRestrictionsRights from posthog.models.organization import OrganizationMembership # The owner (aka creator) has full permissions if user_id == self.created_by_id: return True effective_project_membership_level = self.team.get_effective_membership_level( user_id) return (effective_project_membership_level is not None and effective_project_membership_level >= OrganizationMembership.Level.ADMIN) def get_analytics_metadata(self) -> Dict[str, Any]: """ Returns serialized information about the object for analytics reporting. """ return { "pinned": self.pinned, "item_count": self.items.count(), "is_shared": self.is_shared, "created_at": self.created_at, "has_description": self.description != "", "tags_count": self.tagged_items.count(), }
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 DashboardItem(models.Model): """ Stores saved insights along with their entire configuration options. Saved insights can be stored as standalone reports or part of a dashboard. """ dashboard: models.ForeignKey = models.ForeignKey("Dashboard", related_name="items", on_delete=models.CASCADE, null=True, blank=True) name: models.CharField = models.CharField(max_length=400, null=True, blank=True) description: models.CharField = models.CharField(max_length=400, null=True, blank=True) team: models.ForeignKey = models.ForeignKey("Team", on_delete=models.CASCADE) filters: models.JSONField = models.JSONField(default=dict) filters_hash: models.CharField = models.CharField(max_length=400, null=True, blank=True) order: models.IntegerField = models.IntegerField(null=True, blank=True) deleted: models.BooleanField = models.BooleanField(default=False) saved: models.BooleanField = models.BooleanField(default=False) created_at: models.DateTimeField = models.DateTimeField(null=True, blank=True, auto_now_add=True) layouts: models.JSONField = models.JSONField(default=dict) color: models.CharField = models.CharField(max_length=400, null=True, blank=True) last_refresh: models.DateTimeField = models.DateTimeField(blank=True, null=True) refreshing: models.BooleanField = models.BooleanField(default=False) created_by: models.ForeignKey = models.ForeignKey( "User", on_delete=models.SET_NULL, null=True, blank=True) is_sample: models.BooleanField = models.BooleanField( default=False, ) # indicates if it's a sample graph generated by dashboard templates short_id: models.CharField = models.CharField( max_length=12, blank=True, default=generate_short_id, ) # Unique ID per team for easy sharing and short links # ----- DEPRECATED ATTRIBUTES BELOW # Deprecated in favour of `display` within the Filter object type: models.CharField = deprecate_field( models.CharField(max_length=400, null=True, blank=True)) # Deprecated as we don't store funnels as a separate model any more funnel: models.ForeignKey = deprecate_field( models.IntegerField(null=True, blank=True)) class Meta: unique_together = ( "team", "short_id", ) def dashboard_filters(self, dashboard: Optional[Dashboard] = None): if dashboard is None: dashboard = self.dashboard if dashboard: return {**self.filters, **dashboard.filters} else: return self.filters