diff --git a/.eslintignore b/.eslintignore
index 5d171a3075d7..462c8d4c0a34 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1,4 +1,5 @@
core/templates/dev/head/expressions/ExpressionParserService.js
+core/templates/dev/head/google-analytics.initializer.ts
backend_prod_files/*
core/tests/protractor.conf.js
extensions/interactions/LogicProof/static/js/generatedDefaultData.ts
diff --git a/.eslintrc b/.eslintrc
index 2a39be6f84aa..ab5cdb333ac3 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -149,6 +149,7 @@
"no-multi-str": [
"error"
],
+ "no-prototype-builtins": "off",
"no-redeclare": [
"off"
],
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index a374b989dc86..0e378ef66937 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -212,7 +212,6 @@
/core/templates/dev/head/domain/objects/ @kevinlee12
/core/templates/dev/head/expressions/ @seanlip
/core/templates/dev/head/services/CodeNormalizerService*.ts @kevinlee12
-/extensions/dependencies/ @seanlip @vojtechjelinek
/extensions/domain*.py @seanlip @DubeySandeep
/extensions/interactions/ @kevinlee12 @vojtechjelinek
/extensions/objects/ @aks681
@@ -305,15 +304,18 @@
# Topic project.
# Instead of * we have used _* to avoid topics_and_skills_dashboard related files.
+/core/controllers/classroom*.py @aks681
/core/controllers/topic_*.py @aks681
/core/domain/subtopic_page_domain*.py @aks681
/core/domain/subtopic_page_services*.py @aks681
/core/domain/topic*.py @aks681
/core/storage/topic/ @aks681
/core/templates/dev/head/components/entity-creation-services/topic-creation.service.ts.ts @aks681
+/core/templates/dev/head/domain/classroom/ @aks681
/core/templates/dev/head/domain/subtopic_viewer/ @aks681
/core/templates/dev/head/domain/topic/ @aks681
/core/templates/dev/head/domain/topic_viewer @aks681
+/core/templates/dev/head/pages/classroom-page/ @aks681
/core/templates/dev/head/pages/subtopic-viewer-page/ @aks681
/core/templates/dev/head/pages/topic-editor-page/ @aks681
/core/templates/dev/head/pages/topic-viewer-page/ @aks681
@@ -333,7 +335,7 @@
/main.py @nithusha21 @DubeySandeep
/feconf.py @seanlip @nithusha21
/constants*.py @seanlip @nithusha21
-/assets/constants.js @seanlip @nithusha21
+/assets/constants.ts @seanlip @nithusha21
/core/controllers/incoming_emails*.py @aks681
/core/controllers/tasks*.py @aks681
/core/domain/email*.py @aks681
@@ -384,7 +386,6 @@
# Rich text editor team.
/core/templates/dev/head/components/ck-editor-helpers/ck-editor-4-rte.directive.ts @aks681
-/core/templates/dev/head/components/ck-editor-helpers/ck-editor-5-rte.directive.ts @aks681 @NishealJ
/core/templates/dev/head/directives/mathjax-bind.directive.ts @aks681
/core/templates/dev/head/mathjaxConfig.ts @aks681
/core/templates/dev/head/components/ck-editor-helpers/ck-editor-4-widgets.initializer.ts @aks681
@@ -393,7 +394,7 @@
/core/domain/rte_component_registry*.py @vojtechjelinek
/extensions/ckeditor_plugins/ @vojtechjelinek
/extensions/rich_text_components/ @vojtechjelinek
-/assets/rich_text_components_definitions.js @vojtechjelinek
+/assets/rich_text_components_definitions.ts @vojtechjelinek
# Suggestion and feedback team.
@@ -433,9 +434,9 @@
# Speed Improvement team.
/app_dev.yaml @vojtechjelinek
+/core/templates/dev/head/google-analytics.initializer.ts @vojtechjelinek
/core/templates/dev/head/base_components/ @jamesjay4199 @vojtechjelinek
/core/templates/dev/head/pages/Base.ts @vojtechjelinek
-/core/templates/dev/head/pages/base.html @vojtechjelinek
/core/templates/dev/head/pages/oppia_footer_directive.html @vojtechjelinek
/core/templates/dev/head/pages/OppiaFooterDirective.ts @vojtechjelinek
/core/templates/dev/head/pages/footer_js_libs.html @vojtechjelinek
@@ -444,7 +445,7 @@
/core/templates/dev/head/services/CsrfTokenService*.ts @jamesjay4199 @vojtechjelinek
/gulpfile.js @vojtechjelinek
/jinja_utils*.py @vojtechjelinek
-/webpack.config.ts @vojtechjelinek
+/webpack.common.config.ts @vojtechjelinek
/webpack.dev.config.ts @vojtechjelinek
/webpack.prod.config.ts @vojtechjelinek
@@ -478,7 +479,6 @@
# in scripts/pre_commit_linter.py in sync with the modifications.
/core/controllers/acl_decorators*.py @seanlip
/core/controllers/base*.py @seanlip
-/core/domain/dependency_registry*.py @seanlip
/core/domain/html*.py @seanlip
/core/domain/rights_manager*.py @seanlip
/core/domain/role_services*.py @seanlip
diff --git a/.gitignore b/.gitignore
index 875fe6cf6ab9..d1d6a5a36f5d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,12 +7,12 @@ gae_runtime/*
# ignore the files and dirs inside the .git dir.
.git/*
third_party/*
-assets/hashes.js
+assets/hashes.json
backend_prod_files/*
local_compiled_js/*
local_compiled_js_for_test/*
core/templates/prod/*
-core/templates/dev/head/dist/*
+webpack_bundles/*
core/tests/.browserstack.env
node_modules/*
.coverage*
diff --git a/.travis.yml b/.travis.yml
index dd48769a1c5e..38240ff17d1e 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -27,9 +27,11 @@ env:
- RUN_E2E_TESTS_ADDITIONAL_EDITOR_AND_PLAYER_FEATURES=true
- RUN_E2E_TESTS_COLLECTIONS=true
- RUN_E2E_TESTS_CORE_EDITOR_AND_PLAYER_FEATURES=true
+ - RUN_E2E_TESTS_CREATOR_DASHBOARD=true
- RUN_E2E_TESTS_EMBEDDING=true
- RUN_E2E_TESTS_EXPLORATION_FEEDBACK_TAB=true
- RUN_E2E_TESTS_EXPLORATION_HISTORY_TAB=true
+ - RUN_E2E_TESTS_EXPLORATION_IMPROVEMENTS_TAB=true
- RUN_E2E_TESTS_EXPLORATION_STATISTICS_TAB=true
- RUN_E2E_TESTS_EXPLORATION_TRANSLATION_TAB=true
- RUN_E2E_TESTS_EXTENSIONS=true
@@ -105,9 +107,11 @@ script:
- if [ "$RUN_E2E_TESTS_ADDITIONAL_EDITOR_AND_PLAYER_FEATURES" == 'true' ]; then travis_retry bash scripts/run_e2e_tests.sh --suite="additionalEditorAndPlayerFeatures" --prod_env; fi
- if [ "$RUN_E2E_TESTS_COLLECTIONS" == 'true' ]; then travis_retry bash scripts/run_e2e_tests.sh --suite="collections" --prod_env; fi
- if [ "$RUN_E2E_TESTS_CORE_EDITOR_AND_PLAYER_FEATURES" == 'true' ]; then travis_retry bash scripts/run_e2e_tests.sh --suite="coreEditorAndPlayerFeatures" --prod_env; fi
+- if [ "$RUN_E2E_TESTS_CREATOR_DASHBOARD" == 'true' ]; then travis_retry bash scripts/run_e2e_tests.sh --suite="creatorDashboard" --prod_env; fi
- if [ "$RUN_E2E_TESTS_EMBEDDING" == 'true' ]; then travis_retry bash scripts/run_e2e_tests.sh --suite="embedding" --prod_env; fi
- if [ "$RUN_E2E_TESTS_EXPLORATION_FEEDBACK_TAB" == 'true' ]; then travis_retry bash scripts/run_e2e_tests.sh --suite="explorationFeedbackTab" --prod_env; fi
- if [ "$RUN_E2E_TESTS_EXPLORATION_HISTORY_TAB" == 'true' ]; then travis_retry bash scripts/run_e2e_tests.sh --suite="explorationHistoryTab" --prod_env; fi
+- if [ "$RUN_E2E_TESTS_EXPLORATION_IMPROVEMENTS_TAB" == 'true' ]; then travis_retry bash scripts/run_e2e_tests.sh --suite="explorationImprovementsTab" --prod_env; fi
- if [ "$RUN_E2E_TESTS_EXPLORATION_STATISTICS_TAB" == 'true' ]; then travis_retry bash scripts/run_e2e_tests.sh --suite="explorationStatisticsTab" --prod_env; fi
- if [ "$RUN_E2E_TESTS_EXPLORATION_TRANSLATION_TAB" == 'true' ]; then travis_retry bash scripts/run_e2e_tests.sh --suite="explorationTranslationTab" --prod_env; fi
- if [ "$RUN_E2E_TESTS_EXTENSIONS" == 'true' ]; then travis_retry bash scripts/run_e2e_tests.sh --suite="extensions" --prod_env; fi
@@ -123,7 +127,6 @@ script:
- if [ "$RUN_E2E_TESTS_TOPICS_AND_SKILLS_DASHBOARD" == 'true' ]; then travis_retry bash scripts/run_e2e_tests.sh --suite="topicsAndSkillsDashboard" --prod_env; fi
- if [ "$RUN_E2E_TESTS_TOPIC_AND_STORY_EDITOR" == 'true' ]; then travis_retry bash scripts/run_e2e_tests.sh --suite="topicAndStoryEditor" --prod_env; fi
- if [ "$RUN_E2E_TESTS_USERS" == 'true' ]; then travis_retry bash scripts/run_e2e_tests.sh --suite="users" --prod_env; fi
-
# These lines are commented out because these checks are being run on CircleCI
# here: https://circleci.com/gh/oppia/oppia
# after_success:
diff --git a/app_dev.yaml b/app_dev.yaml
index 9f30ebe65be3..f60a1b05734c 100644
--- a/app_dev.yaml
+++ b/app_dev.yaml
@@ -43,8 +43,8 @@ handlers:
expiration: "90d"
# DEVELOPMENT STATIC
-- url: /dist
- static_dir: core/templates/dev/head/dist
+- url: /webpack_bundles
+ static_dir: webpack_bundles
secure: always
application_readable: true
expiration: "0"
@@ -140,8 +140,8 @@ handlers:
# STATIC PAGES.
- url: /about
- static_files: core/templates/dev/head/dist/about-page.mainpage.html
- upload: core/templates/dev/head/dist/about-page.mainpage.html
+ static_files: webpack_bundles/about-page.mainpage.html
+ upload: webpack_bundles/about-page.mainpage.html
http_headers:
Pragma: no-cache
Strict-Transport-Security: "max-age=31536000; includeSubDomains"
@@ -151,8 +151,8 @@ handlers:
secure: always
expiration: "0"
- url: /contact
- static_files: core/templates/dev/head/dist/contact-page.mainpage.html
- upload: core/templates/dev/head/dist/contact-page.mainpage.html
+ static_files: webpack_bundles/contact-page.mainpage.html
+ upload: webpack_bundles/contact-page.mainpage.html
http_headers:
Pragma: no-cache
Strict-Transport-Security: "max-age=31536000; includeSubDomains"
@@ -162,8 +162,8 @@ handlers:
secure: always
expiration: "0"
- url: /donate
- static_files: core/templates/dev/head/dist/donate-page.mainpage.html
- upload: core/templates/dev/head/dist/donate-page.mainpage.html
+ static_files: webpack_bundles/donate-page.mainpage.html
+ upload: webpack_bundles/donate-page.mainpage.html
http_headers:
Pragma: no-cache
Strict-Transport-Security: "max-age=31536000; includeSubDomains"
@@ -173,8 +173,8 @@ handlers:
secure: always
expiration: "0"
- url: /get_started
- static_files: core/templates/dev/head/dist/get-started-page.mainpage.html
- upload: core/templates/dev/head/dist/get-started-page.mainpage.html
+ static_files: webpack_bundles/get-started-page.mainpage.html
+ upload: webpack_bundles/get-started-page.mainpage.html
http_headers:
Pragma: no-cache
Strict-Transport-Security: "max-age=31536000; includeSubDomains"
@@ -184,8 +184,8 @@ handlers:
secure: always
expiration: "0"
- url: /privacy
- static_files: core/templates/dev/head/dist/privacy-page.mainpage.html
- upload: core/templates/dev/head/dist/privacy-page.mainpage.html
+ static_files: webpack_bundles/privacy-page.mainpage.html
+ upload: webpack_bundles/privacy-page.mainpage.html
http_headers:
Pragma: no-cache
Strict-Transport-Security: "max-age=31536000; includeSubDomains"
@@ -195,8 +195,8 @@ handlers:
secure: always
expiration: "0"
- url: /splash
- static_files: core/templates/dev/head/dist/splash-page.mainpage.html
- upload: core/templates/dev/head/dist/splash-page.mainpage.html
+ static_files: webpack_bundles/splash-page.mainpage.html
+ upload: webpack_bundles/splash-page.mainpage.html
http_headers:
Pragma: no-cache
Strict-Transport-Security: "max-age=31536000; includeSubDomains"
@@ -206,8 +206,8 @@ handlers:
secure: always
expiration: "0"
- url: /teach
- static_files: core/templates/dev/head/dist/teach-page.mainpage.html
- upload: core/templates/dev/head/dist/teach-page.mainpage.html
+ static_files: webpack_bundles/teach-page.mainpage.html
+ upload: webpack_bundles/teach-page.mainpage.html
http_headers:
Pragma: no-cache
Strict-Transport-Security: "max-age=31536000; includeSubDomains"
@@ -217,8 +217,8 @@ handlers:
secure: always
expiration: "0"
- url: /terms
- static_files: core/templates/dev/head/dist/terms-page.mainpage.html
- upload: core/templates/dev/head/dist/terms-page.mainpage.html
+ static_files: webpack_bundles/terms-page.mainpage.html
+ upload: webpack_bundles/terms-page.mainpage.html
http_headers:
Pragma: no-cache
Strict-Transport-Security: "max-age=31536000; includeSubDomains"
@@ -228,8 +228,8 @@ handlers:
secure: always
expiration: "0"
- url: /thanks
- static_files: core/templates/dev/head/dist/thanks-page.mainpage.html
- upload: core/templates/dev/head/dist/thanks-page.mainpage.html
+ static_files: webpack_bundles/thanks-page.mainpage.html
+ upload: webpack_bundles/thanks-page.mainpage.html
http_headers:
Pragma: no-cache
Strict-Transport-Security: "max-age=31536000; includeSubDomains"
@@ -264,7 +264,7 @@ skip_files:
# Karma test files
- ^(.*/)Spec.js$
# Typescript files
-- ^(.*/)?.*\.ts$
+- ^core/(.*/)?.*\.ts$
# Typescript output log file
- ^(.*/)tsc_output_log.txt$
# Other folders to ignore
diff --git a/assets/constants.js b/assets/constants.ts
similarity index 98%
rename from assets/constants.js
rename to assets/constants.ts
index b01b7a6c1748..c8874d10bd7b 100644
--- a/assets/constants.js
+++ b/assets/constants.ts
@@ -13,14 +13,10 @@
* @fileoverview Initializes constants for the Oppia codebase.
*/
-var constants = {
+export = {
// Whether to allow custom event reporting to Google Analytics.
"CAN_SEND_ANALYTICS_EVENTS": false,
- // This specifies the current editor in use and used to switch
- // between CK4 & CK5.
- "CURRENT_RTE_IS_CKEDITOR_4": true,
-
"ALL_CATEGORIES": ["Algebra", "Algorithms", "Architecture", "Arithmetic",
"Art", "Astronomy", "Biology", "Business", "Calculus", "Chemistry",
"Combinatorics", "Computing", "Economics", "Education", "Engineering",
@@ -539,7 +535,7 @@ var constants = {
"ENABLE_NEW_STRUCTURE_PLAYERS": false,
- "ENABLE_SOLICIT_ANSWER_DETAILS_FEATURE": false,
+ "ENABLE_SOLICIT_ANSWER_DETAILS_FEATURE": true,
"MAX_SKILLS_PER_QUESTION": 3,
diff --git a/assets/i18n/en.json b/assets/i18n/en.json
index d3af3f4932ae..268876c859da 100644
--- a/assets/i18n/en.json
+++ b/assets/i18n/en.json
@@ -432,6 +432,16 @@
"I18N_PREFERENCES_USERNAME": "Username",
"I18N_PREFERENCES_USERNAME_NOT_SELECTED": "Not yet selected",
"I18N_PROFILE_NO_EXPLORATIONS": "This user hasn't created or edited any explorations yet.",
+ "I18N_QUESTION_PLAYER_BOOST_SCORE": "Boost Score",
+ "I18N_QUESTION_PLAYER_LEARN_MORE_ABOUT_SCORE": "Learn more about your score",
+ "I18N_QUESTION_PLAYER_MY_DASHBOARD": "My Dashboard",
+ "I18N_QUESTION_PLAYER_NEW_SESSION": "New Session",
+ "I18N_QUESTION_PLAYER_RETRY_TEST": "Retry Test",
+ "I18N_QUESTION_PLAYER_RETURN_TO_STORY": "Return To Story",
+ "I18N_QUESTION_PLAYER_SCORE": "Score",
+ "I18N_QUESTION_PLAYER_SKILL_DESCRIPTIONS": "Skill Descriptions",
+ "I18N_QUESTION_PLAYER_TEST_FAILED": "Test failed. Please review the skills and try again",
+ "I18N_QUESTION_PLAYER_TEST_PASSED": "Test complete. Well done!",
"I18N_REGISTRATION_SESSION_EXPIRED_HEADING": "Registration Session Expired",
"I18N_REGISTRATION_SESSION_EXPIRED_MESSAGE": "Sorry, your registration session has expired. Please click \"Continue Registration\" to restart the process.",
"I18N_SIDEBAR_ABOUT_LINK": "About",
@@ -474,6 +484,8 @@
"I18N_SIGNUP_USERNAME_EXPLANATION": "Your username will be shown next to your contributions.",
"I18N_SIGNUP_WAIVER_OBJECTIVE": "The waiver of the attribution (BY) requirement means that, if someone reuses this work, they are not required to attribute the authors. However, all of your individual contributions to explorations will be available on this site in the exploration change log, and people who reuse the exploration are encouraged (but not required) to include a link to this page.",
"I18N_SIGNUP_WHY_LICENSE": "Why CC-BY-SA?",
+ "I18N_SOLICIT_ANSWER_DETAILS_FEEDBACK": "Okay, now let's go back to your answer.",
+ "I18N_SOLICIT_ANSWER_DETAILS_QUESTION": "Could you explain why you picked this answer?",
"I18N_SPLASH_FIRST_EXPLORATION_DESCRIPTION": "Oppia's lessons, also known as explorations, provide more immersive experiences than static videos or text, helping users learn by doing.",
"I18N_SPLASH_JAVASCRIPT_ERROR_DESCRIPTION": "Oppia is a free, open-source learning platform full of interactive activities called 'explorations'. Sadly, Oppia requires JavaScript to be enabled in your web browser in order to function properly and your web browser has JavaScript disabled. If you need help enabling JavaScript, \">click here.",
"I18N_SPLASH_JAVASCRIPT_ERROR_THANKS": "Thank you.",
diff --git a/assets/i18n/qqq.json b/assets/i18n/qqq.json
index a7067ab99954..7bc21ad412d9 100644
--- a/assets/i18n/qqq.json
+++ b/assets/i18n/qqq.json
@@ -432,6 +432,16 @@
"I18N_PREFERENCES_USERNAME": "Text displayed in the preferences page. - Text shown at the left of the text entry where the user can change his username.\n{{Identical|Username}}",
"I18N_PREFERENCES_USERNAME_NOT_SELECTED": "Text displayed in the preferences page. - Text shown in the text entry for the username when there is no username assigned to the user.",
"I18N_PROFILE_NO_EXPLORATIONS": "Text displayed on the Profile page. - This message is shown on a user's profile when the user has zero created/edited explorations.",
+ "I18N_QUESTION_PLAYER_BOOST_SCORE": "Text displayed in Practice Session and Review Test pages. - Text of the button that opens up a modal showing a concept card of skills that the learner can improve upon.",
+ "I18N_QUESTION_PLAYER_LEARN_MORE_ABOUT_SCORE": "Text displayed in Practice Session and Review Test pages. - Text that is shown above the breakdown of the score that indicates users can learn more about their score below.",
+ "I18N_QUESTION_PLAYER_MY_DASHBOARD":"Text displayed in the Practice Session page. - Text of the button that go to the Topics Dashboard on click.",
+ "I18N_QUESTION_PLAYER_NEW_SESSION": "Text displayed in the Practice Session page. - Text of the button that starts a new practice session",
+ "I18N_QUESTION_PLAYER_RETRY_TEST": "Text displayed in Review Test page. - Text of the button that retries a review test.",
+ "I18N_QUESTION_PLAYER_RETURN_TO_STORY": "Text displayed in Review Test page. - Text of the button that redirects back to the Story page.",
+ "I18N_QUESTION_PLAYER_SCORE": "Text displayed in Practice Session and Review Test pages. - Text that is the heading of a column with scores.",
+ "I18N_QUESTION_PLAYER_SKILL_DESCRIPTIONS": "Text displayed in Practice Session and Review Test pages. - Text that is the heading of a column with skill descriptions.",
+ "I18N_QUESTION_PLAYER_TEST_FAILED": "Text displayed in Practice Session and Review Test pages. - Text that is shown above the score wheel when the user fails the test.",
+ "I18N_QUESTION_PLAYER_TEST_PASSED": "Text displayed in Practice Session and Review Test pages. - Text that is shown above the score wheel when the user passes the test.",
"I18N_REGISTRATION_SESSION_EXPIRED_HEADING": "Text which appears as the heading for registration session expired modal on Signup page.",
"I18N_REGISTRATION_SESSION_EXPIRED_MESSAGE": "Text which appears on registration session expired modal on Signup page - This message informs user that their registration session has expired and they need to click on Continue Registration to restart the registration.",
"I18N_SIDEBAR_ABOUT_LINK": "Text displayed in the side navigation bar. - When the user clicks the link, they are redirected to the about page.\n{{Identical|About}}",
@@ -474,6 +484,8 @@
"I18N_SIGNUP_USERNAME_EXPLANATION": "Text displayed next to an entry box in the signup page - Explains how the username selected in the box is going to be used inside the site.",
"I18N_SIGNUP_WAIVER_OBJECTIVE": "Text displayed inside a information dialog widget in the signup page. - It explains the waiver of attribution. See I18N_SIGNUP_WHY_LICENSE.",
"I18N_SIGNUP_WHY_LICENSE": "Text displayed inside a information dialog widget in the signup page. - It's the title of a dialog that contains additional information about why the creative commons licence was chosen for Oppia.",
+ "I18N_SOLICIT_ANSWER_DETAILS_FEEDBACK": "Text to introduce the regular feedback that Oppia shows after the student submits the answer details.",
+ "I18N_SOLICIT_ANSWER_DETAILS_QUESTION": "Text for the question which asks the students to explain why they entered a particular answer.",
"I18N_SPLASH_FIRST_EXPLORATION_DESCRIPTION": "Paragraph describing Oppia. - Shown in the splash page next to an image.",
"I18N_SPLASH_JAVASCRIPT_ERROR_DESCRIPTION": "Main content of an error page - The error is triggered when the user has disabled javascript in the browser. There is no space constraints for this paragraph and the priority is to clearly explain the user what caused the problem and how to solve it.",
"I18N_SPLASH_JAVASCRIPT_ERROR_THANKS": "Closing message of an error page - Thanks the user in a informal language.\n{{Identical|Thank you}}",
diff --git a/assets/rich_text_components_definitions.js b/assets/rich_text_components_definitions.ts
similarity index 99%
rename from assets/rich_text_components_definitions.js
rename to assets/rich_text_components_definitions.ts
index 4ba4498bdad8..ffaeb7086bda 100644
--- a/assets/rich_text_components_definitions.js
+++ b/assets/rich_text_components_definitions.ts
@@ -12,7 +12,7 @@
* @fileoverview Definitions for rich text components.
*/
-var richTextComponents = {
+export = {
"Collapsible": {
"backend_id": "Collapsible",
"category": "Basic Input",
diff --git a/constants.py b/constants.py
index 815bcb3cb86d..4e6ccd8dffef 100644
--- a/constants.py
+++ b/constants.py
@@ -48,5 +48,5 @@ class Constants(dict):
__getattr__ = dict.__getitem__
-with python_utils.open_file(os.path.join('assets', 'constants.js'), 'r') as f:
+with python_utils.open_file(os.path.join('assets', 'constants.ts'), 'r') as f:
constants = Constants(parse_json_from_js(f))
diff --git a/constants_test.py b/constants_test.py
index c634ab19e674..c68fa99cb585 100644
--- a/constants_test.py
+++ b/constants_test.py
@@ -28,12 +28,12 @@ class ConstantsTests(test_utils.GenericTestBase):
def test_constants_file_is_existing(self):
"""Test if the constants file is existing."""
self.assertTrue(os.path.isfile(os.path.join(
- 'assets', 'constants.js')))
+ 'assets', 'constants.ts')))
def test_constants_file_contains_valid_json(self):
"""Test if the constants file is valid json file."""
with python_utils.open_file(
- os.path.join('assets', 'constants.js'), 'r') as f:
+ os.path.join('assets', 'constants.ts'), 'r') as f:
json = constants.parse_json_from_js(f)
self.assertTrue(isinstance(json, dict))
self.assertEqual(json['TESTING_CONSTANT'], 'test')
diff --git a/core/controllers/acl_decorators.py b/core/controllers/acl_decorators.py
index b9bd2899f1bd..7f18d40acc0a 100644
--- a/core/controllers/acl_decorators.py
+++ b/core/controllers/acl_decorators.py
@@ -961,6 +961,7 @@ def test_can_edit(self, exploration_id, *args, **kwargs):
exploration_rights = rights_manager.get_exploration_rights(
exploration_id, strict=False)
+
if exploration_rights is None:
raise base.UserFacingExceptions.PageNotFoundException
@@ -1021,6 +1022,54 @@ def test_can_voiceover(self, exploration_id, **kwargs):
return test_can_voiceover
+def can_save_exploration(handler):
+ """Decorator to check whether user can save exploration.
+
+ Args:
+ handler: function. The function to be decorated.
+
+ Returns:
+ function. The newly decorated function that checks if
+ a user has permission to save a given exploration.
+ """
+
+ def test_can_save(self, exploration_id, **kwargs):
+ """Checks if the user can save the exploration.
+
+ Args:
+ exploration_id: str. The exploration id.
+ **kwargs: dict(str: *). Keyword arguments.
+
+ Returns:
+ *. The return value of the decorated function.
+
+ Raises:
+ NotLoggedInException: The user is not logged in.
+ PageNotFoundException: The page is not found.
+ UnauthorizedUserException: The user does not have
+ credentials to save changes to this exploration.
+ """
+
+ if not self.user_id:
+ raise base.UserFacingExceptions.NotLoggedInException
+
+ exploration_rights = rights_manager.get_exploration_rights(
+ exploration_id, strict=False)
+ if exploration_rights is None:
+ raise base.UserFacingExceptions.PageNotFoundException
+
+ if rights_manager.check_can_save_activity(
+ self.user, exploration_rights):
+ return handler(self, exploration_id, **kwargs)
+ else:
+ raise base.UserFacingExceptions.UnauthorizedUserException(
+ 'You do not have permissions to save this exploration.')
+
+ test_can_save.__wrapped__ = True
+
+ return test_can_save
+
+
def can_delete_exploration(handler):
"""Decorator to check whether user can delete exploration.
diff --git a/core/controllers/acl_decorators_test.py b/core/controllers/acl_decorators_test.py
index ce095f4eb550..eadf19c164e9 100644
--- a/core/controllers/acl_decorators_test.py
+++ b/core/controllers/acl_decorators_test.py
@@ -2948,3 +2948,120 @@ def test_cannot_edit_entity_invalid_entity(self):
with self.swap(self, 'testapp', self.mock_testapp):
self.get_json('/mock_edit_entity/%s/%s' % (
'invalid_entity_type', 'q_id'), expected_status_int=404)
+
+
+class SaveExplorationTests(test_utils.GenericTestBase):
+ """Tests for can_save_exploration decorator."""
+ role = rights_manager.ROLE_VOICE_ARTIST
+ username = 'user'
+ user_email = 'user@example.com'
+ banned_username = 'banneduser'
+ banned_user_email = 'banneduser@example.com'
+ published_exp_id_1 = 'exp_1'
+ published_exp_id_2 = 'exp_2'
+ private_exp_id_1 = 'exp_3'
+ private_exp_id_2 = 'exp_4'
+
+ class MockHandler(base.BaseHandler):
+ GET_HANDLER_ERROR_RETURN_TYPE = feconf.HANDLER_TYPE_JSON
+
+ @acl_decorators.can_save_exploration
+ def get(self, exploration_id):
+ self.render_json({'exploration_id': exploration_id})
+
+ def setUp(self):
+ super(SaveExplorationTests, self).setUp()
+ self.signup(self.OWNER_EMAIL, self.OWNER_USERNAME)
+ self.signup(self.MODERATOR_EMAIL, self.MODERATOR_USERNAME)
+ self.signup(self.ADMIN_EMAIL, self.ADMIN_USERNAME)
+ self.signup(self.user_email, self.username)
+ self.signup(self.banned_user_email, self.banned_username)
+ self.signup(self.VOICE_ARTIST_EMAIL, self.VOICE_ARTIST_USERNAME)
+ self.owner_id = self.get_user_id_from_email(self.OWNER_EMAIL)
+ self.voice_artist_id = self.get_user_id_from_email(
+ self.VOICE_ARTIST_EMAIL)
+ self.set_moderators([self.MODERATOR_USERNAME])
+ self.set_admins([self.ADMIN_USERNAME])
+ self.set_banned_users([self.banned_username])
+ self.owner = user_services.UserActionsInfo(self.owner_id)
+ self.mock_testapp = webtest.TestApp(webapp2.WSGIApplication(
+ [webapp2.Route('/mock/', self.MockHandler)],
+ debug=feconf.DEBUG,
+ ))
+ self.save_new_valid_exploration(
+ self.published_exp_id_1, self.owner_id)
+ self.save_new_valid_exploration(
+ self.published_exp_id_2, self.owner_id)
+ self.save_new_valid_exploration(
+ self.private_exp_id_1, self.owner_id)
+ self.save_new_valid_exploration(
+ self.private_exp_id_2, self.owner_id)
+ rights_manager.publish_exploration(self.owner, self.published_exp_id_1)
+ rights_manager.publish_exploration(self.owner, self.published_exp_id_2)
+
+ rights_manager.assign_role_for_exploration(
+ self.owner, self.published_exp_id_1, self.voice_artist_id,
+ self.role)
+ rights_manager.assign_role_for_exploration(
+ self.owner, self.private_exp_id_1, self.voice_artist_id, self.role)
+
+ def test_unautheticated_user_cannot_save_exploration(self):
+ with self.swap(self, 'testapp', self.mock_testapp):
+ self.get_json(
+ '/mock/%s' % self.private_exp_id_1, expected_status_int=401)
+
+ def test_can_not_save_exploration_with_invalid_exp_id(self):
+ self.login(self.OWNER_EMAIL)
+ with self.swap(self, 'testapp', self.mock_testapp):
+ self.get_json(
+ '/mock/invalid_exp_id', expected_status_int=404)
+ self.logout()
+
+ def test_banned_user_cannot_save_exploration(self):
+ self.login(self.banned_user_email)
+ with self.swap(self, 'testapp', self.mock_testapp):
+ self.get_json(
+ '/mock/%s' % self.private_exp_id_1, expected_status_int=401)
+ self.logout()
+
+ def test_owner_can_save_exploration(self):
+ self.login(self.OWNER_EMAIL)
+ with self.swap(self, 'testapp', self.mock_testapp):
+ response = self.get_json('/mock/%s' % self.private_exp_id_1)
+ self.assertEqual(response['exploration_id'], self.private_exp_id_1)
+ self.logout()
+
+ def test_moderator_can_save_public_exploration(self):
+ self.login(self.MODERATOR_EMAIL)
+ with self.swap(self, 'testapp', self.mock_testapp):
+ response = self.get_json('/mock/%s' % self.published_exp_id_1)
+ self.assertEqual(response['exploration_id'], self.published_exp_id_1)
+ self.logout()
+
+ def test_moderator_cannot_save_private_exploration(self):
+ self.login(self.MODERATOR_EMAIL)
+ with self.swap(self, 'testapp', self.mock_testapp):
+ self.get_json(
+ '/mock/%s' % self.private_exp_id_1, expected_status_int=401)
+ self.logout()
+
+ def test_admin_can_save_private_exploration(self):
+ self.login(self.ADMIN_EMAIL)
+ with self.swap(self, 'testapp', self.mock_testapp):
+ response = self.get_json('/mock/%s' % self.private_exp_id_1)
+ self.assertEqual(response['exploration_id'], self.private_exp_id_1)
+ self.logout()
+
+ def test_voice_artist_can_only_save_assigned_exploration(self):
+ self.login(self.VOICE_ARTIST_EMAIL)
+ # Checking voice artist can only save assigned public exploration.
+ with self.swap(self, 'testapp', self.mock_testapp):
+ response = self.get_json('/mock/%s' % self.published_exp_id_1)
+ self.assertEqual(response['exploration_id'], self.published_exp_id_1)
+
+ # Checking voice artist cannot save public exploration which he/she
+ # is not assigned for.
+ with self.swap(self, 'testapp', self.mock_testapp):
+ self.get_json(
+ '/mock/%s' % self.published_exp_id_2, expected_status_int=401)
+ self.logout()
diff --git a/core/controllers/admin.py b/core/controllers/admin.py
index 36eb0d18143b..c3f2954577c6 100644
--- a/core/controllers/admin.py
+++ b/core/controllers/admin.py
@@ -51,7 +51,7 @@ class AdminPage(base.BaseHandler):
def get(self):
"""Handles GET requests."""
- self.render_template('dist/admin-page.mainpage.html')
+ self.render_template('admin-page.mainpage.html')
class AdminHandler(base.BaseHandler):
diff --git a/core/controllers/base.py b/core/controllers/base.py
index f73bf64ba3ae..30320e01368d 100755
--- a/core/controllers/base.py
+++ b/core/controllers/base.py
@@ -177,7 +177,6 @@ def __init__(self, request, response): # pylint: disable=super-init-not-called
self.is_super_admin = (
current_user_services.is_current_user_super_admin())
- self.values['additional_angular_modules'] = []
self.values['iframed'] = False
self.values['is_moderator'] = user_services.is_at_least_moderator(
self.user_id)
@@ -362,10 +361,10 @@ def _render_exception_json_or_html(self, return_type, values):
self.values.update(values)
if 'iframed' in self.values and self.values['iframed']:
self.render_template(
- 'pages/error-pages/error-iframed.mainpage.html',
+ 'error-iframed.mainpage.html',
iframe_restriction=None)
else:
- self.render_template('dist/error-page.mainpage.html')
+ self.render_template('error-page.mainpage.html')
else:
if return_type != feconf.HANDLER_TYPE_JSON and (
return_type != feconf.HANDLER_TYPE_DOWNLOADABLE):
diff --git a/core/controllers/base_test.py b/core/controllers/base_test.py
index 9c95adf9c165..8cbade944931 100644
--- a/core/controllers/base_test.py
+++ b/core/controllers/base_test.py
@@ -527,10 +527,6 @@ class EscapingTests(test_utils.GenericTestBase):
class FakePage(base.BaseHandler):
"""Fake page for testing autoescaping."""
- def get(self):
- """Handles GET requests."""
- self.render_template('tests/jinja_escaping.html')
-
def post(self):
"""Handles POST requests."""
self.render_json({'big_value': u'\n