Exemple #1
0
    def put(self, entity_type, entity_id):
        """"Handles the PUT requests. Stores the answer details submitted
        by the learner.
        """
        if not constants.ENABLE_SOLICIT_ANSWER_DETAILS_FEATURE:
            raise self.PageNotFoundException

        interaction_id = self.payload.get('interaction_id')
        if entity_type == feconf.ENTITY_TYPE_EXPLORATION:
            state_name = self.payload.get('state_name')
            state_reference = (
                stats_services.get_state_reference_for_exploration(
                    entity_id, state_name))
            if interaction_id != exp_services.get_interaction_id_for_state(
                    entity_id, state_name):
                raise utils.InvalidInputException(
                    'Interaction id given does not match with the '
                    'interaction id of the state')
        elif entity_type == feconf.ENTITY_TYPE_QUESTION:
            state_reference = (
                stats_services.get_state_reference_for_question(entity_id))
            if interaction_id != (
                    question_services.get_interaction_id_for_question(
                        entity_id)):
                raise utils.InvalidInputException(
                    'Interaction id given does not match with the '
                    'interaction id of the question')

        answer = self.payload.get('answer')
        answer_details = self.payload.get('answer_details')
        stats_services.record_learner_answer_info(
            entity_type, state_reference,
            interaction_id, answer, answer_details)
        self.render_json({})
def calculate_new_stats_count_for_parameter(current_stats_map: Dict[str, int],
                                            current_value: str,
                                            delta: int) -> Dict[Any, int]:
    """Helper to increment or initialize the stats count for a parameter.

    Args:
        current_stats_map: dict. The current stats map for the parameter we are
            updating; keys correspond to the possible value for a single
            parameter.
        current_value: str. The value for the parameter that we are updating
            the stats of.
        delta: int. The amount to increment the current count by, either -1 or
            +1.

    Returns:
        dict. The new stats values for the given parameter.
    """
    if current_value in current_stats_map:
        current_stats_map[current_value] += delta
    else:
        # The stats did not previously have this parameter value.
        if delta < 0:
            raise utils.InvalidInputException(
                'Cannot decrement a count for a parameter value that does not '
                'exist for this stats model.')
        # Update the stats so that it now contains this new value.
        current_stats_map[current_value] = 1
    return current_stats_map
Exemple #3
0
def _require_valid_suggestion_and_target_types(target_type, suggestion_type):
    """Checks whether the given target_type and suggestion_type are valid.

    Args:
        target_type: str. The type of the suggestion target.
        suggestion_type: str. The type of the suggestion.

    Raises:
        InvalidInputException: If the given target_type of suggestion_type are
            invalid.
    """
    if target_type not in suggestion_models.TARGET_TYPE_CHOICES:
        raise utils.InvalidInputException('Invalid target_type: %s' %
                                          target_type)

    if suggestion_type not in suggestion_models.SUGGESTION_TYPE_CHOICES:
        raise utils.InvalidInputException('Invalid suggestion_type: %s' %
                                          suggestion_type)
Exemple #4
0
    def _migrate_to_latest_yaml_version(cls, yaml_content):
        """Return the YAML content of the collection in the latest schema
        format.

        Args:
            yaml_content: str. The YAML representation of the collection.

        Returns:
            str. The YAML representation of the collection, in the latest
            schema format.

        Raises:
            InvalidInputException. The 'yaml_content' or the schema version
                is not specified.
            Exception. The collection schema version is not valid.
        """
        try:
            collection_dict = utils.dict_from_yaml(yaml_content)
        except utils.InvalidInputException as e:
            raise utils.InvalidInputException(
                'Please ensure that you are uploading a YAML text file, not '
                'a zip file. The YAML parser returned the following error: %s'
                % e)

        collection_schema_version = collection_dict.get('schema_version')
        if collection_schema_version is None:
            raise utils.InvalidInputException(
                'Invalid YAML file: no schema version specified.')
        if not (1 <= collection_schema_version
                <= feconf.CURRENT_COLLECTION_SCHEMA_VERSION):
            raise Exception(
                'Sorry, we can only process v1 to v%s collection YAML files at '
                'present.' % feconf.CURRENT_COLLECTION_SCHEMA_VERSION)

        while (collection_schema_version <
               feconf.CURRENT_COLLECTION_SCHEMA_VERSION):
            conversion_fn = getattr(
                cls, '_convert_v%s_dict_to_v%s_dict' % (
                    collection_schema_version, collection_schema_version + 1))
            collection_dict = conversion_fn(collection_dict)
            collection_schema_version += 1

        return collection_dict
