Repository: oppia/oppia

Issue:
[Donate Page] Unable to pay via PayPal or Credit Card
<!--
  - Thanks for taking the time to report a bug in the Oppia project.
  - Before filing a new issue, please do a quick search to check that it hasn't
  - already been filed on the [issue tracker](https://github.com/oppia/oppia/issues)._
  -->

**Describe the bug**
Unable to pay via PayPal or Credit Card. Upon clicking PayPal or Credit Card Button nothing happens

**To Reproduce**
Steps to reproduce the behavior:
 1. Go to https://www.oppia.org/donate
 2. Click on PayPal or Credit Card

**Observed behavior**
No response.

**Expected behavior**
Redirect to payment gateway.

**Screenshots**
If applicable, add screenshots to help explain your problem.
![image](https://user-images.githubusercontent.com/30050862/103414774-5a345c00-4ba5-11eb-977c-efeea51a84c9.png)

**Desktop (please complete the following information; delete this section if the issue does not arise on desktop):**
- OS: Windows 10
- Browser: Chrome
- Version: 87.0.4280.88



Discussion:

Resolving patch:
diff --git a/core/domain/skill_domain.py b/core/domain/skill_domain.py
--- a/core/domain/skill_domain.py
+++ b/core/domain/skill_domain.py
@@ -1510,14 +1510,14 @@ def to_dict(self):
 
 class AugmentedSkillSummary(python_utils.OBJECT):
     """Domain object for Augmented Skill Summary, which has all the properties
-    of SkillSummary along with the topic name to which the skill is assigned
-    and the classroom name to which the topic is assigned.
+    of SkillSummary along with the topic names to which the skill is assigned
+    and the classroom names to which the topics are assigned.
     """
 
     def __init__(
             self, skill_id, description, language_code, version,
-            misconception_count, worked_examples_count, topic_name,
-            classroom_name, skill_model_created_on, skill_model_last_updated):
+            misconception_count, worked_examples_count, topic_names,
+            classroom_names, skill_model_created_on, skill_model_last_updated):
         """Constructs an AugmentedSkillSummary domain object.
 
         Args:
@@ -1529,9 +1529,9 @@ def __init__(
                 with the skill.
             worked_examples_count: int. The number of worked examples in the
                 skill.
-            topic_name: str. The name of the topic to which the skill
+            topic_names: list(str). The names of the topics to which the skill
                 is assigned.
-            classroom_name: str. The name of the classroom to which the
+            classroom_names: list(str). The names of the classrooms to which the
                 skill is assigned.
             skill_model_created_on: datetime.datetime. Date and time when
                 the skill model is created.
@@ -1546,8 +1546,8 @@ def __init__(
         self.worked_examples_count = worked_examples_count
         self.skill_model_created_on = skill_model_created_on
         self.skill_model_last_updated = skill_model_last_updated
-        self.topic_name = topic_name
-        self.classroom_name = classroom_name
+        self.topic_names = topic_names
+        self.classroom_names = classroom_names
 
     def to_dict(self):
         """Returns a dictionary representation of this domain object.
@@ -1562,8 +1562,8 @@ def to_dict(self):
             'version': self.version,
             'misconception_count': self.misconception_count,
             'worked_examples_count': self.worked_examples_count,
-            'topic_name': self.topic_name,
-            'classroom_name': self.classroom_name,
+            'topic_names': self.topic_names,
+            'classroom_names': self.classroom_names,
             'skill_model_created_on': utils.get_time_in_millisecs(
                 self.skill_model_created_on),
             'skill_model_last_updated': utils.get_time_in_millisecs(
diff --git a/core/domain/skill_services.py b/core/domain/skill_services.py
--- a/core/domain/skill_services.py
+++ b/core/domain/skill_services.py
@@ -17,6 +17,7 @@
 from __future__ import absolute_import  # pylint: disable=import-only-modules
 from __future__ import unicode_literals  # pylint: disable=import-only-modules
 
+import collections
 import logging
 
 from constants import constants
@@ -173,7 +174,10 @@ def _get_augmented_skill_summaries_in_batches(
         _get_skill_summaries_in_batches(
             num_skills_to_fetch, urlsafe_start_cursor, sort_by))
 
-    assigned_skill_ids = {}
+    assigned_skill_ids = collections.defaultdict(lambda: {
+        'topic_names': [],
+        'classroom_names': []
+    })
 
     all_topic_models = topic_models.TopicModel.get_all()
     all_topics = [topic_fetchers.get_topic_from_model(topic_model)
@@ -189,19 +193,18 @@ def _get_augmented_skill_summaries_in_batches(
 
     for topic in all_topics:
         for skill_id in topic.get_all_skill_ids():
-            assigned_skill_ids[skill_id] = {
-                'topic_name': topic.name,
-                'classroom_name': topic_classroom_dict.get(topic.id, None)
-            }
+            assigned_skill_ids[skill_id]['topic_names'].append(topic.name)
+            assigned_skill_ids[skill_id]['classroom_names'].append(
+                topic_classroom_dict.get(topic.id, None))
 
     augmented_skill_summaries = []
     for skill_summary in skill_summaries:
-        topic_name = None
-        classroom_name = None
+        topic_names = []
+        classroom_names = []
         if skill_summary.id in assigned_skill_ids:
-            topic_name = assigned_skill_ids[skill_summary.id]['topic_name']
-            classroom_name = (
-                assigned_skill_ids[skill_summary.id]['classroom_name'])
+            topic_names = assigned_skill_ids[skill_summary.id]['topic_names']
+            classroom_names = (
+                assigned_skill_ids[skill_summary.id]['classroom_names'])
 
         augmented_skill_summary = skill_domain.AugmentedSkillSummary(
             skill_summary.id,
@@ -210,8 +213,8 @@ def _get_augmented_skill_summaries_in_batches(
             skill_summary.version,
             skill_summary.misconception_count,
             skill_summary.worked_examples_count,
-            topic_name,
-            classroom_name,
+            topic_names,
+            classroom_names,
             skill_summary.skill_model_created_on,
             skill_summary.skill_model_last_updated)
         augmented_skill_summaries.append(augmented_skill_summary)
@@ -238,7 +241,7 @@ def _filter_skills_by_status(augmented_skill_summaries, status):
     elif status == constants.SKILL_STATUS_OPTIONS['UNASSIGNED']:
         unassigned_augmented_skill_summaries = []
         for augmented_skill_summary in augmented_skill_summaries:
-            if augmented_skill_summary.topic_name is None:
+            if not augmented_skill_summary.topic_names:
                 unassigned_augmented_skill_summaries.append(
                     augmented_skill_summary)
 
@@ -247,7 +250,7 @@ def _filter_skills_by_status(augmented_skill_summaries, status):
     elif status == constants.SKILL_STATUS_OPTIONS['ASSIGNED']:
         assigned_augmented_skill_summaries = []
         for augmented_skill_summary in augmented_skill_summaries:
-            if augmented_skill_summary.topic_name is not None:
+            if augmented_skill_summary.topic_names:
                 assigned_augmented_skill_summaries.append(
                     augmented_skill_summary)
         return assigned_augmented_skill_summaries
@@ -272,7 +275,7 @@ def _filter_skills_by_classroom(augmented_skill_summaries, classroom_name):
 
     augmented_skill_summaries_with_classroom_name = []
     for augmented_skill_summary in augmented_skill_summaries:
-        if augmented_skill_summary.classroom_name == classroom_name:
+        if classroom_name in augmented_skill_summary.classroom_names:
             augmented_skill_summaries_with_classroom_name.append(
                 augmented_skill_summary)
 


Test patch:
diff --git a/core/domain/skill_domain_test.py b/core/domain/skill_domain_test.py
--- a/core/domain/skill_domain_test.py
+++ b/core/domain/skill_domain_test.py
@@ -810,7 +810,7 @@ def setUp(self):
 
         self.augmented_skill_summary = skill_domain.AugmentedSkillSummary(
             'skill_id', 'description', 'en', 1, 1, 1,
-            'topic1', 'math', current_time, current_time)
+            ['topic1'], ['math'], current_time, current_time)
 
     def test_augmented_skill_summary_gets_created(self):
         augmented_skill_summary_dict = {
@@ -820,8 +820,8 @@ def test_augmented_skill_summary_gets_created(self):
             'version': 1,
             'misconception_count': 1,
             'worked_examples_count': 1,
-            'topic_name': 'topic1',
-            'classroom_name': 'math',
+            'topic_names': ['topic1'],
+            'classroom_names': ['math'],
             'skill_model_created_on': self.time_in_millisecs,
             'skill_model_last_updated': self.time_in_millisecs
         }
diff --git a/core/domain/skill_services_test.py b/core/domain/skill_services_test.py
--- a/core/domain/skill_services_test.py
+++ b/core/domain/skill_services_test.py
@@ -428,7 +428,7 @@ def test_filter_skills_by_status_assigned(self):
             skill_services.get_filtered_skill_summaries(
                 self.num_queries_to_fetch, 'Assigned', None,
                 None, None, None))
-        self.assertEqual(augmented_skill_summaries[0].topic_name, 'topic1')
+        self.assertEqual(augmented_skill_summaries[0].topic_names, ['topic1'])
         self.assertEqual(augmented_skill_summaries[0].id, self.SKILL_ID2)
         self.assertEqual(next_cursor, None)
         self.assertFalse(more)
@@ -482,10 +482,10 @@ def test_filter_skills_by_classroom_name(self):
             skill_services.get_filtered_skill_summaries(
                 self.num_queries_to_fetch, None, 'math', None,
                 None, None))
-        self.assertEqual(augmented_skill_summaries[0].topic_name, 'topic1')
+        self.assertEqual(augmented_skill_summaries[0].topic_names, ['topic1'])
         self.assertEqual(augmented_skill_summaries[0].id, self.SKILL_ID2)
         self.assertEqual(
-            augmented_skill_summaries[0].classroom_name, 'math')
+            augmented_skill_summaries[0].classroom_names, ['math'])
         self.assertEqual(next_cursor, None)
         self.assertFalse(more)
 
diff --git a/core/tests/protractor_desktop/navigation.js b/core/tests/protractor_desktop/navigation.js
--- a/core/tests/protractor_desktop/navigation.js
+++ b/core/tests/protractor_desktop/navigation.js
@@ -15,6 +15,7 @@
 /**
  * @fileoverview End-to-end tests for general site navigation.
  */
+var action = require('../protractor_utils/action.js');
 var general = require('../protractor_utils/general.js');
 var waitFor = require('../protractor_utils/waitFor.js');
 var GetStartedPage = require('../protractor_utils/GetStartedPage.js');
@@ -103,6 +104,23 @@ describe('DEV MODE Test', function() {
   });
 });
 
+describe('Donation flow', function() {
+  var payPalButton = element(by.css('.protractor-test-paypal-donate-button'));
+  var creditCardButton = element(
+    by.css('.protractor-test-credit-card-donate-button'));
+  it('should be able to donate via PayPal', async function() {
+    await browser.get('/donate');
+    await action.click('PayPal button', payPalButton);
+    expect(await browser.driver.getCurrentUrl()).toContain('www.paypal.com');
+  });
+
+  it('should be able to donate via credit card', async function() {
+    await browser.get('/donate');
+    await action.click('Credit Card button', creditCardButton);
+    expect(await browser.driver.getCurrentUrl()).toContain('www.paypal.com');
+  });
+});
+
 describe('Static Pages Tour', function() {
   var getStartedPage = new GetStartedPage.GetStartedPage();
   it('should visit the Get started page', async function() {