예제 #1
0
class Response(models.Model):
    study = models.ForeignKey(Study,
                              on_delete=models.DO_NOTHING,
                              related_name='responses')
    participant = models.ForeignKey(User, on_delete=models.DO_NOTHING)
    demographic_snapshot = models.ForeignKey(DemographicData,
                                             on_delete=models.DO_NOTHING)
    results = DateTimeAwareJSONField(default=dict)

    def __str__(self):
        return f'<Response: {self.study} {self.participant.get_short_name}>'

    class Meta:
        permissions = (('view_response', 'View Response'), )
예제 #2
0
class StudyLog(Log):
    action = models.CharField(max_length=128, db_index=True)
    extra = DateTimeAwareJSONField(null=True)
    study = models.ForeignKey(
        Study,
        on_delete=models.
        CASCADE,  # If a study is deleted, delete its logs also
        related_name="logs",
        related_query_name="logs",
    )

    def __str__(self):
        return f"<StudyLog: {self.action} on {self.study.name} at {self.created_at}"  # noqa

    class JSONAPIMeta:
        resource_name = "study-logs"
        lookup_field = "uuid"

    class Meta:
        index_together = ("study", "action")
예제 #3
0
class DemographicData(models.Model):
    RACE_CHOICES = Choices(
        ("white", _("White")),
        ("hisp", _("Hispanic, Latino, or Spanish origin")),
        ("black", _("Black or African American")),
        ("asian", _("Asian")),
        ("native", _("American Indian or Alaska Native")),
        ("mideast-naf", _("Middle Eastern or North African")),
        ("hawaiian-pac-isl", _("Native Hawaiian or Other Pacific Islander")),
        ("other", _("Another race, ethnicity, or origin")),
    )
    GENDER_CHOICES = Choices(
        ("m", _("male")),
        ("f", _("female")),
        ("o", _("other")),
        ("na", _("prefer not to answer")),
    )
    EDUCATION_CHOICES = Choices(
        ("some", _("some or attending high school")),
        ("hs", _("high school diploma or GED")),
        ("col", _("some or attending college")),
        ("assoc", _("2-year college degree")),
        ("bach", _("4-year college degree")),
        ("grad", _("some or attending graduate or professional school")),
        ("prof", _("graduate or professional degree")),
    )
    SPOUSE_EDUCATION_CHOICES = Choices(
        ("some", _("some or attending high school")),
        ("hs", _("high school diploma or GED")),
        ("col", _("some or attending college")),
        ("assoc", _("2-year college degree")),
        ("bach", _("4-year college degree")),
        ("grad", _("some or attending graduate or professional school")),
        ("prof", _("graduate or professional degree")),
        ("na", _("not applicable - no spouse or partner")),
    )
    NO_CHILDREN_CHOICES = Choices(
        ("0", _("0")),
        ("1", _("1")),
        ("2", _("2")),
        ("3", _("3")),
        ("4", _("4")),
        ("5", _("5")),
        ("6", _("6")),
        ("7", _("7")),
        ("8", _("8")),
        ("9", _("9")),
        ("10", _("10")),
        (">10", _("More than 10")),
    )
    AGE_CHOICES = Choices(
        ("<18", _("under 18")),
        ("18-21", _("18-21")),
        ("22-24", _("22-24")),
        ("25-29", _("25-29")),
        ("30-34", _("30-34")),
        ("35-39", _("35-39")),
        ("40-44", _("40-44")),
        ("45-49", _("45-49")),
        ("50s", _("50-59")),
        ("60s", _("60-69")),
        (">70", _("70 or over")),
    )

    GUARDIAN_CHOICES = Choices(
        ("1", _("1")), ("2", _("2")), ("3>", _("3 or more")), ("varies", _("varies"))
    )
    INCOME_CHOICES = Choices(
        ("0", _("0")),
        ("5000", _("5000")),
        ("10000", _("10000")),
        ("15000", _("15000")),
        ("20000", _("20000")),
        ("30000", _("30000")),
        ("40000", _("40000")),
        ("50000", _("50000")),
        ("60000", _("60000")),
        ("70000", _("70000")),
        ("80000", _("80000")),
        ("90000", _("90000")),
        ("100000", _("100000")),
        ("110000", _("110000")),
        ("120000", _("120000")),
        ("130000", _("130000")),
        ("140000", _("140000")),
        ("150000", _("150000")),
        ("160000", _("160000")),
        ("170000", _("170000")),
        ("180000", _("180000")),
        ("190000", _("190000")),
        (">200000", _("over 200000")),
        ("na", _("prefer not to answer")),
    )
    DENSITY_CHOICES = Choices(
        ("urban", _("urban")), ("suburban", _("suburban")), ("rural", _("rural"))
    )
    user = models.ForeignKey(
        User,
        on_delete=models.CASCADE,  # If deleting user, delete their demographic data
        null=True,
        related_name="demographics",
        related_query_name="demographics",
    )
    created_at = models.DateTimeField(auto_now_add=True)
    previous = models.ForeignKey(
        "self",
        on_delete=models.CASCADE,
        related_name="next_demographic_data",
        related_query_name="next_demographic_data",
        null=True,
        blank=True,
    )

    uuid = models.UUIDField(
        verbose_name="identifier", default=uuid.uuid4, unique=True, db_index=True
    )
    number_of_children = models.CharField(
        choices=NO_CHILDREN_CHOICES, max_length=3, blank=True
    )
    child_birthdays = ArrayField(
        models.DateField(), verbose_name="children's birthdays", blank=True
    )
    languages_spoken_at_home = models.TextField(
        verbose_name="languages spoken at home", blank=True
    )
    number_of_guardians = models.CharField(
        choices=GUARDIAN_CHOICES, max_length=6, blank=True
    )
    number_of_guardians_explanation = models.TextField(blank=True)
    race_identification = MultiSelectField(choices=RACE_CHOICES, blank=True)
    age = models.CharField(max_length=5, choices=AGE_CHOICES, blank=True)
    gender = models.CharField(max_length=2, choices=GENDER_CHOICES, blank=True)
    education_level = models.CharField(
        max_length=5, choices=EDUCATION_CHOICES, blank=True
    )
    spouse_education_level = models.CharField(
        max_length=5, choices=SPOUSE_EDUCATION_CHOICES, blank=True
    )
    annual_income = models.CharField(max_length=7, choices=INCOME_CHOICES, blank=True)
    former_lookit_annual_income = models.CharField(max_length=30, blank=True)
    number_of_books = models.IntegerField(null=True, blank=True, default=None)
    additional_comments = models.TextField(blank=True)
    country = CountryField(blank=True)
    state = USStateField(
        blank=True, choices=("XX", _("Select a State")) + USPS_CHOICES[:]
    )
    density = models.CharField(max_length=8, choices=DENSITY_CHOICES, blank=True)
    lookit_referrer = models.TextField(blank=True)
    extra = DateTimeAwareJSONField(null=True)

    class Meta:
        ordering = ["-created_at"]

    class JSONAPIMeta:
        resource_name = "demographics"
        lookup_field = "uuid"

    def __str__(self):
        return f"<DemographicData: @ {self.created_at:%c}>"

    def to_display(self):
        return dict(
            user=self.user.uuid.hex,
            created_at=self.created_at.isoformat(),
            number_of_children=self.get_number_of_children_display(),
            child_birthdays=[birthday.isoformat() for birthday in self.child_birthdays],
            languages_spoken_at_home=self.languages_spoken_at_home,
            number_of_guardians=self.get_number_of_guardians_display(),
            number_of_guardians_explanation=self.number_of_guardians_explanation,
            race_identification=self.get_race_identification_display(),
            age=self.get_age_display(),
            gender=self.get_gender_display(),
            education_level=self.get_education_level_display(),
            spouse_education_level=self.get_spouse_education_level_display(),
            annual_income=self.get_annual_income_display(),
            number_of_books=self.number_of_books,
            additional_comments=self.additional_comments,
            country=str(self.country),
            state=self.get_state_display(),
            density=self.get_density_display(),
            extra=self.extra,
            lookit_referrer=self.lookit_referrer,
        )
