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

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

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

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

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

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

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

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

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

        if adding:
            self.set_default_interfaces()

        self.assign_permissions()
        self.assign_workstation_permissions()

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

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

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

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

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

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

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

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

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

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

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

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

        return w

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        return detail[0]

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

    class Meta(ChallengeBase.Meta):
        verbose_name = "challenge"
        verbose_name_plural = "challenges"
Ejemplo n.º 9
0
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