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