def try_upgrading_draft_to_exp_version(
        draft_change_list, current_draft_version, to_exp_version, exp_id):
    """Try upgrading a list of ExplorationChange domain objects to match the
    latest exploration version.

    For now, this handles the scenario where all commits between
    current_draft_version and to_exp_version migrate only the state schema.

    Args:
        draft_change_list: list(ExplorationChange). The list of
            ExplorationChange domain objects to upgrade.
        current_draft_version: int. Current draft version.
        to_exp_version: int. Target exploration version.
        exp_id: str. Exploration id.

    Returns:
        list(ExplorationChange) or None. A list of ExplorationChange domain
        objects after upgrade or None if upgrade fails.

    Raises:
        InvalidInputException. The current_draft_version is greater than
            to_exp_version.
    """
    if current_draft_version > to_exp_version:
        raise utils.InvalidInputException(
            'Current draft version is greater than the exploration version.')
    if current_draft_version == to_exp_version:
        return None

    exp_versions = list(
        python_utils.RANGE(current_draft_version + 1, to_exp_version + 1))
    commits_list = (
        exp_models.ExplorationCommitLogEntryModel.get_multi(
            exp_id, exp_versions))
    upgrade_times = 0
    while current_draft_version + upgrade_times < to_exp_version:
        commit = commits_list[upgrade_times]
        if (
                len(commit.commit_cmds) != 1 or
                commit.commit_cmds[0]['cmd'] !=
                exp_domain.CMD_MIGRATE_STATES_SCHEMA_TO_LATEST_VERSION):
            return None
        conversion_fn_name = '_convert_states_v%s_dict_to_v%s_dict' % (
            commit.commit_cmds[0]['from_version'],
            commit.commit_cmds[0]['to_version'])
        if not hasattr(DraftUpgradeUtil, conversion_fn_name):
            logging.warning('%s is not implemented' % conversion_fn_name)
            return None
        conversion_fn = getattr(DraftUpgradeUtil, conversion_fn_name)
        try:
            draft_change_list = conversion_fn(draft_change_list)
        except InvalidDraftConversionException:
            return None
        upgrade_times += 1
    return draft_change_list
Exemple #6
0
def get_state_reference_for_question(question_id):
    """Returns the generated state reference for the given question id.

    Args:
        question_id: str. ID of the question.

    Returns:
        str. The generated state reference.
    """
    question = question_services.get_question_by_id(question_id, strict=False)
    if question is None:
        raise utils.InvalidInputException(
            'No question with the given question id exists.')
    return (stats_models.LearnerAnswerDetailsModel.
            get_state_reference_for_question(question_id))
Exemple #7
0
def get_state_reference_for_exploration(exp_id, state_name):
    """Returns the generated state reference for the given exploration id and
    state name.

    Args:
        exp_id: str. ID of the exploration.
        state_name: str. Name of the state.

    Returns:
        str. The generated state reference.
    """
    exploration = exp_fetchers.get_exploration_by_id(exp_id)
    if not exploration.has_state_name(state_name):
        raise utils.InvalidInputException(
            'No state with the given state name was found in the '
            'exploration with id %s' % exp_id)
    return (stats_models.LearnerAnswerDetailsModel.
            get_state_reference_for_exploration(exp_id, state_name))
Exemple #8
0
    def get_filter_options_for_field(cls, filter_field: str) -> List[str]:
        """Fetches values that can be used to filter reports by.

        Args:
            filter_field: FILTER_FIELD_NAME. The enum type of the field we want
                to fetch all possible values for.

        Returns:
            list(str). The possible values that the field name can have.
        """
        query = cls.query(projection=[filter_field.name],
                          distinct=True)  # type: ignore[attr-defined]
        filter_values = []
        if filter_field == FILTER_FIELD_NAMES.report_type:
            filter_values = [model.report_type for model in query]
        elif filter_field == FILTER_FIELD_NAMES.platform:
            filter_values = [model.platform for model in query]
        elif filter_field == FILTER_FIELD_NAMES.entry_point:
            filter_values = [model.entry_point for model in query]
        elif filter_field == FILTER_FIELD_NAMES.submitted_on:
            filter_values = [model.submitted_on.date() for model in query]
        elif filter_field == FILTER_FIELD_NAMES.android_device_model:
            filter_values = [model.android_device_model for model in query]
        elif filter_field == FILTER_FIELD_NAMES.android_sdk_version:
            filter_values = [model.android_sdk_version for model in query]
        elif filter_field == FILTER_FIELD_NAMES.text_language_code:
            filter_values = [model.text_language_code for model in query]
        elif filter_field == FILTER_FIELD_NAMES.audio_language_code:
            filter_values = [model.audio_language_code for model in query]
        elif filter_field == FILTER_FIELD_NAMES.platform_version:
            filter_values = [model.platform_version for model in query]
        elif filter_field == (
                FILTER_FIELD_NAMES.android_device_country_locale_code):
            filter_values = [
                model.android_device_country_locale_code for model in query
            ]
        else:
            raise utils.InvalidInputException(
                'The field %s is not a valid field to filter reports on' %
                (filter_field.name))  # type: ignore[attr-defined]
        return filter_values
