Exemplo n.º 1
0
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
Exemplo n.º 2
0
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