class TranslationContributionStatsModel(base_models.BaseModel): """Records the contributor dashboard translation contribution stats. There is one instance of this model per (language_code, contributor_user_id, topic_id) tuple. See related design doc for more details: https://docs.google.com/document/d/1JEDiy-f1vnBLwibu8hsfuo3JObBWiaFvDTTU9L18zpY/edit# """ # We use the model id as a key in the Takeout dict. ID_IS_USED_AS_TAKEOUT_KEY = True # The ISO 639-1 language code for which the translation contributions were # made. language_code = datastore_services.StringProperty(required=True, indexed=True) # The user ID of the translation contributor. contributor_user_id = datastore_services.StringProperty(required=True, indexed=True) # The topic ID of the translation contribution. topic_id = datastore_services.StringProperty(required=True, indexed=True) # The number of submitted translations. submitted_translations_count = datastore_services.IntegerProperty( required=True, indexed=True) # The total word count of submitted translations. Excludes HTML tags and # attributes. submitted_translation_word_count = datastore_services.IntegerProperty( required=True, indexed=True) # The number of accepted translations. accepted_translations_count = datastore_services.IntegerProperty( required=True, indexed=True) # The number of accepted translations without reviewer edits. accepted_translations_without_reviewer_edits_count = ( datastore_services.IntegerProperty(required=True, indexed=True)) # The total word count of accepted translations. Excludes HTML tags and # attributes. accepted_translation_word_count = datastore_services.IntegerProperty( required=True, indexed=True) # The number of rejected translations. rejected_translations_count = datastore_services.IntegerProperty( required=True, indexed=True) # The total word count of rejected translations. Excludes HTML tags and # attributes. rejected_translation_word_count = datastore_services.IntegerProperty( required=True, indexed=True) # The unique last_updated dates of the translation suggestions. contribution_dates = datastore_services.DateProperty(repeated=True, indexed=True) @classmethod def create(cls, language_code: str, contributor_user_id: str, topic_id: str, submitted_translations_count: int, submitted_translation_word_count: int, accepted_translations_count: int, accepted_translations_without_reviewer_edits_count: int, accepted_translation_word_count: int, rejected_translations_count: int, rejected_translation_word_count: int, contribution_dates: List[datetime.date]) -> str: """Creates a new TranslationContributionStatsModel instance and returns its ID. """ entity_id = cls.generate_id(language_code, contributor_user_id, topic_id) entity = cls( id=entity_id, language_code=language_code, contributor_user_id=contributor_user_id, topic_id=topic_id, submitted_translations_count=submitted_translations_count, submitted_translation_word_count=submitted_translation_word_count, accepted_translations_count=accepted_translations_count, accepted_translations_without_reviewer_edits_count=( accepted_translations_without_reviewer_edits_count), accepted_translation_word_count=accepted_translation_word_count, rejected_translations_count=rejected_translations_count, rejected_translation_word_count=rejected_translation_word_count, contribution_dates=contribution_dates) entity.update_timestamps() entity.put() return entity_id @staticmethod def generate_id(language_code: str, contributor_user_id: str, topic_id: str) -> str: """Generates a unique ID for a TranslationContributionStatsModel instance. Args: language_code: str. ISO 639-1 language code. contributor_user_id: str. User ID. topic_id: str. Topic ID. Returns: str. An ID of the form: [language_code].[contributor_user_id].[topic_id] """ return ('%s.%s.%s' % (language_code, contributor_user_id, topic_id)) # We have ignored [override] here because the signature of this method # doesn't match with BaseModel.get(). # https://mypy.readthedocs.io/en/stable/error_code_list.html#check-validity-of-overrides-override @classmethod def get( # type: ignore[override] cls, language_code: str, contributor_user_id: str, topic_id: str) -> Optional['TranslationContributionStatsModel']: """Gets the TranslationContributionStatsModel matching the supplied language_code, contributor_user_id, topic_id. Returns: TranslationContributionStatsModel|None. The matching TranslationContributionStatsModel, or None if no such model instance exists. """ entity_id = cls.generate_id(language_code, contributor_user_id, topic_id) return cls.get_by_id(entity_id) @classmethod def get_all_by_user_id( cls, user_id: str) -> List['TranslationContributionStatsModel']: """Gets all TranslationContributionStatsModels matching the supplied user_id. Returns: list(TranslationContributionStatsModel). The matching TranslationContributionStatsModels. """ return cast( List[TranslationContributionStatsModel], cls.get_all().filter(cls.contributor_user_id == user_id).fetch( feconf.DEFAULT_QUERY_LIMIT)) @classmethod def has_reference_to_user_id(cls, user_id: str) -> bool: """Check whether TranslationContributionStatsModel references the supplied user. Args: user_id: str. The ID of the user whose data should be checked. Returns: bool. Whether any models refer to the given user ID. """ return cls.query(cls.contributor_user_id == user_id).get( keys_only=True) is not None @classmethod def get_deletion_policy(cls) -> base_models.DELETION_POLICY: """Model contains corresponding to a user: contributor_user_id.""" return base_models.DELETION_POLICY.DELETE @staticmethod def get_model_association_to_user( ) -> base_models.MODEL_ASSOCIATION_TO_USER: """Model is exported as multiple instances per user since there are multiple languages and topics relevant to a user. """ return base_models.MODEL_ASSOCIATION_TO_USER.MULTIPLE_INSTANCES_PER_USER @classmethod def get_export_policy(cls) -> Dict[str, base_models.EXPORT_POLICY]: """Model contains data to export corresponding to a user.""" return dict( super(cls, cls).get_export_policy(), **{ 'language_code': base_models.EXPORT_POLICY.EXPORTED, # User ID is not exported in order to keep internal ids private. 'contributor_user_id': base_models.EXPORT_POLICY.NOT_APPLICABLE, 'topic_id': base_models.EXPORT_POLICY.EXPORTED, 'submitted_translations_count': base_models.EXPORT_POLICY.EXPORTED, 'submitted_translation_word_count': base_models.EXPORT_POLICY.EXPORTED, 'accepted_translations_count': base_models.EXPORT_POLICY.EXPORTED, 'accepted_translations_without_reviewer_edits_count': base_models.EXPORT_POLICY.EXPORTED, 'accepted_translation_word_count': base_models.EXPORT_POLICY.EXPORTED, 'rejected_translations_count': base_models.EXPORT_POLICY.EXPORTED, 'rejected_translation_word_count': base_models.EXPORT_POLICY.EXPORTED, 'contribution_dates': base_models.EXPORT_POLICY.EXPORTED }) @classmethod def apply_deletion_policy(cls, user_id: str) -> None: """Delete instances of TranslationContributionStatsModel for the user. Args: user_id: str. The ID of the user whose data should be deleted. """ datastore_services.delete_multi( cls.query(cls.contributor_user_id == user_id).fetch( keys_only=True)) @classmethod def export_data( cls, user_id: str) -> Dict[str, Dict[str, Union[str, int, List[str]]]]: """Exports the data from TranslationContributionStatsModel into dict format for Takeout. Args: user_id: str. The ID of the user whose data should be exported. Returns: dict. Dictionary of the data from TranslationContributionStatsModel. """ user_data = {} stats_models = cast( List[TranslationContributionStatsModel], cls.get_all().filter(cls.contributor_user_id == user_id).fetch()) for model in stats_models: user_data[model.id] = { 'language_code': model.language_code, 'topic_id': model.topic_id, 'submitted_translations_count': (model.submitted_translations_count), 'submitted_translation_word_count': (model.submitted_translation_word_count), 'accepted_translations_count': (model.accepted_translations_count), 'accepted_translations_without_reviewer_edits_count': (model.accepted_translations_without_reviewer_edits_count), 'accepted_translation_word_count': (model.accepted_translation_word_count), 'rejected_translations_count': (model.rejected_translations_count), 'rejected_translation_word_count': (model.rejected_translation_word_count), 'contribution_dates': [date.isoformat() for date in model.contribution_dates] } return user_data
class AppFeedbackReportStatsModel(base_models.BaseModel): """Model for storing aggregate report stats on the tickets created. Instances of this model contain statistics for different report types based on the ticket they are assigned to and the date of the aggregation is on. The id of each model instance is calculated by concatenating the platform, ticket ID, and the date (in isoformat) this entity is tracking stats for. """ # The unique ticket ID that this entity is aggregating for. ticket_id = datastore_services.StringProperty(required=True, indexed=True) # The platform that these statistics are for. platform = datastore_services.StringProperty(required=True, indexed=True, choices=PLATFORM_CHOICES) # The date in UTC that this entity is tracking on -- this should correspond # to the creation date of the reports aggregated in this model. stats_tracking_date = datastore_services.DateProperty(required=True, indexed=True) # The total number of reports submitted on this date. total_reports_submitted = datastore_services.IntegerProperty(required=True, indexed=True) # JSON struct that maps the daily statistics for this ticket on the date # specified in stats_tracking_date. The JSON will map each param_name # (defined by a domain const ALLOWED_STATS_PARAM_NAMES) to a dictionary of # all the possible param_values for that parameter and the number of reports # submitted on that day that satisfy that param value, similar to e.g.: # # param_name1 : { param_value1 : report_count1, # param_value2 : report_count2, # param_value3 : report_count3 }, # param_name2 : { param_value1 : report_count1, # param_value2 : report_count2, # param_value3 : report_count3 } }. daily_param_stats = datastore_services.JsonProperty(required=True, indexed=False) # The schema version for parameter statistics in this entity. daily_param_stats_schema_version = datastore_services.IntegerProperty( required=True, indexed=True) @classmethod def create(cls, entity_id: str, platform: str, ticket_id: str, stats_tracking_date: datetime.date, total_reports_submitted: int, daily_param_stats: Dict[str, Dict[str, int]]) -> str: """Creates a new AppFeedbackReportStatsModel instance and returns its ID. Args: entity_id: str. The ID used for this entity. ticket_id: str. The ID for the ticket these stats aggregate on. platform: str. The platform the stats are aggregating for. stats_tracking_date: datetime.date. The date in UTC that this entity is tracking stats for. total_reports_submitted: int. The total number of reports submitted on this date. daily_param_stats: dict. The daily stats for this entity, keyed by the parameter witch each value mapping a parameter value to the number of reports that satisfy that parameter value. Returns: AppFeedbackReportStatsModel. The newly created AppFeedbackReportStatsModel instance. """ stats_entity = cls( id=entity_id, ticket_id=ticket_id, platform=platform, stats_tracking_date=stats_tracking_date, total_reports_submitted=total_reports_submitted, daily_param_stats=daily_param_stats, daily_param_stats_schema_version=( feconf.CURRENT_FEEDBACK_REPORT_STATS_SCHEMA_VERSION)) stats_entity.update_timestamps() stats_entity.put() return entity_id @classmethod def calculate_id(cls, platform: str, ticket_id: Optional[str], stats_tracking_date: datetime.date) -> str: """Generates key for the instance of AppFeedbackReportStatsModel class in the required format with the arguments provided. Args: platform: str. The platform this entity is aggregating on. ticket_id: str. The ID for the ticket these stats aggregate on. stats_tracking_date: date. The date these stats are tracking on. Returns: str. The ID for this entity of the form '[platform]:[ticket_id]:[stats_date in YYYY-MM-DD]'. """ if ticket_id is None: ticket_id = UNTICKETED_ANDROID_REPORTS_STATS_TICKET_ID return '%s:%s:%s' % (platform, ticket_id, stats_tracking_date.isoformat()) @classmethod def get_stats_for_ticket( cls, ticket_id: str) -> Sequence['AppFeedbackReportStatsModel']: """Fetches the stats for a single ticket. Args: ticket_id: str. The ID of the ticket to get stats for. Returns: list(str). A list of IDs corresponding to AppFeedbackReportStatsModel entities that record stats on the ticket. """ return cls.query(cls.ticket_id == ticket_id).fetch() @staticmethod def get_deletion_policy() -> base_models.DELETION_POLICY: """Model doesn't contain any information directly corresponding to a user. """ return base_models.DELETION_POLICY.NOT_APPLICABLE @classmethod def get_export_policy(cls) -> Dict[str, base_models.EXPORT_POLICY]: """Model doesn't contain any data directly corresponding to a user.""" return dict( super(cls, cls).get_export_policy(), **{ 'ticket_id': base_models.EXPORT_POLICY.NOT_APPLICABLE, 'platform': base_models.EXPORT_POLICY.NOT_APPLICABLE, 'stats_tracking_date': base_models.EXPORT_POLICY.NOT_APPLICABLE, 'total_reports_submitted': base_models.EXPORT_POLICY.NOT_APPLICABLE, 'daily_param_stats_schema_version': base_models.EXPORT_POLICY.NOT_APPLICABLE, 'daily_param_stats': base_models.EXPORT_POLICY.NOT_APPLICABLE }) @staticmethod def get_model_association_to_user( ) -> base_models.MODEL_ASSOCIATION_TO_USER: """Model doesn't contain any data directly corresponding to a user.""" return base_models.MODEL_ASSOCIATION_TO_USER.NOT_CORRESPONDING_TO_USER @staticmethod def get_lowest_supported_role() -> str: """The lowest supported role for feedback reports stats will be moderator. """ return feconf.ROLE_ID_MODERATOR