Exemple #9
0
def get_user_id_from_email(email):
    """Given an email address, returns a user id.

    Returns None if the email address does not correspond to a valid user id.
    """
    class _FakeUser(ndb.Model):
        _use_memcache = False
        _use_cache = False
        user = ndb.UserProperty(required=True)

    try:
        u = users.User(email)
    except users.UserNotFoundError:
        raise utils.InvalidInputException(
            'User with email address %s not found' % email)

    key = _FakeUser(id=email, user=u).put()
    obj = _FakeUser.get_by_id(key.id())
    user_id = obj.user.user_id()
    if user_id:
        return unicode(user_id)
    else:
        return None
Exemple #10
0
def update_state_reference(
        entity_type, old_state_reference, new_state_reference):
    """Updates the state_reference field of the LearnerAnswerDetails model
    instance with the new_state_reference received and then saves the instance
    in the datastore.

    Args:
        entity_type: str. The type of entity i.e ENTITY_TYPE_EXPLORATION or
            ENTITY_TYPE_QUESTION, which are declared in feconf.py.
        old_state_reference: str. The old state reference which needs to be
            changed.
        new_state_reference: str. The new state reference which needs to be
            updated.
    """
    learner_answer_details = get_learner_answer_details(
        entity_type, old_state_reference)
    if learner_answer_details is None:
        raise utils.InvalidInputException(
            'No learner answer details found with the given state '
            'reference and entity')
    learner_answer_details.update_state_reference(new_state_reference)
    save_learner_answer_details(
        entity_type, old_state_reference, learner_answer_details)
Exemple #11
0
def delete_learner_answer_info(entity_type, state_reference,
                               learner_answer_info_id):
    """Deletes the learner answer info in the model, and then saves it.

    Args:
        entity_type: str. The type of entity i.e ENTITY_TYPE_EXPLORATION or
            ENTITY_TYPE_QUESTION, which are declared in feconf.py.
        state_reference: str. This is used to refer to a state in an exploration
            or question. For an exploration the value will be equal to
            'exp_id:state_name' and for question this will be equal to
            'question_id'.
        learner_answer_info_id: str. The unique ID of the learner answer info
            which needs to be deleted.
    """
    learner_answer_details = get_learner_answer_details(
        entity_type, state_reference)
    if learner_answer_details is None:
        raise utils.InvalidInputException(
            'No learner answer details found with the given state '
            'reference and entity')
    learner_answer_details.delete_learner_answer_info(learner_answer_info_id)
    save_learner_answer_details(entity_type, state_reference,
                                learner_answer_details)
def reassign_ticket(
    report: app_feedback_report_domain.AppFeedbackReport,
    new_ticket: Optional[app_feedback_report_domain.AppFeedbackReportTicket]
) -> None:
    """Reassign the ticket the report is associated with.

    Args:
        report: AppFeedbackReport. The report being assigned to a new ticket.
        new_ticket: AppFeedbackReportTicket|None. The ticket domain object to
            reassign the report to or None if removing the report form a ticket
            wihtout reassigning.
    """
    if report.platform == PLATFORM_WEB:
        raise NotImplementedError(
            'Assigning web reports to tickets has not been implemented yet.')

    platform = report.platform
    stats_date = report.submitted_on_timestamp.date()
    # Remove the report from the stats model associated with the old ticket.
    old_ticket_id = report.ticket_id
    if old_ticket_id is None:
        _update_report_stats_model_in_transaction(
            constants.UNTICKETED_ANDROID_REPORTS_STATS_TICKET_ID, platform,
            stats_date, report, -1)
    else:
        # The report was ticketed so the report needs to be removed from its old
        # ticket in storage.
        old_ticket_model = (
            app_feedback_report_models.AppFeedbackReportTicketModel.get_by_id(
                old_ticket_id))
        if old_ticket_model is None:
            raise utils.InvalidInputException(
                'The report is being removed from an invalid ticket id: %s.' %
                old_ticket_id)
        old_ticket_obj = get_ticket_from_model(old_ticket_model)
        old_ticket_obj.reports.remove(report.report_id)
        if len(old_ticket_obj.reports) == 0:
            # We are removing the only report associated with this ticket.
            old_ticket_obj.newest_report_creation_timestamp = None  # type: ignore[assignment]
        else:
            if old_ticket_obj.newest_report_creation_timestamp == (
                    report.submitted_on_timestamp):
                # Update the newest report timestamp.
                optional_report_models = get_report_models(
                    old_ticket_obj.reports)
                report_models = cast(
                    List[app_feedback_report_models.AppFeedbackReportModel],
                    optional_report_models)
                latest_timestamp = report_models[0].submitted_on
                for index in python_utils.RANGE(1, len(report_models)):
                    if report_models[index].submitted_on > (latest_timestamp):
                        latest_timestamp = (report_models[index].submitted_on)
                old_ticket_obj.newest_report_creation_timestamp = (
                    latest_timestamp)
        _save_ticket(old_ticket_obj)
        _update_report_stats_model_in_transaction(old_ticket_id, platform,
                                                  stats_date, report, -1)

    # Add the report to the new ticket.
    new_ticket_id = constants.UNTICKETED_ANDROID_REPORTS_STATS_TICKET_ID
    if new_ticket is not None:
        new_ticket_id = new_ticket.ticket_id
    new_ticket_model = (app_feedback_report_models.
                        AppFeedbackReportTicketModel.get_by_id(new_ticket_id))
    new_ticket_obj = get_ticket_from_model(new_ticket_model)
    new_ticket_obj.reports.append(report.report_id)
    if report.submitted_on_timestamp > (
            new_ticket_obj.newest_report_creation_timestamp):
        new_ticket_obj.newest_report_creation_timestamp = (
            report.submitted_on_timestamp)
    _save_ticket(new_ticket_obj)

    # Update the stats model for the new ticket.
    platform = report.platform
    stats_date = report.submitted_on_timestamp.date()
    _update_report_stats_model_in_transaction(new_ticket_id, platform,
                                              stats_date, report, 1)

    # Update the report model to the new ticket id.
    report.ticket_id = new_ticket_id
    save_feedback_report_to_storage(report)
