Repository: e-valuation/EvaP

Issue:
Excel importer: Data types of cells. Float cells might cause import errors
Successor of #1493.

With the current code, we had occurences of
```
AttributeError
'float' object has no attribute 'strip'
/evap/evap/staff/importers.py in __init__, line 40
```
which we think is caused by the excel file cell being set to type "float", so xlrd gives us a float value instead of a string value, but we expect it to be a string.

@niklasmohrin had some proposals on what to do in #1499. I think our last consent is given in https://github.com/e-valuation/EvaP/pull/1499#issuecomment-688454822 by @janno42:
> we just use strings, so every cell could be parsed as string value, regardless of the excel data type.





Discussion:

Resolving patch:
diff --git a/evap/staff/importers.py b/evap/staff/importers.py
--- a/evap/staff/importers.py
+++ b/evap/staff/importers.py
@@ -330,6 +330,18 @@ def check_user_data_sanity(self, test_run):
             if len(users_same_name) > 0:
                 self._create_user_name_collision_warning(user_data, users_same_name)
 
+    def check_data_type_correctness(self):
+        """
+        Checks that all cells after the skipped rows contain string values (not floats or integers).
+        """
+        for sheet in self.book.sheets():
+            for row in range(self.skip_first_n_rows, sheet.nrows):
+                if not all(isinstance(cell, str) for cell in sheet.row_values(row)):
+                    self.errors[ImporterError.SCHEMA].append(
+                            _("Wrong data type in sheet '{}' in row {}."
+                              " Please make sure all cells are string types, not numerical.").format(sheet.name, row + 1)
+                    )
+
 
 class EnrollmentImporter(ExcelImporter):
     def __init__(self):
@@ -579,6 +591,7 @@ def process(cls, excel_content, test_run):
                 return [], importer.success_messages, importer.warnings, importer.errors
 
             importer.check_column_count(4)
+            importer.check_data_type_correctness()
             if importer.errors:
                 importer.errors[ImporterError.GENERAL].append(_("The input data is malformed. No data was imported."))
                 return [], importer.success_messages, importer.warnings, importer.errors


Test patch:
diff --git a/evap/staff/tests/test_importers.py b/evap/staff/tests/test_importers.py
--- a/evap/staff/tests/test_importers.py
+++ b/evap/staff/tests/test_importers.py
@@ -16,6 +16,7 @@ class TestUserImporter(TestCase):
     filename_duplicate = os.path.join(settings.BASE_DIR, "staff/fixtures/duplicate_user_import.xls")
     filename_invalid = os.path.join(settings.BASE_DIR, "staff/fixtures/invalid_user_import.xls")
     filename_random = os.path.join(settings.BASE_DIR, "staff/fixtures/random.random")
+    filename_numerics = os.path.join(settings.BASE_DIR, "staff/fixtures/numerical_data_in_user_data.xls")
 
     # valid user import tested in tests/test_views.py, TestUserImportView
 
@@ -29,6 +30,8 @@ def setUpTestData(cls):
             cls.random_excel_content = excel_file.read()
         with open(cls.filename_duplicate, "rb") as excel_file:
             cls.duplicate_excel_content = excel_file.read()
+        with open(cls.filename_numerics, "rb") as excel_file:
+            cls.numerical_excel_content = excel_file.read()
 
     def test_test_run_does_not_change_database(self):
         original_users = list(UserProfile.objects.all())
@@ -133,6 +136,22 @@ def test_unhandled_exception(self, mocked_db_access):
             errors[ImporterError.GENERAL],
         )
 
+    def test_disallow_non_string_types(self):
+        imported_users, __, __, errors = UserImporter.process(self.numerical_excel_content, test_run=False)
+        self.assertEqual(len(imported_users), 0)
+        self.assertIn(
+            "The input data is malformed. No data was imported.",
+            errors[ImporterError.GENERAL]
+        )
+        # The sheet has a float in row 3 and an int row 4. All others rows only contain strings.
+        self.assertSetEqual(
+            {
+                "Wrong data type in sheet 'Users' in row 3. Please make sure all cells are string types, not numerical.",
+                "Wrong data type in sheet 'Users' in row 4. Please make sure all cells are string types, not numerical."
+            },
+            set(errors[ImporterError.SCHEMA])
+        )
+
 
 class TestEnrollmentImporter(TestCase):
     filename_valid = os.path.join(settings.BASE_DIR, "staff/fixtures/test_enrollment_data.xls")