예제 #4
0
class Response(models.Model):
    uuid = models.UUIDField(default=uuid.uuid4, unique=True, db_index=True)
    study = models.ForeignKey(
        Study, on_delete=models.PROTECT, related_name="responses"
    )  # Integrity constraints will also prevent deleting study that has responses
    completed = models.BooleanField(default=False)
    completed_consent_frame = models.BooleanField(default=False)
    exp_data = DateTimeAwareJSONField(default=dict)
    conditions = DateTimeAwareJSONField(default=dict)
    sequence = ArrayField(models.CharField(max_length=128),
                          blank=True,
                          default=list)
    date_modified = models.DateTimeField(auto_now=True)
    date_created = models.DateTimeField(auto_now_add=True)
    global_event_timings = DateTimeAwareJSONField(default=dict)
    # For now, don't allow deleting Child still associated with responses. If we need to
    # delete all data on parent request, delete the associated responses manually. May want
    # to be able to keep some minimal info about those responses though (e.g. #, # unique
    # users they came from).
    child = models.ForeignKey(Child, on_delete=models.PROTECT)
    is_preview = models.BooleanField(default=False)
    demographic_snapshot = models.ForeignKey(
        DemographicData, on_delete=models.SET_NULL, null=True
    )  # Allow deleting a demographic snapshot even though a response points to it
    objects = models.Manager()
    related_manager = ResponseApiManager()

    def __str__(self):
        return self.display_name

    class Meta:
        permissions = ((
            "view_all_response_data_in_analytics",
            "View all response data in analytics",
        ), )
        ordering = ["-demographic_snapshot__created_at"]
        base_manager_name = "related_manager"

    class JSONAPIMeta:
        resource_name = "responses"
        lookup_field = "uuid"

    def _get_recent_consent_ruling(self):
        return self.consent_rulings.first()

    @cached_property
    def display_name(self):
        return f"{self.date_created.strftime('%c')}; Child({self.child.given_name}); Parent({self.child.user.nickname})"

    @property
    def most_recent_ruling(self):
        """Gets the most recent ruling for a Response/Session.

        XXX: This is EXPENSIVE if not called within the context of a prefetched query set!
        """
        ruling = self._get_recent_consent_ruling()
        return ruling.action if ruling else PENDING

    @property
    def has_valid_consent(self):
        return self.most_recent_ruling == ACCEPTED

    @property
    def pending_consent_judgement(self):
        return self.most_recent_ruling == PENDING

    @property
    def currently_rejected(self):
        return self.most_recent_ruling == REJECTED

    @property
    def most_recent_ruling_comment(self):
        ruling = self._get_recent_consent_ruling()
        return ruling.comments if ruling else None

    @property
    def comment_or_reason_for_absence(self):
        ruling = self.consent_rulings.first()
        if ruling:
            if ruling.comments:
                return ruling.comments
            else:
                return "No comment on previous ruling."
        else:
            return "No previous ruling."

    @property
    def most_recent_ruling_date(self):
        ruling = self._get_recent_consent_ruling()
        return ruling.created_at.strftime("%Y-%m-%d %H:%M") if ruling else None

    @property
    def most_recent_ruling_arbiter(self):
        ruling = self._get_recent_consent_ruling()
        return ruling.arbiter.get_full_name() if ruling else None

    @property
    def current_consent_details(self):
        return {
            "ruling": self.most_recent_ruling,
            "arbiter": self.most_recent_ruling_arbiter,
            "comment": self.most_recent_ruling_comment,
            "date": self.most_recent_ruling_date,
        }

    def exit_frame_properties(self, property):
        exit_frame_values = [
            f.get(property, None) for f in self.exp_data.values()
            if f.get("frameType", None) == "EXIT"
        ]
        if exit_frame_values and exit_frame_values != [None]:
            # return " ".join(list(set(([val for val in exit_frame_values if val is not None]))))
            return exit_frame_values[-1]
        else:
            return None

    @property
    def withdrawn(self):
        return bool(self.exit_frame_properties("withdrawal"))

    @property
    def databrary(self):
        return self.exit_frame_properties("databraryShare")

    @property
    def privacy(self):
        return self.exit_frame_properties("useOfMedia")

    @property
    def parent_feedback(self):
        return self.exit_frame_properties("feedback")

    @property
    def birthdate_difference(self):
        """Difference between birthdate on exit survey (if any) and registered child's birthday, """
        exit_survey_birthdate = self.exit_frame_properties("birthDate")
        registered_birthdate = self.child.birthday
        if exit_survey_birthdate and registered_birthdate:
            try:
                return (datetime.strptime(exit_survey_birthdate[:10],
                                          "%Y-%m-%d").date() -
                        self.child.birthday).days
            except (ValueError, TypeError):
                return None
        else:
            return None

    def generate_videos_from_events(self):
        """Creates the video containers/representations for this given response.

        We should only really invoke this as part of a migration as of right now (2/8/2019),
        but it's quite possible we'll have the need for dynamic upsertion later.
        """

        seen_ids = set()
        video_objects = []

        # Using a constructive approach here, but with an ancillary seen_ids list b/c Django models without
        # primary keys are unhashable.
        for frame_id, event_data in self.exp_data.items():
            if event_data.get("videoList", None) and event_data.get(
                    "videoId", None):
                # We've officially captured video here!
                events = event_data.get("eventTimings", [])
                for event in events:
                    video_id = event["videoId"]
                    pipe_name = event[
                        "pipeId"]  # what we call "ID" they call "name"
                    if (video_id not in seen_ids and pipe_name
                            and event["streamTime"] > 0):
                        # Try looking for the regular ID first.
                        file_obj = S3_RESOURCE.Object(settings.BUCKET_NAME,
                                                      f"{video_id}.mp4")
                        try:
                            response = file_obj.get()
                        except ClientError:
                            try:  # If that doesn't work, use the pipe name.
                                file_obj = S3_RESOURCE.Object(
                                    settings.BUCKET_NAME, f"{pipe_name}.mp4")
                                response = file_obj.get()
                            except ClientError:
                                logger.warning(
                                    f"could not find {video_id} or {pipe_name} in S3!"
                                )
                                continue
                        # Read first 32 bytes from streaming body (file header) to get actual filetype.
                        streaming_body = response["Body"]
                        file_header_buffer: bytes = streaming_body.read(32)
                        file_info = fleep.get(file_header_buffer)
                        streaming_body.close()

                        video_objects.append(
                            Video(
                                pipe_name=pipe_name,
                                created_at=date_parser.parse(
                                    event["timestamp"]),
                                date_modified=response["LastModified"],
                                #  Can't get the *actual* pipe id property, it's in the webhook payload...
                                frame_id=frame_id,
                                full_name=
                                f"{video_id}.{file_info.extension[0]}",
                                study=self.study,
                                response=self,
                                is_consent_footage=event_data.get(
                                    "frameType", None) == "CONSENT",
                            ))
                        seen_ids.add(video_id)

        return Video.objects.bulk_create(video_objects)