def save_feedback_report_to_storage(
        report: app_feedback_report_domain.AppFeedbackReport,
        new_incoming_report: bool = False) -> None:
    """Saves the AppFeedbackReport domain object to persistent storage.

    Args:
        report: AppFeedbackReport. The domain object of the report to save.
        new_incoming_report: bool. Whether the report is a new incoming report
            that does not have a corresponding model entity.
    """
    if report.platform == PLATFORM_WEB:
        raise utils.InvalidInputException(
            'Web report domain objects have not been defined.')

    report.validate()
    user_supplied_feedback = report.user_supplied_feedback
    device_system_context = cast(
        app_feedback_report_domain.AndroidDeviceSystemContext,
        report.device_system_context)
    app_context = cast(app_feedback_report_domain.AndroidAppContext,
                       report.app_context)
    entry_point = app_context.entry_point

    report_info_json = {
        'user_feedback_selected_items':
        (user_supplied_feedback.user_feedback_selected_items),
        'user_feedback_other_text_input':
        (user_supplied_feedback.user_feedback_other_text_input)
    }

    report_info_json = {
        'user_feedback_selected_items':
        (user_supplied_feedback.user_feedback_selected_items),
        'user_feedback_other_text_input':
        (user_supplied_feedback.user_feedback_other_text_input),
        'event_logs':
        app_context.event_logs,
        'logcat_logs':
        app_context.logcat_logs,
        'package_version_code':
        python_utils.UNICODE(device_system_context.package_version_code),
        'android_device_language_locale_code':
        (device_system_context.device_language_locale_code),
        'build_fingerprint':
        device_system_context.build_fingerprint,
        'network_type':
        device_system_context.network_type.name,
        'text_size':
        app_context.text_size.name,
        'only_allows_wifi_download_and_update':
        python_utils.UNICODE(app_context.only_allows_wifi_download_and_update),
        'automatically_update_topics':
        python_utils.UNICODE(app_context.automatically_update_topics),
        'account_is_profile_admin':
        python_utils.UNICODE(app_context.account_is_profile_admin)
    }

    if new_incoming_report:
        app_feedback_report_models.AppFeedbackReportModel.create(
            report.report_id, report.platform, report.submitted_on_timestamp,
            report.local_timezone_offset_hrs,
            user_supplied_feedback.report_type.name,
            user_supplied_feedback.category.name,
            device_system_context.version_name,
            device_system_context.device_country_locale_code,
            device_system_context.sdk_version,
            device_system_context.device_model, entry_point.entry_point_name,
            entry_point.topic_id, entry_point.story_id,
            entry_point.exploration_id, entry_point.subtopic_id,
            app_context.text_language_code, app_context.audio_language_code,
            None, None)
    model_entity = app_feedback_report_models.AppFeedbackReportModel.get_by_id(
        report.report_id)
    model_entity.android_report_info = report_info_json
    model_entity.ticket_id = report.ticket_id
    model_entity.scrubbed_by = report.scrubbed_by
    model_entity.update_timestamps()
    model_entity.put()