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
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)
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
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))
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))
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
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
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)
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()