예제 #5
0
class Study(models.Model):

    MONITORING_FIELDS = [
        "structure",
        "name",
        "short_description",
        "long_description",
        "criteria",
        "duration",
        "contact_info",
        "image",
        "exit_url",
        "metadata",
        "study_type",
        "compensation_description",
        "lab",
    ]

    DAY_CHOICES = [(i, i) for i in range(0, 32)]
    MONTH_CHOICES = [(i, i) for i in range(0, 12)]
    YEAR_CHOICES = [(i, i) for i in range(0, 19)]
    salt = models.UUIDField(default=uuid.uuid4, unique=True)
    hash_digits = models.IntegerField(default=6)
    uuid = models.UUIDField(default=uuid.uuid4, unique=True, db_index=True)
    name = models.CharField(max_length=255,
                            blank=False,
                            null=False,
                            db_index=True)
    date_modified = models.DateTimeField(auto_now=True)
    short_description = models.TextField()
    long_description = models.TextField()
    criteria = models.TextField()
    duration = models.TextField()
    contact_info = models.TextField()
    min_age_days = models.IntegerField(default=0, choices=DAY_CHOICES)
    min_age_months = models.IntegerField(default=0, choices=MONTH_CHOICES)
    min_age_years = models.IntegerField(default=0, choices=YEAR_CHOICES)
    max_age_days = models.IntegerField(default=0, choices=DAY_CHOICES)
    max_age_months = models.IntegerField(default=0, choices=MONTH_CHOICES)
    max_age_years = models.IntegerField(default=0, choices=YEAR_CHOICES)
    image = models.ImageField(null=True, upload_to="study_images/")
    comments = models.TextField(blank=True, null=True)
    study_type = models.ForeignKey(
        "StudyType",
        on_delete=models.PROTECT,
        null=False,
        blank=False,
        verbose_name="type",
    )
    lab = models.ForeignKey(
        Lab,
        on_delete=models.
        PROTECT,  # don't allow deleting lab without moving out studies. Could also switch to a default lab.
        related_name="studies",
        related_query_name="study",
        null=True,
    )
    structure = DateTimeAwareJSONField(default=default_study_structure)
    display_full_screen = models.BooleanField(default=True)
    exit_url = models.URLField(default="")
    state = models.CharField(
        choices=workflow.STATE_CHOICES,
        max_length=25,
        default=workflow.STATE_CHOICES.created,
        db_index=True,
    )
    public = models.BooleanField(default=False)
    shared_preview = models.BooleanField(default=False)
    creator = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)

    metadata = DateTimeAwareJSONField(default=dict)
    built = models.BooleanField(default=False)
    is_building = models.BooleanField(default=False)
    compensation_description = models.TextField(blank=True)
    criteria_expression = models.TextField(blank=True)

    # Groups
    # The related_name convention seems silly, but django complains about reverse
    # accessor clashes if these aren't unique :/ regardless, we won't be using
    # the reverse accessors much so it doesn't really matter.
    preview_group = models.OneToOneField(Group,
                                         related_name="study_to_preview",
                                         on_delete=models.SET_NULL,
                                         null=True)
    design_group = models.OneToOneField(Group,
                                        related_name="study_to_design",
                                        on_delete=models.SET_NULL,
                                        null=True)
    analysis_group = models.OneToOneField(Group,
                                          related_name="study_for_analysis",
                                          on_delete=models.SET_NULL,
                                          null=True)
    submission_processor_group = models.OneToOneField(
        Group,
        related_name="study_for_submission_processing",
        on_delete=models.SET_NULL,
        null=True,
    )
    researcher_group = models.OneToOneField(Group,
                                            related_name="study_for_research",
                                            on_delete=models.SET_NULL,
                                            null=True)
    manager_group = models.OneToOneField(Group,
                                         related_name="study_to_manage",
                                         on_delete=models.SET_NULL,
                                         null=True)
    admin_group = models.OneToOneField(Group,
                                       related_name="study_to_administer",
                                       on_delete=models.SET_NULL,
                                       null=True)

    def all_study_groups(self):
        """Returns a list of all the groups that grant permissions on this study"""
        return [
            self.preview_group,
            self.design_group,
            self.analysis_group,
            self.submission_processor_group,
            self.researcher_group,
            self.manager_group,
            self.admin_group,
        ]

    def get_group_of_researcher(self, user):
        """Returns label for the highest-level group the researcher is in for this study, or None if not in any study groups"""
        user_groups = user.groups.all()
        if self.admin_group in user_groups:
            return "Admin"
        if self.manager_group in user_groups:
            return "Manager"
        if self.researcher_group in user_groups:
            return "Researcher"
        if self.submission_processor_group in user_groups:
            return "Submission Processor"
        if self.analysis_group in user_groups:
            return "Analysis"
        if self.design_group in user_groups:
            return "Design"
        if self.preview_group in user_groups:
            return "Preview"
        return None

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

        super(Study, self).__init__(*args, **kwargs)
        self.machine = Machine(
            self,
            states=workflow.states,
            transitions=workflow.transitions,
            initial=self.state,
            send_event=True,
            after_state_change="_finalize_state_change",
        )

    def __str__(self):
        return f"<Study: {self.name} ({self.uuid})>"

    class Meta:
        permissions = StudyPermission
        ordering = ["name"]

    class JSONAPIMeta:
        resource_name = "studies"
        lookup_field = "uuid"

    def users_with_study_perms(self, study_perm: StudyPermission):
        users_with_perms = get_users_with_perms(
            self, only_with_perms_in=[study_perm.codename])
        if self.lab and study_perm in UMBRELLA_LAB_PERMISSION_MAP:
            umbrella_lab_perm = UMBRELLA_LAB_PERMISSION_MAP.get(study_perm)
            users_with_perms = users_with_perms.union(
                get_users_with_perms(
                    self.lab, only_with_perms_in=[umbrella_lab_perm.codename]))
        return users_with_perms

    @cached_property
    def begin_date(self):
        try:
            return self.logs.filter(action="active").first().created_at
        except AttributeError:
            return None

    @property
    def participants(self):
        """Get all participants for the given study."""
        participants = self.responses.values_list("child__user", flat=True)
        return User.objects.filter(pk__in=participants)

    @property
    def judgeable_responses(self):
        return self.responses.filter(completed_consent_frame=True)

    @property
    def responses_with_consent_videos(self):
        """Custom Queryset for the Consent Manager view."""
        return (self.judgeable_responses.prefetch_related(
            models.Prefetch(
                "videos",
                queryset=Video.objects.filter(is_consent_footage=True),
                # to_attr="consent_videos",
            ),
            "consent_rulings",
        ).select_related("child",
                         "child__user").order_by("-date_created").all())

    @property
    def responses_with_all_videos(self):
        """Custom Queryset for the Consent Manager view."""
        return (self.judgeable_responses.prefetch_related(
            "videos", "consent_rulings").select_related(
                "child", "child__user").order_by("-date_created").all())

    @property
    def consented_responses(self):
        """Get responses for which we have a valid "accepted" consent ruling."""
        # Create the subquery where we get the action from the most recent ruling.
        newest_ruling_subquery = models.Subquery(
            ConsentRuling.objects.filter(response=models.OuterRef(
                "pk")).order_by("-created_at").values("action")[:1])

        # Annotate that value as "current ruling" on our response queryset.
        annotated = self.responses_with_all_videos.annotate(
            current_ruling=newest_ruling_subquery)

        # Only return the things for which our annotated property == accepted.
        return annotated.filter(current_ruling="accepted")

    def responses_for_researcher(self, user):
        """Return all responses to this study that the researcher has access to read"""
        responses = self.consented_responses
        if not user.has_study_perms(StudyPermission.READ_STUDY_RESPONSE_DATA,
                                    self):
            responses = responses.filter(is_preview=True)
        if not user.has_study_perms(StudyPermission.READ_STUDY_PREVIEW_DATA,
                                    self):
            responses = responses.filter(is_preview=False)
        return responses

    @property
    def videos_for_consented_responses(self):
        """Gets videos but only for consented responses."""
        return Video.objects.filter(response_id__in=self.consented_responses)

    @property
    def consent_videos(self):
        return self.videos.filter(is_consent_footage=True)

    @property
    def end_date(self):
        try:
            return self.logs.filter(action="deactivated").first().created_at
        except AttributeError:
            return None

    # WORKFLOW CALLBACKS

    def clone(self):
        """ Create a new, unsaved copy of the study. """
        copy = self.__class__.objects.get(pk=self.pk)
        copy.id = None
        copy.salt = uuid.uuid4()
        copy.public = False
        copy.state = "created"
        copy.name = "Copy of " + copy.name

        # empty the fks
        fk_field_names = [
            f.name for f in self._meta.model._meta.get_fields()
            if isinstance(f, (models.ForeignKey))
        ]
        for field_name in fk_field_names:
            setattr(copy, field_name, None)
        try:
            copy.uuid = uuid.uuid4()
        except AttributeError:
            pass
        return copy

    def notify_administrators_of_submission(self, ev):
        context = {
            "lab_name": self.lab.name,
            "study_name": self.name,
            "study_id": self.pk,
            "study_uuid": str(self.uuid),
            "researcher_name": ev.kwargs.get("user").get_short_name(),
            "action": ev.transition.dest,
            "comments": self.comments,
        }
        send_mail.delay(
            "notify_admins_of_study_action",
            "Study Submission Notification",
            settings.EMAIL_FROM_ADDRESS,
            bcc=list(
                Group.objects.get(name=SiteAdminGroup.LOOKIT_ADMIN.name).
                user_set.values_list("username", flat=True)),
            **context,
        )

    def notify_submitter_of_approval(self, ev):

        context = {
            "study_name": self.name,
            "study_id": self.pk,
            "approved": True,
            "comments": self.comments,
        }
        send_mail.delay(
            "notify_researchers_of_approval_decision",
            "{} Approval Notification".format(self.name),
            settings.EMAIL_FROM_ADDRESS,
            bcc=list(
                self.users_with_study_perms(
                    StudyPermission.CHANGE_STUDY_STATUS).values_list(
                        "username", flat=True)),
            **context,
        )

    def notify_submitter_of_rejection(self, ev):
        context = {
            "study_name": self.name,
            "study_id": self.pk,
            "approved": False,
            "comments": self.comments,
        }
        send_mail.delay(
            "notify_researchers_of_approval_decision",
            "{}: Changes requested notification".format(self.name),
            settings.EMAIL_FROM_ADDRESS,
            bcc=list(
                self.users_with_study_perms(
                    StudyPermission.CHANGE_STUDY_STATUS).values_list(
                        "username", flat=True)),
            **context,
        )

    def notify_submitter_of_recission(self, ev):
        context = {"study_name": self.name}
        send_mail.delay(
            "notify_researchers_of_approval_rescission",
            "{} Rescinded Notification".format(self.name),
            settings.EMAIL_FROM_ADDRESS,
            bcc=list(
                self.users_with_study_perms(
                    StudyPermission.CHANGE_STUDY_STATUS).values_list(
                        "username", flat=True)),
            **context,
        )

    def notify_administrators_of_retraction(self, ev):
        context = {
            "lab_name": self.lab.name,
            "study_name": self.name,
            "study_id": self.pk,
            "study_uuid": str(self.uuid),
            "researcher_name": ev.kwargs.get("user").get_short_name(),
            "action": ev.transition.dest,
        }
        send_mail.delay(
            "notify_admins_of_study_action",
            "Study Retraction Notification",
            settings.EMAIL_FROM_ADDRESS,
            bcc=list(
                Group.objects.get(name=SiteAdminGroup.LOOKIT_ADMIN.name).
                user_set.values_list("username", flat=True)),
            **context,
        )

    def check_if_built(self, ev):
        """Check if study is built.

        :param ev: The event object
        :type ev: transitions.core.EventData
        :raise: RuntimeError
        """
        if not self.built:
            raise RuntimeError(
                f'Cannot activate study - experiment runner for "{self.name}" ({self.id}) has not been built!'
            )

    def notify_administrators_of_activation(self, ev):
        context = {
            "lab_name": self.lab.name,
            "study_name": self.name,
            "study_id": self.pk,
            "study_uuid": str(self.uuid),
            "researcher_name": ev.kwargs.get("user").get_short_name(),
            "action": ev.transition.dest,
        }
        send_mail.delay(
            "notify_admins_of_study_action",
            "Study Activation Notification",
            settings.EMAIL_FROM_ADDRESS,
            bcc=list(
                Group.objects.get(name=SiteAdminGroup.LOOKIT_ADMIN.name).
                user_set.values_list("username", flat=True)),
            **context,
        )

    def notify_administrators_of_pause(self, ev):
        context = {
            "lab_name": self.lab.name,
            "study_name": self.name,
            "study_id": self.pk,
            "study_uuid": str(self.uuid),
            "researcher_name": ev.kwargs.get("user").get_short_name(),
            "action": ev.transition.dest,
        }
        send_mail.delay(
            "notify_admins_of_study_action",
            "Study Pause Notification",
            settings.EMAIL_FROM_ADDRESS,
            bcc=list(
                Group.objects.get(name=SiteAdminGroup.LOOKIT_ADMIN.name).
                user_set.values_list("username", flat=True)),
            **context,
        )

    def notify_administrators_of_deactivation(self, ev):
        context = {
            "lab_name": self.lab.name,
            "study_name": self.name,
            "study_id": self.pk,
            "study_uuid": str(self.uuid),
            "researcher_name": ev.kwargs.get("user").get_short_name(),
            "action": ev.transition.dest,
        }
        send_mail.delay(
            "notify_admins_of_study_action",
            "Study Deactivation Notification",
            settings.EMAIL_FROM_ADDRESS,
            bcc=list(
                Group.objects.get(name=SiteAdminGroup.LOOKIT_ADMIN.name).
                user_set.values_list("username", flat=True)),
            **context,
        )

    # Runs for every transition to log action
    def _log_action(self, ev):
        if ev.event.name in ["submit", "resubmit", "reject"]:
            StudyLog.objects.create(
                action=ev.state.name,
                study=ev.model,
                user=ev.kwargs.get("user"),
                extra={"comments": ev.model.comments},
            )
        else:
            StudyLog.objects.create(action=ev.state.name,
                                    study=ev.model,
                                    user=ev.kwargs.get("user"))

    # Runs for every transition to save state and log action
    def _finalize_state_change(self, ev):
        ev.model.save()
        self._log_action(ev)
