def __init__(self, node_id, title, destination_node_ids, acquired_skill_ids, prerequisite_skill_ids, outline, outline_is_finalized, exploration_id): """Initializes a StoryNode domain object. Args: node_id: str. The unique id for each node. title: str. The title of the story node. destination_node_ids: list(str). The list of destination node ids that this node points to in the story graph. acquired_skill_ids: list(str). The list of skill ids acquired by the user on completion of the node. prerequisite_skill_ids: list(str). The list of skill ids required before starting a node. outline: str. Free-form annotations that a lesson implementer can use to construct the exploration. It describes the basic theme or template of the story and is to be provided in html form. outline_is_finalized: bool. Whether the outline for the story node is finalized or not. exploration_id: str or None. The valid exploration id that fits the story node. It can be None initially, when the story creator has just created a story with the basic storyline (by providing outlines) without linking an exploration to any node. """ self.id = node_id self.title = title self.destination_node_ids = destination_node_ids self.acquired_skill_ids = acquired_skill_ids self.prerequisite_skill_ids = prerequisite_skill_ids self.outline = html_cleaner.clean(outline) self.outline_is_finalized = outline_is_finalized self.exploration_id = exploration_id
def update_content(self, content): """Updates the content of the blog post. Args: content: str. The new content of the blog post. """ self.content = html_cleaner.clean(content)
def update_html_data(self, new_html_data): """The new value for the html data field. Args: new_html_data: str. The new html data for the subtopic page. """ self.html_data = html_cleaner.clean(new_html_data)
def __init__(self, blog_post_id, author_id, title, content, url_fragment, tags, thumbnail_filename=None, last_updated=None, published_on=None): """Constructs a BlogPost domain object. Args: blog_post_id: str. The unique ID of the blog post. author_id: str. The user ID of the author. title: str. The title of the blog post. content: str. The html content of the blog post. url_fragment: str. The url fragment for the blog post. tags: list(str). The list of tags for the blog post. thumbnail_filename: str|None. The thumbnail filename of blog post . last_updated: datetime.datetime. Date and time when the blog post was last updated. published_on: datetime.datetime. Date and time when the blog post is last published. """ self.id = blog_post_id self.author_id = author_id self.title = title self.content = html_cleaner.clean(content) self.url_fragment = url_fragment self.tags = tags self.thumbnail_filename = thumbnail_filename self.last_updated = last_updated self.published_on = published_on
def test_good_tags_allowed(self): test_data = [( '<a href="http://www.google.com">Hello</a>', '<a href="http://www.google.com">Hello</a>' ), ( '<a href="http://www.google.com" target="_blank">Hello</a>', '<a href="http://www.google.com" target="_blank">Hello</a>' ), ( '<a href="http://www.google.com" title="Hello">Hello</a>', '<a href="http://www.google.com" title="Hello">Hello</a>' ), ( 'Just some text 12345', 'Just some text 12345' ), ( '<code>Unfinished HTML', '<code>Unfinished HTML</code>', ), ( '<br/>', '<br>' ), ( 'A big mix <div>Hello</div> Yes <span>No</span>', 'A big mix <div>Hello</div> Yes <span>No</span>' )] for datum in test_data: self.assertEqual( html_cleaner.clean(datum[0]), datum[1], msg='\n\nOriginal text: %s' % datum[0])
def __init__( self, question_id, question_content, misconception_ids, interaction_id, question_model_created_on=None, question_model_last_updated=None): """Constructs a Question Summary domain object. Args: question_id: str. The ID of the question. question_content: str. The static HTML of the question shown to the learner. misconception_ids: str. The misconception ids addressed in the question. This includes tagged misconceptions ids as well as inapplicable misconception ids in the question. interaction_id: str. The ID of the interaction. question_model_created_on: datetime.datetime. Date and time when the question model is created. question_model_last_updated: datetime.datetime. Date and time when the question model was last updated. """ self.id = question_id self.question_content = html_cleaner.clean(question_content) self.misconception_ids = misconception_ids self.interaction_id = interaction_id self.created_on = question_model_created_on self.last_updated = question_model_last_updated
def __init__( self, story_id, title, description, notes, story_contents, schema_version, language_code, version, created_on=None, last_updated=None): """Constructs a Story domain object. Args: story_id: str. The unique ID of the story. title: str. The title of the story. description: str. The high level description of the story. notes: str. A set of notes, that describe the characters, main storyline, and setting. To be provided in html form. story_contents: StoryContents. The StoryContents instance representing the contents (like nodes) that are part of the story. created_on: datetime.datetime. Date and time when the story is created. last_updated: datetime.datetime. Date and time when the story was last updated. schema_version: int. The schema version for the story nodes object. language_code: str. The ISO 639-1 code for the language this story is written in. version: int. The version of the story. """ self.id = story_id self.title = title self.description = description self.notes = html_cleaner.clean(notes) self.story_contents = story_contents self.schema_version = schema_version self.language_code = language_code self.created_on = created_on self.last_updated = last_updated self.version = version
def test_bad_tags_suppressed(self): test_data = [( '<incomplete-bad-tag>', '' ), ( '<complete-bad-tag></complete-bad-tag>', '' ), ( '<incomplete-bad-tag><div>OK tag</div>', '<div>OK tag</div>' ), ( '<complete-bad-tag></complete-bad-tag><span>OK tag</span>', '<span>OK tag</span>' ), ( '<bad-tag></bad-tag>Just some text 12345', 'Just some text 12345' ), ( '<script>alert(\'Here is some JS\');</script>', 'alert(\'Here is some JS\');' ), ( '<iframe src="https://oppiaserver.appspot.com"></iframe>', '' )] for datum in test_data: self.assertEqual( html_cleaner.clean(datum[0]), datum[1], '\n\nOriginal text: %s' % datum[0])
def test_bad_tags_suppressed(self): TEST_DATA = [( '<incomplete-bad-tag>', '' ), ( '<complete-bad-tag></complete-bad-tag>', '' ), ( '<incomplete-bad-tag><div>OK tag</div>', '<div>OK tag</div>' ), ( '<complete-bad-tag></complete-bad-tag><span>OK tag</span>', '<span>OK tag</span>' ), ( '<bad-tag></bad-tag>Just some text 12345', 'Just some text 12345' ), ( '<script>alert(\'Here is some JS\');</script>', 'alert(\'Here is some JS\');' ), ( '<iframe src="https://oppiaserver.appspot.com"></iframe>', '' )] for datum in TEST_DATA: self.assertEqual( html_cleaner.clean(datum[0]), datum[1], '\n\nOriginal text: %s' % datum[0])
def normalize(cls, raw): """Validates and normalizes a raw Python object.""" try: assert isinstance(raw, basestring) return html_cleaner.clean(unicode(raw)) except Exception as e: raise TypeError('Cannot convert to HTML string: %s. Error: %s' % (raw, e))
def __init__(self, misconception_id, name, notes, feedback): """Initializes a Misconception domain object. Args: misconception_id: int. The unique id of each misconception. name: str. The name of the misconception. notes: str. General advice for creators about the misconception (including examples) and general notes. This should be an html string. feedback: str. This can auto-populate the feedback field when an answer group has been tagged with a misconception. This should be an html string. """ self.id = misconception_id self.name = name self.notes = html_cleaner.clean(notes) self.feedback = html_cleaner.clean(feedback)
def update_content(self, content: str) -> None: """Updates the content of the blog post. Args: content: str. The new content of the blog post. """ self.content = html_cleaner.clean( content) # type: ignore[no-untyped-call]
def _send_bulk_mail( recipient_ids, sender_id, intent, email_subject, email_html_body, sender_email, sender_name, instance_id=None ): """Sends an email to all given recipients. Args: recipient_ids: list(str). The user IDs of the email recipients. sender_id: str. The ID of the user sending the email. intent: str. The intent string, i.e. the purpose of the email. email_subject: str. The subject of the email. email_html_body: str. The body (message) of the email. sender_email: str. The sender's email address. sender_name: str. The name to be shown in the "sender" field of the email. instance_id: str or None. The ID of the BulkEmailModel entity instance. """ _require_sender_id_is_valid(intent, sender_id) recipients_settings = user_services.get_users_settings(recipient_ids) recipient_emails = [user.email for user in recipients_settings] cleaned_html_body = html_cleaner.clean(email_html_body) if cleaned_html_body != email_html_body: log_new_error( "Original email HTML body does not match cleaned HTML body:\n" "Original:\n%s\n\nCleaned:\n%s\n" % (email_html_body, cleaned_html_body) ) return raw_plaintext_body = ( cleaned_html_body.replace("<br/>", "\n") .replace("<br>", "\n") .replace("<li>", "<li>- ") .replace("</p><p>", "</p>\n<p>") ) cleaned_plaintext_body = html_cleaner.strip_html_tags(raw_plaintext_body) def _send_bulk_mail_in_transaction(instance_id=None): sender_name_email = "%s <%s>" % (sender_name, sender_email) email_services.send_bulk_mail( sender_name_email, recipient_emails, email_subject, cleaned_plaintext_body, cleaned_html_body ) if instance_id is None: instance_id = email_models.BulkEmailModel.get_new_id("") email_models.BulkEmailModel.create( instance_id, recipient_ids, sender_id, sender_name_email, intent, email_subject, cleaned_html_body, datetime.datetime.utcnow(), ) return transaction_services.run_in_transaction(_send_bulk_mail_in_transaction, instance_id)
def _send_bulk_mail(recipient_ids, sender_id, intent, email_subject, email_html_body, sender_email, sender_name, instance_id=None): """Sends an email to all given recipients. Args: recipient_ids: list(str). The user IDs of the email recipients. sender_id: str. The ID of the user sending the email. intent: str. The intent string, i.e. the purpose of the email. email_subject: str. The subject of the email. email_html_body: str. The body (message) of the email. sender_email: str. The sender's email address. sender_name: str. The name to be shown in the "sender" field of the email. instance_id: str or None. The ID of the BulkEmailModel entity instance. """ _require_sender_id_is_valid(intent, sender_id) recipients_settings = user_services.get_users_settings(recipient_ids) recipient_emails = [user.email for user in recipients_settings] cleaned_html_body = html_cleaner.clean(email_html_body) if cleaned_html_body != email_html_body: log_new_error( 'Original email HTML body does not match cleaned HTML body:\n' 'Original:\n%s\n\nCleaned:\n%s\n' % (email_html_body, cleaned_html_body)) return raw_plaintext_body = cleaned_html_body.replace('<br/>', '\n').replace( '<br>', '\n').replace('<li>', '<li>- ').replace('</p><p>', '</p>\n<p>') cleaned_plaintext_body = html_cleaner.strip_html_tags(raw_plaintext_body) def _send_bulk_mail_in_transaction(instance_id=None): """Sends the emails in bulk to the recipients.""" sender_name_email = '%s <%s>' % (sender_name, sender_email) email_services.send_bulk_mail(sender_name_email, recipient_emails, email_subject, cleaned_plaintext_body, cleaned_html_body) if instance_id is None: instance_id = email_models.BulkEmailModel.get_new_id('') email_models.BulkEmailModel.create(instance_id, recipient_ids, sender_id, sender_name_email, intent, email_subject, cleaned_html_body, datetime.datetime.utcnow()) transaction_services.run_in_transaction(_send_bulk_mail_in_transaction, instance_id)
def __init__(self, difficulty, explanation): """Initializes a Rubric domain object. Args: difficulty: str. The question difficulty that this rubric addresses. explanation: str. The explanation for the corresponding difficulty. """ self.difficulty = difficulty self.explanation = html_cleaner.clean(explanation)
def _send_email(recipient_id, sender_id, intent, email_subject, email_html_body, sender_email, bcc_admin=False, sender_name=None): """Sends an email to the given recipient. This function should be used for sending all user-facing emails. Raises an Exception if the sender_id is not appropriate for the given intent. Currently we support only system-generated emails and emails initiated by moderator actions. """ if sender_name is None: sender_name = EMAIL_SENDER_NAME.value _require_sender_id_is_valid(intent, sender_id) recipient_email = user_services.get_email_from_user_id(recipient_id) cleaned_html_body = html_cleaner.clean(email_html_body) if cleaned_html_body != email_html_body: log_new_error( 'Original email HTML body does not match cleaned HTML body:\n' 'Original:\n%s\n\nCleaned:\n%s\n' % (email_html_body, cleaned_html_body)) return raw_plaintext_body = cleaned_html_body.replace('<br/>', '\n').replace( '<br>', '\n').replace('<li>', '<li>- ').replace('</p><p>', '</p>\n<p>') cleaned_plaintext_body = html_cleaner.strip_html_tags(raw_plaintext_body) if email_models.SentEmailModel.check_duplicate_message( recipient_id, email_subject, cleaned_plaintext_body): log_new_error('Duplicate email:\n' 'Details:\n%s %s\n%s\n\n' % (recipient_id, email_subject, cleaned_plaintext_body)) return def _send_email_in_transaction(): sender_name_email = '%s <%s>' % (sender_name, sender_email) email_services.send_mail(sender_name_email, recipient_email, email_subject, cleaned_plaintext_body, cleaned_html_body, bcc_admin) email_models.SentEmailModel.create(recipient_id, recipient_email, sender_id, sender_name_email, intent, email_subject, cleaned_html_body, datetime.datetime.utcnow()) return transaction_services.run_in_transaction(_send_email_in_transaction)
def __init__(self, misconception_id, name, notes, feedback, must_be_addressed): """Initializes a Misconception domain object. Args: misconception_id: int. The unique id of each misconception. name: str. The name of the misconception. notes: str. General advice for creators about the misconception (including examples) and general notes. This should be an html string. feedback: str. This can auto-populate the feedback field when an answer group has been tagged with a misconception. This should be an html string. must_be_addressed: bool. Whether the misconception should necessarily be addressed in all questions linked to the skill. """ self.id = misconception_id self.name = name self.notes = html_cleaner.clean(notes) self.feedback = html_cleaner.clean(feedback) self.must_be_addressed = must_be_addressed
def __init__(self, explanation, worked_examples): """Constructs a SkillContents domain object. Args: explanation: str. An explanation on how to apply the skill. worked_examples: list(str). A list of worked examples for the skill. Each element should be an html string. """ self.explanation = explanation self.worked_examples = [ html_cleaner.clean(example) for example in worked_examples ]
def test_oppia_custom_tags(self): TEST_DATA = [('<oppia-noninteractive-image filepath-with-value="1"/>', '<oppia-noninteractive-image filepath-with-value="1">' '</oppia-noninteractive-image>'), ('<oppia-noninteractive-image filepath-with-value="1">' '</oppia-noninteractive-image>', '<oppia-noninteractive-image filepath-with-value="1">' '</oppia-noninteractive-image>'), ('<oppia-fake-tag></oppia-fake-tag>', '')] for datum in TEST_DATA: self.assertEqual(html_cleaner.clean(datum[0]), datum[1], '\n\nOriginal text: %s' % datum[0])
def test_good_tags_allowed(self): TEST_DATA = [('<a href="http://www.google.com">Hello</a>', '<a href="http://www.google.com">Hello</a>'), ('Just some text 12345', 'Just some text 12345'), ( '<code>Unfinished HTML', '<code>Unfinished HTML</code>', ), ('<br/>', '<br>'), ('A big mix <div>Hello</div> Yes <span>No</span>', 'A big mix <div>Hello</div> Yes <span>No</span>')] for datum in TEST_DATA: self.assertEqual(html_cleaner.clean(datum[0]), datum[1], '\n\nOriginal text: %s' % datum[0])
def _send_email( recipient_id, sender_id, intent, email_subject, email_html_body, sender_email, bcc_admin=False, sender_name=None): """Sends an email to the given recipient. This function should be used for sending all user-facing emails. Raises an Exception if the sender_id is not appropriate for the given intent. Currently we support only system-generated emails and emails initiated by moderator actions. """ if sender_name is None: sender_name = EMAIL_SENDER_NAME.value _require_sender_id_is_valid(intent, sender_id) recipient_email = user_services.get_email_from_user_id(recipient_id) cleaned_html_body = html_cleaner.clean(email_html_body) if cleaned_html_body != email_html_body: log_new_error( 'Original email HTML body does not match cleaned HTML body:\n' 'Original:\n%s\n\nCleaned:\n%s\n' % (email_html_body, cleaned_html_body)) return raw_plaintext_body = cleaned_html_body.replace('<br/>', '\n').replace( '<br>', '\n').replace('<li>', '<li>- ').replace('</p><p>', '</p>\n<p>') cleaned_plaintext_body = html_cleaner.strip_html_tags(raw_plaintext_body) if email_models.SentEmailModel.check_duplicate_message( recipient_id, email_subject, cleaned_plaintext_body): log_new_error( 'Duplicate email:\n' 'Details:\n%s %s\n%s\n\n' % (recipient_id, email_subject, cleaned_plaintext_body)) return def _send_email_in_transaction(): sender_name_email = '%s <%s>' % (sender_name, sender_email) email_services.send_mail( sender_name_email, recipient_email, email_subject, cleaned_plaintext_body, cleaned_html_body, bcc_admin) email_models.SentEmailModel.create( recipient_id, recipient_email, sender_id, sender_name_email, intent, email_subject, cleaned_html_body, datetime.datetime.utcnow()) return transaction_services.run_in_transaction(_send_email_in_transaction)
def test_oppia_custom_tags(self) -> None: test_data: List[Tuple[str, ...]] = [ ('<oppia-noninteractive-image filepath-with-value="1"/>', '<oppia-noninteractive-image filepath-with-value="1">' '</oppia-noninteractive-image>'), ('<oppia-noninteractive-image filepath-with-value="1">' '</oppia-noninteractive-image>', '<oppia-noninteractive-image filepath-with-value="1">' '</oppia-noninteractive-image>'), ('<oppia-fake-tag></oppia-fake-tag>', '') ] for datum in test_data: self.assertEqual(html_cleaner.clean(datum[0]), datum[1], msg='\n\nOriginal text: %s' % datum[0])
def __init__( self, subtopic_page_id, topic_id, html_data, language_code, version): """Constructs a SubtopicPage domain object. Args: subtopic_page_id: str. The unique ID of the subtopic page. topic_id: str. The ID of the topic that this subtopic is a part of. html_data: str. The HTML content of the subtopic page. language_code: str. The ISO 639-1 code for the language this subtopic page is written in. version: int. The current version of the subtopic. """ self.id = subtopic_page_id self.topic_id = topic_id self.html_data = html_cleaner.clean(html_data) self.language_code = language_code self.version = version
def __init__( self, question_id, question_content, question_model_created_on=None, question_model_last_updated=None): """Constructs a Question Summary domain object. Args: question_id: str. The ID of the question. question_content: str. The static HTML of the question shown to the learner. question_model_created_on: datetime.datetime. Date and time when the question model is created. question_model_last_updated: datetime.datetime. Date and time when the question model was last updated. """ self.id = question_id self.question_content = html_cleaner.clean(question_content) self.created_on = question_model_created_on self.last_updated = question_model_last_updated
def test_oppia_custom_tags(self): TEST_DATA = [( '<oppia-noninteractive-image filepath-with-value="1"/>', '<oppia-noninteractive-image filepath-with-value="1">' '</oppia-noninteractive-image>' ), ( '<oppia-noninteractive-image filepath-with-value="1">' '</oppia-noninteractive-image>', '<oppia-noninteractive-image filepath-with-value="1">' '</oppia-noninteractive-image>' ), ( '<oppia-fake-tag></oppia-fake-tag>', '' )] for datum in TEST_DATA: self.assertEqual( html_cleaner.clean(datum[0]), datum[1], '\n\nOriginal text: %s' % datum[0])
def _normalize_value(raw): """Validates and normalizes the value fields of the translatable value. Args: raw: *. A translatable Python object whose values are to be normalized. Returns: dict. A normalized translatable Python object with its values normalized. Raises: TypeError. The Python object cannot be normalized. """ if not isinstance(raw['html'], python_utils.BASESTRING): raise TypeError('Invalid HTML: %s' % raw['html']) raw['html'] = html_cleaner.clean(raw['html']) return raw
def _send_email(recipient_id, sender_id, intent, email_subject, email_html_body): """Sends an email to the given recipient. This function should be used for sending all user-facing emails. Raises an Exception if the sender_id is not appropriate for the given intent. Currently we support only system-generated emails and emails initiated by moderator actions. """ _require_sender_id_is_valid(intent, sender_id) recipient_email = user_services.get_email_from_user_id(recipient_id) cleaned_html_body = html_cleaner.clean(email_html_body) if cleaned_html_body != email_html_body: log_new_error( "Original email HTML body does not match cleaned HTML body:\n" "Original:\n%s\n\nCleaned:\n%s\n" % (email_html_body, cleaned_html_body) ) return raw_plaintext_body = cleaned_html_body.replace("<br/>", "\n").replace("<br>", "\n").replace("</p><p>", "</p>\n<p>") cleaned_plaintext_body = html_cleaner.strip_html_tags(raw_plaintext_body) def _send_email_in_transaction(): sender_email = "%s <%s>" % (EMAIL_SENDER_NAME.value, feconf.SYSTEM_EMAIL_ADDRESS) email_services.send_mail( sender_email, recipient_email, email_subject, cleaned_plaintext_body, cleaned_html_body ) email_models.SentEmailModel.create( recipient_id, recipient_email, sender_id, sender_email, intent, email_subject, cleaned_html_body, datetime.datetime.utcnow(), ) return transaction_services.run_in_transaction(_send_email_in_transaction)
def test_good_tags_allowed(self): TEST_DATA = [( '<a href="http://www.google.com">Hello</a>', '<a href="http://www.google.com">Hello</a>' ), ( 'Just some text 12345', 'Just some text 12345' ), ( '<code>Unfinished HTML', '<code>Unfinished HTML</code>', ), ( '<br/>', '<br>' ), ( 'A big mix <div>Hello</div> Yes <span>No</span>', 'A big mix <div>Hello</div> Yes <span>No</span>' )] for datum in TEST_DATA: self.assertEqual( html_cleaner.clean(datum[0]), datum[1], '\n\nOriginal text: %s' % datum[0])
def _send_bulk_mail( recipient_ids, sender_id, intent, email_subject, email_html_body, sender_email, sender_name, instance_id=None): """Sends an email to all given recipients.""" _require_sender_id_is_valid(intent, sender_id) recipients_settings = user_services.get_users_settings(recipient_ids) recipient_emails = [user.email for user in recipients_settings] cleaned_html_body = html_cleaner.clean(email_html_body) if cleaned_html_body != email_html_body: log_new_error( 'Original email HTML body does not match cleaned HTML body:\n' 'Original:\n%s\n\nCleaned:\n%s\n' % (email_html_body, cleaned_html_body)) return raw_plaintext_body = cleaned_html_body.replace('<br/>', '\n').replace( '<br>', '\n').replace('<li>', '<li>- ').replace('</p><p>', '</p>\n<p>') cleaned_plaintext_body = html_cleaner.strip_html_tags(raw_plaintext_body) def _send_bulk_mail_in_transaction(instance_id=None): sender_name_email = '%s <%s>' % (sender_name, sender_email) email_services.send_bulk_mail( sender_name_email, recipient_emails, email_subject, cleaned_plaintext_body, cleaned_html_body) if instance_id is None: instance_id = email_models.BulkEmailModel.get_new_id('') email_models.BulkEmailModel.create( instance_id, recipient_ids, sender_id, sender_name_email, intent, email_subject, cleaned_html_body, datetime.datetime.utcnow()) return transaction_services.run_in_transaction( _send_bulk_mail_in_transaction, instance_id)
def normalize_against_schema( obj: Any, schema: Dict[str, Any], apply_custom_validators: bool = True, global_validators: Optional[List[Dict[str, Any]]] = None ) -> Any: """Validate the given object using the schema, normalizing if necessary. Args: obj: *. The object to validate and normalize. schema: dict(str, *). The schema to validate and normalize the value against. apply_custom_validators: bool. Whether to validate the normalized object using the validators defined in the schema. global_validators: list(dict). List of additional validators that will verify all the values in the schema. Returns: *. The normalized object. Raises: AssertionError. The object fails to validate against the schema. """ normalized_obj: Any = None if schema[SCHEMA_KEY_TYPE] == SCHEMA_TYPE_BOOL: assert isinstance(obj, bool), ('Expected bool, received %s' % obj) normalized_obj = obj elif schema[SCHEMA_KEY_TYPE] == SCHEMA_TYPE_CUSTOM: # Importing this at the top of the file causes a circular dependency. # TODO(sll): Either get rid of custom objects or find a way to merge # them into the schema framework -- probably the latter. from core.domain import object_registry obj_class = object_registry.Registry.get_object_class_by_type( # type: ignore[no-untyped-call] schema[SCHEMA_KEY_OBJ_TYPE]) if not apply_custom_validators: normalized_obj = normalize_against_schema( obj, obj_class.get_schema(), apply_custom_validators=False) else: normalized_obj = obj_class.normalize(obj) elif schema[SCHEMA_KEY_TYPE] == SCHEMA_TYPE_DICT: assert isinstance(obj, dict), ('Expected dict, received %s' % obj) expected_dict_keys = [ p[SCHEMA_KEY_NAME] for p in schema[SCHEMA_KEY_PROPERTIES]] missing_keys = list(sorted(set(expected_dict_keys) - set(obj.keys()))) extra_keys = list(sorted(set(obj.keys()) - set(expected_dict_keys))) assert set(obj.keys()) == set(expected_dict_keys), ( 'Missing keys: %s, Extra keys: %s' % (missing_keys, extra_keys)) normalized_obj = {} for prop in schema[SCHEMA_KEY_PROPERTIES]: key = prop[SCHEMA_KEY_NAME] normalized_obj[key] = normalize_against_schema( obj[key], prop[SCHEMA_KEY_SCHEMA], global_validators=global_validators ) elif schema[SCHEMA_KEY_TYPE] == SCHEMA_TYPE_FLOAT: try: obj = float(obj) except Exception: raise Exception('Could not convert %s to float: %s' % ( type(obj).__name__, obj)) assert isinstance(obj, numbers.Real), ( 'Expected float, received %s' % obj) normalized_obj = obj elif schema[SCHEMA_KEY_TYPE] == SCHEMA_TYPE_INT: try: obj = int(obj) except Exception: raise Exception('Could not convert %s to int: %s' % ( type(obj).__name__, obj)) assert isinstance(obj, numbers.Integral), ( 'Expected int, received %s' % obj) assert isinstance(obj, int), ('Expected int, received %s' % obj) normalized_obj = obj elif schema[SCHEMA_KEY_TYPE] == SCHEMA_TYPE_HTML: assert isinstance(obj, python_utils.BASESTRING), ( 'Expected unicode HTML string, received %s' % obj) if isinstance(obj, bytes): obj = obj.decode('utf-8') else: obj = python_utils.UNICODE(obj) assert isinstance(obj, python_utils.UNICODE), ( 'Expected unicode, received %s' % obj) normalized_obj = html_cleaner.clean(obj) # type: ignore[no-untyped-call] elif schema[SCHEMA_KEY_TYPE] == SCHEMA_TYPE_LIST: assert isinstance(obj, list), ('Expected list, received %s' % obj) item_schema = schema[SCHEMA_KEY_ITEMS] if SCHEMA_KEY_LEN in schema: assert len(obj) == schema[SCHEMA_KEY_LEN], ( 'Expected length of %s got %s' % ( schema[SCHEMA_KEY_LEN], len(obj))) normalized_obj = [ normalize_against_schema( item, item_schema, global_validators=global_validators ) for item in obj ] elif schema[SCHEMA_KEY_TYPE] == SCHEMA_TYPE_BASESTRING: assert isinstance(obj, python_utils.BASESTRING), ( 'Expected string, received %s' % obj) normalized_obj = obj elif schema[SCHEMA_KEY_TYPE] == SCHEMA_TYPE_UNICODE: assert isinstance(obj, python_utils.BASESTRING), ( 'Expected unicode string, received %s' % obj) if isinstance(obj, bytes): obj = obj.decode('utf-8') else: obj = python_utils.UNICODE(obj) assert isinstance(obj, python_utils.UNICODE), ( 'Expected unicode, received %s' % obj) normalized_obj = obj elif schema[SCHEMA_KEY_TYPE] == SCHEMA_TYPE_UNICODE_OR_NONE: assert obj is None or isinstance(obj, python_utils.BASESTRING), ( 'Expected unicode string or None, received %s' % obj) if obj is not None: if isinstance(obj, bytes): obj = obj.decode('utf-8') else: obj = python_utils.UNICODE(obj) assert isinstance(obj, python_utils.UNICODE), ( 'Expected unicode, received %s' % obj) normalized_obj = obj elif schema[SCHEMA_KEY_TYPE] == SCHEMA_TYPE_OBJECT_DICT: # The schema type 'object_dict' accepts either of the keys # 'object_class' or 'validation_method'. # 'object_class' key is the most commonly used case, when the object is # initialized from_dict() method and the validation is done from # validate() method. # 'validation_method' key is used for some rare cases like if they have # validate_dict method instead of validate method, or if they need some # extra flags like for strict validation. The methods are written in the # domain_objects_validator file. if SCHEMA_KEY_OBJECT_CLASS in schema: validate_class = schema[SCHEMA_KEY_OBJECT_CLASS] domain_object = validate_class.from_dict(obj) domain_object.validate() else: validation_method = schema[SCHEMA_KEY_VALIDATION_METHOD] validation_method(obj) normalized_obj = obj else: raise Exception('Invalid schema type: %s' % schema[SCHEMA_KEY_TYPE]) if SCHEMA_KEY_CHOICES in schema: assert normalized_obj in schema[SCHEMA_KEY_CHOICES], ( 'Received %s which is not in the allowed range of choices: %s' % (normalized_obj, schema[SCHEMA_KEY_CHOICES])) # When type normalization is finished, apply the post-normalizers in the # given order. if SCHEMA_KEY_POST_NORMALIZERS in schema: for normalizer in schema[SCHEMA_KEY_POST_NORMALIZERS]: kwargs = dict(normalizer) del kwargs['id'] normalized_obj = Normalizers.get(normalizer['id'])( normalized_obj, **kwargs) # Validate the normalized object. if apply_custom_validators: if SCHEMA_KEY_VALIDATORS in schema: for validator in schema[SCHEMA_KEY_VALIDATORS]: kwargs = dict(validator) del kwargs['id'] assert get_validator( validator['id'])(normalized_obj, **kwargs), ( 'Validation failed: %s (%s) for object %s' % ( validator['id'], kwargs, normalized_obj)) if global_validators is not None: for validator in global_validators: kwargs = dict(validator) del kwargs['id'] assert get_validator( validator['id'])(normalized_obj, **kwargs), ( 'Validation failed: %s (%s) for object %s' % ( validator['id'], kwargs, normalized_obj)) return normalized_obj
def _send_email( recipient_id, sender_id, intent, email_subject, email_html_body, sender_email, bcc_admin=False, sender_name=None, reply_to_id=None): """Sends an email to the given recipient. This function should be used for sending all user-facing emails. Raises an Exception if the sender_id is not appropriate for the given intent. Currently we support only system-generated emails and emails initiated by moderator actions. Args: recipient_id: str. The user ID of the recipient. sender_id: str. The user ID of the sender. intent: str. The intent string for the email, i.e. the purpose/type. email_subject: str. The subject of the email. email_html_body: str. The body (message) of the email. sender_email: str. The sender's email address. bcc_admin: bool. Whether to send a copy of the email to the admin's email address. sender_name: str or None. The name to be shown in the "sender" field of the email. reply_to_id: str or None. The unique reply-to id used in reply-to email address sent to recipient. """ if sender_name is None: sender_name = EMAIL_SENDER_NAME.value _require_sender_id_is_valid(intent, sender_id) recipient_email = user_services.get_email_from_user_id(recipient_id) cleaned_html_body = html_cleaner.clean(email_html_body) if cleaned_html_body != email_html_body: log_new_error( 'Original email HTML body does not match cleaned HTML body:\n' 'Original:\n%s\n\nCleaned:\n%s\n' % (email_html_body, cleaned_html_body)) return raw_plaintext_body = cleaned_html_body.replace('<br/>', '\n').replace( '<br>', '\n').replace('<li>', '<li>- ').replace('</p><p>', '</p>\n<p>') cleaned_plaintext_body = html_cleaner.strip_html_tags(raw_plaintext_body) if email_models.SentEmailModel.check_duplicate_message( recipient_id, email_subject, cleaned_plaintext_body): log_new_error( 'Duplicate email:\n' 'Details:\n%s %s\n%s\n\n' % (recipient_id, email_subject, cleaned_plaintext_body)) return def _send_email_in_transaction(): sender_name_email = '%s <%s>' % (sender_name, sender_email) email_services.send_mail( sender_name_email, recipient_email, email_subject, cleaned_plaintext_body, cleaned_html_body, bcc_admin, reply_to_id=reply_to_id) email_models.SentEmailModel.create( recipient_id, recipient_email, sender_id, sender_name_email, intent, email_subject, cleaned_html_body, datetime.datetime.utcnow()) return transaction_services.run_in_transaction(_send_email_in_transaction)
def normalize_against_schema(obj, schema, apply_custom_validators=True): """Validate the given object using the schema, normalizing if necessary. Args: obj: *. The object to validate and normalize. schema: dict(str, *). The schema to validate and normalize the value against. apply_custom_validators: bool. Whether to validate the normalized object using the validators defined in the schema. Returns: *. The normalized object. Raises: AssertionError: The object fails to validate against the schema. """ normalized_obj = None if schema[SCHEMA_KEY_TYPE] == SCHEMA_TYPE_BOOL: assert isinstance(obj, bool), ('Expected bool, received %s' % obj) normalized_obj = obj elif schema[SCHEMA_KEY_TYPE] == SCHEMA_TYPE_CUSTOM: # Importing this at the top of the file causes a circular dependency. # TODO(sll): Either get rid of custom objects or find a way to merge # them into the schema framework -- probably the latter. from core.domain import obj_services obj_class = obj_services.Registry.get_object_class_by_type( schema[SCHEMA_KEY_OBJ_TYPE]) if not apply_custom_validators: normalized_obj = normalize_against_schema( obj, obj_class.SCHEMA, apply_custom_validators=False) else: normalized_obj = obj_class.normalize(obj) elif schema[SCHEMA_KEY_TYPE] == SCHEMA_TYPE_DICT: assert isinstance(obj, dict), ('Expected dict, received %s' % obj) expected_dict_keys = [ p[SCHEMA_KEY_NAME] for p in schema[SCHEMA_KEY_PROPERTIES] ] assert set(obj.keys()) == set(expected_dict_keys), ( 'Missing keys: %s, Extra keys: %s' % (list(set(expected_dict_keys) - set(obj.keys())), list(set(obj.keys()) - set(expected_dict_keys)))) normalized_obj = {} for prop in schema[SCHEMA_KEY_PROPERTIES]: key = prop[SCHEMA_KEY_NAME] normalized_obj[key] = normalize_against_schema( obj[key], prop[SCHEMA_KEY_SCHEMA]) elif schema[SCHEMA_KEY_TYPE] == SCHEMA_TYPE_FLOAT: obj = float(obj) assert isinstance(obj, numbers.Real), ('Expected float, received %s' % obj) normalized_obj = obj elif schema[SCHEMA_KEY_TYPE] == SCHEMA_TYPE_INT: obj = int(obj) assert isinstance( obj, numbers.Integral), ('Expected int, received %s' % obj) assert isinstance(obj, int), ('Expected int, received %s' % obj) normalized_obj = obj elif schema[SCHEMA_KEY_TYPE] == SCHEMA_TYPE_HTML: assert isinstance(obj, python_utils.BASESTRING), ( 'Expected unicode HTML string, received %s' % obj) if isinstance(obj, bytes): obj = obj.decode('utf-8') else: obj = python_utils.UNICODE(obj) assert isinstance( obj, python_utils.UNICODE), ('Expected unicode, received %s' % obj) normalized_obj = html_cleaner.clean(obj) elif schema[SCHEMA_KEY_TYPE] == SCHEMA_TYPE_LIST: assert isinstance(obj, list), ('Expected list, received %s' % obj) item_schema = schema[SCHEMA_KEY_ITEMS] if SCHEMA_KEY_LEN in schema: assert len(obj) == schema[SCHEMA_KEY_LEN] normalized_obj = [ normalize_against_schema(item, item_schema) for item in obj ] elif schema[SCHEMA_KEY_TYPE] == SCHEMA_TYPE_UNICODE: assert isinstance( obj, python_utils.BASESTRING), ('Expected unicode string, received %s' % obj) if isinstance(obj, bytes): obj = obj.decode('utf-8') else: obj = python_utils.UNICODE(obj) assert isinstance( obj, python_utils.UNICODE), ('Expected unicode, received %s' % obj) normalized_obj = obj else: raise Exception('Invalid schema type: %s' % schema[SCHEMA_KEY_TYPE]) if SCHEMA_KEY_CHOICES in schema: assert normalized_obj in schema[SCHEMA_KEY_CHOICES], ( 'Received %s which is not in the allowed range of choices: %s' % (normalized_obj, schema[SCHEMA_KEY_CHOICES])) # When type normalization is finished, apply the post-normalizers in the # given order. if SCHEMA_KEY_POST_NORMALIZERS in schema: for normalizer in schema[SCHEMA_KEY_POST_NORMALIZERS]: kwargs = dict(normalizer) del kwargs['id'] normalized_obj = Normalizers.get(normalizer['id'])(normalized_obj, **kwargs) # Validate the normalized object. if apply_custom_validators: if SCHEMA_KEY_VALIDATORS in schema: for validator in schema[SCHEMA_KEY_VALIDATORS]: kwargs = dict(validator) del kwargs['id'] assert get_validator(validator['id'])( normalized_obj, **kwargs), ('Validation failed: %s (%s) for object %s' % (validator['id'], kwargs, normalized_obj)) return normalized_obj
def normalize_against_schema(obj, schema): """Validate the given object using the schema, normalizing if necessary. Returns: the normalized object. Raises: AssertionError: if the object fails to validate against the schema. """ normalized_obj = None if schema[SCHEMA_KEY_TYPE] == SCHEMA_TYPE_BOOL: assert isinstance(obj, bool), ('Expected bool, received %s' % obj) normalized_obj = obj elif schema[SCHEMA_KEY_TYPE] == SCHEMA_TYPE_DICT: assert isinstance(obj, dict), ('Expected dict, received %s' % obj) assert set(obj.keys()) == set(schema[SCHEMA_KEY_PROPERTIES].keys()) normalized_obj = { key: normalize_against_schema(obj[key], schema[SCHEMA_KEY_PROPERTIES][key]) for key in schema[SCHEMA_KEY_PROPERTIES] } elif schema[SCHEMA_KEY_TYPE] == SCHEMA_TYPE_FLOAT: obj = float(obj) assert isinstance(obj, numbers.Real), ('Expected float, received %s' % obj) normalized_obj = obj elif schema[SCHEMA_KEY_TYPE] == SCHEMA_TYPE_INT: obj = int(obj) assert isinstance( obj, numbers.Integral), ('Expected int, received %s' % obj) assert isinstance(obj, int), ('Expected int, received %s' % obj) normalized_obj = obj elif schema[SCHEMA_KEY_TYPE] == SCHEMA_TYPE_HTML: assert isinstance( obj, basestring), ('Expected unicode HTML string, received %s' % obj) obj = unicode(obj) assert isinstance(obj, unicode), ('Expected unicode, received %s' % obj) normalized_obj = html_cleaner.clean(obj) elif schema[SCHEMA_KEY_TYPE] == SCHEMA_TYPE_LIST: assert isinstance(obj, list), ('Expected list, received %s' % obj) item_schema = schema[SCHEMA_KEY_ITEMS] if SCHEMA_KEY_LENGTH in schema: assert len(obj) == schema[SCHEMA_KEY_LENGTH] normalized_obj = [ normalize_against_schema(item, item_schema) for item in obj ] elif schema[SCHEMA_KEY_TYPE] == SCHEMA_TYPE_UNICODE: assert isinstance( obj, basestring), ('Expected unicode string, received %s' % obj) obj = unicode(obj) assert isinstance(obj, unicode), ('Expected unicode, received %s' % obj) normalized_obj = obj else: raise Exception('Invalid schema type: %s' % schema[SCHEMA_KEY_TYPE]) # When type normalization is finished, apply the post-normalizers in the # given order. if SCHEMA_KEY_POST_NORMALIZERS in schema: for normalizer in schema[SCHEMA_KEY_POST_NORMALIZERS]: kwargs = dict(normalizer) del kwargs['id'] normalized_obj = Normalizers.get(normalizer['id'])(normalized_obj, **kwargs) return normalized_obj
def from_yaml(cls, exploration_id, title, category, yaml_content): """Creates and returns exploration from a YAML text string.""" exploration_dict = utils.dict_from_yaml(yaml_content) exploration_schema_version = exploration_dict.get('schema_version') if exploration_schema_version is None: raise Exception('Invalid YAML file: no schema version specified.') if not (1 <= exploration_schema_version <= cls.CURRENT_EXPLORATION_SCHEMA_VERSION): raise Exception( 'Sorry, we can only process v1 and v2 YAML files at present.') if exploration_schema_version == 1: exploration_dict = cls._convert_v1_dict_to_v2_dict( exploration_dict) exploration = cls.create_default_exploration( exploration_id, title, category) exploration.param_specs = { ps_name: param_domain.ParamSpec.from_dict(ps_val) for (ps_name, ps_val) in exploration_dict['param_specs'].iteritems() } init_state_name = exploration_dict['init_state_name'] exploration.rename_state(exploration.init_state_name, init_state_name) exploration.add_states([ state_name for state_name in exploration_dict['states'] if state_name != init_state_name]) for (state_name, sdict) in exploration_dict['states'].iteritems(): state = exploration.states[state_name] state.content = [ Content(item['type'], html_cleaner.clean(item['value'])) for item in sdict['content'] ] state.param_changes = [param_domain.ParamChange( pc['name'], pc['generator_id'], pc['customization_args'] ) for pc in sdict['param_changes']] for pc in state.param_changes: if pc.name not in exploration.param_specs: raise Exception('Parameter %s was used in a state but not ' 'declared in the exploration param_specs.' % pc.name) wdict = sdict['widget'] widget_handlers = [AnswerHandlerInstance.from_dict({ 'name': handler['name'], 'rule_specs': [{ 'definition': rule_spec['definition'], 'dest': rule_spec['dest'], 'feedback': [html_cleaner.clean(feedback) for feedback in rule_spec['feedback']], 'param_changes': rule_spec.get('param_changes', []), } for rule_spec in handler['rule_specs']], }) for handler in wdict['handlers']] state.widget = WidgetInstance( wdict['widget_id'], wdict['customization_args'], widget_handlers, wdict['sticky']) exploration.states[state_name] = state exploration.default_skin = exploration_dict['default_skin'] exploration.param_changes = [ param_domain.ParamChange.from_dict(pc) for pc in exploration_dict['param_changes']] return exploration
def update_widget_handlers(self, widget_handlers_dict): if not isinstance(widget_handlers_dict, dict): raise Exception( 'Expected widget_handlers to be a dictionary, received %s' % widget_handlers_dict) ruleset = widget_handlers_dict['submit'] if not isinstance(ruleset, list): raise Exception( 'Expected widget_handlers[submit] to be a list, received %s' % ruleset) widget_handlers = [AnswerHandlerInstance('submit', [])] generic_widget = widget_registry.Registry.get_widget_by_id( 'interactive', self.widget.widget_id) # TODO(yanamal): Do additional calculations here to get the # parameter changes, if necessary. for rule_ind in range(len(ruleset)): rule_dict = ruleset[rule_ind] rule_dict['feedback'] = [html_cleaner.clean(feedback) for feedback in rule_dict['feedback']] if 'param_changes' not in rule_dict: rule_dict['param_changes'] = [] rule_spec = RuleSpec.from_dict(rule_dict) rule_type = rule_spec.definition['rule_type'] if rule_ind == len(ruleset) - 1: if rule_type != rule_domain.DEFAULT_RULE_TYPE: raise ValueError( 'Invalid ruleset %s: the last rule should be a ' 'default rule' % rule_dict) else: if rule_type == rule_domain.DEFAULT_RULE_TYPE: raise ValueError( 'Invalid ruleset %s: rules other than the ' 'last one should not be default rules.' % rule_dict) # TODO(sll): Generalize this to Boolean combinations of rules. matched_rule = generic_widget.get_rule_by_name( 'submit', rule_spec.definition['name']) # Normalize and store the rule params. # TODO(sll): Generalize this to Boolean combinations of rules. rule_inputs = rule_spec.definition['inputs'] if not isinstance(rule_inputs, dict): raise Exception( 'Expected rule_inputs to be a dict, received %s' % rule_inputs) for param_name, value in rule_inputs.iteritems(): param_type = rule_domain.get_obj_type_for_param_name( matched_rule, param_name) if (isinstance(value, basestring) and '{{' in value and '}}' in value): # TODO(jacobdavis11): Create checks that all parameters # referred to exist and have the correct types normalized_param = value else: try: normalized_param = param_type.normalize(value) except TypeError: raise Exception( '%s has the wrong type. It should be a %s.' % (value, param_type.__name__)) rule_inputs[param_name] = normalized_param widget_handlers[0].rule_specs.append(rule_spec) self.widget.handlers = widget_handlers
def normalize_against_schema(obj, schema, apply_custom_validators=True): """Validate the given object using the schema, normalizing if necessary. Returns: the normalized object. Raises: AssertionError: if the object fails to validate against the schema. """ normalized_obj = None if schema[SCHEMA_KEY_TYPE] == SCHEMA_TYPE_BOOL: assert isinstance(obj, bool), ('Expected bool, received %s' % obj) normalized_obj = obj elif schema[SCHEMA_KEY_TYPE] == SCHEMA_TYPE_CUSTOM: # Importing this at the top of the file causes a circular dependency. # TODO(sll): Either get rid of custom objects or find a way to merge # them into the schema framework -- probably the latter. from core.domain import obj_services # pylint: disable=relative-import obj_class = obj_services.Registry.get_object_class_by_type( schema[SCHEMA_KEY_OBJ_TYPE]) normalized_obj = obj_class.normalize(obj) elif schema[SCHEMA_KEY_TYPE] == SCHEMA_TYPE_DICT: assert isinstance(obj, dict), ('Expected dict, received %s' % obj) expected_dict_keys = [ p[SCHEMA_KEY_NAME] for p in schema[SCHEMA_KEY_PROPERTIES]] assert set(obj.keys()) == set(expected_dict_keys) normalized_obj = {} for prop in schema[SCHEMA_KEY_PROPERTIES]: key = prop[SCHEMA_KEY_NAME] normalized_obj[key] = normalize_against_schema( obj[key], prop[SCHEMA_KEY_SCHEMA]) elif schema[SCHEMA_KEY_TYPE] == SCHEMA_TYPE_FLOAT: obj = float(obj) assert isinstance(obj, numbers.Real), ( 'Expected float, received %s' % obj) normalized_obj = obj elif schema[SCHEMA_KEY_TYPE] == SCHEMA_TYPE_INT: obj = int(obj) assert isinstance(obj, numbers.Integral), ( 'Expected int, received %s' % obj) assert isinstance(obj, int), ('Expected int, received %s' % obj) normalized_obj = obj elif schema[SCHEMA_KEY_TYPE] == SCHEMA_TYPE_HTML: assert isinstance(obj, basestring), ( 'Expected unicode HTML string, received %s' % obj) obj = unicode(obj) assert isinstance(obj, unicode), ( 'Expected unicode, received %s' % obj) normalized_obj = html_cleaner.clean(obj) elif schema[SCHEMA_KEY_TYPE] == SCHEMA_TYPE_LIST: assert isinstance(obj, list), ('Expected list, received %s' % obj) item_schema = schema[SCHEMA_KEY_ITEMS] if SCHEMA_KEY_LEN in schema: assert len(obj) == schema[SCHEMA_KEY_LEN] normalized_obj = [ normalize_against_schema(item, item_schema) for item in obj ] elif schema[SCHEMA_KEY_TYPE] == SCHEMA_TYPE_UNICODE: assert isinstance(obj, basestring), ( 'Expected unicode string, received %s' % obj) obj = unicode(obj) assert isinstance(obj, unicode), ( 'Expected unicode, received %s' % obj) normalized_obj = obj else: raise Exception('Invalid schema type: %s' % schema[SCHEMA_KEY_TYPE]) if SCHEMA_KEY_CHOICES in schema: assert normalized_obj in schema[SCHEMA_KEY_CHOICES], ( 'Received %s which is not in the allowed range of choices: %s' % (normalized_obj, schema[SCHEMA_KEY_CHOICES])) # When type normalization is finished, apply the post-normalizers in the # given order. if SCHEMA_KEY_POST_NORMALIZERS in schema: for normalizer in schema[SCHEMA_KEY_POST_NORMALIZERS]: kwargs = dict(normalizer) del kwargs['id'] normalized_obj = Normalizers.get(normalizer['id'])( normalized_obj, **kwargs) # Validate the normalized object. if apply_custom_validators: if SCHEMA_KEY_VALIDATORS in schema: for validator in schema[SCHEMA_KEY_VALIDATORS]: kwargs = dict(validator) del kwargs['id'] assert _Validators.get( validator['id'])(normalized_obj, **kwargs), ( 'Validation failed: %s (%s) for object %s' % ( validator['id'], kwargs, normalized_obj)) return normalized_obj
def _send_email(recipient_id, sender_id, intent, email_subject, email_html_body, sender_email, bcc_admin=False, sender_name=None, reply_to_id=None): """Sends an email to the given recipient. This function should be used for sending all user-facing emails. Raises an Exception if the sender_id is not appropriate for the given intent. Currently we support only system-generated emails and emails initiated by moderator actions. Args: recipient_id: str. The user ID of the recipient. sender_id: str. The user ID of the sender. intent: str. The intent string for the email, i.e. the purpose/type. email_subject: str. The subject of the email. email_html_body: str. The body (message) of the email. sender_email: str. The sender's email address. bcc_admin: bool. Whether to send a copy of the email to the admin's email address. sender_name: str or None. The name to be shown in the "sender" field of the email. reply_to_id: str or None. The unique reply-to id used in reply-to email address sent to recipient. """ if sender_name is None: sender_name = EMAIL_SENDER_NAME.value _require_sender_id_is_valid(intent, sender_id) recipient_email = user_services.get_email_from_user_id(recipient_id) cleaned_html_body = html_cleaner.clean(email_html_body) if cleaned_html_body != email_html_body: log_new_error( 'Original email HTML body does not match cleaned HTML body:\n' 'Original:\n%s\n\nCleaned:\n%s\n' % (email_html_body, cleaned_html_body)) return raw_plaintext_body = cleaned_html_body.replace('<br/>', '\n').replace( '<br>', '\n').replace('<li>', '<li>- ').replace('</p><p>', '</p>\n<p>') cleaned_plaintext_body = html_cleaner.strip_html_tags(raw_plaintext_body) if email_models.SentEmailModel.check_duplicate_message( recipient_id, email_subject, cleaned_plaintext_body): log_new_error('Duplicate email:\n' 'Details:\n%s %s\n%s\n\n' % (recipient_id, email_subject, cleaned_plaintext_body)) return def _send_email_in_transaction(): """Sends the email to a single recipient.""" sender_name_email = '%s <%s>' % (sender_name, sender_email) email_services.send_mail(sender_name_email, recipient_email, email_subject, cleaned_plaintext_body, cleaned_html_body, bcc_admin, reply_to_id=reply_to_id) email_models.SentEmailModel.create(recipient_id, recipient_email, sender_id, sender_name_email, intent, email_subject, cleaned_html_body, datetime.datetime.utcnow()) transaction_services.run_in_transaction(_send_email_in_transaction)