diff --git a/evap/evaluation/tests/test_misc.py b/evap/evaluation/tests/test_misc.py index 1d58f72f3bcad5de4078ce1760557db13f1600e4..56f1dd077e546b6a7772d0477c2c6dd3be6dca8e 100644 --- a/evap/evaluation/tests/test_misc.py +++ b/evap/evaluation/tests/test_misc.py @@ -14,7 +14,7 @@ from evap.staff.tests.utils import WebTestStaffMode @override_settings(INSTITUTION_EMAIL_DOMAINS=["institution.com", "student.institution.com"]) -class SampleXlsTests(WebTestStaffMode): +class SampleTableImport(WebTestStaffMode): @classmethod def setUpTestData(cls): cls.manager = make_manager() @@ -24,13 +24,13 @@ class SampleXlsTests(WebTestStaffMode): Degree.objects.filter(name_de="Bachelor").update(import_names=["Bachelor", "B. Sc."]) Degree.objects.filter(name_de="Master").update(import_names=["Master", "M. Sc."]) - def test_sample_xls(self): + def test_sample_semester_file(self): page = self.app.get(reverse("staff:semester_import", args=[self.semester.pk]), user=self.manager) original_user_count = UserProfile.objects.count() form = page.forms["semester-import-form"] - form["excel_file"] = (os.path.join(settings.BASE_DIR, "static", "sample.xls"),) + form["excel_file"] = (os.path.join(settings.BASE_DIR, "static", "sample.xlsx"),) page = form.submit(name="operation", value="test") form = page.forms["semester-import-form"] @@ -40,13 +40,13 @@ class SampleXlsTests(WebTestStaffMode): self.assertEqual(UserProfile.objects.count(), original_user_count + 4) - def test_sample_user_xls(self): + def test_sample_user_file(self): page = self.app.get("/staff/user/import", user=self.manager) original_user_count = UserProfile.objects.count() form = page.forms["user-import-form"] - form["excel_file"] = (os.path.join(settings.BASE_DIR, "static", "sample_user.xls"),) + form["excel_file"] = (os.path.join(settings.BASE_DIR, "static", "sample_user.xlsx"),) page = form.submit(name="operation", value="test") form = page.forms["user-import-form"] diff --git a/evap/staff/fixtures/excel_files_test_data.py b/evap/staff/fixtures/excel_files_test_data.py index fb687f01d08ba9046dc125ed65f1c6cb23a16552..08d9204ad948b8cda1d333ac7edd693c4e45451c 100644 --- a/evap/staff/fixtures/excel_files_test_data.py +++ b/evap/staff/fixtures/excel_files_test_data.py @@ -1,6 +1,8 @@ import io -import xlwt +import openpyxl + +# fmt: off duplicate_user_import_filedata = { 'Users': [ @@ -152,14 +154,17 @@ valid_user_courses_import_filedata = { ] } +# fmt: on + def create_memory_excel_file(data): memory_excel_file = io.BytesIO() - workbook = xlwt.Workbook() + workbook = openpyxl.Workbook() for sheet_name, sheet_data in data.items(): - sheet = workbook.add_sheet(sheet_name) - for (row_num, row_data) in enumerate(sheet_data): - for (column_num, cell_data) in enumerate(row_data): - sheet.write(row_num, column_num, cell_data) + sheet = workbook.create_sheet(sheet_name) + for row_num, row_data in enumerate(sheet_data, 1): + for column_num, cell_data in enumerate(row_data, 1): + # openpyxl rows start at 1 + sheet.cell(row=row_num, column=column_num).value = cell_data workbook.save(memory_excel_file) return memory_excel_file.getvalue() diff --git a/evap/staff/fixtures/invalid_user_import.xls b/evap/staff/fixtures/invalid_user_import.xls deleted file mode 100644 index 523509ae56c1b934324560f624ca4681da4eb7f7..0000000000000000000000000000000000000000 Binary files a/evap/staff/fixtures/invalid_user_import.xls and /dev/null differ diff --git a/evap/staff/fixtures/invalid_user_import.xlsx b/evap/staff/fixtures/invalid_user_import.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..b812ed6e10e723339da0e7809112b3ae062ecb1f Binary files /dev/null and b/evap/staff/fixtures/invalid_user_import.xlsx differ diff --git a/evap/staff/fixtures/valid_user_import.xls b/evap/staff/fixtures/valid_user_import.xls deleted file mode 100644 index 62828387bc779d976d0fa66b33d9045a64c5da37..0000000000000000000000000000000000000000 Binary files a/evap/staff/fixtures/valid_user_import.xls and /dev/null differ diff --git a/evap/staff/fixtures/valid_user_import.xlsx b/evap/staff/fixtures/valid_user_import.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..f6f49f8b3aece7aa0c68abeb1c78a611b8d87e9a Binary files /dev/null and b/evap/staff/fixtures/valid_user_import.xlsx differ diff --git a/evap/staff/forms.py b/evap/staff/forms.py index 8dae2b034ce4d210976f120f86390644bb3d356e..a3a2ef254ad2e5b22f2899551175d41a78fc6d04 100644 --- a/evap/staff/forms.py +++ b/evap/staff/forms.py @@ -101,7 +101,11 @@ class ImportForm(forms.Form): vote_start_datetime = forms.DateTimeField(label=_("Start of evaluation"), localize=True, required=False) vote_end_date = forms.DateField(label=_("End of evaluation"), localize=True, required=False) - excel_file = forms.FileField(label=_("Excel file"), required=False) + excel_file = forms.FileField( + label=_("Excel file"), + required=False, + widget=forms.FileInput(attrs={"accept": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}), + ) class UserImportForm(forms.Form): diff --git a/evap/staff/importers.py b/evap/staff/importers.py index e2e156e54bc91d876434a7d37a2acf456a5c68f3..1fa1972e616b4eb1c3ac64a99fe13c17822438c1 100644 --- a/evap/staff/importers.py +++ b/evap/staff/importers.py @@ -1,9 +1,10 @@ from collections import OrderedDict, defaultdict from dataclasses import dataclass from enum import Enum +from io import BytesIO from typing import Dict, Set -import xlrd +import openpyxl from django.conf import settings from django.core.exceptions import ValidationError from django.db import transaction @@ -223,33 +224,36 @@ class ExcelImporter: # (otherwise, testing is a pain) self.users = OrderedDict() - def read_book(self, file_content): + def read_book(self, file_content: bytes): try: - self.book = xlrd.open_workbook(file_contents=file_content) - except xlrd.XLRDError as e: + self.book = openpyxl.load_workbook(BytesIO(file_content)) + except Exception as e: # pylint: disable=broad-except self.errors[ImporterError.SCHEMA].append(_("Couldn't read the file. Error: {}").format(e)) def check_column_count(self, expected_column_count): - for sheet in self.book.sheets(): - if sheet.nrows <= self.skip_first_n_rows: + for sheet in self.book: + if sheet.max_row <= self.skip_first_n_rows: continue - if sheet.ncols != expected_column_count: + if sheet.max_column != expected_column_count: self.errors[ImporterError.SCHEMA].append( _("Wrong number of columns in sheet '{}'. Expected: {}, actual: {}").format( - sheet.name, expected_column_count, sheet.ncols + sheet.title, expected_column_count, sheet.max_column ) ) def for_each_row_in_excel_file_do(self, row_function): - for sheet in self.book.sheets(): + for sheet in self.book: try: - for row in range(self.skip_first_n_rows, sheet.nrows): - # see https://stackoverflow.com/questions/2077897/substitute-multiple-whitespace-with-single-whitespace-in-python - row_function([" ".join(cell.split()) for cell in sheet.row_values(row)], sheet, row) - self.success_messages.append(_("Successfully read sheet '%s'.") % sheet.name) + for row_index in range(self.skip_first_n_rows, sheet.max_row): + values = [cell.value if cell.value is not None else "" for cell in sheet[row_index + 1]] + # see https://stackoverflow.com/questions/2077898/substitute-multiple-whitespace-with-single-whitespace-in-python + cleaned_values = [" ".join(value.split()) for value in values] + row_function(cleaned_values, sheet, row_index) + + self.success_messages.append(_("Successfully read sheet '%s'.") % sheet.title) except Exception: self.warnings[ImporterWarning.GENERAL].append( - _("A problem occured while reading sheet {}.").format(sheet.name) + _("A problem occured while reading sheet {}.").format(sheet.title) ) raise self.success_messages.append(_("Successfully read Excel file.")) @@ -350,14 +354,14 @@ class ExcelImporter: """ 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)): + for sheet in self.book: + for row_idx, row in enumerate(sheet.values, 1): + if not all(isinstance(cell, str) or cell is None for cell in 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) + ).format(sheet.title, row_idx) ) @@ -383,15 +387,15 @@ class EnrollmentImporter(ExcelImporter): is_graded=data[5], responsible_email=responsible_data.email, ) - self.associations[(sheet.name, row)] = (student_data, responsible_data, evaluation_data) + self.associations[(sheet.title, row)] = (student_data, responsible_data, evaluation_data) - def process_evaluation(self, evaluation_data, sheet, row): + def process_evaluation(self, evaluation_data, sheetname, row): evaluation_id = evaluation_data.name_en if evaluation_id not in self.evaluations: if evaluation_data.name_de in self.names_de: self.errors[ImporterError.COURSE].append( _('Sheet "{}", row {}: The German name for course "{}" already exists for another course.').format( - sheet, row + 1, evaluation_data.name_en + sheetname, row + 1, evaluation_data.name_en ) ) else: @@ -403,7 +407,7 @@ class EnrollmentImporter(ExcelImporter): _( 'Sheet "{}", row {}: The course\'s "{}" degree differs from it\'s degree in a previous row.' " Both degrees have been set for the course." - ).format(sheet, row + 1, evaluation_data.name_en) + ).format(sheetname, row + 1, evaluation_data.name_en) ) self.evaluations[evaluation_id].degrees |= evaluation_data.degrees self.evaluations[evaluation_id].errors = merge_dictionaries_of_sets( @@ -412,7 +416,7 @@ class EnrollmentImporter(ExcelImporter): elif evaluation_data != self.evaluations[evaluation_id]: self.errors[ImporterError.COURSE].append( _('Sheet "{}", row {}: The course\'s "{}" data differs from it\'s data in a previous row.').format( - sheet, row + 1, evaluation_data.name_en + sheetname, row + 1, evaluation_data.name_en ) ) @@ -568,15 +572,15 @@ class UserImporter(ExcelImporter): def read_one_user(self, data, sheet, row): user_data = UserData(title=data[0], first_name=data[1], last_name=data[2], email=data[3], is_responsible=False) - self.associations[(sheet.name, row)] = user_data + self.associations[(sheet.title, row)] = user_data if user_data not in self._read_user_data: - self._read_user_data[user_data] = (sheet.name, row) + self._read_user_data[user_data] = (sheet.title, row) else: orig_sheet, orig_row = self._read_user_data[user_data] warningstring = _( "The duplicated row {row} in sheet '{sheet}' was ignored. It was first found in sheet '{orig_sheet}' on row {orig_row}." ).format( - sheet=sheet.name, + sheet=sheet.title, row=row + 1, orig_sheet=orig_sheet, orig_row=orig_row + 1, diff --git a/evap/staff/templates/staff_evaluation_person_management.html b/evap/staff/templates/staff_evaluation_person_management.html index d65aaa1a2b32b9e5fe7dc71cbbe074939de35c4f..2dc5f46bbef84f4c90f331580cb4a9761f2c1077 100644 --- a/evap/staff/templates/staff_evaluation_person_management.html +++ b/evap/staff/templates/staff_evaluation_person_management.html @@ -16,7 +16,7 @@ <h4 class="card-title">{% trans 'Import participants' %}</h4> <h6 class="card-subtitle mb-2 text-muted">{% trans 'From Excel file' %}</h6> <p class="card-text"> - {% trans 'Upload Excel file with participant data' %} (<a href="{% url 'staff:download_sample_xls' 'sample_user.xls' %}">{% trans 'Download sample file' %}</a>, + {% trans 'Upload Excel file with participant data' %} (<a href="{% url 'staff:download_sample_file' 'sample_user.xlsx' %}">{% trans 'Download sample file' %}</a>, <button type="button" class="btn-link" onclick="copyHeaders(['Title', 'First name', 'Last name', 'Email'])">{% trans 'Copy headers to clipboard' %}</button>). {% trans 'This will create all containing users.' %} </p> @@ -65,7 +65,7 @@ <h6 class="card-subtitle mb-2 text-muted">{% trans 'From Excel file' %}</h6> <p class="card-text"> {% trans 'Upload Excel file with contributor data' %} - (<a href="{% url 'staff:download_sample_xls' 'sample_user.xls' %}">{% trans 'Download sample file' %}</a>, + (<a href="{% url 'staff:download_sample_file' 'sample_user.xlsx' %}">{% trans 'Download sample file' %}</a>, <button type="button" class="btn-link" onclick="copyHeaders(['Title', 'First name', 'Last name', 'Email'])">{% trans 'Copy headers to clipboard' %}</button>). {% trans 'This will create all containing users.' %} </p> diff --git a/evap/staff/templates/staff_semester_import.html b/evap/staff/templates/staff_semester_import.html index 3bf69b8f0c7a41f4012d4ba1704f55be9bd159c7..5d58d79697242a9043a1d9f0d421a4ca56bc8b64 100644 --- a/evap/staff/templates/staff_semester_import.html +++ b/evap/staff/templates/staff_semester_import.html @@ -18,7 +18,7 @@ <div class="card-body"> <p> {% trans 'Upload Excel file' %} - (<a href="{% url 'staff:download_sample_xls' 'sample.xls' %}">{% trans 'Download sample file' %}</a>, + (<a href="{% url 'staff:download_sample_file' 'sample.xlsx' %}">{% trans 'Download sample file' %}</a>, <button type="button" class="btn-link" onClick="copyHeaders(['Degree', 'Participant last name', 'Participant first name', 'Participant email address', 'Course kind', 'Course is graded', 'Course name (de)', 'Course name (en)', 'Responsible title', 'Responsible last name', 'Responsible first name', 'Responsible email address'])"> {% trans 'Copy headers to clipboard' %}</button>). {% trans 'This will create all containing participants, contributors and courses and connect them. It will also set the entered values as default for all evaluations.' %} diff --git a/evap/staff/templates/staff_user_import.html b/evap/staff/templates/staff_user_import.html index 89f50229d6c7e1a755a71c9d8f459265cbae356c..d10662b8c60eb73bfde0c4d42b84ac3542711ecc 100644 --- a/evap/staff/templates/staff_user_import.html +++ b/evap/staff/templates/staff_user_import.html @@ -20,7 +20,7 @@ <div class="card-body"> <p> {% trans 'Upload Excel file' %} - (<a href="{% url 'staff:download_sample_xls' 'sample_user.xls' %}">{% trans 'Download sample file' %}</a>, + (<a href="{% url 'staff:download_sample_file' 'sample_user.xlsx' %}">{% trans 'Download sample file' %}</a>, <button type="button" class="btn-link" onclick="copyHeaders(['Title', 'First name', 'Last name', 'Email'])">{% trans 'Copy headers to clipboard' %}</button>). {% trans 'This will create all contained users.' %} </p> diff --git a/evap/staff/tests/test_importers.py b/evap/staff/tests/test_importers.py index bcc2223388c222d0ba63242bc763d9c4f7c83bba..2d7b26e9b06fee7ed59fff07e82984de966f4a5c 100644 --- a/evap/staff/tests/test_importers.py +++ b/evap/staff/tests/test_importers.py @@ -13,8 +13,8 @@ from evap.staff.tools import ImportType class TestUserImporter(TestCase): - filename_valid = os.path.join(settings.BASE_DIR, "staff/fixtures/valid_user_import.xls") - filename_invalid = os.path.join(settings.BASE_DIR, "staff/fixtures/invalid_user_import.xls") + filename_valid = os.path.join(settings.BASE_DIR, "staff/fixtures/valid_user_import.xlsx") + filename_invalid = os.path.join(settings.BASE_DIR, "staff/fixtures/invalid_user_import.xlsx") filename_random = os.path.join(settings.BASE_DIR, "staff/fixtures/random.random") # valid user import tested in tests/test_views.py, TestUserImportView @@ -104,7 +104,7 @@ class TestUserImporter(TestCase): self.assertEqual(errors_test, errors_no_test) self.assertEqual( errors_test[ImporterError.SCHEMA], - ["Couldn't read the file. Error: Unsupported format, or corrupt file: Expected BOF record; found b'42\\n'"], + ["Couldn't read the file. Error: File is not a zip file"], ) self.assertEqual(UserProfile.objects.count(), original_user_count) @@ -297,7 +297,7 @@ class TestEnrollmentImporter(TestCase): self.assertEqual(errors_test, errors_no_test) self.assertEqual( errors_test[ImporterError.SCHEMA], - ["Couldn't read the file. Error: Unsupported format, or corrupt file: Expected BOF record; found b'42\\n'"], + ["Couldn't read the file. Error: File is not a zip file"], ) self.assertEqual(UserProfile.objects.count(), original_user_count) diff --git a/evap/staff/tests/test_views.py b/evap/staff/tests/test_views.py index ea404e47c6e7f84859dd0c745ca6d87761d35945..d40010704b32193e8a1827f140db424230c743c6 100644 --- a/evap/staff/tests/test_views.py +++ b/evap/staff/tests/test_views.py @@ -1,7 +1,9 @@ import datetime import os +from io import BytesIO from unittest.mock import PropertyMock, patch +import openpyxl import xlrd from django.conf import settings from django.contrib.auth.models import Group @@ -51,8 +53,8 @@ from evap.staff.views import get_evaluations_with_prefetched_data from evap.student.models import TextAnswerWarning -class TestDownloadSampleXlsView(WebTestStaffMode): - url = "/staff/download_sample_xls/sample.xls" +class TestDownloadSampleFileView(WebTestStaffMode): + url = "/staff/download_sample_file/sample.xlsx" email_placeholder = "institution.com" @classmethod @@ -63,13 +65,16 @@ class TestDownloadSampleXlsView(WebTestStaffMode): page = self.app.get(self.url, user=self.manager) found_institution_domains = 0 - book = xlrd.open_workbook(file_contents=page.body) - for sheet in book.sheets(): - for row in sheet.get_rows(): + book = openpyxl.load_workbook(BytesIO(page.body)) + for sheet in book: + for row in sheet.values: for cell in row: - value = cell.value - self.assertNotIn(self.email_placeholder, value) - if "@" + settings.INSTITUTION_EMAIL_DOMAINS[0] in value: + if cell is None: + continue + + self.assertNotIn(self.email_placeholder, cell) + + if "@" + settings.INSTITUTION_EMAIL_DOMAINS[0] in cell: found_institution_domains += 1 self.assertEqual(found_institution_domains, 2) @@ -413,8 +418,8 @@ class TestUserBulkUpdateView(WebTestStaffMode): class TestUserImportView(WebTestStaffMode): url = "/staff/user/import" - filename_valid = os.path.join(settings.BASE_DIR, "staff/fixtures/valid_user_import.xls") - filename_invalid = os.path.join(settings.BASE_DIR, "staff/fixtures/invalid_user_import.xls") + filename_valid = os.path.join(settings.BASE_DIR, "staff/fixtures/valid_user_import.xlsx") + filename_invalid = os.path.join(settings.BASE_DIR, "staff/fixtures/invalid_user_import.xlsx") filename_random = os.path.join(settings.BASE_DIR, "staff/fixtures/random.random") @classmethod @@ -1925,8 +1930,8 @@ class TestEvaluationPreviewView(WebTestStaffModeWith200Check): class TestEvaluationImportPersonsView(WebTestStaffMode): url = "/staff/semester/1/evaluation/1/person_management" url2 = "/staff/semester/1/evaluation/2/person_management" - filename_valid = os.path.join(settings.BASE_DIR, "staff/fixtures/valid_user_import.xls") - filename_invalid = os.path.join(settings.BASE_DIR, "staff/fixtures/invalid_user_import.xls") + filename_valid = os.path.join(settings.BASE_DIR, "staff/fixtures/valid_user_import.xlsx") + filename_invalid = os.path.join(settings.BASE_DIR, "staff/fixtures/invalid_user_import.xlsx") filename_random = os.path.join(settings.BASE_DIR, "staff/fixtures/random.random") @classmethod diff --git a/evap/staff/urls.py b/evap/staff/urls.py index c4b31f0f268c87f49ce651b9fa71df88e083dd1f..b2f954f5ba4b87b8fe46acd988530687253a436a 100644 --- a/evap/staff/urls.py +++ b/evap/staff/urls.py @@ -83,7 +83,7 @@ urlpatterns = [ path("faq/", views.faq_index, name="faq_index"), path("faq/<int:section_id>", views.faq_section, name="faq_section"), - path("download_sample_xls/<str:filename>", views.download_sample_xls, name="download_sample_xls"), + path("download_sample_file/<str:filename>", views.download_sample_file, name="download_sample_file"), path("export_contributor_results/<int:contributor_id>", views.export_contributor_results_view, name="export_contributor_results"), diff --git a/evap/staff/views.py b/evap/staff/views.py index e635405e2beb43439df42e46e04ae2024cba31f3..ff7a728eb780a917db8a5665c66ee28e66f39dae 100644 --- a/evap/staff/views.py +++ b/evap/staff/views.py @@ -5,6 +5,7 @@ from dataclasses import dataclass from datetime import date, datetime from typing import Any, Container, Dict, Optional +import openpyxl from django.conf import settings from django.contrib import messages from django.core.exceptions import PermissionDenied, SuspiciousOperation @@ -21,8 +22,6 @@ from django.utils.translation import get_language from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy, ngettext from django.views.decorators.http import require_POST -from xlrd import open_workbook -from xlutils.copy import copy as copy_workbook from evap.contributor.views import export_contributor_results from evap.evaluation.auth import manager_required, reviewer_required, staff_permission_required @@ -2229,25 +2228,21 @@ def faq_section(request, section_id): @manager_required -def download_sample_xls(_request, filename): +def download_sample_file(_request, filename): email_placeholder = "institution.com" - if filename not in ["sample.xls", "sample_user.xls"]: + if filename not in ["sample.xlsx", "sample_user.xlsx"]: raise SuspiciousOperation("Invalid file name.") - read_book = open_workbook(settings.STATICFILES_DIRS[0] + "/" + filename, formatting_info=True) - write_book = copy_workbook(read_book) - for sheet_index in range(read_book.nsheets): - read_sheet = read_book.sheet_by_index(sheet_index) - write_sheet = write_book.get_sheet(sheet_index) - for row in range(read_sheet.nrows): - for col in range(read_sheet.ncols): - value = read_sheet.cell(row, col).value - if email_placeholder in value: - write_sheet.write(row, col, value.replace(email_placeholder, settings.INSTITUTION_EMAIL_DOMAINS[0])) - - response = FileResponse(filename, content_type="application/vnd.ms-excel") - write_book.save(response) + book = openpyxl.load_workbook(filename=settings.STATICFILES_DIRS[0] + "/" + filename) + for sheet in book: + for row in sheet: + for cell in row: + if cell.value is not None: + cell.value = cell.value.replace(email_placeholder, settings.INSTITUTION_EMAIL_DOMAINS[0]) + + response = FileResponse(filename, content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + book.save(response) return response diff --git a/evap/static/sample.xls b/evap/static/sample.xls deleted file mode 100644 index 3c9c57e86d559b47080ab539c433222ee3d70369..0000000000000000000000000000000000000000 Binary files a/evap/static/sample.xls and /dev/null differ diff --git a/evap/static/sample.xlsx b/evap/static/sample.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..f3f3bf3183229ae7926a7496965ba38c2d918f3e Binary files /dev/null and b/evap/static/sample.xlsx differ diff --git a/evap/static/sample_user.xls b/evap/static/sample_user.xls deleted file mode 100644 index 27b08cdbf76c5e1cd8cdb2dd71171f7d0d559e0c..0000000000000000000000000000000000000000 Binary files a/evap/static/sample_user.xls and /dev/null differ diff --git a/evap/static/sample_user.xlsx b/evap/static/sample_user.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..93216c795eec95b3a3e6bf1161366e7cbaf53ee9 Binary files /dev/null and b/evap/static/sample_user.xlsx differ diff --git a/pyproject.toml b/pyproject.toml index d033c206065922024937f44adad20c8d44b97ca8..4ffccbff9d1ca16f27a350b943b4cc9586526e40 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,6 @@ line-length = 120 target-version = ['py37'] include = 'evap.*\.pyi?$' extend-exclude = """\ - evap/staff/fixtures/excel_files_test_data\\.py|\ .*/urls\\.py|\ .*/migrations/.*\ """ diff --git a/requirements-dev.txt b/requirements-dev.txt index 45e64e18d1e104311e95f770f91f3603fd6dd9c6..c365d46cad49a41dbf86c73e131f53ec735f5e06 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,5 +6,7 @@ pylint==2.9.5 pylint-django==2.4.4 black==21.12b0 isort==5.10.1 +openpyxl-stubs==0.1.21 +xlrd == 2.0.1 mypy django-stubs diff --git a/requirements.txt b/requirements.txt index e7e301af43d4191cff4e471037227855ef8a6ab7..e5bfb8a98e319192b972c109c081e562a40fad2f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ django >= 3.2, <3.3 -xlrd == 2.0.1 xlwt == 1.3.0 xlutils == 2.0.0 +openpyxl == 3.0.9 psycopg2-binary == 2.9.3 redis == 4.1.1 django-redis == 5.2.0