예제 #6
0
class StudyType(models.Model):
    name = models.CharField(max_length=255, blank=False, null=False)
    configuration = DateTimeAwareJSONField(default=default_configuration)

    def __str__(self):
        return f"<Study Type: {self.name}>"
예제 #7
0
class DemographicData(models.Model):
    RACE_CHOICES = Choices(
        ('white', _('White')),
        ('hisp', _('Hispanic, Latino, or Spanish origin')),
        ('black', _('Black or African American')),
        ('asian', _('Asian')),
        ('native', _('American Indian or Alaska Native')),
        ('mideast-naf', _('Middle Eastern or North African')),
        ('hawaiian-pac-isl', _('Native Hawaiian or Other Pacific Islander')),
        ('other', _('Another race, ethnicity, or origin')),
    )
    GENDER_CHOICES = Choices(
        ('m', _('male')),
        ('f', _('female')),
        ('o', _('other')),
        ('na', _('prefer not to answer')),
    )
    EDUCATION_CHOICES = Choices(
        ('some', _('some or attending high school')),
        ('hs', _('high school diploma or GED')),
        ('col', _('some or attending college')),
        ('assoc', _('2-year college degree')),
        ('bach', _('4-year college degree')),
        ('grad', _('some or attending graduate or professional school')),
        ('prof', _('graduate or professional degree')),
    )
    SPOUSE_EDUCATION_CHOICES = Choices(
        ('some', _('some or attending high school')),
        ('hs', _('high school diploma or GED')),
        ('col', _('some or attending college')),
        ('assoc', _('2-year college degree')),
        ('bach', _('4-year college degree')),
        ('grad', _('some or attending graduate or professional school')),
        ('prof', _('graduate or professional degree')),
        ('na', _('not applicable - no spouse or partner')),
    )
    NO_CHILDREN_CHOICES = Choices(
        ('0', _('0')),
        ('1', _('1')),
        ('2', _('2')),
        ('3', _('3')),
        ('4', _('4')),
        ('5', _('5')),
        ('6', _('6')),
        ('7', _('7')),
        ('8', _('8')),
        ('9', _('9')),
        ('10', _('10')),
        ('>10', _('More than 10')),
    )
    AGE_CHOICES = Choices(
        ('<18', _('under 18')),
        ('18-21', _('18-21')),
        ('22-24', _('22-24')),
        ('25-29', _('25-29')),
        ('30-34', _('30-34')),
        ('35-39', _('35-39')),
        ('40-44', _('40-44')),
        ('45-59', _('45-49')),
        ('50s', _('50-59')),
        ('60s', _('60-69')),
        ('>70', _('70 or over')),
    )

    GUARDIAN_CHOICES = Choices(
        ('1', _('1')),
        ('2', _('2')),
        ('3>', _('3 or more')),
        ('varies', _('varies')),
    )
    INCOME_CHOICES = Choices(
        ('0', _('0')),
        ('5000', _('5000')),
        ('10000', _('10000')),
        ('15000', _('15000')),
        ('20000', _('20000')),
        ('30000', _('30000')),
        ('40000', _('40000')),
        ('50000', _('50000')),
        ('60000', _('60000')),
        ('70000', _('70000')),
        ('80000', _('80000')),
        ('90000', _('90000')),
        ('100000', _('100000')),
        ('110000', _('110000')),
        ('120000', _('120000')),
        ('130000', _('130000')),
        ('140000', _('140000')),
        ('150000', _('150000')),
        ('160000', _('160000')),
        ('170000', _('170000')),
        ('180000', _('180000')),
        ('190000', _('190000')),
        ('>200000', _('over 200000')),
        ('na', _('prefer not to answer')),
    )
    DENSITY_CHOICES = Choices(
        ('urban', _('urban')),
        ('suburban', _('suburban')),
        ('rural', _('rural')),
    )
    user = models.ForeignKey(
        User, on_delete=models.CASCADE, null=True,
        related_name='demographics', related_query_name='demographics'
    )
    created_at = models.DateTimeField(auto_now_add=True)
    previous = models.ForeignKey(
        'self', on_delete=models.CASCADE,
        related_name='next_demographic_data',
        related_query_name='next_demographic_data', null=True, blank=True
    )

    uuid = models.UUIDField(verbose_name='identifier', default=uuid.uuid4, unique=True, db_index=True)
    number_of_children = models.CharField(choices=NO_CHILDREN_CHOICES, max_length=3, blank=True)
    child_birthdays = ArrayField(models.DateField(), verbose_name='children\'s birthdays', blank=True)
    languages_spoken_at_home = models.TextField(verbose_name='languages spoken at home', blank=True)
    number_of_guardians = models.CharField(choices=GUARDIAN_CHOICES, max_length=6, blank=True)
    number_of_guardians_explanation = models.TextField(blank=True)
    race_identification = MultiSelectField(choices=RACE_CHOICES, blank=True)
    age = models.CharField(max_length=5, choices=AGE_CHOICES, blank=True)
    gender = models.CharField(max_length=2, choices=GENDER_CHOICES, blank=True)
    education_level = models.CharField(max_length=5, choices=EDUCATION_CHOICES, blank=True)
    spouse_education_level = models.CharField(max_length=5, choices=SPOUSE_EDUCATION_CHOICES, blank=True)
    annual_income = models.CharField(max_length=7, choices=INCOME_CHOICES, blank=True)
    former_lookit_annual_income = models.CharField(max_length=30, blank=True)
    number_of_books = models.IntegerField(null=True, blank=True, default=None)
    additional_comments = models.TextField(blank=True)
    country = CountryField(blank=True)
    state = USStateField(blank=True, choices=('XX', _('Select a State')) + USPS_CHOICES[:])
    density = models.CharField(max_length=8, choices=DENSITY_CHOICES, blank=True)
    lookit_referrer = models.TextField(blank=True)
    extra = DateTimeAwareJSONField(null=True)

    class Meta:
        ordering = ['-created_at']

    class JSONAPIMeta:
        resource_name = 'demographics'
        lookup_field = 'uuid'

    def __str__(self):
        return f'<DemographicData: {self.user.nickname} @ {self.created_at:%c}>'

    def to_display(self):
        return dict(
            user=self.user.uuid.hex,
            created_at=self.created_at.isoformat(),
            number_of_children=self.get_number_of_children_display(),
            child_birthdays=[birthday.isoformat() for birthday in self.child_birthdays],
            languages_spoken_at_home=self.languages_spoken_at_home,
            number_of_guardians=self.get_number_of_guardians_display(),
            number_of_guardians_explanation=self.number_of_guardians_explanation,
            race_identification=self.get_race_identification_display(),
            age=self.get_age_display(),
            gender=self.get_gender_display(),
            education_level=self.get_education_level_display(),
            spouse_education_level=self.get_spouse_education_level_display(),
            annual_income=self.get_annual_income_display(),
            number_of_books=self.number_of_books,
            additional_comments=self.additional_comments,
            country=str(self.country),
            state=self.get_state_display(),
            density=self.get_density_display(),
            extra=self.extra,
            lookit_referrer=self.lookit_referrer
        )
예제 #8
0
class Study(models.Model):
    uuid = models.UUIDField(default=uuid.uuid4)
    name = models.CharField(max_length=255, blank=False, null=False)
    short_description = models.TextField()
    long_description = models.TextField()
    criteria = models.TextField()
    duration = models.TextField()
    contact_info = models.TextField()
    image = models.ImageField(null=True)
    organization = models.ForeignKey(Organization,
                                     on_delete=models.DO_NOTHING,
                                     related_name='studies',
                                     related_query_name='study')
    blocks = DateTimeAwareJSONField(default=dict)
    state = models.CharField(choices=workflow.STATE_CHOICES,
                             max_length=25,
                             default=workflow.STATE_CHOICES[0][0])
    public = models.BooleanField(default=False)

    def __init__(self, *args, **kwargs):
        super(Study, self).__init__(*args, **kwargs)
        self.machine = Machine(self,
                               states=workflow.states,
                               transitions=workflow.transitions,
                               initial=self.state,
                               send_event=True,
                               before_state_change='check_permission',
                               after_state_change='_finalize_state_change')

    def __str__(self):
        return f'<Study: {self.name}>'

    class Meta:
        permissions = (
            ('can_view', 'View Study'),
            ('can_edit', 'Edit Study'),
            ('can_submit', 'Submit Study'),
            ('can_respond', 'Can Respond'),
        )

    # WORKFLOW CALLBACKS
    def check_permission(self, ev):
        user = ev.kwargs.get('user')
        if user.is_superuser:
            return
        raise

    def notify_administrators_of_submission(self, ev):
        # TODO
        pass

    def notify_submitter_of_approval(self, ev):
        # TODO
        pass

    def notify_submitter_of_rejection(self, ev):
        # TODO
        pass

    def notify_administrators_of_retraction(self, ev):
        # TODO
        pass

    def notify_administrators_of_activation(self, ev):
        # TODO
        pass

    def notify_administrators_of_pause(self, ev):
        # TODO
        pass

    def notify_administrators_of_deactivation(self, ev):
        # TODO
        pass

    # Runs for every transition to log action
    def _log_action(self, ev):
        StudyLog.objects.create(action=ev.state.name,
                                study=ev.model,
                                user=ev.kwargs.get('user'))

    # Runs for every transition to save state and log action
    def _finalize_state_change(self, ev):
        ev.model.save()
        self._log_action(ev)