From 0dadd842b233c9c9ccf22f5aa679bfc95fa21c37 Mon Sep 17 00:00:00 2001 From: Douglas Cardoso Date: Tue, 19 Sep 2023 22:25:15 -0300 Subject: [PATCH 01/44] translate to english --- README.md | 123 +++++++++++++++++++++++++++--------------------------- 1 file changed, 62 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index ad04ee3..cc85200 100644 --- a/README.md +++ b/README.md @@ -1,61 +1,62 @@ -# Controle de notas de alunos de faculdadde -## Introdução -Este projeto tem o objetivo de exercitar os "code smells" relacionados a testes unitários utilizando um aplicativo que simula um negócio real. -O código será desenvolvido com a técnica TDD ao estilo Detroid, daí o nome do repositório. -O aplicativo simula o controle de notas dos alunos de uma universidade. Abaixo segue a especificação do aplicativo: -Definição de Feito (Definition of Done): -1. Testes unitários estão cobrindo a funcionalidade -2. A funcionalidade está desenvolvidade para ser utilizada via CLI -3. Os dados estão sendo salvos no banco de dados -# Entregas -Construção das funções básicas do sistema -1. Cada aluno terá um controle de notas chamado "coeficiente de rendimento" (CR) -2. O CR é a média das notas do aluno nas disciplinas já cursadas -3. O aluno é considerado aprovado na universidade se seu CR for acima ou igual a 7 (sete) ao final do curso -4. Caso o aluno curse a mesma matéria mais de uma vez, a maior nota será considerada no cálculo do CR -5. A faculdade terá inicialmente 3 cursos com 3 matérias cada -6. As matérias de cada curso podem ter nomes iguais, mas serão diferenciadas pelo número identificador único (niu) -7. O sistema deve calcular a situação do aluno levando em consideração as matérias cursadas e o total de matérias de cada curso -8. O aluno só poderá cursar matérias do seu curso -9. Os cursos devem ter identificador único e nome -10. O nome dos curso pode ser igual, mas o identificador único de cada curso deve ser diferente -11. Um curso não pode ter duas matérias com mesmo nome, mesmo que o niu seja diferente -12. A nota máxima de um aluno em uma matéria é 10 -13. A nota mínima de um aluno em uma matéria é 0 -14. O aluno pode trancar o curso e neste caso não pode atualizar suas notas ou matérias cursadas -15. O coordenador do curso pode listar os alunos, notas em cada matéria e cr dos alunos -16. O Aluno pode destrancar o curso e sua situação volta para a anterior -17. Os alunos devem ter nomes -18. O coordenador geral pode listar de todos os curso os alunos e notas em cada matéria e cr de cada aluno -19. O coordenador do curso pode eliminar matérias e neste caso os alunos não podem atualizar suas notas nesta matéria -20. Os alunos só podem atualizar suas notas na matérias em que estão inscritos -21. O nome dos cursos e materias tem que ter no máximo 10 letras -22. Os cursos podem ter nomes iguais se forem de unidades diferentes -23. O aluno só pode se increver em um curso -24. O coordenador pode ser corrdenador de mais de um curso -25. O coordenador pode listar os alunos, materias e notas, e crs de todos os seus cursos (coordenador de mais de um curso) -26. O curso pode ser cancelado -27. Os cursos cancelados não podem aceitar incrições de alunos -28. Os cursos cancelados não pode ter coordenadores -29. Cada matéria pode ter no máximo 30 alunos inscritos -30. O aluno tem que se inscrever em 3 matérias no mínimo -31. Caso o número de matérias faltantes de um aluno seja menor do que 3, ele pode se inscrever em 1 matéria -32. Caso o aluno não se increva no número mínimo de matérias por semestre, ele será automaticamente reprovado -33. O aluno deve ter CPF validado no sistema externo de validação de CPFs (sistema do governo) -34. Adicionar o nome do curso nos relatórios dos coordenadores -35. O Aluno só é aprovado se tirar a nota mínima em todas as matérias do curso, mesmo se seu cr seja acima do mínimo -36. O usuário deve ser capaz de criar alunos com informações básicas -37. O usuário deve ser capaz de inscrever o aluno em um curso -38. O usuário deve ser capaz de criar cursos com o número mínimo de matérias -39. O adminstrador e somente ele deve ser capaz de listar todos os alunos com informações detalhadas (todas as informações disponíveis) -40. O admnistrador e somente ele deve ser capaz de listar todos os cursos com todas as informações disponíveis -41. O administrador e somente ele deve ser capaz de listar a relação de alunos por curso -42. O administrador e somente ele deve ser capaz de listar a relação de matérias por aluno -43. O aluno deve ser capaz de listar todas as matérias somente de seu curso -44. O aluno deve ser capaz de listar todas as suas matérias cursadas -45. O aludo deve ser capaz de listar as matérias faltantes -46. O administrador deve ser capaz se listar todos os coordenadores de cursos com as informação disponíveisop -47. O aluno tem 10 semestres para se formar -48. Caso o aluno exceda os 10 semestres, ele é automaticamente reprovado -49. O coordenador só pode ser coordenador de 3 cursos no máximo -50. O coordenador geral não pode ser coordenador de cursos \ No newline at end of file +{Translated from Portuguese to English using AI} +# College Student Grade Control +## Introduction +This project aims to practice "code smells" related to unit tests using an application that simulates a real business scenario. +The code will be developed using the TDD technique in the Detroit style, hence the name of the repository. +The application simulates the control of grades for university students. Below is the specification of the application: +Definition of Done: +1. Unit tests cover the functionality. +2. The functionality is developed to be used via CLI (Command Line Interface). +3. Data is being saved in the database. +# Deliverables +Construction of the basic functions of the system +1. Each student will have a grade control called "grade point average" (GPA). +2. The GPA is the average of the student's grades in the courses already taken. +3. The student is considered approved at the university if their GPA is above or equal to 7 (seven) at the end of the course. +4. If a student takes the same subject more than once, the highest grade will be considered in the GPA calculation. +5. Initially, the university will have 3 courses with 3 subjects each. +6. Subjects in each course may have the same names but will be differentiated by a unique identifier (niu). +7. The system must calculate the student's situation taking into account the subjects taken and the total number of subjects in each course. +8. The student can only take subjects from their course. +9. Courses must have a unique identifier and name. +10. Course names can be the same, but the unique identifier for each course must be different. +11. A course cannot have two subjects with the same name, even if the niu is different. +12. The maximum grade for a student in a subject is 10. +13. The minimum grade for a student in a subject is 0. +14. The student can lock the course, and in this case, they cannot update their grades or the subjects taken. +15. The course coordinator can list the students, grades in each subject, and GPAs of the students. +16. The student can unlock the course, and their situation returns to the previous state. +17. Students must have names. +18. The general coordinator can list all courses, students, grades in each subject, and GPAs of each student. +19. The course coordinator can remove subjects, and in this case, students cannot update their grades in that subject. +20. Students can only update their grades in the subjects they are enrolled in. +21. Course and subject names must have a maximum of 10 letters. +22. Courses can have the same names if they are from different units. +23. The student can only enroll in one course. +24. The coordinator can coordinate a maximum of three courses. +25. The coordinator can list the students, subjects, grades, and GPAs of all their courses (coordinator of more than one course). +26. The course can be canceled. +27. Canceled courses cannot accept student enrollments. +28. Canceled courses cannot have coordinators. +29. Each subject can have a maximum of 30 enrolled students. +30. The student must enroll in a minimum of 3 subjects. +31. If the number of subjects missing for a student is less than 3, they can enroll in 1 subject. +32. If the student does not enroll in the minimum number of subjects per semester, they will be automatically failed. +33. The student must have a validated CPF (Brazilian Social Security Number) in the external CPF validation system (government system). +34. Add the course name to the coordinator's reports. +35. The student is only approved if they achieve the minimum grade in all course subjects, even if their GPA is above the minimum. +36. The user must be able to create students with basic information. +37. The user must be able to enroll the student in a course. +38. The user must be able to create courses with the minimum number of subjects. +39. The administrator, and only the administrator, must be able to list all students with detailed information (all available information). +40. The administrator, and only the administrator, must be able to list all courses with all available information. +41. The administrator, and only the administrator, must be able to list the list of students per course. +42. The administrator, and only the administrator, must be able to list the list of subjects per student. +43. The student must be able to list all subjects only from their course. +44. The student must be able to list all subjects they have taken. +45. The student must be able to list the missing subjects. +46. The administrator must be able to list all course coordinators with available information. +47. The student has 10 semesters to graduate. +48. If the student exceeds the 10 semesters, they are automatically failed. +49. The coordinator can only coordinate a maximum of 3 courses. +50. The general coordinator cannot be a coordinator of courses. \ No newline at end of file From 7bc11ef5bead63e81fb327a9271826c29ed83391 Mon Sep 17 00:00:00 2001 From: Douglas Cardoso Date: Wed, 10 Apr 2024 02:42:07 -0300 Subject: [PATCH 02/44] Add Student take and enrollment --- README.md | 30 +++++++++++++++++++++++------- src/codigo_qualquer.py | 1 - tests/test_qualquer.py | 1 - 3 files changed, 23 insertions(+), 9 deletions(-) delete mode 100644 src/codigo_qualquer.py delete mode 100644 tests/test_qualquer.py diff --git a/README.md b/README.md index cc85200..c31ba83 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,17 @@ ## Introduction This project aims to practice "code smells" related to unit tests using an application that simulates a real business scenario. The code will be developed using the TDD technique in the Detroit style, hence the name of the repository. -The application simulates the control of grades for university students. Below is the specification of the application: +The application simulates the control of grades for university students. + +# Installation +``` +# create database +sqlite3 university.db +sqlite> .quit +``` + + +Below is the specification of the application: Definition of Done: 1. Unit tests cover the functionality. 2. The functionality is developed to be used via CLI (Command Line Interface). @@ -11,15 +21,16 @@ Definition of Done: # Deliverables Construction of the basic functions of the system 1. Each student will have a grade control called "grade point average" (GPA). -2. The GPA is the average of the student's grades in the courses already taken. +2. The GPA is the average of the student's grades in the ~~courses~~ subjects already taken. 3. The student is considered approved at the university if their GPA is above or equal to 7 (seven) at the end of the course. 4. If a student takes the same subject more than once, the highest grade will be considered in the GPA calculation. + 5. Initially, the university will have 3 courses with 3 subjects each. 6. Subjects in each course may have the same names but will be differentiated by a unique identifier (niu). 7. The system must calculate the student's situation taking into account the subjects taken and the total number of subjects in each course. 8. The student can only take subjects from their course. 9. Courses must have a unique identifier and name. -10. Course names can be the same, but the unique identifier for each course must be different. +10. ~~Course~~ Subject names can be the same, but the unique identifier for each ~~course~~ subject must be different. 11. A course cannot have two subjects with the same name, even if the niu is different. 12. The maximum grade for a student in a subject is 10. 13. The minimum grade for a student in a subject is 0. @@ -32,11 +43,15 @@ Construction of the basic functions of the system 20. Students can only update their grades in the subjects they are enrolled in. 21. Course and subject names must have a maximum of 10 letters. 22. Courses can have the same names if they are from different units. + 23. The student can only enroll in one course. 24. The coordinator can coordinate a maximum of three courses. 25. The coordinator can list the students, subjects, grades, and GPAs of all their courses (coordinator of more than one course). -26. The course can be canceled. + + +26. The course can be canceled by the general cordinator. 27. Canceled courses cannot accept student enrollments. + 28. Canceled courses cannot have coordinators. 29. Each subject can have a maximum of 30 enrolled students. 30. The student must enroll in a minimum of 3 subjects. @@ -45,9 +60,10 @@ Construction of the basic functions of the system 33. The student must have a validated CPF (Brazilian Social Security Number) in the external CPF validation system (government system). 34. Add the course name to the coordinator's reports. 35. The student is only approved if they achieve the minimum grade in all course subjects, even if their GPA is above the minimum. -36. The user must be able to create students with basic information. -37. The user must be able to enroll the student in a course. -38. The user must be able to create courses with the minimum number of subjects. +36. The ~~user~~ student (person) must be able to create students with basic information. + +37. ~~The user must be able to enroll the student in a course.~~ +38. The ~~user~~ general coordinator must be able to create courses with the minimum number of subjects. 39. The administrator, and only the administrator, must be able to list all students with detailed information (all available information). 40. The administrator, and only the administrator, must be able to list all courses with all available information. 41. The administrator, and only the administrator, must be able to list the list of students per course. diff --git a/src/codigo_qualquer.py b/src/codigo_qualquer.py deleted file mode 100644 index 6c3ea6e..0000000 --- a/src/codigo_qualquer.py +++ /dev/null @@ -1 +0,0 @@ -# Coloque o código fonte nesta pasta \ No newline at end of file diff --git a/tests/test_qualquer.py b/tests/test_qualquer.py deleted file mode 100644 index 0faa224..0000000 --- a/tests/test_qualquer.py +++ /dev/null @@ -1 +0,0 @@ -# Coloque os testes nesta pasta \ No newline at end of file From 01c6a5fe6dc1a25f6efc92de331dd012cc668836 Mon Sep 17 00:00:00 2001 From: Douglas Cardoso Date: Wed, 10 Apr 2024 02:51:08 -0300 Subject: [PATCH 03/44] Move models to handler files --- requirements.txt | 1 + src/__init__.py | 0 src/mock_database.py | 2 + src/models/__init__.py | 0 src/services/__init__.py | 0 src/services/course_handler.py | 50 ++++++++++++++++ src/services/enrollment_validator.py | 16 +++++ src/services/semester_monitor.py | 20 +++++++ src/services/student_handler.py | 87 ++++++++++++++++++++++++++++ src/services/subject_handler.py | 58 +++++++++++++++++++ tests/__init__.py | 0 tests/test_models.py | 48 +++++++++++++++ tests/test_student.py | 80 +++++++++++++++++++++++++ 13 files changed, 362 insertions(+) create mode 100644 requirements.txt create mode 100644 src/__init__.py create mode 100644 src/mock_database.py create mode 100644 src/models/__init__.py create mode 100644 src/services/__init__.py create mode 100644 src/services/course_handler.py create mode 100644 src/services/enrollment_validator.py create mode 100644 src/services/semester_monitor.py create mode 100644 src/services/student_handler.py create mode 100644 src/services/subject_handler.py create mode 100644 tests/__init__.py create mode 100644 tests/test_models.py create mode 100644 tests/test_student.py diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..55b033e --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +pytest \ No newline at end of file diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/mock_database.py b/src/mock_database.py new file mode 100644 index 0000000..9c60eb3 --- /dev/null +++ b/src/mock_database.py @@ -0,0 +1,2 @@ +SUBJECT = "any" +SUBJECT_MAX_ENROLL = 10 diff --git a/src/models/__init__.py b/src/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/services/__init__.py b/src/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/services/course_handler.py b/src/services/course_handler.py new file mode 100644 index 0000000..7694b58 --- /dev/null +++ b/src/services/course_handler.py @@ -0,0 +1,50 @@ +import uuid +import datetime + + +class CourseHandler: + + def __init__(self, identifier=None) -> None: + if not identifier: + self.__identifier = uuid.uuid5( + uuid.NAMESPACE_URL, str(datetime.datetime.now()) + ) + self.__state = "inactive" # TODO use enum + self.__enrolled_students = [] + self.__subjects = [] + self.__max_enrollment = 0 + + @property + def identifier(self): + return self.__identifier + + @property + def state(self): + return self.__state + + @property + def enrolled_students(self): + return self.__enrolled_students + + @property + def subjects(self): + return self.__subjects + + @property + def name(self): + return self.__name + + @name.setter + def name(self, value): + self.__name = value + + @property + def max_enrollment(self): + return self.__max_enrollment + + @max_enrollment.setter + def max_enrollment(self, value): + self.__max_enrollment = value + + def enroll_student(self, student_identifier): + self.__enrolled_students.append(student_identifier) diff --git a/src/services/enrollment_validator.py b/src/services/enrollment_validator.py new file mode 100644 index 0000000..9b0552f --- /dev/null +++ b/src/services/enrollment_validator.py @@ -0,0 +1,16 @@ +import uuid + + +class EnrollmentValidator: + def validate_student(self, name, cpf, course_identifier): + # TODO do the true check + dummy_identifier = "290f2113c2e6579c8bb6ec395ea56572" + return ( + self.generate_student_identifier(name, cpf, course_identifier) + == dummy_identifier + ) + + def generate_student_identifier(self, name, cpf, course_identifier): + return uuid.uuid5( + uuid.NAMESPACE_URL, str(f"{name}{cpf}{course_identifier}") + ).hex diff --git a/src/services/semester_monitor.py b/src/services/semester_monitor.py new file mode 100644 index 0000000..d2a95aa --- /dev/null +++ b/src/services/semester_monitor.py @@ -0,0 +1,20 @@ +import uuid +import datetime + + +class SemesterHandler: + def __init__(self) -> None: + self.__identifier = "2024-1" # TODO get next from database + self.__state = "open" + + @property + def identifier(self): + return self.__identifier + + @property + def state(self): + return self.__state + + @state.setter + def state(self, value): + self.__state = value diff --git a/src/services/student_handler.py b/src/services/student_handler.py new file mode 100644 index 0000000..c90d19f --- /dev/null +++ b/src/services/student_handler.py @@ -0,0 +1,87 @@ +from src.services.enrollment_validator import EnrollmentValidator +from src.services.course_handler import CourseHandler +from src.services.subject_handler import SubjectHandler + + +class StudentHandler: + def __init__(self) -> None: + self.__identifier = None + self.__state = None + self.__gpa = 0 + self.__subjects = [] + self.__course = None + + @property + def identifier(self): + return self.__identifier + + @identifier.setter + def identifier(self, value): + self.__identifier = value + + @property + def state(self): + return self.__state + + @property + def gpa(self): + return self.__gpa + + @property + def subjects(self): + return self.__subjects + + @property + def name(self): + return self.__name + + @name.setter + def name(self, value): + self.__name = value + + @property + def cpf(self): + return self.__cpf + + @cpf.setter + def cpf(self, value): + self.__cpf = value + + def enroll_to_course(self, course_identifier): + enrollment_validator = EnrollmentValidator() + is_valid_student = enrollment_validator.validate_student( + self.name, self.cpf, course_identifier + ) + if is_valid_student: + self.__identifier = enrollment_validator.generate_student_identifier( + self.name, self.cpf, course_identifier + ) + self.__course = course_identifier + course = CourseHandler(course_identifier) + course.enroll_student(self.identifier) + return self.identifier in course.enrolled_students + raise NonValidStudent() + + def take_subject(self, subject_identifier): + enrollment_validator = EnrollmentValidator() + is_valid_student = enrollment_validator.validate_student( + self.name, self.cpf, self.__course + ) + if not is_valid_student: + raise NonValidStudent() + + subject_handler = SubjectHandler(subject_identifier) + if subject_handler.course != self.__course: + raise NonValidSubject() + if not subject_handler.is_available(): + raise NonValidSubject() + + return self.subjects.append(subject_identifier) + + +class NonValidStudent(Exception): + pass + + +class NonValidSubject(Exception): + pass diff --git a/src/services/subject_handler.py b/src/services/subject_handler.py new file mode 100644 index 0000000..38fd162 --- /dev/null +++ b/src/services/subject_handler.py @@ -0,0 +1,58 @@ +import uuid +import datetime +from src import mock_database + + +class SubjectHandler: + + def __init__(self, subject_identifier=None) -> None: + # TODO get from database + if subject_identifier: + self.__identifier = uuid.uuid5(uuid.NAMESPACE_URL, subject_identifier).hex + else: + self.__identifier = uuid.uuid5( + uuid.NAMESPACE_URL, str(datetime.datetime.now()) + ) + self.__state = None + self.__enrolled_students = [] + self.__course = None + self.__max_enrollment = 0 + if subject_identifier: + # TODO get from database + self.__course = mock_database.SUBJECT + self.__max_enrollment = mock_database.SUBJECT_MAX_ENROLL + + @property + def identifier(self): + return self.__identifier + + @property + def state(self): + return self.__state + + @property + def enrolled_students(self): + return self.__enrolled_students + + @property + def course(self): + return self.__course + + @property + def name(self): + return self.__name + + @name.setter + def name(self, value): + self.__name = value + + @property + def max_enrollment(self): + return self.__max_enrollment + + @max_enrollment.setter + def max_enrollment(self, value): + self.__max_enrollment = value + + def is_available(self): + return len(self.enrolled_students) < self.__max_enrollment diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 0000000..b9d48da --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,48 @@ +from src.services.student_handler import StudentHandler +from src.services.course_handler import CourseHandler +from src.services.subject_handler import SubjectHandler +from src.services.semester_monitor import SemesterHandler + + +def test_semester_model(): + semester = SemesterHandler() + + assert semester.identifier == "2024-1" + assert semester.state == "open" + + +def test_subject_model(): + subject = SubjectHandler() + subject.name = "any_name" + + assert subject.name == "any_name" + assert subject.identifier is not None + assert subject.state == None + assert subject.enrolled_students == [] + assert subject.max_enrollment == 0 + assert subject.course == None + + +def test_course_model(): + course = CourseHandler() + course.name = "any_name" + + assert course.name == "any_name" + assert course.identifier is not None + assert course.state == "inactive" + assert course.enrolled_students == [] + assert course.max_enrollment == 0 + assert course.subjects == [] + + +def test_student_model(): + student = StudentHandler() + student.name = "any_name" + student.cpf = "123.456.789-10" + + assert student.name == "any_name" + assert student.cpf == "123.456.789-10" + assert student.identifier is None + assert student.state == None + assert student.gpa == 0 + assert student.subjects == [] diff --git a/tests/test_student.py b/tests/test_student.py new file mode 100644 index 0000000..0c211db --- /dev/null +++ b/tests/test_student.py @@ -0,0 +1,80 @@ +import pytest +from src.services.student_handler import ( + StudentHandler, + NonValidStudent, + NonValidSubject, +) +from src import mock_database + + +@pytest.fixture(autouse=True) +def restart_database(): + mock_database.SUBJECT = "any" + mock_database.SUBJECT_MAX_ENROLL = 10 + + +# def test_lock_course(): +# student = Student() +# student.name = "any" +# student.cpf = "123.456.789-10" +# subject = "any" + +# student.enroll_to_course("any") +# student.take_subject(subject) +# assert subject in student.subjects + + +def test_take_full_subject_from_course_return_error(): + student = StudentHandler() + student.name = "any" + student.cpf = "123.456.789-10" + subject = "invalid" + mock_database.SUBJECT = "invalid" + mock_database.SUBJECT_MAX_ENROLL = -1 + + student.enroll_to_course("any") + with pytest.raises(NonValidSubject): + student.take_subject(subject) + + +def test_take_invalid_subject_from_course_return_error(): + student = StudentHandler() + student.name = "any" + student.cpf = "123.456.789-10" + subject = "invalid" + mock_database.SUBJECT = "invalid" + + student.enroll_to_course("any") + with pytest.raises(NonValidSubject): + student.take_subject(subject) + + +def test_take_subject_from_course(): + student = StudentHandler() + student.name = "any" + student.cpf = "123.456.789-10" + subject = "any" + + student.enroll_to_course("any") + student.take_subject(subject) + assert subject in student.subjects + + +def test_enroll_invalid_student_to_course_retunr_error(): + student = StudentHandler() + student.name = "invalid" + student.cpf = "123.456.789-10" + + with pytest.raises(NonValidStudent): + student.enroll_to_course("any") + + +def test_enroll_student_to_course(): + student = StudentHandler() + student.name = "any" + student.cpf = "123.456.789-10" + + student.enroll_to_course("any") + + # generate using student name, cpf and course + assert student.identifier == "290f2113c2e6579c8bb6ec395ea56572" From 093328a39fa306d221d78170bb80bdac19ff45fb Mon Sep 17 00:00:00 2001 From: Douglas Cardoso Date: Wed, 10 Apr 2024 03:13:28 -0300 Subject: [PATCH 04/44] Add fake database --- src/services/student_handler.py | 35 ++++++++++++++++++++++++- tests/test_student.py | 46 ++++++++++++++++++++++++++------- 2 files changed, 71 insertions(+), 10 deletions(-) diff --git a/src/services/student_handler.py b/src/services/student_handler.py index c90d19f..efbe243 100644 --- a/src/services/student_handler.py +++ b/src/services/student_handler.py @@ -4,13 +4,27 @@ class StudentHandler: - def __init__(self) -> None: + LOCKED = "locked" + + def __init__(self, database=None): self.__identifier = None self.__state = None self.__gpa = 0 self.__subjects = [] self.__course = None + self.__database = database + if not database: + + class Database: + class DbStudent: + name = None + state = None + + student = DbStudent() + + self.__database = Database() + @property def identifier(self): return self.__identifier @@ -47,6 +61,13 @@ def cpf(self): def cpf(self, value): self.__cpf = value + def __is_locked(self): + return self.__state == self.LOCKED + + def __save(self): + self.__database.student.name = self.name + self.__database.student.state = self.state + def enroll_to_course(self, course_identifier): enrollment_validator = EnrollmentValidator() is_valid_student = enrollment_validator.validate_student( @@ -70,6 +91,9 @@ def take_subject(self, subject_identifier): if not is_valid_student: raise NonValidStudent() + if self.__is_locked(): + raise NonValidStudent + subject_handler = SubjectHandler(subject_identifier) if subject_handler.course != self.__course: raise NonValidSubject() @@ -78,6 +102,15 @@ def take_subject(self, subject_identifier): return self.subjects.append(subject_identifier) + def unlock_course(self): + self.__state = None + return self.state + + def lock_course(self): + self.__state = self.LOCKED + self.__save() + return self.state + class NonValidStudent(Exception): pass diff --git a/tests/test_student.py b/tests/test_student.py index 0c211db..498bd2c 100644 --- a/tests/test_student.py +++ b/tests/test_student.py @@ -13,15 +13,44 @@ def restart_database(): mock_database.SUBJECT_MAX_ENROLL = 10 -# def test_lock_course(): -# student = Student() -# student.name = "any" -# student.cpf = "123.456.789-10" -# subject = "any" +def test_unlock_course(): + student = StudentHandler() + student.name = "any" + student.cpf = "123.456.789-10" + + student.unlock_course() + assert student.state == None + + +def test_lock_course(): + class Database: + class DbStudent: + name = None + state = None + + student = DbStudent() + + database = Database() + student = StudentHandler(database) + student.name = "any" + student.cpf = "123.456.789-10" + + student.lock_course() + assert student.state == "locked" + assert database.student.state == "locked" -# student.enroll_to_course("any") -# student.take_subject(subject) -# assert subject in student.subjects + +def test_take_subject_from_course_when_locked_stuend_return_error(): + + student = StudentHandler() + student.name = "any" + student.cpf = "123.456.789-10" + student.lock_course() + subject = "any" + + student.enroll_to_course("any") + with pytest.raises(NonValidStudent): + student.take_subject(subject) def test_take_full_subject_from_course_return_error(): @@ -29,7 +58,6 @@ def test_take_full_subject_from_course_return_error(): student.name = "any" student.cpf = "123.456.789-10" subject = "invalid" - mock_database.SUBJECT = "invalid" mock_database.SUBJECT_MAX_ENROLL = -1 student.enroll_to_course("any") From 893886749fff15f54555ddc4fccf192f8ebb2be9 Mon Sep 17 00:00:00 2001 From: Douglas Cardoso Date: Wed, 10 Apr 2024 16:31:56 -0300 Subject: [PATCH 05/44] Add database to StudentHanler --- src/mock_database.py | 8 ++++++++ src/services/student_handler.py | 13 +------------ tests/test_models.py | 4 +++- tests/test_student.py | 31 +++++++++++++++---------------- 4 files changed, 27 insertions(+), 29 deletions(-) diff --git a/src/mock_database.py b/src/mock_database.py index 9c60eb3..fee5f23 100644 --- a/src/mock_database.py +++ b/src/mock_database.py @@ -1,2 +1,10 @@ SUBJECT = "any" SUBJECT_MAX_ENROLL = 10 + + +class Database: + class DbStudent: + name = None + state = None + + student = DbStudent() diff --git a/src/services/student_handler.py b/src/services/student_handler.py index efbe243..2cd2bf5 100644 --- a/src/services/student_handler.py +++ b/src/services/student_handler.py @@ -6,24 +6,13 @@ class StudentHandler: LOCKED = "locked" - def __init__(self, database=None): + def __init__(self, database): self.__identifier = None self.__state = None self.__gpa = 0 self.__subjects = [] self.__course = None - self.__database = database - if not database: - - class Database: - class DbStudent: - name = None - state = None - - student = DbStudent() - - self.__database = Database() @property def identifier(self): diff --git a/tests/test_models.py b/tests/test_models.py index b9d48da..ef514e2 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -2,6 +2,7 @@ from src.services.course_handler import CourseHandler from src.services.subject_handler import SubjectHandler from src.services.semester_monitor import SemesterHandler +from src import mock_database def test_semester_model(): @@ -36,7 +37,8 @@ def test_course_model(): def test_student_model(): - student = StudentHandler() + database = mock_database.Database() + student = StudentHandler(database) student.name = "any_name" student.cpf = "123.456.789-10" diff --git a/tests/test_student.py b/tests/test_student.py index 498bd2c..850b2c0 100644 --- a/tests/test_student.py +++ b/tests/test_student.py @@ -14,7 +14,8 @@ def restart_database(): def test_unlock_course(): - student = StudentHandler() + database = mock_database.Database() + student = StudentHandler(database) student.name = "any" student.cpf = "123.456.789-10" @@ -23,14 +24,7 @@ def test_unlock_course(): def test_lock_course(): - class Database: - class DbStudent: - name = None - state = None - - student = DbStudent() - - database = Database() + database = mock_database.Database() student = StudentHandler(database) student.name = "any" student.cpf = "123.456.789-10" @@ -41,8 +35,8 @@ class DbStudent: def test_take_subject_from_course_when_locked_stuend_return_error(): - - student = StudentHandler() + database = mock_database.Database() + student = StudentHandler(database) student.name = "any" student.cpf = "123.456.789-10" student.lock_course() @@ -54,7 +48,8 @@ def test_take_subject_from_course_when_locked_stuend_return_error(): def test_take_full_subject_from_course_return_error(): - student = StudentHandler() + database = mock_database.Database() + student = StudentHandler(database) student.name = "any" student.cpf = "123.456.789-10" subject = "invalid" @@ -66,7 +61,8 @@ def test_take_full_subject_from_course_return_error(): def test_take_invalid_subject_from_course_return_error(): - student = StudentHandler() + database = mock_database.Database() + student = StudentHandler(database) student.name = "any" student.cpf = "123.456.789-10" subject = "invalid" @@ -78,7 +74,8 @@ def test_take_invalid_subject_from_course_return_error(): def test_take_subject_from_course(): - student = StudentHandler() + database = mock_database.Database() + student = StudentHandler(database) student.name = "any" student.cpf = "123.456.789-10" subject = "any" @@ -89,7 +86,8 @@ def test_take_subject_from_course(): def test_enroll_invalid_student_to_course_retunr_error(): - student = StudentHandler() + database = mock_database.Database() + student = StudentHandler(database) student.name = "invalid" student.cpf = "123.456.789-10" @@ -98,7 +96,8 @@ def test_enroll_invalid_student_to_course_retunr_error(): def test_enroll_student_to_course(): - student = StudentHandler() + database = mock_database.Database() + student = StudentHandler(database) student.name = "any" student.cpf = "123.456.789-10" From aa1c631107f4b924f7b2ae390f4052ae40a604ab Mon Sep 17 00:00:00 2001 From: Douglas Cardoso Date: Wed, 10 Apr 2024 18:39:47 -0300 Subject: [PATCH 06/44] Student CLI working. Adding database to Enrollment --- README.md | 17 +++++++++++ app.py | 1 - src/mock_database.py | 10 ------- src/services/enrollment_validator.py | 14 +++++---- src/services/student_handler.py | 43 +++++++++++++++++++--------- src/services/subject_handler.py | 6 ++-- tests/test_models.py | 12 ++++++-- tests/test_student.py | 40 +++++++++++++++----------- 8 files changed, 90 insertions(+), 53 deletions(-) delete mode 100644 app.py delete mode 100644 src/mock_database.py diff --git a/README.md b/README.md index c31ba83..afd29ea 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,23 @@ sqlite3 university.db sqlite> .quit ``` +# CLI Usage +``` +python cli.py --help +Usage: cli.py [OPTIONS] + +Options: + --name TEXT Name of the student. + --cpf TEXT CPD of the student. + --course-identifier TEXT Course number identifier. + --help Show this message and exit. + + +# enroll student to course +python cli.py --name any --cpf 123.456.789-10 --course-identifier any + +``` + Below is the specification of the application: Definition of Done: diff --git a/app.py b/app.py deleted file mode 100644 index dc2b1df..0000000 --- a/app.py +++ /dev/null @@ -1 +0,0 @@ -# Coloque o código do CLI aqui \ No newline at end of file diff --git a/src/mock_database.py b/src/mock_database.py deleted file mode 100644 index fee5f23..0000000 --- a/src/mock_database.py +++ /dev/null @@ -1,10 +0,0 @@ -SUBJECT = "any" -SUBJECT_MAX_ENROLL = 10 - - -class Database: - class DbStudent: - name = None - state = None - - student = DbStudent() diff --git a/src/services/enrollment_validator.py b/src/services/enrollment_validator.py index 9b0552f..739ef8b 100644 --- a/src/services/enrollment_validator.py +++ b/src/services/enrollment_validator.py @@ -1,14 +1,16 @@ import uuid +from src import mocks class EnrollmentValidator: + def __init__(self, database=None): + database = mocks.Database() + self.__database = database + def validate_student(self, name, cpf, course_identifier): - # TODO do the true check - dummy_identifier = "290f2113c2e6579c8bb6ec395ea56572" - return ( - self.generate_student_identifier(name, cpf, course_identifier) - == dummy_identifier - ) + # the valid students are predifined as the list of approved person in the given course + identifier = self.generate_student_identifier(name, cpf, course_identifier) + return identifier in self.__database.enrollment.select(identifier) def generate_student_identifier(self, name, cpf, course_identifier): return uuid.uuid5( diff --git a/src/services/student_handler.py b/src/services/student_handler.py index 2cd2bf5..519de24 100644 --- a/src/services/student_handler.py +++ b/src/services/student_handler.py @@ -5,6 +5,7 @@ class StudentHandler: LOCKED = "locked" + ENROLLED = "enrolled" def __init__(self, database): self.__identifier = None @@ -53,30 +54,39 @@ def cpf(self, value): def __is_locked(self): return self.__state == self.LOCKED - def __save(self): + def __is_valid_student(self, course_identifier=None): + course = self.__course + if course_identifier: + course = course_identifier + return EnrollmentValidator().validate_student(self.name, self.cpf, course) + + def save(self): self.__database.student.name = self.name self.__database.student.state = self.state + self.__database.student.cpf = self.cpf + self.__database.student.identifier = self.identifier + self.__database.student.gpa = self.gpa + self.__database.student.subjects = self.subjects + self.__database.student.course = self.__course + self.__database.student.save() def enroll_to_course(self, course_identifier): - enrollment_validator = EnrollmentValidator() - is_valid_student = enrollment_validator.validate_student( - self.name, self.cpf, course_identifier - ) + is_valid_student = self.__is_valid_student(course_identifier) if is_valid_student: + enrollment_validator = EnrollmentValidator() self.__identifier = enrollment_validator.generate_student_identifier( self.name, self.cpf, course_identifier ) self.__course = course_identifier course = CourseHandler(course_identifier) course.enroll_student(self.identifier) + self.__state = self.ENROLLED + self.save() return self.identifier in course.enrolled_students raise NonValidStudent() def take_subject(self, subject_identifier): - enrollment_validator = EnrollmentValidator() - is_valid_student = enrollment_validator.validate_student( - self.name, self.cpf, self.__course - ) + is_valid_student = self.__is_valid_student() if not is_valid_student: raise NonValidStudent() @@ -92,13 +102,18 @@ def take_subject(self, subject_identifier): return self.subjects.append(subject_identifier) def unlock_course(self): - self.__state = None - return self.state + if self.__is_valid_student(): + self.__state = None + self.save() + return self.state + raise NonValidStudent() def lock_course(self): - self.__state = self.LOCKED - self.__save() - return self.state + if self.__is_valid_student(): + self.__state = self.LOCKED + self.save() + return self.state + raise NonValidStudent() class NonValidStudent(Exception): diff --git a/src/services/subject_handler.py b/src/services/subject_handler.py index 38fd162..ead1c5f 100644 --- a/src/services/subject_handler.py +++ b/src/services/subject_handler.py @@ -1,6 +1,6 @@ import uuid import datetime -from src import mock_database +from src import mocks class SubjectHandler: @@ -19,8 +19,8 @@ def __init__(self, subject_identifier=None) -> None: self.__max_enrollment = 0 if subject_identifier: # TODO get from database - self.__course = mock_database.SUBJECT - self.__max_enrollment = mock_database.SUBJECT_MAX_ENROLL + self.__course = mocks.SUBJECT + self.__max_enrollment = mocks.SUBJECT_MAX_ENROLL @property def identifier(self): diff --git a/tests/test_models.py b/tests/test_models.py index ef514e2..aed818e 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -2,7 +2,7 @@ from src.services.course_handler import CourseHandler from src.services.subject_handler import SubjectHandler from src.services.semester_monitor import SemesterHandler -from src import mock_database +from src import mocks def test_semester_model(): @@ -37,10 +37,11 @@ def test_course_model(): def test_student_model(): - database = mock_database.Database() + database = mocks.Database() student = StudentHandler(database) student.name = "any_name" student.cpf = "123.456.789-10" + student.save() assert student.name == "any_name" assert student.cpf == "123.456.789-10" @@ -48,3 +49,10 @@ def test_student_model(): assert student.state == None assert student.gpa == 0 assert student.subjects == [] + + assert database.student.name == "any_name" + assert database.student.cpf == "123.456.789-10" + assert database.student.identifier is None + assert database.student.state == None + assert database.student.gpa == 0 + assert database.student.subjects == [] diff --git a/tests/test_student.py b/tests/test_student.py index 850b2c0..d9fa900 100644 --- a/tests/test_student.py +++ b/tests/test_student.py @@ -4,56 +4,57 @@ NonValidStudent, NonValidSubject, ) -from src import mock_database +from src import mocks @pytest.fixture(autouse=True) def restart_database(): - mock_database.SUBJECT = "any" - mock_database.SUBJECT_MAX_ENROLL = 10 + mocks.SUBJECT = "any" + mocks.SUBJECT_MAX_ENROLL = 10 def test_unlock_course(): - database = mock_database.Database() + database = mocks.Database() student = StudentHandler(database) student.name = "any" student.cpf = "123.456.789-10" + student.enroll_to_course("any") student.unlock_course() assert student.state == None def test_lock_course(): - database = mock_database.Database() + database = mocks.Database() student = StudentHandler(database) student.name = "any" student.cpf = "123.456.789-10" + student.enroll_to_course("any") - student.lock_course() - assert student.state == "locked" + assert student.lock_course() == "locked" assert database.student.state == "locked" -def test_take_subject_from_course_when_locked_stuend_return_error(): - database = mock_database.Database() +def test_take_subject_from_course_when_locked_student_return_error(): + database = mocks.Database() student = StudentHandler(database) student.name = "any" student.cpf = "123.456.789-10" - student.lock_course() subject = "any" student.enroll_to_course("any") + student.lock_course() with pytest.raises(NonValidStudent): student.take_subject(subject) def test_take_full_subject_from_course_return_error(): - database = mock_database.Database() + database = mocks.Database() student = StudentHandler(database) student.name = "any" student.cpf = "123.456.789-10" subject = "invalid" - mock_database.SUBJECT_MAX_ENROLL = -1 + mocks.SUBJECT_MAX_ENROLL = -1 student.enroll_to_course("any") with pytest.raises(NonValidSubject): @@ -61,12 +62,12 @@ def test_take_full_subject_from_course_return_error(): def test_take_invalid_subject_from_course_return_error(): - database = mock_database.Database() + database = mocks.Database() student = StudentHandler(database) student.name = "any" student.cpf = "123.456.789-10" subject = "invalid" - mock_database.SUBJECT = "invalid" + mocks.SUBJECT = "invalid" student.enroll_to_course("any") with pytest.raises(NonValidSubject): @@ -74,7 +75,7 @@ def test_take_invalid_subject_from_course_return_error(): def test_take_subject_from_course(): - database = mock_database.Database() + database = mocks.Database() student = StudentHandler(database) student.name = "any" student.cpf = "123.456.789-10" @@ -86,7 +87,7 @@ def test_take_subject_from_course(): def test_enroll_invalid_student_to_course_retunr_error(): - database = mock_database.Database() + database = mocks.Database() student = StudentHandler(database) student.name = "invalid" student.cpf = "123.456.789-10" @@ -96,7 +97,7 @@ def test_enroll_invalid_student_to_course_retunr_error(): def test_enroll_student_to_course(): - database = mock_database.Database() + database = mocks.Database() student = StudentHandler(database) student.name = "any" student.cpf = "123.456.789-10" @@ -105,3 +106,8 @@ def test_enroll_student_to_course(): # generate using student name, cpf and course assert student.identifier == "290f2113c2e6579c8bb6ec395ea56572" + + assert database.student.identifier == "290f2113c2e6579c8bb6ec395ea56572" + assert database.student.state == "enrolled" + assert database.student.course == "any" + assert database.student.subjects == [] From 8996d8550215fa303a656d0b2e80874f7cf588f5 Mon Sep 17 00:00:00 2001 From: Douglas Cardoso Date: Thu, 11 Apr 2024 01:30:49 -0300 Subject: [PATCH 07/44] Add database to Course --- cli.py | 30 +++++++ for_admin.py | 14 +++ manual_test.sh | 4 + src/cli_helper.py | 20 +++++ src/database.py | 129 +++++++++++++++++++++++++++ src/mocks.py | 50 +++++++++++ src/services/course_handler.py | 76 +++++++++++++++- src/services/enrollment_validator.py | 10 +-- src/services/student_handler.py | 36 ++++---- tests/test_cli.py | 26 ++++++ tests/test_course.py | 107 ++++++++++++++++++++++ tests/test_models.py | 27 ++++-- 12 files changed, 497 insertions(+), 32 deletions(-) create mode 100644 cli.py create mode 100644 for_admin.py create mode 100755 manual_test.sh create mode 100644 src/cli_helper.py create mode 100644 src/database.py create mode 100644 src/mocks.py create mode 100644 tests/test_cli.py create mode 100644 tests/test_course.py diff --git a/cli.py b/cli.py new file mode 100644 index 0000000..672d3a1 --- /dev/null +++ b/cli.py @@ -0,0 +1,30 @@ +import logging +import click +from src import cli_helper +from src.database import Database + +logging.basicConfig( + filename="cli.log", + datefmt="%Y-%m-%d %H:%M:%S", + format="%(asctime)s - %(levelname)s: %(message)s", + filemode="a", +) + + +@click.command() +@click.option("--name", prompt="Student name", help="Name of the student.") +@click.option( + "--cpf", prompt="Student CPF. E.g. 123.456.789-10", help="CPF of the student." +) +@click.option( + "--course-identifier", + prompt="Course number identifier", + help="Course number identifier.", +) +def enroll_student(name, cpf, course_identifier): + database = Database() + cli_helper.enroll_student(database, name, cpf, course_identifier) + + +if __name__ == "__main__": + enroll_student() diff --git a/for_admin.py b/for_admin.py new file mode 100644 index 0000000..21b14ac --- /dev/null +++ b/for_admin.py @@ -0,0 +1,14 @@ +from src.database import Database + +db = Database() +# TODO need to check if the courses are available +db.enrollment.populate("douglas", "098.765.432.12", "adm") +db.enrollment.populate("maria", "028.745.462.18", "mat") +db.enrollment.populate("joana", "038.745.452.19", "port") +db.enrollment.populate("any", "123.456.789-10", "any") + +db.course.populate("adm") +db.course.populate("mat") +db.course.populate("port") +db.course.populate("any") +db.course.populate("noise") diff --git a/manual_test.sh b/manual_test.sh new file mode 100755 index 0000000..af82432 --- /dev/null +++ b/manual_test.sh @@ -0,0 +1,4 @@ +set -x +python cli.py --name any --cpf 123.456.789-10 --course-identifier any +python cli.py --name douglas --cpf 098.765.432.12 --course-identifier adm +python cli.py --name any --cpf 123.456.789-10 --course-identifier invalid \ No newline at end of file diff --git a/src/cli_helper.py b/src/cli_helper.py new file mode 100644 index 0000000..68fe911 --- /dev/null +++ b/src/cli_helper.py @@ -0,0 +1,20 @@ +import logging +from src.services.student_handler import StudentHandler, NonValidStudent + + +def enroll_student(database, name, cpf, course_identifier): + try: + student = StudentHandler(database) + student.name = name + student.cpf = cpf + student.enroll_to_course(course_identifier) + print("Student enrolled.") + return True + except NonValidStudent: + print( + f"Student '{name}' with CPF '{cpf}' is not valid in course '{course_identifier}'" + ) + except Exception as e: + logging.error(str(e)) + print("Unexpected error. Consult the system adminstrator.") + return False diff --git a/src/database.py b/src/database.py new file mode 100644 index 0000000..b4fcdd3 --- /dev/null +++ b/src/database.py @@ -0,0 +1,129 @@ +import sqlite3 + +# TODO test concurrency +DATABASE_NAME = "university.db" +con = sqlite3.connect(DATABASE_NAME) +cur = con.cursor() + + +class Database: + class DbStudent: + TABLE = "student" + name = None + state = None + cpf = None + identifier = None + gpa = None + subjects = None + course = None + + def save(self): + cur.execute( + f""" + INSERT INTO {self.TABLE} VALUES + ('{self.name}', + '{self.state}', + '{self.cpf}', + '{self.identifier}', + {self.gpa}, + '{self.subjects}', + '{self.course}') + """ + ) + con.commit() + + def __init__(self): + cur.execute( + f"CREATE TABLE IF NOT EXISTS {self.TABLE} (name, state, cpf, identifier, gpa, subjects, course)" + ) + + class DbEnrollment: + TABLE = "enrollment" + + def __init__(self): + cur.execute(f"CREATE TABLE IF NOT EXISTS {self.TABLE} (student_identifier)") + + # Just for admin. The university has a predefined list of approved students to each course. + # TODO create a public funtion + def populate(self, name, cpf, course_identifier): + import uuid + + student_identifier = uuid.uuid5( + uuid.NAMESPACE_URL, str(f"{name}{cpf}{course_identifier}") + ).hex + cur.execute( + f""" + INSERT INTO {self.TABLE} VALUES ('{student_identifier}') + """ + ) + con.commit() + + def select(self, student_identifier): + return ( + cur.execute( + f"SELECT * FROM {self.TABLE} WHERE student_identifier = '{student_identifier}'" + ).fetchone() + is not None + ) + + class DbCourse: + TABLE = "course" + name = None + state = None + identifier = None + enrolled_students = None + max_enrollment = None + subjects = None + + def __init__(self): + cur.execute( + f"CREATE TABLE IF NOT EXISTS {self.TABLE} (name, state, identifier, enrolled_students, max_enrollment, subjects)" + ) + + # Just for admin. Necessary because there is not a user story to create courses + # TODO create a public funtion + def populate(self, name): + import uuid + + identifier = uuid.uuid5(uuid.NAMESPACE_URL, str(f"{name}")).hex + cur.execute( + f""" + INSERT INTO {self.TABLE} VALUES + ('{name}', + 'active', + '{identifier}', + '[]', + '10', + '[]') + """ + ) + con.commit() + + def save(self): + cur.execute( + f""" + INSERT INTO {self.TABLE} VALUES + ('{self.name}', + '{self.state}', + '{self.identifier}', + {self.enrolled_students}, + '{self.max_enrollment}', + '{self.subjects}') + """ + ) + con.commit() + + def load_from_database(self, name): + result = cur.execute( + f"SELECT * FROM {self.TABLE} WHERE name = '{name}'" + ).fetchone() + self.name = result[0] + self.state = result[1] + self.identifier = result[2] + self.enrolled_students = result[3].split(",") + self.max_enrollment = result[4] + self.subjects = result[5].split(",") + + student = DbStudent() + enrollment = DbEnrollment() + course = DbCourse() diff --git a/src/mocks.py b/src/mocks.py new file mode 100644 index 0000000..1339280 --- /dev/null +++ b/src/mocks.py @@ -0,0 +1,50 @@ +SUBJECT = "any" +SUBJECT_MAX_ENROLL = 10 + + +class Database: + class DbStudent: + name = None + state = None + cpf = None + identifier = None + gpa = None + subjects = None + course = None + + def save(self): + pass + + class DbEnrollment: + def select(self, identifier): + # NAME any, CPF 123.456.789-10, COURSE any + DEFAULT = "290f2113c2e6579c8bb6ec395ea56572" + if identifier == DEFAULT: + return True + return False + + class DbCourse: + name = None + state = None + identifier = None + enrolled_students = None + max_enrollment = None + subjects = None + + def save(self): + pass + + def load_from_database(self, course_identifier): + self.name = "any" + self.state = "active" + self.identifier = None + self.enrolled_students = [] + self.max_enrollment = None + self.subjects = [] + + def select(self, course_identifier): + return "anything not None" + + student = DbStudent() + enrollment = DbEnrollment() + course = DbCourse() diff --git a/src/services/course_handler.py b/src/services/course_handler.py index 7694b58..e33c366 100644 --- a/src/services/course_handler.py +++ b/src/services/course_handler.py @@ -1,18 +1,25 @@ import uuid import datetime +import logging class CourseHandler: + ACTIVE = "active" + INACTIVE = "inactive" + CANCELLED = "cancelled" - def __init__(self, identifier=None) -> None: + def __init__(self, database, identifier=None) -> None: + # TODO check necessity of this conditions. It is weird. if not identifier: self.__identifier = uuid.uuid5( uuid.NAMESPACE_URL, str(datetime.datetime.now()) ) - self.__state = "inactive" # TODO use enum + self.__name = None + self.__state = self.INACTIVE # TODO use enum self.__enrolled_students = [] self.__subjects = [] self.__max_enrollment = 0 + self.__database = database @property def identifier(self): @@ -46,5 +53,70 @@ def max_enrollment(self): def max_enrollment(self, value): self.__max_enrollment = value + def save(self): + self.__database.course.name = self.name + self.__database.course.state = self.state + self.__database.course.identifier = self.identifier + self.__database.course.enrolled_students = self.enrolled_students + self.__database.course.max_enrollment = self.max_enrollment + self.__database.course.subjects = self.subjects + self.__database.course.save() + + def load_from_database(self, name): + try: + self.__database.course.load_from_database(name) + + self.name = self.__database.course.name + self.__state = self.__database.course.state + self.__identifier = self.__database.course.identifier + self.__enrolled_students = self.__database.course.enrolled_students + self.max_enrollment = self.__database.course.max_enrollment + self.__subjects = self.__database.course.subjects + + except Exception as e: + logging.error(str(e)) + raise NonValidCourse("Course not found.") + def enroll_student(self, student_identifier): + if not self.state == self.ACTIVE: + raise NonValidCourse("Course is not active.") self.__enrolled_students.append(student_identifier) + return True + + def add_subject(self, subject): + self.subjects.append(subject) + + def cancel(self): + if not self.name: + raise NonValidCourse("No name set to course.") + self.__state = self.CANCELLED + self.save() + return self.__state + + def deactivate(self): + if not self.name: + raise NonValidCourse("No name set to course.") + + if self.state == self.ACTIVE: + self.__state = self.INACTIVE + self.save() + return self.__state + + def activate(self): + if not self.name: + raise NonValidCourse("No name set to course.") + + MINIMUM = 3 + if not len(self.subjects) >= MINIMUM: + raise NonValidCourse( + f"Need '{MINIMUM}' subjects. Set '{len(self.subjects)}'" + ) + + self.__identifier = uuid.uuid5(uuid.NAMESPACE_URL, f"{self.name}") + self.__state = self.ACTIVE + self.save() + return self.__state + + +class NonValidCourse(Exception): + pass diff --git a/src/services/enrollment_validator.py b/src/services/enrollment_validator.py index 739ef8b..de1f72f 100644 --- a/src/services/enrollment_validator.py +++ b/src/services/enrollment_validator.py @@ -1,16 +1,16 @@ import uuid -from src import mocks class EnrollmentValidator: - def __init__(self, database=None): - database = mocks.Database() + def __init__(self, database): self.__database = database def validate_student(self, name, cpf, course_identifier): # the valid students are predifined as the list of approved person in the given course - identifier = self.generate_student_identifier(name, cpf, course_identifier) - return identifier in self.__database.enrollment.select(identifier) + student_identifier = self.generate_student_identifier( + name, cpf, course_identifier + ) + return self.__database.enrollment.select(student_identifier) def generate_student_identifier(self, name, cpf, course_identifier): return uuid.uuid5( diff --git a/src/services/student_handler.py b/src/services/student_handler.py index 519de24..d92180f 100644 --- a/src/services/student_handler.py +++ b/src/services/student_handler.py @@ -1,5 +1,5 @@ from src.services.enrollment_validator import EnrollmentValidator -from src.services.course_handler import CourseHandler +from src.services.course_handler import CourseHandler, NonValidCourse from src.services.subject_handler import SubjectHandler @@ -58,7 +58,9 @@ def __is_valid_student(self, course_identifier=None): course = self.__course if course_identifier: course = course_identifier - return EnrollmentValidator().validate_student(self.name, self.cpf, course) + return EnrollmentValidator(self.__database).validate_student( + self.name, self.cpf, course + ) def save(self): self.__database.student.name = self.name @@ -70,20 +72,22 @@ def save(self): self.__database.student.course = self.__course self.__database.student.save() - def enroll_to_course(self, course_identifier): - is_valid_student = self.__is_valid_student(course_identifier) - if is_valid_student: - enrollment_validator = EnrollmentValidator() - self.__identifier = enrollment_validator.generate_student_identifier( - self.name, self.cpf, course_identifier - ) - self.__course = course_identifier - course = CourseHandler(course_identifier) - course.enroll_student(self.identifier) - self.__state = self.ENROLLED - self.save() - return self.identifier in course.enrolled_students - raise NonValidStudent() + def enroll_to_course(self, name): + if not self.__is_valid_student(name): + raise NonValidStudent() + + course = CourseHandler(self.__database) + course.load_from_database(name) + + enrollment_validator = EnrollmentValidator(self.__database) + self.__identifier = enrollment_validator.generate_student_identifier( + self.name, self.cpf, name + ) + self.__course = name + course.enroll_student(self.identifier) + self.__state = self.ENROLLED + self.save() + return self.identifier in course.enrolled_students def take_subject(self, subject_identifier): is_valid_student = self.__is_valid_student() diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..2ce7a69 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,26 @@ +from src import cli_helper +from src import mocks + + +def test_enroll_student_to_invalid_course(): + name = "any" + cpf = "123.456.789-10" + course_identifier = "invalid" + database = mocks.Database() + assert cli_helper.enroll_student(database, name, cpf, course_identifier) == False + + +def test_enroll_student_to_invalid_course(): + name = "any" + cpf = "123.456.789-10" + course_identifier = "invalid" + database = mocks.Database() + assert cli_helper.enroll_student(database, name, cpf, course_identifier) == False + + +def test_enroll_student_to_course(): + name = "any" + cpf = "123.456.789-10" + course_identifier = "any" + database = mocks.Database() + assert cli_helper.enroll_student(database, name, cpf, course_identifier) == True diff --git a/tests/test_course.py b/tests/test_course.py new file mode 100644 index 0000000..aaa45d7 --- /dev/null +++ b/tests/test_course.py @@ -0,0 +1,107 @@ +import pytest +from src.services.course_handler import CourseHandler, NonValidCourse +from src import mocks + + +def test_enroll_student_to_inactive_course_return_error(): + database = mocks.Database() + course_handler = CourseHandler(database) + course_handler.name = "adm" + course_handler.add_subject("any1") + course_handler.add_subject("any2") + course_handler.add_subject("any3") + course_handler.deactivate() + + with pytest.raises(NonValidCourse): + course_handler.enroll_student("any") + + +def test_enroll_student_to_active_course(): + database = mocks.Database() + course_handler = CourseHandler(database) + course_handler.name = "adm" + course_handler.add_subject("any1") + course_handler.add_subject("any2") + course_handler.add_subject("any3") + course_handler.activate() + + assert course_handler.enroll_student("any") == True + + assert database.course.enrolled_students == ["any"] + + +def test_cancel_inactive_course(): + database = mocks.Database() + course_handler = CourseHandler(database) + course_handler.name = "adm" + course_handler.add_subject("any1") + course_handler.add_subject("any2") + course_handler.add_subject("any3") + course_handler.deactivate() + assert course_handler.cancel() == "cancelled" + + assert database.course.state == "cancelled" + + +def test_cancel_active_course(): + database = mocks.Database() + course_handler = CourseHandler(database) + course_handler.name = "adm" + course_handler.add_subject("any1") + course_handler.add_subject("any2") + course_handler.add_subject("any3") + course_handler.activate() + assert course_handler.cancel() == "cancelled" + + assert database.course.state == "cancelled" + + +def test_deactivate_non_active_course_return_error(): + database = mocks.Database() + course_handler = CourseHandler(database) + with pytest.raises(NonValidCourse): + course_handler.deactivate() + + assert database.course.state != "inactive" + + +def test_deactivate_course(): + database = mocks.Database() + course_handler = CourseHandler(database) + course_handler.name = "adm" + course_handler.add_subject("any1") + course_handler.add_subject("any2") + course_handler.add_subject("any3") + course_handler.activate() + assert course_handler.deactivate() == "inactive" + + assert database.course.state == "inactive" + + +def test_activate_course_without_minimum_subjects_return_error(): + database = mocks.Database() + course_handler = CourseHandler(database) + course_handler.name = "any" + with pytest.raises(NonValidCourse): + course_handler.activate() + assert database.course.state != "active" + + +def test_activate_course_without_name_return_error(): + database = mocks.Database() + course_handler = CourseHandler(database) + with pytest.raises(NonValidCourse): + course_handler.activate() + assert database.course.state != "active" + + +def test_activate_course(): + database = mocks.Database() + course_handler = CourseHandler(database) + course_handler.name = "adm" + course_handler.add_subject("any1") + course_handler.add_subject("any2") + course_handler.add_subject("any3") + assert course_handler.activate() == "active" + + assert database.course.state == "active" diff --git a/tests/test_models.py b/tests/test_models.py index aed818e..75f1a45 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -25,15 +25,24 @@ def test_subject_model(): def test_course_model(): - course = CourseHandler() - course.name = "any_name" - - assert course.name == "any_name" - assert course.identifier is not None - assert course.state == "inactive" - assert course.enrolled_students == [] - assert course.max_enrollment == 0 - assert course.subjects == [] + database = mocks.Database() + course_handler = CourseHandler(database) + course_handler.name = "any_name" + course_handler.save() + + assert course_handler.name == "any_name" + assert course_handler.identifier is not None + assert course_handler.state == "inactive" + assert course_handler.enrolled_students == [] + assert course_handler.max_enrollment == 0 + assert course_handler.subjects == [] + + assert database.course.name == "any_name" + assert database.course.identifier is not None + assert database.course.state == "inactive" + assert database.course.enrolled_students == [] + assert database.course.max_enrollment == 0 + assert database.course.subjects == [] def test_student_model(): From 0d633ef228a186411feaf3289867a9214afc4fa6 Mon Sep 17 00:00:00 2001 From: Douglas Cardoso Date: Thu, 11 Apr 2024 02:43:15 -0300 Subject: [PATCH 08/44] Add command to activate course --- cli.py | 20 ++++++++++++++++++-- for_admin.py | 1 + manual_test.sh | 7 ++++--- src/cli_helper.py | 25 +++++++++++++++++++++++-- src/database.py | 33 +++++++++++++++++++-------------- src/mocks.py | 2 +- src/services/course_handler.py | 16 +++++++--------- tests/test_cli.py | 8 +++----- tests/test_course.py | 2 +- tests/test_models.py | 4 ++-- 10 files changed, 79 insertions(+), 39 deletions(-) diff --git a/cli.py b/cli.py index 672d3a1..2089d1f 100644 --- a/cli.py +++ b/cli.py @@ -6,11 +6,24 @@ logging.basicConfig( filename="cli.log", datefmt="%Y-%m-%d %H:%M:%S", - format="%(asctime)s - %(levelname)s: %(message)s", + format="%(asctime)s - %(levelname)s: [%(filename)s:%(lineno)s - %(funcName)20s() ] %(message)s", filemode="a", ) +@click.group() +def cli(): + # do nothing # + pass + + +@click.command() +@click.option("--name", prompt="Course name", help="Name of the course.") +def activate_course(name): + database = Database() + cli_helper.activate_course(database, name) + + @click.command() @click.option("--name", prompt="Student name", help="Name of the student.") @click.option( @@ -26,5 +39,8 @@ def enroll_student(name, cpf, course_identifier): cli_helper.enroll_student(database, name, cpf, course_identifier) +cli.add_command(enroll_student) +cli.add_command(activate_course) + if __name__ == "__main__": - enroll_student() + cli() diff --git a/for_admin.py b/for_admin.py index 21b14ac..2b32dc8 100644 --- a/for_admin.py +++ b/for_admin.py @@ -12,3 +12,4 @@ db.course.populate("port") db.course.populate("any") db.course.populate("noise") +db.course.populate("deact") diff --git a/manual_test.sh b/manual_test.sh index af82432..8a0470a 100755 --- a/manual_test.sh +++ b/manual_test.sh @@ -1,4 +1,5 @@ set -x -python cli.py --name any --cpf 123.456.789-10 --course-identifier any -python cli.py --name douglas --cpf 098.765.432.12 --course-identifier adm -python cli.py --name any --cpf 123.456.789-10 --course-identifier invalid \ No newline at end of file +python cli.py enroll-student --name any --cpf 123.456.789-10 --course-identifier any +python cli.py enroll-student --name douglas --cpf 098.765.432.12 --course-identifier adm +python cli.py enroll-student --name any --cpf 123.456.789-10 --course-identifier invalid +python cli.py activate-course --name deact diff --git a/src/cli_helper.py b/src/cli_helper.py index 68fe911..b4f173e 100644 --- a/src/cli_helper.py +++ b/src/cli_helper.py @@ -1,5 +1,25 @@ import logging from src.services.student_handler import StudentHandler, NonValidStudent +from src.services.course_handler import CourseHandler, NonValidCourse + +UNEXPECTED_ERROR = "Unexpected error. Consult the system adminstrator." + + +def activate_course(database, name): + try: + course_handler = CourseHandler(database) + course_handler.name = name + course_handler.load_from_database(name) + course_handler.activate() + print(f"Course activated.") + return True + except NonValidCourse as e: + logging.error(str(e)) + print(f"Course '{name}' is not valid.") + except Exception as e: + logging.error(str(e)) + print(UNEXPECTED_ERROR) + return False def enroll_student(database, name, cpf, course_identifier): @@ -10,11 +30,12 @@ def enroll_student(database, name, cpf, course_identifier): student.enroll_to_course(course_identifier) print("Student enrolled.") return True - except NonValidStudent: + except NonValidStudent as e: + logging.error(str(e)) print( f"Student '{name}' with CPF '{cpf}' is not valid in course '{course_identifier}'" ) except Exception as e: logging.error(str(e)) - print("Unexpected error. Consult the system adminstrator.") + print(UNEXPECTED_ERROR) return False diff --git a/src/database.py b/src/database.py index b4fcdd3..b2cae36 100644 --- a/src/database.py +++ b/src/database.py @@ -1,4 +1,5 @@ import sqlite3 +import logging # TODO test concurrency DATABASE_NAME = "university.db" @@ -92,26 +93,30 @@ def populate(self, name): ('{name}', 'active', '{identifier}', - '[]', + '', '10', - '[]') + 'any1,any2,any3') """ ) con.commit() def save(self): - cur.execute( - f""" - INSERT INTO {self.TABLE} VALUES - ('{self.name}', - '{self.state}', - '{self.identifier}', - {self.enrolled_students}, - '{self.max_enrollment}', - '{self.subjects}') - """ - ) - con.commit() + try: + cur.execute( + f""" + INSERT INTO {self.TABLE} VALUES + ('{self.name}', + '{self.state}', + '{self.identifier}', + '{self.enrolled_students}', + '{self.max_enrollment}', + '{self.subjects}') + """ + ) + con.commit() + except Exception as e: + logging.error(str(e)) + raise def load_from_database(self, name): result = cur.execute( diff --git a/src/mocks.py b/src/mocks.py index 1339280..b1784ba 100644 --- a/src/mocks.py +++ b/src/mocks.py @@ -40,7 +40,7 @@ def load_from_database(self, course_identifier): self.identifier = None self.enrolled_students = [] self.max_enrollment = None - self.subjects = [] + self.subjects = ["any1", "any2", "any3"] def select(self, course_identifier): return "anything not None" diff --git a/src/services/course_handler.py b/src/services/course_handler.py index e33c366..1735e70 100644 --- a/src/services/course_handler.py +++ b/src/services/course_handler.py @@ -8,12 +8,8 @@ class CourseHandler: INACTIVE = "inactive" CANCELLED = "cancelled" - def __init__(self, database, identifier=None) -> None: - # TODO check necessity of this conditions. It is weird. - if not identifier: - self.__identifier = uuid.uuid5( - uuid.NAMESPACE_URL, str(datetime.datetime.now()) - ) + def __init__(self, database, identifier=-1) -> None: + self.__identifier = identifier self.__name = None self.__state = self.INACTIVE # TODO use enum self.__enrolled_students = [] @@ -57,9 +53,9 @@ def save(self): self.__database.course.name = self.name self.__database.course.state = self.state self.__database.course.identifier = self.identifier - self.__database.course.enrolled_students = self.enrolled_students + self.__database.course.enrolled_students = ",".join(self.enrolled_students) self.__database.course.max_enrollment = self.max_enrollment - self.__database.course.subjects = self.subjects + self.__database.course.subjects = ",".join(self.subjects) self.__database.course.save() def load_from_database(self, name): @@ -81,10 +77,12 @@ def enroll_student(self, student_identifier): if not self.state == self.ACTIVE: raise NonValidCourse("Course is not active.") self.__enrolled_students.append(student_identifier) + self.save() return True def add_subject(self, subject): self.subjects.append(subject) + self.save() def cancel(self): if not self.name: @@ -112,7 +110,7 @@ def activate(self): f"Need '{MINIMUM}' subjects. Set '{len(self.subjects)}'" ) - self.__identifier = uuid.uuid5(uuid.NAMESPACE_URL, f"{self.name}") + self.__identifier = uuid.uuid5(uuid.NAMESPACE_URL, f"{self.name}").hex self.__state = self.ACTIVE self.save() return self.__state diff --git a/tests/test_cli.py b/tests/test_cli.py index 2ce7a69..90639c3 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -2,12 +2,10 @@ from src import mocks -def test_enroll_student_to_invalid_course(): - name = "any" - cpf = "123.456.789-10" - course_identifier = "invalid" +def test_activate_course(): + name = "deact" database = mocks.Database() - assert cli_helper.enroll_student(database, name, cpf, course_identifier) == False + assert cli_helper.activate_course(database, name) == True def test_enroll_student_to_invalid_course(): diff --git a/tests/test_course.py b/tests/test_course.py index aaa45d7..d68f808 100644 --- a/tests/test_course.py +++ b/tests/test_course.py @@ -27,7 +27,7 @@ def test_enroll_student_to_active_course(): assert course_handler.enroll_student("any") == True - assert database.course.enrolled_students == ["any"] + assert database.course.enrolled_students == "any" def test_cancel_inactive_course(): diff --git a/tests/test_models.py b/tests/test_models.py index 75f1a45..74390a0 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -40,9 +40,9 @@ def test_course_model(): assert database.course.name == "any_name" assert database.course.identifier is not None assert database.course.state == "inactive" - assert database.course.enrolled_students == [] + assert database.course.enrolled_students == "" assert database.course.max_enrollment == 0 - assert database.course.subjects == [] + assert database.course.subjects == "" def test_student_model(): From 309cf79944785d45109853456cef9b73be0c21aa Mon Sep 17 00:00:00 2001 From: Douglas Cardoso Date: Thu, 11 Apr 2024 15:36:30 -0300 Subject: [PATCH 09/44] Add mock database to Subject --- cli.py | 16 +++++++++ for_admin.py | 1 + manual_test.sh | 6 +++- src/cli_helper.py | 33 ++++++++++++++++- src/mocks.py | 21 +++++++++++ src/services/course_handler.py | 5 ++- src/services/student_handler.py | 9 +++-- src/services/subject_handler.py | 64 ++++++++++++++++++++++++++++----- tests/test_cli.py | 14 +++++++- tests/test_models.py | 11 +++++- tests/test_student.py | 9 +++-- tests/test_subject.py | 44 +++++++++++++++++++++++ 12 files changed, 213 insertions(+), 20 deletions(-) create mode 100644 tests/test_subject.py diff --git a/cli.py b/cli.py index 2089d1f..2d951aa 100644 --- a/cli.py +++ b/cli.py @@ -17,6 +17,20 @@ def cli(): pass +@click.command() +@click.option("--name", prompt="Course name", help="Name of the course.") +def cancel_course(name): + database = Database() + cli_helper.cancel_course(database, name) + + +@click.command() +@click.option("--name", prompt="Course name", help="Name of the course.") +def deactivate_course(name): + database = Database() + cli_helper.deactivate_course(database, name) + + @click.command() @click.option("--name", prompt="Course name", help="Name of the course.") def activate_course(name): @@ -41,6 +55,8 @@ def enroll_student(name, cpf, course_identifier): cli.add_command(enroll_student) cli.add_command(activate_course) +cli.add_command(deactivate_course) +cli.add_command(cancel_course) if __name__ == "__main__": cli() diff --git a/for_admin.py b/for_admin.py index 2b32dc8..3da379d 100644 --- a/for_admin.py +++ b/for_admin.py @@ -13,3 +13,4 @@ db.course.populate("any") db.course.populate("noise") db.course.populate("deact") +db.course.populate("act") diff --git a/manual_test.sh b/manual_test.sh index 8a0470a..cc0ec9e 100755 --- a/manual_test.sh +++ b/manual_test.sh @@ -1,5 +1,9 @@ -set -x +# set -x +rm university.db +python for_admin.py python cli.py enroll-student --name any --cpf 123.456.789-10 --course-identifier any python cli.py enroll-student --name douglas --cpf 098.765.432.12 --course-identifier adm python cli.py enroll-student --name any --cpf 123.456.789-10 --course-identifier invalid python cli.py activate-course --name deact +python cli.py deactivate-course --name act +python cli.py cancel-course --name any diff --git a/src/cli_helper.py b/src/cli_helper.py index b4f173e..06c7953 100644 --- a/src/cli_helper.py +++ b/src/cli_helper.py @@ -5,10 +5,41 @@ UNEXPECTED_ERROR = "Unexpected error. Consult the system adminstrator." +def cancel_course(database, name): + try: + course_handler = CourseHandler(database) + course_handler.load_from_database(name) + course_handler.cancel() + print(f"Course cancelled.") + return True + except NonValidCourse as e: + logging.error(str(e)) + print(f"Course '{name}' is not valid.") + except Exception as e: + logging.error(str(e)) + print(UNEXPECTED_ERROR) + return False + + +def deactivate_course(database, name): + try: + course_handler = CourseHandler(database) + course_handler.load_from_database(name) + course_handler.deactivate() + print(f"Course deactivated.") + return True + except NonValidCourse as e: + logging.error(str(e)) + print(f"Course '{name}' is not valid.") + except Exception as e: + logging.error(str(e)) + print(UNEXPECTED_ERROR) + return False + + def activate_course(database, name): try: course_handler = CourseHandler(database) - course_handler.name = name course_handler.load_from_database(name) course_handler.activate() print(f"Course activated.") diff --git a/src/mocks.py b/src/mocks.py index b1784ba..20066d0 100644 --- a/src/mocks.py +++ b/src/mocks.py @@ -1,5 +1,6 @@ SUBJECT = "any" SUBJECT_MAX_ENROLL = 10 +COURSE = "any" class Database: @@ -45,6 +46,26 @@ def load_from_database(self, course_identifier): def select(self, course_identifier): return "anything not None" + class DbSubject: + name = None + state = None + enrolled_students = None + max_enrollment = None + identifier = None + course = None + + def load(self, course_identifier): + self.name = SUBJECT + self.state = "active" + self.identifier = None + self.enrolled_students = [] + self.max_enrollment = SUBJECT_MAX_ENROLL + self.course = COURSE + + def save(self): + pass + student = DbStudent() enrollment = DbEnrollment() course = DbCourse() + subject = DbSubject() diff --git a/src/services/course_handler.py b/src/services/course_handler.py index 1735e70..2d7b6ab 100644 --- a/src/services/course_handler.py +++ b/src/services/course_handler.py @@ -1,5 +1,4 @@ import uuid -import datetime import logging @@ -8,8 +7,8 @@ class CourseHandler: INACTIVE = "inactive" CANCELLED = "cancelled" - def __init__(self, database, identifier=-1) -> None: - self.__identifier = identifier + def __init__(self, database) -> None: + self.__identifier = -1 self.__name = None self.__state = self.INACTIVE # TODO use enum self.__enrolled_students = [] diff --git a/src/services/student_handler.py b/src/services/student_handler.py index d92180f..365b8dd 100644 --- a/src/services/student_handler.py +++ b/src/services/student_handler.py @@ -90,6 +90,10 @@ def enroll_to_course(self, name): return self.identifier in course.enrolled_students def take_subject(self, subject_identifier): + """ + subject_identifier (str): The unique identifier of the subject. It follows the pattern + - + """ is_valid_student = self.__is_valid_student() if not is_valid_student: raise NonValidStudent() @@ -97,10 +101,11 @@ def take_subject(self, subject_identifier): if self.__is_locked(): raise NonValidStudent - subject_handler = SubjectHandler(subject_identifier) + subject_handler = SubjectHandler(self.__database) + subject_handler.load_from_database(subject_identifier) if subject_handler.course != self.__course: raise NonValidSubject() - if not subject_handler.is_available(): + if not subject_handler.is_available() or not subject_handler.is_active(): raise NonValidSubject() return self.subjects.append(subject_identifier) diff --git a/src/services/subject_handler.py b/src/services/subject_handler.py index ead1c5f..6c98ccd 100644 --- a/src/services/subject_handler.py +++ b/src/services/subject_handler.py @@ -1,22 +1,24 @@ import uuid import datetime +import logging from src import mocks class SubjectHandler: - def __init__(self, subject_identifier=None) -> None: - # TODO get from database - if subject_identifier: - self.__identifier = uuid.uuid5(uuid.NAMESPACE_URL, subject_identifier).hex - else: - self.__identifier = uuid.uuid5( - uuid.NAMESPACE_URL, str(datetime.datetime.now()) - ) + REMOVED = "removed" + ACTIVE = "active" + + def __init__(self, database, subject_identifier=None) -> None: + self.__database = database + self.__identifier = -1 + if self.__database.subject.identifier: + self.__identifier = database.subject.identifier self.__state = None self.__enrolled_students = [] self.__course = None self.__max_enrollment = 0 + self.__name = None if subject_identifier: # TODO get from database self.__course = mocks.SUBJECT @@ -56,3 +58,49 @@ def max_enrollment(self, value): def is_available(self): return len(self.enrolled_students) < self.__max_enrollment + + def is_active(self): + return self.__state == self.ACTIVE + + def activate(self): + if not self.name: + raise NonValidSubject() + + if self.state == self.REMOVED: + raise NonValidSubject() + + self.__state = self.ACTIVE + return self.__state + + def remove(self): + if not self.name: + raise NonValidSubject() + self.__state = self.REMOVED + return self.__state + + def save(self): + self.__database.subject.name = self.name + self.__database.subject.identifier = self.identifier + self.__database.subject.course = self.__course + self.__database.subject.enrolled_students = self.__enrolled_students + self.__database.subject.max_enrollment = self.__max_enrollment + self.__database.subject.save() + + def load_from_database(self, subject_identifier): + try: + self.__database.subject.load(subject_identifier) + + self.name = self.__database.subject.name + self.__state = self.__database.subject.state + self.__identifier = self.__database.subject.identifier + self.__enrolled_students = self.__database.subject.enrolled_students + self.max_enrollment = self.__database.subject.max_enrollment + self.__course = self.__database.subject.course + + except Exception as e: + logging.error(str(e)) + raise NonValidSubject("Subject not found.") + + +class NonValidSubject(Exception): + pass diff --git a/tests/test_cli.py b/tests/test_cli.py index 90639c3..d63fb4d 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -2,7 +2,19 @@ from src import mocks -def test_activate_course(): +def test_cancel_course_by_cli(): + name = "any" + database = mocks.Database() + assert cli_helper.cancel_course(database, name) == True + + +def test_deactivate_course_by_cli(): + name = "act" + database = mocks.Database() + assert cli_helper.deactivate_course(database, name) == True + + +def test_activate_course_cli(): name = "deact" database = mocks.Database() assert cli_helper.activate_course(database, name) == True diff --git a/tests/test_models.py b/tests/test_models.py index 74390a0..9049667 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -13,8 +13,10 @@ def test_semester_model(): def test_subject_model(): - subject = SubjectHandler() + database = mocks.Database() + subject = SubjectHandler(database) subject.name = "any_name" + subject.save() assert subject.name == "any_name" assert subject.identifier is not None @@ -23,6 +25,13 @@ def test_subject_model(): assert subject.max_enrollment == 0 assert subject.course == None + assert database.subject.name == "any_name" + assert database.subject.identifier is not None + assert database.subject.state == None + assert database.subject.enrolled_students == [] + assert database.subject.max_enrollment == 0 + assert database.subject.course == None + def test_course_model(): database = mocks.Database() diff --git a/tests/test_student.py b/tests/test_student.py index d9fa900..6e5683b 100644 --- a/tests/test_student.py +++ b/tests/test_student.py @@ -11,6 +11,7 @@ def restart_database(): mocks.SUBJECT = "any" mocks.SUBJECT_MAX_ENROLL = 10 + mocks.COURSE = "any" def test_unlock_course(): @@ -68,6 +69,7 @@ def test_take_invalid_subject_from_course_return_error(): student.cpf = "123.456.789-10" subject = "invalid" mocks.SUBJECT = "invalid" + mocks.COURSE = "other" student.enroll_to_course("any") with pytest.raises(NonValidSubject): @@ -79,11 +81,12 @@ def test_take_subject_from_course(): student = StudentHandler(database) student.name = "any" student.cpf = "123.456.789-10" - subject = "any" + student.enroll_to_course("any") + subject_identifier = "any-subject" student.enroll_to_course("any") - student.take_subject(subject) - assert subject in student.subjects + student.take_subject(subject_identifier) + assert subject_identifier in student.subjects def test_enroll_invalid_student_to_course_retunr_error(): diff --git a/tests/test_subject.py b/tests/test_subject.py new file mode 100644 index 0000000..2252aef --- /dev/null +++ b/tests/test_subject.py @@ -0,0 +1,44 @@ +import pytest +from src.services.subject_handler import SubjectHandler, NonValidSubject +from src import mocks + + +def test_remove_invalid_subject_return_error(): + database = mocks.Database() + subject_handler = SubjectHandler(database) + + with pytest.raises(NonValidSubject): + subject_handler.remove() + + +def test_remove(): + database = mocks.Database() + subject_handler = SubjectHandler(database) + subject_handler.name = "any" + assert subject_handler.remove() == "removed" + + +def test_activate_removed_subject_return_error(): + database = mocks.Database() + subject_handler = SubjectHandler(database) + subject_handler.name = "any" + subject_handler.remove() + + with pytest.raises(NonValidSubject): + subject_handler.activate() + + +def test_activate_invalid_subject_return_error(): + database = mocks.Database() + subject_handler = SubjectHandler(database) + + with pytest.raises(NonValidSubject): + subject_handler.activate() + + +def test_activate(): + database = mocks.Database() + subject_handler = SubjectHandler(database) + subject_handler.name = "any" + + assert subject_handler.activate() == "active" From 69ba390a2208c5c579b114cc42cd81872909a405 Mon Sep 17 00:00:00 2001 From: Douglas Cardoso Date: Thu, 11 Apr 2024 18:43:14 -0300 Subject: [PATCH 10/44] tests passing, but still using mocked database --- for_admin.py | 5 ++ src/database.py | 86 ++++++++++++++++++++++++++++++++- src/mocks.py | 9 +++- src/services/student_handler.py | 33 +++++++++---- src/services/subject_handler.py | 24 ++++++--- tests/conftest.py | 27 +++++++++++ tests/test_cli.py | 6 ++- tests/test_models.py | 10 +--- tests/test_student.py | 33 +++++-------- tests/test_subject.py | 19 +++++--- 10 files changed, 197 insertions(+), 55 deletions(-) create mode 100644 tests/conftest.py diff --git a/for_admin.py b/for_admin.py index 3da379d..ed88fef 100644 --- a/for_admin.py +++ b/for_admin.py @@ -14,3 +14,8 @@ db.course.populate("noise") db.course.populate("deact") db.course.populate("act") + +db.subject.populate("any", "any1") # e4c858cd917f518194c9d93c9d13def8 +db.subject.populate("any", "any2") # 283631d2292c54879b9aa72e27a1b4ff +db.subject.populate("any", "any3") # 0eaaeb1a39ed5d04a62b31cd951f34ce +db.subject.populate("any", "any4", 0) # ef15a071407953bd858cfca59ad99056 diff --git a/src/database.py b/src/database.py index b2cae36..a03a3e8 100644 --- a/src/database.py +++ b/src/database.py @@ -2,7 +2,7 @@ import logging # TODO test concurrency -DATABASE_NAME = "university.db" +DATABASE_NAME = ":memory:" con = sqlite3.connect(DATABASE_NAME) cur = con.cursor() @@ -38,6 +38,18 @@ def __init__(self): f"CREATE TABLE IF NOT EXISTS {self.TABLE} (name, state, cpf, identifier, gpa, subjects, course)" ) + def load(self, identifier): + result = cur.execute( + f"SELECT * FROM {self.TABLE} WHERE identifier = '{identifier}'" + ).fetchone() + self.name = result[0] + self.state = result[1] + self.cpf = result[2] + self.identifier = result[3] + self.gpq = result[4] + self.subjects = result[5].split(",") + self.course = result[6] + class DbEnrollment: TABLE = "enrollment" @@ -129,6 +141,78 @@ def load_from_database(self, name): self.max_enrollment = result[4] self.subjects = result[5].split(",") + class DbSubject: + TABLE = "subject" + name = None + state = None + enrolled_students = None + max_enrollment = None + identifier = None + course = None + + def __init__(self): + cur.execute( + f"CREATE TABLE IF NOT EXISTS {self.TABLE} (name, state, identifier, enrolled_students, max_enrollment, course)" + ) + + # Just for admin. The university has a predefined list of approved students to each course. + # TODO create a public funtion + def populate(self, course, name, max_enrollment=10): + import uuid + + identifier = uuid.uuid5(uuid.NAMESPACE_URL, str(f"{name}{course}")).hex + cur.execute( + f""" + INSERT INTO {self.TABLE} VALUES + ('{name}', + 'active', + '{identifier}', + '', + {max_enrollment}, + '{course}') + """ + ) + con.commit() + + def load(self, subject_identifier): + try: + result = cur.execute( + f"SELECT * FROM {self.TABLE} WHERE identifier = '{subject_identifier}'" + ).fetchone() + if not result: + raise NotFoundError() + + self.name = result[0] + self.state = result[1] + self.identifier = result[2] + self.enrolled_students = result[3].split(",") + self.max_enrollment = result[4] + self.course = result[5] + except Exception as e: + logging.error(str(e)) + raise + + def save(self): + try: + cmd = f""" + UPDATE {self.TABLE} + SET state = '{self.state}', + enrolled_students = '{self.enrolled_students}', + max_enrollment = '{self.max_enrollment}' + WHERE identifier = '{self.identifier}'; + """ + cur.execute(cmd) + + con.commit() + except Exception as e: + logging.error(str(e)) + raise + student = DbStudent() enrollment = DbEnrollment() course = DbCourse() + subject = DbSubject() + + +class NotFoundError(Exception): + pass diff --git a/src/mocks.py b/src/mocks.py index 20066d0..94f8fcf 100644 --- a/src/mocks.py +++ b/src/mocks.py @@ -1,6 +1,7 @@ SUBJECT = "any" SUBJECT_MAX_ENROLL = 10 COURSE = "any" +SUBJECT_STATE = "active" class Database: @@ -16,6 +17,12 @@ class DbStudent: def save(self): pass + def load(self, identifier): + self.name = "any" + self.state = "enrolled" + self.identifier = "any" + self.course = "any" + class DbEnrollment: def select(self, identifier): # NAME any, CPF 123.456.789-10, COURSE any @@ -56,7 +63,7 @@ class DbSubject: def load(self, course_identifier): self.name = SUBJECT - self.state = "active" + self.state = SUBJECT_STATE self.identifier = None self.enrolled_students = [] self.max_enrollment = SUBJECT_MAX_ENROLL diff --git a/src/services/student_handler.py b/src/services/student_handler.py index 365b8dd..d6aed75 100644 --- a/src/services/student_handler.py +++ b/src/services/student_handler.py @@ -1,5 +1,6 @@ +import logging from src.services.enrollment_validator import EnrollmentValidator -from src.services.course_handler import CourseHandler, NonValidCourse +from src.services.course_handler import CourseHandler from src.services.subject_handler import SubjectHandler @@ -72,22 +73,30 @@ def save(self): self.__database.student.course = self.__course self.__database.student.save() - def enroll_to_course(self, name): - if not self.__is_valid_student(name): + def enroll_to_course(self, course_name): + if not self.__is_valid_student(course_name): raise NonValidStudent() course = CourseHandler(self.__database) - course.load_from_database(name) + course.load_from_database(course_name) enrollment_validator = EnrollmentValidator(self.__database) self.__identifier = enrollment_validator.generate_student_identifier( - self.name, self.cpf, name + self.name, self.cpf, course_name ) - self.__course = name + self.__course = course_name course.enroll_student(self.identifier) self.__state = self.ENROLLED self.save() - return self.identifier in course.enrolled_students + + # post condition + self.__database.student.load(self.identifier) + assert self.__database.student.identifier == self.identifier + assert self.__database.student.state == self.ENROLLED + assert self.__database.student.course == self.__course + assert self.identifier in course.enrolled_students + + return True def take_subject(self, subject_identifier): """ @@ -99,12 +108,18 @@ def take_subject(self, subject_identifier): raise NonValidStudent() if self.__is_locked(): - raise NonValidStudent + raise NonValidStudent() subject_handler = SubjectHandler(self.__database) - subject_handler.load_from_database(subject_identifier) + try: + subject_handler.load_from_database(subject_identifier) + except Exception as e: + logging.error(str(e)) + raise NonValidSubject() + if subject_handler.course != self.__course: raise NonValidSubject() + if not subject_handler.is_available() or not subject_handler.is_active(): raise NonValidSubject() diff --git a/src/services/subject_handler.py b/src/services/subject_handler.py index 6c98ccd..58fdfe4 100644 --- a/src/services/subject_handler.py +++ b/src/services/subject_handler.py @@ -11,7 +11,7 @@ class SubjectHandler: def __init__(self, database, subject_identifier=None) -> None: self.__database = database - self.__identifier = -1 + self.__identifier = subject_identifier if self.__database.subject.identifier: self.__identifier = database.subject.identifier self.__state = None @@ -63,27 +63,37 @@ def is_active(self): return self.__state == self.ACTIVE def activate(self): - if not self.name: + if not self.identifier: raise NonValidSubject() if self.state == self.REMOVED: raise NonValidSubject() self.__state = self.ACTIVE + self.__save() + + # post condition + self.__database.subject.load(self.identifier) + assert self.__database.subject.state == self.ACTIVE return self.__state def remove(self): - if not self.name: + print("xxxxx ", self.state) + if not self.state == self.ACTIVE: raise NonValidSubject() + self.__state = self.REMOVED + self.__save() + + # post condition + self.__database.subject.load(self.identifier) + assert self.__database.subject.state == self.REMOVED return self.__state - def save(self): - self.__database.subject.name = self.name - self.__database.subject.identifier = self.identifier - self.__database.subject.course = self.__course + def __save(self): self.__database.subject.enrolled_students = self.__enrolled_students self.__database.subject.max_enrollment = self.__max_enrollment + self.__database.subject.state = self.__state self.__database.subject.save() def load_from_database(self, subject_identifier): diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..df351e1 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,27 @@ +import pytest +from src import database +from src.database import Database + + +@pytest.fixture(autouse=True, scope="session") +def set_in_memory_database(): + database.DATABASE_NAME = ":memory:" + db = Database() + # TODO need to check if the courses are available + db.enrollment.populate("douglas", "098.765.432.12", "adm") + db.enrollment.populate("maria", "028.745.462.18", "mat") + db.enrollment.populate("joana", "038.745.452.19", "port") + db.enrollment.populate("any", "123.456.789-10", "any") + + db.course.populate("adm") + db.course.populate("mat") + db.course.populate("port") + db.course.populate("any") + db.course.populate("noise") + db.course.populate("deact") + db.course.populate("act") + + db.subject.populate("any", "any1") # e4c858cd917f518194c9d93c9d13def8 + db.subject.populate("any", "any2") # 283631d2292c54879b9aa72e27a1b4ff + db.subject.populate("any", "any3") # 0eaaeb1a39ed5d04a62b31cd951f34ce + db.subject.populate("any", "any4", 0) # ef15a071407953bd858cfca59ad99056 diff --git a/tests/test_cli.py b/tests/test_cli.py index d63fb4d..1765da3 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,5 +1,7 @@ +import pytest from src import cli_helper from src import mocks +from src import database as db def test_cancel_course_by_cli(): @@ -28,9 +30,9 @@ def test_enroll_student_to_invalid_course(): assert cli_helper.enroll_student(database, name, cpf, course_identifier) == False -def test_enroll_student_to_course(): +def test_enroll_student_to_course_by_cli(): name = "any" cpf = "123.456.789-10" course_identifier = "any" - database = mocks.Database() + database = db.Database() assert cli_helper.enroll_student(database, name, cpf, course_identifier) == True diff --git a/tests/test_models.py b/tests/test_models.py index 9049667..77bbe0a 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -16,22 +16,14 @@ def test_subject_model(): database = mocks.Database() subject = SubjectHandler(database) subject.name = "any_name" - subject.save() assert subject.name == "any_name" - assert subject.identifier is not None + assert subject.identifier is None assert subject.state == None assert subject.enrolled_students == [] assert subject.max_enrollment == 0 assert subject.course == None - assert database.subject.name == "any_name" - assert database.subject.identifier is not None - assert database.subject.state == None - assert database.subject.enrolled_students == [] - assert database.subject.max_enrollment == 0 - assert database.subject.course == None - def test_course_model(): database = mocks.Database() diff --git a/tests/test_student.py b/tests/test_student.py index 6e5683b..5bf9820 100644 --- a/tests/test_student.py +++ b/tests/test_student.py @@ -5,6 +5,7 @@ NonValidSubject, ) from src import mocks +from src import database as db @pytest.fixture(autouse=True) @@ -15,7 +16,7 @@ def restart_database(): def test_unlock_course(): - database = mocks.Database() + database = db.Database() student = StudentHandler(database) student.name = "any" student.cpf = "123.456.789-10" @@ -26,7 +27,7 @@ def test_unlock_course(): def test_lock_course(): - database = mocks.Database() + database = db.Database() student = StudentHandler(database) student.name = "any" student.cpf = "123.456.789-10" @@ -37,7 +38,7 @@ def test_lock_course(): def test_take_subject_from_course_when_locked_student_return_error(): - database = mocks.Database() + database = db.Database() student = StudentHandler(database) student.name = "any" student.cpf = "123.456.789-10" @@ -50,11 +51,11 @@ def test_take_subject_from_course_when_locked_student_return_error(): def test_take_full_subject_from_course_return_error(): - database = mocks.Database() + database = db.Database() student = StudentHandler(database) student.name = "any" student.cpf = "123.456.789-10" - subject = "invalid" + subject = "ef15a071407953bd858cfca59ad99056" mocks.SUBJECT_MAX_ENROLL = -1 student.enroll_to_course("any") @@ -63,7 +64,7 @@ def test_take_full_subject_from_course_return_error(): def test_take_invalid_subject_from_course_return_error(): - database = mocks.Database() + database = db.Database() student = StudentHandler(database) student.name = "any" student.cpf = "123.456.789-10" @@ -77,12 +78,12 @@ def test_take_invalid_subject_from_course_return_error(): def test_take_subject_from_course(): - database = mocks.Database() + database = db.Database() student = StudentHandler(database) student.name = "any" student.cpf = "123.456.789-10" student.enroll_to_course("any") - subject_identifier = "any-subject" + subject_identifier = "e4c858cd917f518194c9d93c9d13def8" student.enroll_to_course("any") student.take_subject(subject_identifier) @@ -90,7 +91,7 @@ def test_take_subject_from_course(): def test_enroll_invalid_student_to_course_retunr_error(): - database = mocks.Database() + database = db.Database() student = StudentHandler(database) student.name = "invalid" student.cpf = "123.456.789-10" @@ -99,18 +100,10 @@ def test_enroll_invalid_student_to_course_retunr_error(): student.enroll_to_course("any") -def test_enroll_student_to_course(): - database = mocks.Database() +def test_enroll_student_to_course_x(): + database = db.Database() student = StudentHandler(database) student.name = "any" student.cpf = "123.456.789-10" - student.enroll_to_course("any") - - # generate using student name, cpf and course - assert student.identifier == "290f2113c2e6579c8bb6ec395ea56572" - - assert database.student.identifier == "290f2113c2e6579c8bb6ec395ea56572" - assert database.student.state == "enrolled" - assert database.student.course == "any" - assert database.student.subjects == [] + assert student.enroll_to_course("any") is True diff --git a/tests/test_subject.py b/tests/test_subject.py index 2252aef..b6b7c1b 100644 --- a/tests/test_subject.py +++ b/tests/test_subject.py @@ -1,6 +1,12 @@ import pytest from src.services.subject_handler import SubjectHandler, NonValidSubject from src import mocks +from src import database as db + + +@pytest.fixture(autouse=True) +def restart_database(): + mocks.SUBJECT_STATE = "active" def test_remove_invalid_subject_return_error(): @@ -12,17 +18,18 @@ def test_remove_invalid_subject_return_error(): def test_remove(): - database = mocks.Database() + database = db.Database() subject_handler = SubjectHandler(database) - subject_handler.name = "any" + subject_handler.load_from_database("e4c858cd917f518194c9d93c9d13def8") assert subject_handler.remove() == "removed" def test_activate_removed_subject_return_error(): - database = mocks.Database() - subject_handler = SubjectHandler(database) - subject_handler.name = "any" + database = db.Database() + subject_handler = SubjectHandler(database, "f65774a5f48d55768dfefac51136724e") + subject_handler.activate() subject_handler.remove() + mocks.SUBJECT_STATE = "removed" with pytest.raises(NonValidSubject): subject_handler.activate() @@ -37,7 +44,7 @@ def test_activate_invalid_subject_return_error(): def test_activate(): - database = mocks.Database() + database = db.Database() subject_handler = SubjectHandler(database) subject_handler.name = "any" From bc77b146c2d8762b9386a278af35303f2817646e Mon Sep 17 00:00:00 2001 From: Douglas Cardoso Date: Thu, 11 Apr 2024 19:10:08 -0300 Subject: [PATCH 11/44] Remove mocked database --- src/database.py | 9 ++-- src/mocks.py | 78 --------------------------------- src/services/subject_handler.py | 7 --- src/utils.py | 5 +++ tests/conftest.py | 7 ++- tests/test_cli.py | 10 ++--- tests/test_course.py | 20 ++++----- tests/test_models.py | 8 ++-- tests/test_student.py | 14 +----- tests/test_subject.py | 21 +++++---- 10 files changed, 45 insertions(+), 134 deletions(-) delete mode 100644 src/mocks.py create mode 100644 src/utils.py diff --git a/src/database.py b/src/database.py index a03a3e8..5843b06 100644 --- a/src/database.py +++ b/src/database.py @@ -1,5 +1,6 @@ import sqlite3 import logging +from src.utils import generate_subject_identifier # TODO test concurrency DATABASE_NAME = ":memory:" @@ -157,15 +158,13 @@ def __init__(self): # Just for admin. The university has a predefined list of approved students to each course. # TODO create a public funtion - def populate(self, course, name, max_enrollment=10): - import uuid - - identifier = uuid.uuid5(uuid.NAMESPACE_URL, str(f"{name}{course}")).hex + def populate(self, course, name, max_enrollment=10, state="active"): + identifier = generate_subject_identifier(course, name) cur.execute( f""" INSERT INTO {self.TABLE} VALUES ('{name}', - 'active', + '{state}', '{identifier}', '', {max_enrollment}, diff --git a/src/mocks.py b/src/mocks.py deleted file mode 100644 index 94f8fcf..0000000 --- a/src/mocks.py +++ /dev/null @@ -1,78 +0,0 @@ -SUBJECT = "any" -SUBJECT_MAX_ENROLL = 10 -COURSE = "any" -SUBJECT_STATE = "active" - - -class Database: - class DbStudent: - name = None - state = None - cpf = None - identifier = None - gpa = None - subjects = None - course = None - - def save(self): - pass - - def load(self, identifier): - self.name = "any" - self.state = "enrolled" - self.identifier = "any" - self.course = "any" - - class DbEnrollment: - def select(self, identifier): - # NAME any, CPF 123.456.789-10, COURSE any - DEFAULT = "290f2113c2e6579c8bb6ec395ea56572" - if identifier == DEFAULT: - return True - return False - - class DbCourse: - name = None - state = None - identifier = None - enrolled_students = None - max_enrollment = None - subjects = None - - def save(self): - pass - - def load_from_database(self, course_identifier): - self.name = "any" - self.state = "active" - self.identifier = None - self.enrolled_students = [] - self.max_enrollment = None - self.subjects = ["any1", "any2", "any3"] - - def select(self, course_identifier): - return "anything not None" - - class DbSubject: - name = None - state = None - enrolled_students = None - max_enrollment = None - identifier = None - course = None - - def load(self, course_identifier): - self.name = SUBJECT - self.state = SUBJECT_STATE - self.identifier = None - self.enrolled_students = [] - self.max_enrollment = SUBJECT_MAX_ENROLL - self.course = COURSE - - def save(self): - pass - - student = DbStudent() - enrollment = DbEnrollment() - course = DbCourse() - subject = DbSubject() diff --git a/src/services/subject_handler.py b/src/services/subject_handler.py index 58fdfe4..261707e 100644 --- a/src/services/subject_handler.py +++ b/src/services/subject_handler.py @@ -1,7 +1,4 @@ -import uuid -import datetime import logging -from src import mocks class SubjectHandler: @@ -19,10 +16,6 @@ def __init__(self, database, subject_identifier=None) -> None: self.__course = None self.__max_enrollment = 0 self.__name = None - if subject_identifier: - # TODO get from database - self.__course = mocks.SUBJECT - self.__max_enrollment = mocks.SUBJECT_MAX_ENROLL @property def identifier(self): diff --git a/src/utils.py b/src/utils.py new file mode 100644 index 0000000..eac3d08 --- /dev/null +++ b/src/utils.py @@ -0,0 +1,5 @@ +import uuid + + +def generate_subject_identifier(course, name): + return uuid.uuid5(uuid.NAMESPACE_URL, str(f"{name}{course}")).hex diff --git a/tests/conftest.py b/tests/conftest.py index df351e1..c97a33f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -24,4 +24,9 @@ def set_in_memory_database(): db.subject.populate("any", "any1") # e4c858cd917f518194c9d93c9d13def8 db.subject.populate("any", "any2") # 283631d2292c54879b9aa72e27a1b4ff db.subject.populate("any", "any3") # 0eaaeb1a39ed5d04a62b31cd951f34ce - db.subject.populate("any", "any4", 0) # ef15a071407953bd858cfca59ad99056 + db.subject.populate( + "course1", "subject_full", 0 + ) # ef15a071407953bd858cfca59ad99056 + db.subject.populate( + "course1", "subject_removed", 0, "removed" + ) # ef15a071407953bd858cfca59ad99056 diff --git a/tests/test_cli.py b/tests/test_cli.py index 1765da3..71be83c 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,24 +1,22 @@ -import pytest from src import cli_helper -from src import mocks from src import database as db def test_cancel_course_by_cli(): name = "any" - database = mocks.Database() + database = db.Database() assert cli_helper.cancel_course(database, name) == True def test_deactivate_course_by_cli(): name = "act" - database = mocks.Database() + database = db.Database() assert cli_helper.deactivate_course(database, name) == True def test_activate_course_cli(): name = "deact" - database = mocks.Database() + database = db.Database() assert cli_helper.activate_course(database, name) == True @@ -26,7 +24,7 @@ def test_enroll_student_to_invalid_course(): name = "any" cpf = "123.456.789-10" course_identifier = "invalid" - database = mocks.Database() + database = db.Database() assert cli_helper.enroll_student(database, name, cpf, course_identifier) == False diff --git a/tests/test_course.py b/tests/test_course.py index d68f808..bf4a4e6 100644 --- a/tests/test_course.py +++ b/tests/test_course.py @@ -1,10 +1,10 @@ import pytest from src.services.course_handler import CourseHandler, NonValidCourse -from src import mocks +from src import database as db def test_enroll_student_to_inactive_course_return_error(): - database = mocks.Database() + database = db.Database() course_handler = CourseHandler(database) course_handler.name = "adm" course_handler.add_subject("any1") @@ -17,7 +17,7 @@ def test_enroll_student_to_inactive_course_return_error(): def test_enroll_student_to_active_course(): - database = mocks.Database() + database = db.Database() course_handler = CourseHandler(database) course_handler.name = "adm" course_handler.add_subject("any1") @@ -31,7 +31,7 @@ def test_enroll_student_to_active_course(): def test_cancel_inactive_course(): - database = mocks.Database() + database = db.Database() course_handler = CourseHandler(database) course_handler.name = "adm" course_handler.add_subject("any1") @@ -44,7 +44,7 @@ def test_cancel_inactive_course(): def test_cancel_active_course(): - database = mocks.Database() + database = db.Database() course_handler = CourseHandler(database) course_handler.name = "adm" course_handler.add_subject("any1") @@ -57,7 +57,7 @@ def test_cancel_active_course(): def test_deactivate_non_active_course_return_error(): - database = mocks.Database() + database = db.Database() course_handler = CourseHandler(database) with pytest.raises(NonValidCourse): course_handler.deactivate() @@ -66,7 +66,7 @@ def test_deactivate_non_active_course_return_error(): def test_deactivate_course(): - database = mocks.Database() + database = db.Database() course_handler = CourseHandler(database) course_handler.name = "adm" course_handler.add_subject("any1") @@ -79,7 +79,7 @@ def test_deactivate_course(): def test_activate_course_without_minimum_subjects_return_error(): - database = mocks.Database() + database = db.Database() course_handler = CourseHandler(database) course_handler.name = "any" with pytest.raises(NonValidCourse): @@ -88,7 +88,7 @@ def test_activate_course_without_minimum_subjects_return_error(): def test_activate_course_without_name_return_error(): - database = mocks.Database() + database = db.Database() course_handler = CourseHandler(database) with pytest.raises(NonValidCourse): course_handler.activate() @@ -96,7 +96,7 @@ def test_activate_course_without_name_return_error(): def test_activate_course(): - database = mocks.Database() + database = db.Database() course_handler = CourseHandler(database) course_handler.name = "adm" course_handler.add_subject("any1") diff --git a/tests/test_models.py b/tests/test_models.py index 77bbe0a..0e87501 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -2,7 +2,7 @@ from src.services.course_handler import CourseHandler from src.services.subject_handler import SubjectHandler from src.services.semester_monitor import SemesterHandler -from src import mocks +from src import database as db def test_semester_model(): @@ -13,7 +13,7 @@ def test_semester_model(): def test_subject_model(): - database = mocks.Database() + database = db.Database() subject = SubjectHandler(database) subject.name = "any_name" @@ -26,7 +26,7 @@ def test_subject_model(): def test_course_model(): - database = mocks.Database() + database = db.Database() course_handler = CourseHandler(database) course_handler.name = "any_name" course_handler.save() @@ -47,7 +47,7 @@ def test_course_model(): def test_student_model(): - database = mocks.Database() + database = db.Database() student = StudentHandler(database) student.name = "any_name" student.cpf = "123.456.789-10" diff --git a/tests/test_student.py b/tests/test_student.py index 5bf9820..6df702e 100644 --- a/tests/test_student.py +++ b/tests/test_student.py @@ -4,15 +4,8 @@ NonValidStudent, NonValidSubject, ) -from src import mocks from src import database as db - - -@pytest.fixture(autouse=True) -def restart_database(): - mocks.SUBJECT = "any" - mocks.SUBJECT_MAX_ENROLL = 10 - mocks.COURSE = "any" +from src.utils import generate_subject_identifier def test_unlock_course(): @@ -55,8 +48,7 @@ def test_take_full_subject_from_course_return_error(): student = StudentHandler(database) student.name = "any" student.cpf = "123.456.789-10" - subject = "ef15a071407953bd858cfca59ad99056" - mocks.SUBJECT_MAX_ENROLL = -1 + subject = generate_subject_identifier("course1", "subject_full") student.enroll_to_course("any") with pytest.raises(NonValidSubject): @@ -69,8 +61,6 @@ def test_take_invalid_subject_from_course_return_error(): student.name = "any" student.cpf = "123.456.789-10" subject = "invalid" - mocks.SUBJECT = "invalid" - mocks.COURSE = "other" student.enroll_to_course("any") with pytest.raises(NonValidSubject): diff --git a/tests/test_subject.py b/tests/test_subject.py index b6b7c1b..2f04ea6 100644 --- a/tests/test_subject.py +++ b/tests/test_subject.py @@ -1,16 +1,11 @@ import pytest from src.services.subject_handler import SubjectHandler, NonValidSubject -from src import mocks from src import database as db - - -@pytest.fixture(autouse=True) -def restart_database(): - mocks.SUBJECT_STATE = "active" +from src.utils import generate_subject_identifier def test_remove_invalid_subject_return_error(): - database = mocks.Database() + database = db.Database() subject_handler = SubjectHandler(database) with pytest.raises(NonValidSubject): @@ -20,24 +15,28 @@ def test_remove_invalid_subject_return_error(): def test_remove(): database = db.Database() subject_handler = SubjectHandler(database) - subject_handler.load_from_database("e4c858cd917f518194c9d93c9d13def8") + subject_handler.load_from_database(generate_subject_identifier("any", "any1")) assert subject_handler.remove() == "removed" def test_activate_removed_subject_return_error(): database = db.Database() - subject_handler = SubjectHandler(database, "f65774a5f48d55768dfefac51136724e") + subject_handler = SubjectHandler( + database, generate_subject_identifier("any", "any1") + ) subject_handler.activate() subject_handler.remove() - mocks.SUBJECT_STATE = "removed" with pytest.raises(NonValidSubject): subject_handler.activate() def test_activate_invalid_subject_return_error(): - database = mocks.Database() + database = db.Database() subject_handler = SubjectHandler(database) + subject_handler.load_from_database( + generate_subject_identifier("course1", "subject_removed") + ) with pytest.raises(NonValidSubject): subject_handler.activate() From 0ac8f2fdc1d05dfda5a9400f9bbfe2cf92ff0935 Mon Sep 17 00:00:00 2001 From: Douglas Cardoso Date: Thu, 11 Apr 2024 19:50:35 -0300 Subject: [PATCH 12/44] Add utils --- cli.py | 1 + src/database.py | 17 +++++++---------- src/services/subject_handler.py | 1 - src/utils.py | 8 ++++++++ tests/conftest.py | 5 ++--- 5 files changed, 18 insertions(+), 14 deletions(-) diff --git a/cli.py b/cli.py index 2d951aa..38454c9 100644 --- a/cli.py +++ b/cli.py @@ -3,6 +3,7 @@ from src import cli_helper from src.database import Database + logging.basicConfig( filename="cli.log", datefmt="%Y-%m-%d %H:%M:%S", diff --git a/src/database.py b/src/database.py index 5843b06..fc78432 100644 --- a/src/database.py +++ b/src/database.py @@ -1,8 +1,9 @@ import sqlite3 import logging -from src.utils import generate_subject_identifier +from src import utils # TODO test concurrency +DATABASE_NAME = "university.db" DATABASE_NAME = ":memory:" con = sqlite3.connect(DATABASE_NAME) cur = con.cursor() @@ -60,11 +61,9 @@ def __init__(self): # Just for admin. The university has a predefined list of approved students to each course. # TODO create a public funtion def populate(self, name, cpf, course_identifier): - import uuid - - student_identifier = uuid.uuid5( - uuid.NAMESPACE_URL, str(f"{name}{cpf}{course_identifier}") - ).hex + student_identifier = utils.generate_student_identifier( + name, cpf, course_identifier + ) cur.execute( f""" INSERT INTO {self.TABLE} VALUES ('{student_identifier}') @@ -97,9 +96,7 @@ def __init__(self): # Just for admin. Necessary because there is not a user story to create courses # TODO create a public funtion def populate(self, name): - import uuid - - identifier = uuid.uuid5(uuid.NAMESPACE_URL, str(f"{name}")).hex + identifier = utils.generate_course_identifier(name) cur.execute( f""" INSERT INTO {self.TABLE} VALUES @@ -159,7 +156,7 @@ def __init__(self): # Just for admin. The university has a predefined list of approved students to each course. # TODO create a public funtion def populate(self, course, name, max_enrollment=10, state="active"): - identifier = generate_subject_identifier(course, name) + identifier = utils.generate_subject_identifier(course, name) cur.execute( f""" INSERT INTO {self.TABLE} VALUES diff --git a/src/services/subject_handler.py b/src/services/subject_handler.py index 261707e..206af65 100644 --- a/src/services/subject_handler.py +++ b/src/services/subject_handler.py @@ -71,7 +71,6 @@ def activate(self): return self.__state def remove(self): - print("xxxxx ", self.state) if not self.state == self.ACTIVE: raise NonValidSubject() diff --git a/src/utils.py b/src/utils.py index eac3d08..5e6ed23 100644 --- a/src/utils.py +++ b/src/utils.py @@ -1,5 +1,13 @@ import uuid +def generate_course_identifier(name): + return uuid.uuid5(uuid.NAMESPACE_URL, str(f"{name}")).hex + + +def generate_student_identifier(name, cpf, course_identifier): + return uuid.uuid5(uuid.NAMESPACE_URL, str(f"{name}{cpf}{course_identifier}")).hex + + def generate_subject_identifier(course, name): return uuid.uuid5(uuid.NAMESPACE_URL, str(f"{name}{course}")).hex diff --git a/tests/conftest.py b/tests/conftest.py index c97a33f..cf01769 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,12 +1,11 @@ import pytest from src import database -from src.database import Database @pytest.fixture(autouse=True, scope="session") def set_in_memory_database(): - database.DATABASE_NAME = ":memory:" - db = Database() + database.db_name = ":memory:" + db = database.Database() # TODO need to check if the courses are available db.enrollment.populate("douglas", "098.765.432.12", "adm") db.enrollment.populate("maria", "028.745.462.18", "mat") From bd496ec6caec3e8a986e83adfa36291552dc5ab7 Mon Sep 17 00:00:00 2001 From: Douglas Cardoso Date: Fri, 12 Apr 2024 01:07:38 -0300 Subject: [PATCH 13/44] Fix test conflicts --- README.md | 8 +++ requirements.txt | 3 +- src/database.py | 118 ++++++++++++++++++++------------ src/services/student_handler.py | 38 ++++++++-- src/services/subject_handler.py | 10 ++- tests/conftest.py | 6 +- tests/test_cli.py | 32 ++++----- tests/test_models.py | 24 +++---- tests/test_student.py | 50 ++++++-------- tests/test_subject.py | 33 ++++----- 10 files changed, 186 insertions(+), 136 deletions(-) diff --git a/README.md b/README.md index afd29ea..01d87c3 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,14 @@ This project aims to practice "code smells" related to unit tests using an appli The code will be developed using the TDD technique in the Detroit style, hence the name of the repository. The application simulates the control of grades for university students. +# Setup and Test +``` +python3.11 -m venv venv +source venv/bin/activate +pip install -r requirements.txt +pytest --random-order +``` + # Installation ``` # create database diff --git a/requirements.txt b/requirements.txt index 55b033e..23c1224 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ -pytest \ No newline at end of file +pytest +pytest-random-order \ No newline at end of file diff --git a/src/database.py b/src/database.py index fc78432..66da5b1 100644 --- a/src/database.py +++ b/src/database.py @@ -3,10 +3,12 @@ from src import utils # TODO test concurrency +# TODO use inmemory just for tests DATABASE_NAME = "university.db" DATABASE_NAME = ":memory:" -con = sqlite3.connect(DATABASE_NAME) -cur = con.cursor() +# con = sqlite3.connect(DATABASE_NAME) +# cur = con.cursor() +# print("conxxx ", con) class Database: @@ -20,30 +22,53 @@ class DbStudent: subjects = None course = None - def save(self): + def __init__(self, con, cur): + self.cur = cur + self.con = con + # TODO move to installation file cur.execute( - f""" - INSERT INTO {self.TABLE} VALUES - ('{self.name}', - '{self.state}', - '{self.cpf}', - '{self.identifier}', - {self.gpa}, - '{self.subjects}', - '{self.course}') - """ + f"CREATE TABLE IF NOT EXISTS {self.TABLE} (name, state, cpf, identifier, gpa, subjects, course)" ) - con.commit() - def __init__(self): - cur.execute( - f"CREATE TABLE IF NOT EXISTS {self.TABLE} (name, state, cpf, identifier, gpa, subjects, course)" + def save(self): + cmd = f""" + UPDATE {self.TABLE} + SET state = '{self.state}', + gpa = '{self.gpa}', + subjects = '{self.subjects}' + WHERE identifier = '{self.identifier}'; + """ + self.cur.execute(cmd) + + self.con.commit() + + # Just for admin. + # TODO create a public funtion + def populate( + self, name, cpf, course_identifier, state="enrolled", gpa=0, subject="" + ): + student_identifier = utils.generate_student_identifier( + name, cpf, course_identifier + ) + self.cur.execute( + f""" + INSERT INTO {self.TABLE} VALUES ( + '{name}', + '{state}', + '{cpf}', + '{student_identifier}', + '{gpa}', + '{subject}', + '{course_identifier}') + """ ) + self.con.commit() def load(self, identifier): - result = cur.execute( - f"SELECT * FROM {self.TABLE} WHERE identifier = '{identifier}'" - ).fetchone() + cmd = f"SELECT * FROM {self.TABLE} WHERE identifier = '{identifier}'" + result = self.cur.execute(cmd).fetchone() + if not result: + raise NotFoundError() self.name = result[0] self.state = result[1] self.cpf = result[2] @@ -55,7 +80,9 @@ def load(self, identifier): class DbEnrollment: TABLE = "enrollment" - def __init__(self): + def __init__(self, con, cur): + self.cur = cur + self.con = con cur.execute(f"CREATE TABLE IF NOT EXISTS {self.TABLE} (student_identifier)") # Just for admin. The university has a predefined list of approved students to each course. @@ -64,16 +91,16 @@ def populate(self, name, cpf, course_identifier): student_identifier = utils.generate_student_identifier( name, cpf, course_identifier ) - cur.execute( + self.cur.execute( f""" INSERT INTO {self.TABLE} VALUES ('{student_identifier}') """ ) - con.commit() + self.con.commit() def select(self, student_identifier): return ( - cur.execute( + self.cur.execute( f"SELECT * FROM {self.TABLE} WHERE student_identifier = '{student_identifier}'" ).fetchone() is not None @@ -88,8 +115,10 @@ class DbCourse: max_enrollment = None subjects = None - def __init__(self): - cur.execute( + def __init__(self, con, cur): + self.con = sqlite3.connect(DATABASE_NAME) + self.cur = self.con.cursor() + self.cur.execute( f"CREATE TABLE IF NOT EXISTS {self.TABLE} (name, state, identifier, enrolled_students, max_enrollment, subjects)" ) @@ -97,7 +126,7 @@ def __init__(self): # TODO create a public funtion def populate(self, name): identifier = utils.generate_course_identifier(name) - cur.execute( + self.cur.execute( f""" INSERT INTO {self.TABLE} VALUES ('{name}', @@ -108,11 +137,11 @@ def populate(self, name): 'any1,any2,any3') """ ) - con.commit() + self.con.commit() def save(self): try: - cur.execute( + self.cur.execute( f""" INSERT INTO {self.TABLE} VALUES ('{self.name}', @@ -123,13 +152,13 @@ def save(self): '{self.subjects}') """ ) - con.commit() + self.con.commit() except Exception as e: logging.error(str(e)) raise def load_from_database(self, name): - result = cur.execute( + result = self.cur.execute( f"SELECT * FROM {self.TABLE} WHERE name = '{name}'" ).fetchone() self.name = result[0] @@ -148,7 +177,9 @@ class DbSubject: identifier = None course = None - def __init__(self): + def __init__(self, con, cur): + self.cur = cur + self.con = con cur.execute( f"CREATE TABLE IF NOT EXISTS {self.TABLE} (name, state, identifier, enrolled_students, max_enrollment, course)" ) @@ -157,7 +188,7 @@ def __init__(self): # TODO create a public funtion def populate(self, course, name, max_enrollment=10, state="active"): identifier = utils.generate_subject_identifier(course, name) - cur.execute( + self.cur.execute( f""" INSERT INTO {self.TABLE} VALUES ('{name}', @@ -168,11 +199,11 @@ def populate(self, course, name, max_enrollment=10, state="active"): '{course}') """ ) - con.commit() + self.con.commit() def load(self, subject_identifier): try: - result = cur.execute( + result = self.cur.execute( f"SELECT * FROM {self.TABLE} WHERE identifier = '{subject_identifier}'" ).fetchone() if not result: @@ -194,20 +225,23 @@ def save(self): UPDATE {self.TABLE} SET state = '{self.state}', enrolled_students = '{self.enrolled_students}', - max_enrollment = '{self.max_enrollment}' + max_enrollment = {self.max_enrollment} WHERE identifier = '{self.identifier}'; """ - cur.execute(cmd) + self.cur.execute(cmd) - con.commit() + self.con.commit() except Exception as e: logging.error(str(e)) raise - student = DbStudent() - enrollment = DbEnrollment() - course = DbCourse() - subject = DbSubject() + def __init__(self) -> None: + con = sqlite3.connect(DATABASE_NAME) + cur = con.cursor() + self.student = self.DbStudent(con, cur) + self.enrollment = self.DbEnrollment(con, cur) + self.course = self.DbCourse(con, cur) + self.subject = self.DbSubject(con, cur) class NotFoundError(Exception): diff --git a/src/services/student_handler.py b/src/services/student_handler.py index d6aed75..b06c222 100644 --- a/src/services/student_handler.py +++ b/src/services/student_handler.py @@ -14,6 +14,8 @@ def __init__(self, database): self.__gpa = 0 self.__subjects = [] self.__course = None + self.__name = None + self.__cpf = None self.__database = database @property @@ -63,13 +65,13 @@ def __is_valid_student(self, course_identifier=None): self.name, self.cpf, course ) - def save(self): + def __save(self): self.__database.student.name = self.name self.__database.student.state = self.state self.__database.student.cpf = self.cpf self.__database.student.identifier = self.identifier self.__database.student.gpa = self.gpa - self.__database.student.subjects = self.subjects + self.__database.student.subjects = ",".join(self.subjects) self.__database.student.course = self.__course self.__database.student.save() @@ -87,7 +89,7 @@ def enroll_to_course(self, course_name): self.__course = course_name course.enroll_student(self.identifier) self.__state = self.ENROLLED - self.save() + self.__save() # post condition self.__database.student.load(self.identifier) @@ -123,22 +125,46 @@ def take_subject(self, subject_identifier): if not subject_handler.is_available() or not subject_handler.is_active(): raise NonValidSubject() - return self.subjects.append(subject_identifier) + self.subjects.append(subject_identifier) + self.__save() + + # post condition + subject_handler.load_from_database(subject_identifier) + assert subject_identifier in self.subjects + + return True def unlock_course(self): if self.__is_valid_student(): self.__state = None - self.save() + self.__save() return self.state raise NonValidStudent() def lock_course(self): if self.__is_valid_student(): self.__state = self.LOCKED - self.save() + self.__save() return self.state raise NonValidStudent() + def load_from_database(self, student_identifier): + try: + self.__database.student.load(student_identifier) + + self.__name = self.__database.student.name + self.__state = self.__database.student.state + self.__cpf = self.__database.student.cpf + self.__identifier = self.__database.student.identifier + self.__gpa = self.__database.student.gpa + self.__subjects = self.__database.student.subjects + self.__course = self.__database.student.course + + except Exception as e: + print("exxxxx ", str(e)) + logging.error(str(e)) + raise NonValidStudent("Student not found.") + class NonValidStudent(Exception): pass diff --git a/src/services/subject_handler.py b/src/services/subject_handler.py index 206af65..1053e17 100644 --- a/src/services/subject_handler.py +++ b/src/services/subject_handler.py @@ -6,11 +6,9 @@ class SubjectHandler: REMOVED = "removed" ACTIVE = "active" - def __init__(self, database, subject_identifier=None) -> None: + def __init__(self, database, subject_identifier=-1) -> None: self.__database = database self.__identifier = subject_identifier - if self.__database.subject.identifier: - self.__identifier = database.subject.identifier self.__state = None self.__enrolled_students = [] self.__course = None @@ -83,7 +81,7 @@ def remove(self): return self.__state def __save(self): - self.__database.subject.enrolled_students = self.__enrolled_students + self.__database.subject.enrolled_students = ",".join(self.__enrolled_students) self.__database.subject.max_enrollment = self.__max_enrollment self.__database.subject.state = self.__state self.__database.subject.save() @@ -92,11 +90,11 @@ def load_from_database(self, subject_identifier): try: self.__database.subject.load(subject_identifier) - self.name = self.__database.subject.name + self.__name = self.__database.subject.name self.__state = self.__database.subject.state self.__identifier = self.__database.subject.identifier self.__enrolled_students = self.__database.subject.enrolled_students - self.max_enrollment = self.__database.subject.max_enrollment + self.__max_enrollment = self.__database.subject.max_enrollment self.__course = self.__database.subject.course except Exception as e: diff --git a/tests/conftest.py b/tests/conftest.py index cf01769..27fce2d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,9 +2,8 @@ from src import database -@pytest.fixture(autouse=True, scope="session") +@pytest.fixture(autouse=True, scope="function") def set_in_memory_database(): - database.db_name = ":memory:" db = database.Database() # TODO need to check if the courses are available db.enrollment.populate("douglas", "098.765.432.12", "adm") @@ -12,6 +11,8 @@ def set_in_memory_database(): db.enrollment.populate("joana", "038.745.452.19", "port") db.enrollment.populate("any", "123.456.789-10", "any") + db.student.populate("any", "123.456.789-10", "any") + db.course.populate("adm") db.course.populate("mat") db.course.populate("port") @@ -29,3 +30,4 @@ def set_in_memory_database(): db.subject.populate( "course1", "subject_removed", 0, "removed" ) # ef15a071407953bd858cfca59ad99056 + yield db diff --git a/tests/test_cli.py b/tests/test_cli.py index 71be83c..41acb7e 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,36 +1,36 @@ from src import cli_helper -from src import database as db -def test_cancel_course_by_cli(): +def test_cancel_course_by_cli(set_in_memory_database): name = "any" - database = db.Database() - assert cli_helper.cancel_course(database, name) == True + assert cli_helper.cancel_course(set_in_memory_database, name) == True -def test_deactivate_course_by_cli(): +def test_deactivate_course_by_cli(set_in_memory_database): name = "act" - database = db.Database() - assert cli_helper.deactivate_course(database, name) == True + assert cli_helper.deactivate_course(set_in_memory_database, name) == True -def test_activate_course_cli(): +def test_activate_course_cli(set_in_memory_database): name = "deact" - database = db.Database() - assert cli_helper.activate_course(database, name) == True + assert cli_helper.activate_course(set_in_memory_database, name) == True -def test_enroll_student_to_invalid_course(): +def test_enroll_student_to_invalid_course(set_in_memory_database): name = "any" cpf = "123.456.789-10" course_identifier = "invalid" - database = db.Database() - assert cli_helper.enroll_student(database, name, cpf, course_identifier) == False + assert ( + cli_helper.enroll_student(set_in_memory_database, name, cpf, course_identifier) + == False + ) -def test_enroll_student_to_course_by_cli(): +def test_enroll_student_to_course_by_cli(set_in_memory_database): name = "any" cpf = "123.456.789-10" course_identifier = "any" - database = db.Database() - assert cli_helper.enroll_student(database, name, cpf, course_identifier) == True + assert ( + cli_helper.enroll_student(set_in_memory_database, name, cpf, course_identifier) + == True + ) diff --git a/tests/test_models.py b/tests/test_models.py index 0e87501..7e6b78f 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -14,15 +14,15 @@ def test_semester_model(): def test_subject_model(): database = db.Database() - subject = SubjectHandler(database) - subject.name = "any_name" + subject_handler = SubjectHandler(database) + subject_handler.name = "any_name" - assert subject.name == "any_name" - assert subject.identifier is None - assert subject.state == None - assert subject.enrolled_students == [] - assert subject.max_enrollment == 0 - assert subject.course == None + assert subject_handler.name == "any_name" + assert subject_handler.identifier is -1 + assert subject_handler.state == None + assert subject_handler.enrolled_students == [] + assert subject_handler.max_enrollment == 0 + assert subject_handler.course == None def test_course_model(): @@ -51,7 +51,6 @@ def test_student_model(): student = StudentHandler(database) student.name = "any_name" student.cpf = "123.456.789-10" - student.save() assert student.name == "any_name" assert student.cpf == "123.456.789-10" @@ -59,10 +58,3 @@ def test_student_model(): assert student.state == None assert student.gpa == 0 assert student.subjects == [] - - assert database.student.name == "any_name" - assert database.student.cpf == "123.456.789-10" - assert database.student.identifier is None - assert database.student.state == None - assert database.student.gpa == 0 - assert database.student.subjects == [] diff --git a/tests/test_student.py b/tests/test_student.py index 6df702e..3692ff8 100644 --- a/tests/test_student.py +++ b/tests/test_student.py @@ -4,13 +4,11 @@ NonValidStudent, NonValidSubject, ) -from src import database as db from src.utils import generate_subject_identifier -def test_unlock_course(): - database = db.Database() - student = StudentHandler(database) +def test_unlock_course(set_in_memory_database): + student = StudentHandler(set_in_memory_database) student.name = "any" student.cpf = "123.456.789-10" student.enroll_to_course("any") @@ -19,8 +17,8 @@ def test_unlock_course(): assert student.state == None -def test_lock_course(): - database = db.Database() +def test_lock_course(set_in_memory_database): + database = set_in_memory_database student = StudentHandler(database) student.name = "any" student.cpf = "123.456.789-10" @@ -30,9 +28,10 @@ def test_lock_course(): assert database.student.state == "locked" -def test_take_subject_from_course_when_locked_student_return_error(): - database = db.Database() - student = StudentHandler(database) +def test_take_subject_from_course_when_locked_student_return_error( + set_in_memory_database, +): + student = StudentHandler(set_in_memory_database) student.name = "any" student.cpf = "123.456.789-10" subject = "any" @@ -43,9 +42,8 @@ def test_take_subject_from_course_when_locked_student_return_error(): student.take_subject(subject) -def test_take_full_subject_from_course_return_error(): - database = db.Database() - student = StudentHandler(database) +def test_take_full_subject_from_course_return_error(set_in_memory_database): + student = StudentHandler(set_in_memory_database) student.name = "any" student.cpf = "123.456.789-10" subject = generate_subject_identifier("course1", "subject_full") @@ -55,9 +53,8 @@ def test_take_full_subject_from_course_return_error(): student.take_subject(subject) -def test_take_invalid_subject_from_course_return_error(): - database = db.Database() - student = StudentHandler(database) +def test_take_invalid_subject_from_course_return_error(set_in_memory_database): + student = StudentHandler(set_in_memory_database) student.name = "any" student.cpf = "123.456.789-10" subject = "invalid" @@ -67,22 +64,20 @@ def test_take_invalid_subject_from_course_return_error(): student.take_subject(subject) -def test_take_subject_from_course(): - database = db.Database() - student = StudentHandler(database) +def test_take_subject_from_course(set_in_memory_database): + student = StudentHandler(set_in_memory_database) student.name = "any" student.cpf = "123.456.789-10" + course = "any" student.enroll_to_course("any") - subject_identifier = "e4c858cd917f518194c9d93c9d13def8" + subject_identifier = generate_subject_identifier(course, "any1") - student.enroll_to_course("any") - student.take_subject(subject_identifier) - assert subject_identifier in student.subjects + student.enroll_to_course(course) + assert student.take_subject(subject_identifier) is True -def test_enroll_invalid_student_to_course_retunr_error(): - database = db.Database() - student = StudentHandler(database) +def test_enroll_invalid_student_to_course_retunr_error(set_in_memory_database): + student = StudentHandler(set_in_memory_database) student.name = "invalid" student.cpf = "123.456.789-10" @@ -90,9 +85,8 @@ def test_enroll_invalid_student_to_course_retunr_error(): student.enroll_to_course("any") -def test_enroll_student_to_course_x(): - database = db.Database() - student = StudentHandler(database) +def test_enroll_student_to_course_x(set_in_memory_database): + student = StudentHandler(set_in_memory_database) student.name = "any" student.cpf = "123.456.789-10" diff --git a/tests/test_subject.py b/tests/test_subject.py index 2f04ea6..481dacf 100644 --- a/tests/test_subject.py +++ b/tests/test_subject.py @@ -1,28 +1,25 @@ import pytest from src.services.subject_handler import SubjectHandler, NonValidSubject from src import database as db -from src.utils import generate_subject_identifier +from src import utils -def test_remove_invalid_subject_return_error(): - database = db.Database() - subject_handler = SubjectHandler(database) +def test_remove_invalid_subject_return_error(set_in_memory_database): + subject_handler = SubjectHandler(set_in_memory_database) with pytest.raises(NonValidSubject): subject_handler.remove() -def test_remove(): - database = db.Database() - subject_handler = SubjectHandler(database) - subject_handler.load_from_database(generate_subject_identifier("any", "any1")) +def test_remove(set_in_memory_database): + subject_handler = SubjectHandler(set_in_memory_database) + subject_handler.load_from_database(utils.generate_subject_identifier("any", "any1")) assert subject_handler.remove() == "removed" -def test_activate_removed_subject_return_error(): - database = db.Database() +def test_activate_removed_subject_return_error(set_in_memory_database): subject_handler = SubjectHandler( - database, generate_subject_identifier("any", "any1") + set_in_memory_database, utils.generate_subject_identifier("any", "any1") ) subject_handler.activate() subject_handler.remove() @@ -31,20 +28,18 @@ def test_activate_removed_subject_return_error(): subject_handler.activate() -def test_activate_invalid_subject_return_error(): - database = db.Database() - subject_handler = SubjectHandler(database) +def test_activate_invalid_subject_return_error(set_in_memory_database): + subject_handler = SubjectHandler(set_in_memory_database) subject_handler.load_from_database( - generate_subject_identifier("course1", "subject_removed") + utils.generate_subject_identifier("course1", "subject_removed") ) with pytest.raises(NonValidSubject): subject_handler.activate() -def test_activate(): - database = db.Database() - subject_handler = SubjectHandler(database) - subject_handler.name = "any" +def test_activate(set_in_memory_database): + subject_identifier = utils.generate_subject_identifier("any", "any1") + subject_handler = SubjectHandler(set_in_memory_database, subject_identifier) assert subject_handler.activate() == "active" From b82dfb7603bf02411a6954be9748eaa5d9867071 Mon Sep 17 00:00:00 2001 From: Douglas Cardoso Date: Fri, 12 Apr 2024 01:51:00 -0300 Subject: [PATCH 14/44] Add Grade Calculator --- src/database.py | 54 +++++++++++++++++++++++++++++--- src/services/grade_calculator.py | 27 ++++++++++++++++ src/services/student_handler.py | 15 +++++++++ src/services/subject_handler.py | 6 ++-- tests/test_grade_calculator.py | 11 +++++++ 5 files changed, 105 insertions(+), 8 deletions(-) create mode 100644 src/services/grade_calculator.py create mode 100644 tests/test_grade_calculator.py diff --git a/src/database.py b/src/database.py index 66da5b1..7cd03a8 100644 --- a/src/database.py +++ b/src/database.py @@ -6,9 +6,6 @@ # TODO use inmemory just for tests DATABASE_NAME = "university.db" DATABASE_NAME = ":memory:" -# con = sqlite3.connect(DATABASE_NAME) -# cur = con.cursor() -# print("conxxx ", con) class Database: @@ -116,8 +113,8 @@ class DbCourse: subjects = None def __init__(self, con, cur): - self.con = sqlite3.connect(DATABASE_NAME) - self.cur = self.con.cursor() + self.con = con + self.cur = cur self.cur.execute( f"CREATE TABLE IF NOT EXISTS {self.TABLE} (name, state, identifier, enrolled_students, max_enrollment, subjects)" ) @@ -235,6 +232,52 @@ def save(self): logging.error(str(e)) raise + class DbGradeCalculator: + TABLE = "grade_calculator" + subject_identifier = None + student_identifier = None + grade = None + + def __init__(self, con, cur): + self.con = con + self.cur = cur + cur.execute( + f"CREATE TABLE IF NOT EXISTS {self.TABLE} (student_identifier, subject_identifier, grade)" + ) + + def load(self, student_identifier, subject_identifier): + try: + result = self.cur.execute( + f"""SELECT * FROM {self.TABLE} + WHERE subject_identifier = '{subject_identifier}' + AND student_identifier = '{student_identifier}' + """ + ).fetchone() + if not result: + raise NotFoundError() + + self.student_identifier = result[0] + self.subject_identifier = result[1] + self.grade = result[2] + except Exception as e: + logging.error(str(e)) + raise + + def add(self): + try: + cmd = f""" + INSERT INTO {self.TABLE} VALUES + ('{self.student_identifier}', + '{self.subject_identifier}', + {self.grade}) + """ + self.cur.execute(cmd) + + self.con.commit() + except Exception as e: + logging.error(str(e)) + raise + def __init__(self) -> None: con = sqlite3.connect(DATABASE_NAME) cur = con.cursor() @@ -242,6 +285,7 @@ def __init__(self) -> None: self.enrollment = self.DbEnrollment(con, cur) self.course = self.DbCourse(con, cur) self.subject = self.DbSubject(con, cur) + self.grade_calculator = self.DbGradeCalculator(con, cur) class NotFoundError(Exception): diff --git a/src/services/grade_calculator.py b/src/services/grade_calculator.py new file mode 100644 index 0000000..e7cee05 --- /dev/null +++ b/src/services/grade_calculator.py @@ -0,0 +1,27 @@ +class GradeCalculator: + row = None + + def __init__(self, database) -> None: + self.__databse = database + + def load_from_database(self, student_identifier, subject_identifier): + self.__databse.grade_calculator.load(student_identifier, subject_identifier) + self.student_identifier = self.__databse.grade_calculator.student_identifier + self.subject_identifier = self.__databse.grade_calculator.subject_identifier + self.grade = self.__databse.grade_calculator.grade + + def add(self, student_identifier, subject_identifier, grade): + self.__databse.grade_calculator.student_identifier = student_identifier + self.__databse.grade_calculator.subject_identifier = subject_identifier + self.__databse.grade_calculator.grade = grade + self.__databse.grade_calculator.add() + + # def calculate_for(self, student_identifier): + # student_handler = StudentHandler(self.__databse) + # student_handler.load_from_database(student_identifier) + # total = 0 + # for subject in student_handler.subjects: + # # get grade + # total += subject.grade + + # return total / len(student_handler.subjects) diff --git a/src/services/student_handler.py b/src/services/student_handler.py index b06c222..93b1d08 100644 --- a/src/services/student_handler.py +++ b/src/services/student_handler.py @@ -2,6 +2,7 @@ from src.services.enrollment_validator import EnrollmentValidator from src.services.course_handler import CourseHandler from src.services.subject_handler import SubjectHandler +from src.services.grade_calculator import GradeCalculator class StudentHandler: @@ -128,10 +129,24 @@ def take_subject(self, subject_identifier): self.subjects.append(subject_identifier) self.__save() + subject_handler.enrolled_students.append(self.identifier) + subject_handler.save() + + grade_calculator = GradeCalculator(self.__database) + grade_calculator.add(self.identifier, subject_identifier, grade=0) + # post condition subject_handler.load_from_database(subject_identifier) + assert self.identifier in subject_handler.enrolled_students + + self.load_from_database(self.identifier) assert subject_identifier in self.subjects + grade_calculator.load_from_database(self.identifier, subject_identifier) + assert self.identifier in grade_calculator.student_identifier + assert subject_identifier in grade_calculator.subject_identifier + assert grade_calculator.grade == 0 + return True def unlock_course(self): diff --git a/src/services/subject_handler.py b/src/services/subject_handler.py index 1053e17..e7262b5 100644 --- a/src/services/subject_handler.py +++ b/src/services/subject_handler.py @@ -61,7 +61,7 @@ def activate(self): raise NonValidSubject() self.__state = self.ACTIVE - self.__save() + self.save() # post condition self.__database.subject.load(self.identifier) @@ -73,14 +73,14 @@ def remove(self): raise NonValidSubject() self.__state = self.REMOVED - self.__save() + self.save() # post condition self.__database.subject.load(self.identifier) assert self.__database.subject.state == self.REMOVED return self.__state - def __save(self): + def save(self): self.__database.subject.enrolled_students = ",".join(self.__enrolled_students) self.__database.subject.max_enrollment = self.__max_enrollment self.__database.subject.state = self.__state diff --git a/tests/test_grade_calculator.py b/tests/test_grade_calculator.py new file mode 100644 index 0000000..f12d5a8 --- /dev/null +++ b/tests/test_grade_calculator.py @@ -0,0 +1,11 @@ +from src.services.grade_calculator import GradeCalculator +from src.services.student_handler import StudentHandler +from src import utils + + +# def test_calculate_student_gpa_when_no_grades(set_in_memory_database): +# gpa_handler = GPAHandler(set_in_memory_database) +# student_identifier = utils.generate_student_identifier( +# "any", "123.456.789-10", "any" +# ) +# assert gpa_handler.calculate_for(student_identifier) == 0 From a74e59daf4dc36c7f8f1d4e7ea9a0fdeb183ce6f Mon Sep 17 00:00:00 2001 From: Douglas Cardoso Date: Fri, 12 Apr 2024 20:48:37 -0300 Subject: [PATCH 15/44] Add set grade and grade calculator --- src/database.py | 81 ++++++++++++++++++++-------- src/services/enrollment_validator.py | 14 ++--- src/services/grade_calculator.py | 61 ++++++++++++++++----- src/services/student_handler.py | 64 +++++++++++++++++----- src/utils.py | 4 +- tests/conftest.py | 2 +- tests/test_course.py | 38 +++++++------ tests/test_grade_calculator.py | 45 +++++++++++++--- tests/test_models.py | 13 +++-- 9 files changed, 229 insertions(+), 93 deletions(-) diff --git a/src/database.py b/src/database.py index 7cd03a8..6caa39d 100644 --- a/src/database.py +++ b/src/database.py @@ -2,13 +2,19 @@ import logging from src import utils + # TODO test concurrency -# TODO use inmemory just for tests -DATABASE_NAME = "university.db" -DATABASE_NAME = ":memory:" +class Database: + def __init__(self, database="university.db"): + con = sqlite3.connect(database) + cur = con.cursor() + self.student = self.DbStudent(con, cur) + self.enrollment = self.DbEnrollment(con, cur) + self.course = self.DbCourse(con, cur) + self.subject = self.DbSubject(con, cur) + self.grade_calculator = self.DbGradeCalculator(con, cur) -class Database: class DbStudent: TABLE = "student" name = None @@ -80,13 +86,15 @@ class DbEnrollment: def __init__(self, con, cur): self.cur = cur self.con = con - cur.execute(f"CREATE TABLE IF NOT EXISTS {self.TABLE} (student_identifier)") + self.cur.execute( + f"CREATE TABLE IF NOT EXISTS {self.TABLE} (student_identifier)" + ) # Just for admin. The university has a predefined list of approved students to each course. # TODO create a public funtion - def populate(self, name, cpf, course_identifier): + def populate(self, name, cpf, course_name): student_identifier = utils.generate_student_identifier( - name, cpf, course_identifier + name, cpf, course_name ) self.cur.execute( f""" @@ -96,12 +104,8 @@ def populate(self, name, cpf, course_identifier): self.con.commit() def select(self, student_identifier): - return ( - self.cur.execute( - f"SELECT * FROM {self.TABLE} WHERE student_identifier = '{student_identifier}'" - ).fetchone() - is not None - ) + cmd = f"SELECT * FROM {self.TABLE} WHERE student_identifier = '{student_identifier}'" + return self.cur.execute(cmd).fetchone() is not None class DbCourse: TABLE = "course" @@ -245,6 +249,34 @@ def __init__(self, con, cur): f"CREATE TABLE IF NOT EXISTS {self.TABLE} (student_identifier, subject_identifier, grade)" ) + def load_all_by_student_identifier(self, student_identifier): + try: + cmd = f"""SELECT * FROM {self.TABLE} + WHERE student_identifier = '{student_identifier}' + """ + result = self.cur.execute(cmd).fetchall() + if not result: + raise NotFoundError( + f"Student '{student_identifier}' not found in table '{self.TABLE}'" + ) + + class GradeCalculatorRow: + student_identifier = None + subject_identifier = None + grade = None + + grade_calculators = [] + for row in result: + grade_calculator_row = GradeCalculatorRow() + grade_calculator_row.student_identifier = row[0] + grade_calculator_row.subject_identifier = row[1] + grade_calculator_row.grade = row[2] + grade_calculators.append(grade_calculator_row) + return grade_calculators + except Exception as e: + logging.error(str(e)) + raise + def load(self, student_identifier, subject_identifier): try: result = self.cur.execute( @@ -278,14 +310,21 @@ def add(self): logging.error(str(e)) raise - def __init__(self) -> None: - con = sqlite3.connect(DATABASE_NAME) - cur = con.cursor() - self.student = self.DbStudent(con, cur) - self.enrollment = self.DbEnrollment(con, cur) - self.course = self.DbCourse(con, cur) - self.subject = self.DbSubject(con, cur) - self.grade_calculator = self.DbGradeCalculator(con, cur) + def save(self): + try: + cmd = f""" + UPDATE {self.TABLE} + SET student_identifier = '{self.student_identifier}', + subject_identifier = '{self.subject_identifier}', + grade = {self.grade} + WHERE student_identifier = '{self.student_identifier}'; + """ + self.cur.execute(cmd) + + self.con.commit() + except Exception as e: + logging.error(str(e)) + raise class NotFoundError(Exception): diff --git a/src/services/enrollment_validator.py b/src/services/enrollment_validator.py index de1f72f..3fdbe02 100644 --- a/src/services/enrollment_validator.py +++ b/src/services/enrollment_validator.py @@ -1,18 +1,12 @@ import uuid +from src import utils class EnrollmentValidator: def __init__(self, database): self.__database = database - def validate_student(self, name, cpf, course_identifier): - # the valid students are predifined as the list of approved person in the given course - student_identifier = self.generate_student_identifier( - name, cpf, course_identifier - ) + def validate_student(self, name, cpf, course_name): + # the valid students are predefined as the list of approved person in the given course + student_identifier = utils.generate_student_identifier(name, cpf, course_name) return self.__database.enrollment.select(student_identifier) - - def generate_student_identifier(self, name, cpf, course_identifier): - return uuid.uuid5( - uuid.NAMESPACE_URL, str(f"{name}{cpf}{course_identifier}") - ).hex diff --git a/src/services/grade_calculator.py b/src/services/grade_calculator.py index e7cee05..0322f02 100644 --- a/src/services/grade_calculator.py +++ b/src/services/grade_calculator.py @@ -1,14 +1,45 @@ class GradeCalculator: - row = None - def __init__(self, database) -> None: + self.__student_identifier = None + self.__subject_identifier = None + self.__grade = None + self.__rows = None self.__databse = database + @property + def student_identifier(self): + return self.__student_identifier + + @student_identifier.setter + def student_identifier(self, value): + self.__student_identifier = value + + @property + def subject_identifier(self): + return self.__subject_identifier + + @subject_identifier.setter + def subject_identifier(self, value): + self.__subject_identifier = value + + @property + def grade(self): + return self.__grade + + @grade.setter + def grade(self, value): + self.__grade = value + def load_from_database(self, student_identifier, subject_identifier): self.__databse.grade_calculator.load(student_identifier, subject_identifier) - self.student_identifier = self.__databse.grade_calculator.student_identifier - self.subject_identifier = self.__databse.grade_calculator.subject_identifier - self.grade = self.__databse.grade_calculator.grade + self.__student_identifier = self.__databse.grade_calculator.student_identifier + self.__subject_identifier = self.__databse.grade_calculator.subject_identifier + self.__grade = self.__databse.grade_calculator.grade + + def __load_all_from_database_by(self, student_identifier): + return self.__databse.grade_calculator.load_all_by_student_identifier( + student_identifier + ) def add(self, student_identifier, subject_identifier, grade): self.__databse.grade_calculator.student_identifier = student_identifier @@ -16,12 +47,16 @@ def add(self, student_identifier, subject_identifier, grade): self.__databse.grade_calculator.grade = grade self.__databse.grade_calculator.add() - # def calculate_for(self, student_identifier): - # student_handler = StudentHandler(self.__databse) - # student_handler.load_from_database(student_identifier) - # total = 0 - # for subject in student_handler.subjects: - # # get grade - # total += subject.grade + def save(self): + self.__databse.grade_calculator.student_identifier = self.student_identifier + self.__databse.grade_calculator.subject_identifier = self.subject_identifier + self.__databse.grade_calculator.grade = self.grade + self.__databse.grade_calculator.save() + + def calculate_gpa_for_student(self, student_identifier): + self.__rows = self.__load_all_from_database_by(student_identifier) + total = 0 + for row in self.__rows: + total += row.grade - # return total / len(student_handler.subjects) + return total / len(self.__rows) diff --git a/src/services/student_handler.py b/src/services/student_handler.py index 93b1d08..6090a50 100644 --- a/src/services/student_handler.py +++ b/src/services/student_handler.py @@ -3,6 +3,7 @@ from src.services.course_handler import CourseHandler from src.services.subject_handler import SubjectHandler from src.services.grade_calculator import GradeCalculator +from src import utils class StudentHandler: @@ -17,6 +18,7 @@ def __init__(self, database): self.__course = None self.__name = None self.__cpf = None + self.__subjects_2 = [] self.__database = database @property @@ -58,12 +60,9 @@ def cpf(self, value): def __is_locked(self): return self.__state == self.LOCKED - def __is_valid_student(self, course_identifier=None): - course = self.__course - if course_identifier: - course = course_identifier + def __is_enrolled_student(self, course_name): return EnrollmentValidator(self.__database).validate_student( - self.name, self.cpf, course + self.name, self.cpf, course_name ) def __save(self): @@ -76,15 +75,51 @@ def __save(self): self.__database.student.course = self.__course self.__database.student.save() + def set_grade_to_subject(self, grade, subject_identifier): + if grade < 0 or grade > 10: + raise NonValidGrade("Grade must be between '0' and '10'") + if not self.__is_valid_subject(subject_identifier): + return NonValidSubject( + f"The student is not enrolled to this subject '{subject_identifier}'" + ) + + class Subject: + identifier = None + grade = None + + subject = Subject() + subject.identifier = subject_identifier + subject.grade = grade + self.__subjects_2.append(subject) + + grade_calculator = GradeCalculator(self.__database) + grade_calculator.student_identifier = self.identifier + grade_calculator.subject_identifier = subject_identifier + grade_calculator.grade = grade + grade_calculator.save() + grade_calculator.load_from_database(self.identifier, subject_identifier) + + # post condition + assert grade_calculator.student_identifier == self.identifier + assert grade_calculator.subject_identifier in [ + s.identifier for s in self.__subjects_2 + ] + + def __is_valid_subject(self, subject_identifier): + return subject_identifier in self.subjects + def enroll_to_course(self, course_name): - if not self.__is_valid_student(course_name): - raise NonValidStudent() + if not self.__name: + raise NonValidStudent("Need to set the student's name.") + if not self.__cpf: + raise NonValidStudent("Need to set the student's CPF.") + if not self.__is_enrolled_student(course_name): + raise NonValidStudent("Student does not appears in enrollment list.") course = CourseHandler(self.__database) course.load_from_database(course_name) - enrollment_validator = EnrollmentValidator(self.__database) - self.__identifier = enrollment_validator.generate_student_identifier( + self.__identifier = utils.generate_student_identifier( self.name, self.cpf, course_name ) self.__course = course_name @@ -106,7 +141,7 @@ def take_subject(self, subject_identifier): subject_identifier (str): The unique identifier of the subject. It follows the pattern - """ - is_valid_student = self.__is_valid_student() + is_valid_student = self.__is_enrolled_student(self.__course) if not is_valid_student: raise NonValidStudent() @@ -150,14 +185,14 @@ def take_subject(self, subject_identifier): return True def unlock_course(self): - if self.__is_valid_student(): + if self.__is_enrolled_student(self.__course): self.__state = None self.__save() return self.state raise NonValidStudent() def lock_course(self): - if self.__is_valid_student(): + if self.__is_enrolled_student(self.__course): self.__state = self.LOCKED self.__save() return self.state @@ -176,7 +211,6 @@ def load_from_database(self, student_identifier): self.__course = self.__database.student.course except Exception as e: - print("exxxxx ", str(e)) logging.error(str(e)) raise NonValidStudent("Student not found.") @@ -187,3 +221,7 @@ class NonValidStudent(Exception): class NonValidSubject(Exception): pass + + +class NonValidGrade(Exception): + pass diff --git a/src/utils.py b/src/utils.py index 5e6ed23..c4a7370 100644 --- a/src/utils.py +++ b/src/utils.py @@ -5,8 +5,8 @@ def generate_course_identifier(name): return uuid.uuid5(uuid.NAMESPACE_URL, str(f"{name}")).hex -def generate_student_identifier(name, cpf, course_identifier): - return uuid.uuid5(uuid.NAMESPACE_URL, str(f"{name}{cpf}{course_identifier}")).hex +def generate_student_identifier(name, cpf, course_name): + return uuid.uuid5(uuid.NAMESPACE_URL, str(f"{name}{cpf}{course_name}")).hex def generate_subject_identifier(course, name): diff --git a/tests/conftest.py b/tests/conftest.py index 27fce2d..5deb1b6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,7 +4,7 @@ @pytest.fixture(autouse=True, scope="function") def set_in_memory_database(): - db = database.Database() + db = database.Database(":memory:") # TODO need to check if the courses are available db.enrollment.populate("douglas", "098.765.432.12", "adm") db.enrollment.populate("maria", "028.745.462.18", "mat") diff --git a/tests/test_course.py b/tests/test_course.py index bf4a4e6..412915a 100644 --- a/tests/test_course.py +++ b/tests/test_course.py @@ -1,11 +1,9 @@ import pytest from src.services.course_handler import CourseHandler, NonValidCourse -from src import database as db -def test_enroll_student_to_inactive_course_return_error(): - database = db.Database() - course_handler = CourseHandler(database) +def test_enroll_student_to_inactive_course_return_error(set_in_memory_database): + course_handler = CourseHandler(set_in_memory_database) course_handler.name = "adm" course_handler.add_subject("any1") course_handler.add_subject("any2") @@ -16,8 +14,8 @@ def test_enroll_student_to_inactive_course_return_error(): course_handler.enroll_student("any") -def test_enroll_student_to_active_course(): - database = db.Database() +def test_enroll_student_to_active_course(set_in_memory_database): + database = set_in_memory_database course_handler = CourseHandler(database) course_handler.name = "adm" course_handler.add_subject("any1") @@ -30,8 +28,8 @@ def test_enroll_student_to_active_course(): assert database.course.enrolled_students == "any" -def test_cancel_inactive_course(): - database = db.Database() +def test_cancel_inactive_course(set_in_memory_database): + database = set_in_memory_database course_handler = CourseHandler(database) course_handler.name = "adm" course_handler.add_subject("any1") @@ -43,8 +41,8 @@ def test_cancel_inactive_course(): assert database.course.state == "cancelled" -def test_cancel_active_course(): - database = db.Database() +def test_cancel_active_course(set_in_memory_database): + database = set_in_memory_database course_handler = CourseHandler(database) course_handler.name = "adm" course_handler.add_subject("any1") @@ -56,8 +54,8 @@ def test_cancel_active_course(): assert database.course.state == "cancelled" -def test_deactivate_non_active_course_return_error(): - database = db.Database() +def test_deactivate_non_active_course_return_error(set_in_memory_database): + database = set_in_memory_database course_handler = CourseHandler(database) with pytest.raises(NonValidCourse): course_handler.deactivate() @@ -65,8 +63,8 @@ def test_deactivate_non_active_course_return_error(): assert database.course.state != "inactive" -def test_deactivate_course(): - database = db.Database() +def test_deactivate_course(set_in_memory_database): + database = set_in_memory_database course_handler = CourseHandler(database) course_handler.name = "adm" course_handler.add_subject("any1") @@ -78,8 +76,8 @@ def test_deactivate_course(): assert database.course.state == "inactive" -def test_activate_course_without_minimum_subjects_return_error(): - database = db.Database() +def test_activate_course_without_minimum_subjects_return_error(set_in_memory_database): + database = set_in_memory_database course_handler = CourseHandler(database) course_handler.name = "any" with pytest.raises(NonValidCourse): @@ -87,16 +85,16 @@ def test_activate_course_without_minimum_subjects_return_error(): assert database.course.state != "active" -def test_activate_course_without_name_return_error(): - database = db.Database() +def test_activate_course_without_name_return_error(set_in_memory_database): + database = set_in_memory_database course_handler = CourseHandler(database) with pytest.raises(NonValidCourse): course_handler.activate() assert database.course.state != "active" -def test_activate_course(): - database = db.Database() +def test_activate_course(set_in_memory_database): + database = set_in_memory_database course_handler = CourseHandler(database) course_handler.name = "adm" course_handler.add_subject("any1") diff --git a/tests/test_grade_calculator.py b/tests/test_grade_calculator.py index f12d5a8..636c6cf 100644 --- a/tests/test_grade_calculator.py +++ b/tests/test_grade_calculator.py @@ -3,9 +3,42 @@ from src import utils -# def test_calculate_student_gpa_when_no_grades(set_in_memory_database): -# gpa_handler = GPAHandler(set_in_memory_database) -# student_identifier = utils.generate_student_identifier( -# "any", "123.456.789-10", "any" -# ) -# assert gpa_handler.calculate_for(student_identifier) == 0 +def test_calculate_student_gpa_when_subjects_have_grades(set_in_memory_database): + course_name = "any" + database = set_in_memory_database + grade_calculator = GradeCalculator(database) + student_handler = StudentHandler(database) + student_handler.name = "any" + student_handler.cpf = "123.456.789-10" + student_handler.enroll_to_course(course_name) + + subject_identifier1 = utils.generate_subject_identifier(course_name, "any1") + subject_identifier2 = utils.generate_subject_identifier(course_name, "any2") + + student_handler.take_subject(subject_identifier1) + student_handler.take_subject(subject_identifier2) + + grade = 7 + student_handler.set_grade_to_subject( + grade=grade, subject_identifier=subject_identifier1 + ) + student_handler.set_grade_to_subject( + grade=grade, subject_identifier=subject_identifier2 + ) + assert ( + grade_calculator.calculate_gpa_for_student(student_handler.identifier) == grade + ) + + +def test_calculate_student_gpa_when_no_grades(set_in_memory_database): + course_name = "any" + database = set_in_memory_database + grade_calculator = GradeCalculator(database) + student_handler = StudentHandler(database) + student_handler.name = "any" + student_handler.cpf = "123.456.789-10" + student_handler.enroll_to_course(course_name) + + student_handler.take_subject(utils.generate_subject_identifier(course_name, "any1")) + student_handler.take_subject(utils.generate_subject_identifier(course_name, "any2")) + assert grade_calculator.calculate_gpa_for_student(student_handler.identifier) == 0 diff --git a/tests/test_models.py b/tests/test_models.py index 7e6b78f..0254cf0 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -2,7 +2,6 @@ from src.services.course_handler import CourseHandler from src.services.subject_handler import SubjectHandler from src.services.semester_monitor import SemesterHandler -from src import database as db def test_semester_model(): @@ -12,8 +11,8 @@ def test_semester_model(): assert semester.state == "open" -def test_subject_model(): - database = db.Database() +def test_subject_model(set_in_memory_database): + database = set_in_memory_database subject_handler = SubjectHandler(database) subject_handler.name = "any_name" @@ -25,8 +24,8 @@ def test_subject_model(): assert subject_handler.course == None -def test_course_model(): - database = db.Database() +def test_course_model(set_in_memory_database): + database = set_in_memory_database course_handler = CourseHandler(database) course_handler.name = "any_name" course_handler.save() @@ -46,8 +45,8 @@ def test_course_model(): assert database.course.subjects == "" -def test_student_model(): - database = db.Database() +def test_student_model(set_in_memory_database): + database = set_in_memory_database student = StudentHandler(database) student.name = "any_name" student.cpf = "123.456.789-10" From b6a399853fff072a91cbc84c555df7d6ce0071fe Mon Sep 17 00:00:00 2001 From: Douglas Cardoso Date: Fri, 12 Apr 2024 21:09:58 -0300 Subject: [PATCH 16/44] Add tests to set grade --- tests/test_grade_calculator.py | 27 ++++++++++++++++++++++----- tests/test_student.py | 34 +++++++++++++++++++++++++++++++--- 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/tests/test_grade_calculator.py b/tests/test_grade_calculator.py index 636c6cf..5000636 100644 --- a/tests/test_grade_calculator.py +++ b/tests/test_grade_calculator.py @@ -1,9 +1,20 @@ +import pytest from src.services.grade_calculator import GradeCalculator from src.services.student_handler import StudentHandler from src import utils -def test_calculate_student_gpa_when_subjects_have_grades(set_in_memory_database): +@pytest.mark.parametrize( + "grade1,grade2,grade3,expected", + [ + (7, 7, 7, 7), + (5, 5, 5, 5), + (5.1, 5.1, 5.1, 5.1), + ], +) +def test_calculate_student_gpa_when_subjects_have_grades( + set_in_memory_database, grade1, grade2, grade3, expected +): course_name = "any" database = set_in_memory_database grade_calculator = GradeCalculator(database) @@ -14,19 +25,25 @@ def test_calculate_student_gpa_when_subjects_have_grades(set_in_memory_database) subject_identifier1 = utils.generate_subject_identifier(course_name, "any1") subject_identifier2 = utils.generate_subject_identifier(course_name, "any2") + subject_identifier3 = utils.generate_subject_identifier(course_name, "any3") student_handler.take_subject(subject_identifier1) student_handler.take_subject(subject_identifier2) + student_handler.take_subject(subject_identifier3) - grade = 7 student_handler.set_grade_to_subject( - grade=grade, subject_identifier=subject_identifier1 + grade=grade1, subject_identifier=subject_identifier1 ) student_handler.set_grade_to_subject( - grade=grade, subject_identifier=subject_identifier2 + grade=grade2, subject_identifier=subject_identifier2 ) + student_handler.set_grade_to_subject( + grade=grade3, subject_identifier=subject_identifier3 + ) + assert ( - grade_calculator.calculate_gpa_for_student(student_handler.identifier) == grade + grade_calculator.calculate_gpa_for_student(student_handler.identifier) + == expected ) diff --git a/tests/test_student.py b/tests/test_student.py index 3692ff8..7611222 100644 --- a/tests/test_student.py +++ b/tests/test_student.py @@ -3,8 +3,36 @@ StudentHandler, NonValidStudent, NonValidSubject, + NonValidGrade, ) -from src.utils import generate_subject_identifier +from src import utils + + +@pytest.mark.parametrize( + "grade", + [ + (-1), + (11), + ], +) +def test_calculate_student_gpa_when_subjects_have_invalid_grades( + set_in_memory_database, grade +): + course_name = "any" + database = set_in_memory_database + student_handler = StudentHandler(database) + student_handler.name = "any" + student_handler.cpf = "123.456.789-10" + student_handler.enroll_to_course(course_name) + + subject_identifier1 = utils.generate_subject_identifier(course_name, "any1") + + student_handler.take_subject(subject_identifier1) + + with pytest.raises(NonValidGrade): + student_handler.set_grade_to_subject( + grade=grade, subject_identifier=subject_identifier1 + ) def test_unlock_course(set_in_memory_database): @@ -46,7 +74,7 @@ def test_take_full_subject_from_course_return_error(set_in_memory_database): student = StudentHandler(set_in_memory_database) student.name = "any" student.cpf = "123.456.789-10" - subject = generate_subject_identifier("course1", "subject_full") + subject = utils.generate_subject_identifier("course1", "subject_full") student.enroll_to_course("any") with pytest.raises(NonValidSubject): @@ -70,7 +98,7 @@ def test_take_subject_from_course(set_in_memory_database): student.cpf = "123.456.789-10" course = "any" student.enroll_to_course("any") - subject_identifier = generate_subject_identifier(course, "any1") + subject_identifier = utils.generate_subject_identifier(course, "any1") student.enroll_to_course(course) assert student.take_subject(subject_identifier) is True From 2399d5dee48c22d3f30e7cad0e071948e4d7756e Mon Sep 17 00:00:00 2001 From: Douglas Cardoso Date: Sat, 13 Apr 2024 22:18:37 -0300 Subject: [PATCH 17/44] Add tests to semester and cli command to GPA --- cli.py | 26 +++++- manual_test.sh | 7 +- src/cli_helper.py | 24 +++++- src/database.py | 121 ++++++++++++++++++++++++--- src/services/enrollment_validator.py | 1 - src/services/semester_monitor.py | 64 ++++++++++++-- src/services/student_handler.py | 84 +++++++++++++------ tests/conftest.py | 6 +- tests/test_grade_calculator.py | 6 +- tests/test_models.py | 6 +- tests/test_semester.py | 40 +++++++++ tests/test_student.py | 10 ++- tests/test_subject.py | 1 - 13 files changed, 328 insertions(+), 68 deletions(-) create mode 100644 tests/test_semester.py diff --git a/cli.py b/cli.py index 38454c9..c7ec3ae 100644 --- a/cli.py +++ b/cli.py @@ -45,16 +45,34 @@ def activate_course(name): "--cpf", prompt="Student CPF. E.g. 123.456.789-10", help="CPF of the student." ) @click.option( - "--course-identifier", + "--course-name", prompt="Course number identifier", help="Course number identifier.", ) -def enroll_student(name, cpf, course_identifier): - database = Database() - cli_helper.enroll_student(database, name, cpf, course_identifier) +def enroll_student(name, cpf, course_name): + try: + database = Database() + cli_helper.enroll_student(database, name, cpf, course_name) + except Exception as e: + logging.error(str(e)) + + +@click.command() +@click.option( + "--student-identifier", + prompt="Student identifier", + help="Student identifier number.", +) +def calculate_student_gpa(student_identifier): + try: + database = Database() + cli_helper.calculate_student_gpa(database, student_identifier) + except Exception as e: + logging.error(str(e)) cli.add_command(enroll_student) +cli.add_command(calculate_student_gpa) cli.add_command(activate_course) cli.add_command(deactivate_course) cli.add_command(cancel_course) diff --git a/manual_test.sh b/manual_test.sh index cc0ec9e..372116b 100755 --- a/manual_test.sh +++ b/manual_test.sh @@ -1,9 +1,10 @@ # set -x rm university.db python for_admin.py -python cli.py enroll-student --name any --cpf 123.456.789-10 --course-identifier any -python cli.py enroll-student --name douglas --cpf 098.765.432.12 --course-identifier adm -python cli.py enroll-student --name any --cpf 123.456.789-10 --course-identifier invalid +python cli.py enroll-student --name any --cpf 123.456.789-10 --course-name any +python cli.py enroll-student --name douglas --cpf 098.765.432.12 --course-name adm +python cli.py enroll-student --name any --cpf 123.456.789-10 --course-name invalid python cli.py activate-course --name deact python cli.py deactivate-course --name act python cli.py cancel-course --name any +python cli.py calculate-student-gpa --student-identifier 290f2113c2e6579c8bb6ec395ea56572 diff --git a/src/cli_helper.py b/src/cli_helper.py index 06c7953..e45d8e2 100644 --- a/src/cli_helper.py +++ b/src/cli_helper.py @@ -1,6 +1,7 @@ import logging from src.services.student_handler import StudentHandler, NonValidStudent from src.services.course_handler import CourseHandler, NonValidCourse +from src.services.grade_calculator import GradeCalculator UNEXPECTED_ERROR = "Unexpected error. Consult the system adminstrator." @@ -53,18 +54,33 @@ def activate_course(database, name): return False -def enroll_student(database, name, cpf, course_identifier): +def calculate_student_gpa(database, student_identifier): + try: + grade_calculator = GradeCalculator(database) + gpa = grade_calculator.calculate_gpa_for_student(student_identifier) + print(f"GPA of student '{student_identifier}' is '{gpa}'.") + return True + except NonValidStudent as e: + logging.error(str(e)) + print(f"Student '{student_identifier}' is not valid'") + except Exception as e: + logging.error(str(e)) + print(UNEXPECTED_ERROR) + return False + + +def enroll_student(database, name, cpf, course_name): try: student = StudentHandler(database) student.name = name student.cpf = cpf - student.enroll_to_course(course_identifier) - print("Student enrolled.") + identifier = student.enroll_to_course(course_name) + print(f"Student enrolled with identifier '{identifier}'.") return True except NonValidStudent as e: logging.error(str(e)) print( - f"Student '{name}' with CPF '{cpf}' is not valid in course '{course_identifier}'" + f"Student '{name}' with CPF '{cpf}' is not valid in course '{course_name}'" ) except Exception as e: logging.error(str(e)) diff --git a/src/database.py b/src/database.py index 6caa39d..2bd806e 100644 --- a/src/database.py +++ b/src/database.py @@ -14,6 +14,7 @@ def __init__(self, database="university.db"): self.course = self.DbCourse(con, cur) self.subject = self.DbSubject(con, cur) self.grade_calculator = self.DbGradeCalculator(con, cur) + self.semester = self.DbSemester(con, cur) class DbStudent: TABLE = "student" @@ -33,6 +34,25 @@ def __init__(self, con, cur): f"CREATE TABLE IF NOT EXISTS {self.TABLE} (name, state, cpf, identifier, gpa, subjects, course)" ) + def add(self): + try: + cmd = f""" + INSERT INTO {self.TABLE} VALUES + ('{self.name}', + '{self.state}', + '{self.cpf}', + '{self.identifier}', + '{self.gpa}', + '{self.subjects}', + '{self.course}') + """ + self.cur.execute(cmd) + + self.con.commit() + except Exception as e: + logging.error(str(e)) + raise + def save(self): cmd = f""" UPDATE {self.TABLE} @@ -68,17 +88,23 @@ def populate( self.con.commit() def load(self, identifier): - cmd = f"SELECT * FROM {self.TABLE} WHERE identifier = '{identifier}'" - result = self.cur.execute(cmd).fetchone() - if not result: - raise NotFoundError() - self.name = result[0] - self.state = result[1] - self.cpf = result[2] - self.identifier = result[3] - self.gpq = result[4] - self.subjects = result[5].split(",") - self.course = result[6] + try: + cmd = f"SELECT * FROM {self.TABLE} WHERE identifier = '{identifier}'" + result = self.cur.execute(cmd).fetchone() + if not result: + raise NotFoundError( + f"Student '{self.identifier}' not found in table '{self.TABLE}'." + ) + self.name = result[0] + self.state = result[1] + self.cpf = result[2] + self.identifier = result[3] + self.gpq = result[4] + self.subjects = result[5].split(",") + self.course = result[6] + except Exception as e: + logging.error(str(e)) + raise class DbEnrollment: TABLE = "enrollment" @@ -238,8 +264,8 @@ def save(self): class DbGradeCalculator: TABLE = "grade_calculator" - subject_identifier = None student_identifier = None + subject_identifier = None grade = None def __init__(self, con, cur): @@ -326,6 +352,77 @@ def save(self): logging.error(str(e)) raise + class DbSemester: + TABLE = "semester" + identifier = None + state = None + + def __init__(self, con, cur): + self.con = con + self.cur = cur + cur.execute(f"CREATE TABLE IF NOT EXISTS {self.TABLE} (identifier, state)") + + # Just for admin. + # TODO create a public funtion + def populate(self, identifier, state): + self.cur.execute( + f""" + INSERT INTO {self.TABLE} VALUES + ('{identifier}', + '{state}') + """ + ) + self.con.commit() + + x = self.cur.execute(f"select * from {self.TABLE}").fetchall() + + def save(self): + try: + cmd = f""" + UPDATE {self.TABLE} + SET state = '{self.state}' + WHERE identifier = '{self.identifier}'; + """ + self.cur.execute(cmd) + + self.con.commit() + except Exception as e: + logging.error(str(e)) + raise + + def load_open(self): + try: + result = self.cur.execute( + f"""SELECT * FROM {self.TABLE} + WHERE state = 'open' + """ + ).fetchone() + + if not result: + raise NotFoundError("No open semester found") + + self.identifier = result[0] + self.state = result[1] + except Exception as e: + logging.error(str(e)) + raise + + def load_by_identifier(self): + try: + result = self.cur.execute( + f"""SELECT * FROM {self.TABLE} + WHERE identifier = '{self.identifier}' + """ + ).fetchone() + if not result: + raise NotFoundError(f"Semester '{self.identifier}' not found") + + self.identifier = result[0] + self.state = result[1] + except Exception as e: + logging.error(str(e)) + raise + class NotFoundError(Exception): pass diff --git a/src/services/enrollment_validator.py b/src/services/enrollment_validator.py index 3fdbe02..c1f15a8 100644 --- a/src/services/enrollment_validator.py +++ b/src/services/enrollment_validator.py @@ -1,4 +1,3 @@ -import uuid from src import utils diff --git a/src/services/semester_monitor.py b/src/services/semester_monitor.py index d2a95aa..a0b99ac 100644 --- a/src/services/semester_monitor.py +++ b/src/services/semester_monitor.py @@ -1,11 +1,14 @@ -import uuid -import datetime +import logging -class SemesterHandler: - def __init__(self) -> None: - self.__identifier = "2024-1" # TODO get next from database - self.__state = "open" +class SemesterMonitor: + + def __init__(self, database, identifier) -> None: + self.__CLOSED = "closed" + self.__OPEN = "open" + self.__identifier = identifier # TODO get next from database + self.__state = self.__OPEN + self.__database = database @property def identifier(self): @@ -18,3 +21,52 @@ def state(self): @state.setter def state(self, value): self.__state = value + + def open(self): + if not self.identifier: + raise NonValidSemester("Need to set the semester identifier") + if self.__state == self.__CLOSED: + raise NonValidOperation( + f"It is not possible to reopen the closed semester '{self.identifier}'" + ) + self.__database.semester.load_open() + if self.identifier != self.__database.semester.identifier: + raise NonValidOperation( + f"Trying to open a new semester. Opened semester is not '{self.identifier}'" + ) + self.__state = self.__OPEN + + self.__database.semester.identifier = self.identifier + self.__database.semester.state = self.state + self.__database.semester.save() + + # post condition + assert self.__state == self.__database.semester.state + return self.__state + + def close(self): + if not self.identifier: + raise NonValidSemester("Need to set the semester identifier.") + + self.__state = self.__CLOSED + self.__database.semester.identifier = self.identifier + self.__database.semester.state = self.state + self.__database.semester.save() + + # post condition + try: + self.__database.semester.load_by_identifier() + except Exception as e: + logging.error(str(e)) + raise NonValidOperation(f"Semester '{self.identifier}' is not valid.") + assert self.identifier == self.__database.semester.identifier + assert self.__state == self.__database.semester.state + return self.__state + + +class NonValidOperation(Exception): + pass + + +class NonValidSemester(Exception): + pass diff --git a/src/services/student_handler.py b/src/services/student_handler.py index 6090a50..308d291 100644 --- a/src/services/student_handler.py +++ b/src/services/student_handler.py @@ -75,7 +75,23 @@ def __save(self): self.__database.student.course = self.__course self.__database.student.save() - def set_grade_to_subject(self, grade, subject_identifier): + def __add(self): + self.__database.student.name = self.name + self.__database.student.state = self.state + self.__database.student.cpf = self.cpf + self.__database.student.identifier = self.identifier + self.__database.student.gpa = self.gpa + self.__database.student.subjects = ",".join(self.subjects) + self.__database.student.course = self.__course + self.__database.student.add() + + def __add_to_grade_calculator(self): + self.__database.grade_calculator.student_identifier = self.identifier + self.__database.grade_calculator.subject_identifier = -1 + self.__database.grade_calculator.grade = 0 + self.__database.grade_calculator.add() + + def update_grade_to_subject(self, grade, subject_identifier): if grade < 0 or grade > 10: raise NonValidGrade("Grade must be between '0' and '10'") if not self.__is_valid_subject(subject_identifier): @@ -109,32 +125,48 @@ def __is_valid_subject(self, subject_identifier): return subject_identifier in self.subjects def enroll_to_course(self, course_name): - if not self.__name: - raise NonValidStudent("Need to set the student's name.") - if not self.__cpf: - raise NonValidStudent("Need to set the student's CPF.") - if not self.__is_enrolled_student(course_name): - raise NonValidStudent("Student does not appears in enrollment list.") - - course = CourseHandler(self.__database) - course.load_from_database(course_name) - - self.__identifier = utils.generate_student_identifier( - self.name, self.cpf, course_name - ) - self.__course = course_name - course.enroll_student(self.identifier) - self.__state = self.ENROLLED - self.__save() - - # post condition - self.__database.student.load(self.identifier) - assert self.__database.student.identifier == self.identifier - assert self.__database.student.state == self.ENROLLED - assert self.__database.student.course == self.__course - assert self.identifier in course.enrolled_students + try: + if not self.__name: + raise NonValidStudent("Need to set the student's name.") + if not self.__cpf: + raise NonValidStudent("Need to set the student's CPF.") + if not self.__is_enrolled_student(course_name): + raise NonValidStudent( + f"Student '{self.identifier}' does not appears in enrollment list." + ) + + course = CourseHandler(self.__database) + course.load_from_database(course_name) + + self.__identifier = utils.generate_student_identifier( + self.name, self.cpf, course_name + ) + self.__course = course_name + course.enroll_student(self.identifier) + self.__state = self.ENROLLED + self.__add() + self.__add_to_grade_calculator() + + # post condition + self.__database.student.load(self.identifier) + assert self.__database.student.identifier == self.identifier + assert self.__database.student.state == self.ENROLLED + assert self.__database.student.course == self.__course + assert self.identifier in course.enrolled_students + + result = self.__database.grade_calculator.load_all_by_student_identifier( + self.identifier + ) + for row in result: + assert row.student_identifier == self.identifier + # assert row.subject_identifier + # assert self.__database.grade_calculator.course == self.__course + # assert self.identifier in course.enrolled_students - return True + return self.identifier + except Exception as e: + logging.error(str(e)) + raise def take_subject(self, subject_identifier): """ diff --git a/tests/conftest.py b/tests/conftest.py index 5deb1b6..492b976 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,8 +11,6 @@ def set_in_memory_database(): db.enrollment.populate("joana", "038.745.452.19", "port") db.enrollment.populate("any", "123.456.789-10", "any") - db.student.populate("any", "123.456.789-10", "any") - db.course.populate("adm") db.course.populate("mat") db.course.populate("port") @@ -30,4 +28,8 @@ def set_in_memory_database(): db.subject.populate( "course1", "subject_removed", 0, "removed" ) # ef15a071407953bd858cfca59ad99056 + + db.semester.populate("2023-2", "closed") + db.semester.populate("2024-1", "open") + yield db diff --git a/tests/test_grade_calculator.py b/tests/test_grade_calculator.py index 5000636..a7a62b6 100644 --- a/tests/test_grade_calculator.py +++ b/tests/test_grade_calculator.py @@ -31,13 +31,13 @@ def test_calculate_student_gpa_when_subjects_have_grades( student_handler.take_subject(subject_identifier2) student_handler.take_subject(subject_identifier3) - student_handler.set_grade_to_subject( + student_handler.update_grade_to_subject( grade=grade1, subject_identifier=subject_identifier1 ) - student_handler.set_grade_to_subject( + student_handler.update_grade_to_subject( grade=grade2, subject_identifier=subject_identifier2 ) - student_handler.set_grade_to_subject( + student_handler.update_grade_to_subject( grade=grade3, subject_identifier=subject_identifier3 ) diff --git a/tests/test_models.py b/tests/test_models.py index 0254cf0..c2902d9 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,11 +1,11 @@ from src.services.student_handler import StudentHandler from src.services.course_handler import CourseHandler from src.services.subject_handler import SubjectHandler -from src.services.semester_monitor import SemesterHandler +from src.services.semester_monitor import SemesterMonitor -def test_semester_model(): - semester = SemesterHandler() +def test_semester_model(set_in_memory_database): + semester = SemesterMonitor(set_in_memory_database, "2024-1") assert semester.identifier == "2024-1" assert semester.state == "open" diff --git a/tests/test_semester.py b/tests/test_semester.py new file mode 100644 index 0000000..8817682 --- /dev/null +++ b/tests/test_semester.py @@ -0,0 +1,40 @@ +import pytest +from src.services.semester_monitor import ( + SemesterMonitor, + NonValidOperation, +) + + +def test_return_error_when_close_invalid_semester(set_in_memory_database): + identifier = "3024-1" + semester_monitor = SemesterMonitor(set_in_memory_database, identifier) + with pytest.raises(NonValidOperation): + semester_monitor.close() + + +def test_return_error_when_open_invalid_semester(set_in_memory_database): + identifier = "3024-1" + semester_monitor = SemesterMonitor(set_in_memory_database, identifier) + with pytest.raises(NonValidOperation): + semester_monitor.open() + + +def test_open_closed_semester_return_error(set_in_memory_database): + identifier = "2024-1" + semester_monitor = SemesterMonitor(set_in_memory_database, identifier) + semester_monitor.close() + with pytest.raises(NonValidOperation): + semester_monitor.open() + + +def test_open_semester(set_in_memory_database): + identifier = "2024-1" + semester_monitor = SemesterMonitor(set_in_memory_database, identifier) + assert semester_monitor.open() == "open" + + +def test_close_semester(set_in_memory_database): + identifier = "2024-1" + semester_monitor = SemesterMonitor(set_in_memory_database, identifier) + + assert semester_monitor.close() == "closed" diff --git a/tests/test_student.py b/tests/test_student.py index 7611222..0ceb7c1 100644 --- a/tests/test_student.py +++ b/tests/test_student.py @@ -30,7 +30,7 @@ def test_calculate_student_gpa_when_subjects_have_invalid_grades( student_handler.take_subject(subject_identifier1) with pytest.raises(NonValidGrade): - student_handler.set_grade_to_subject( + student_handler.update_grade_to_subject( grade=grade, subject_identifier=subject_identifier1 ) @@ -113,9 +113,13 @@ def test_enroll_invalid_student_to_course_retunr_error(set_in_memory_database): student.enroll_to_course("any") -def test_enroll_student_to_course_x(set_in_memory_database): +def test_enroll_student_to_course(set_in_memory_database): student = StudentHandler(set_in_memory_database) student.name = "any" student.cpf = "123.456.789-10" + course_name = "any" + identifier = utils.generate_student_identifier( + student.name, student.cpf, course_name + ) - assert student.enroll_to_course("any") is True + assert student.enroll_to_course(course_name) == identifier diff --git a/tests/test_subject.py b/tests/test_subject.py index 481dacf..3adb54b 100644 --- a/tests/test_subject.py +++ b/tests/test_subject.py @@ -1,6 +1,5 @@ import pytest from src.services.subject_handler import SubjectHandler, NonValidSubject -from src import database as db from src import utils From 7c5903c74458b39c71ca0a62b03b8575b4f2dd63 Mon Sep 17 00:00:00 2001 From: Douglas Cardoso Date: Mon, 15 Apr 2024 22:45:25 -0300 Subject: [PATCH 18/44] Add architecture and smoke test files --- .gitignore | 5 +++-- README.md | 6 +++++- architecture.odp | Bin 0 -> 25327 bytes coverage.sh | 2 +- manual_test.sh => manual_smoke_test.sh | 0 run.sh | 1 - 6 files changed, 9 insertions(+), 5 deletions(-) create mode 100644 architecture.odp rename manual_test.sh => manual_smoke_test.sh (100%) delete mode 100755 run.sh diff --git a/.gitignore b/.gitignore index 1483c72..a35bca1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ unit_test/ -venv/ +venv*/ tdd_detroid/ htmlcov/ -sample.db +*.db *.pyc +*.log .coverage \ No newline at end of file diff --git a/README.md b/README.md index 01d87c3..a2518a9 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,13 @@ {Translated from Portuguese to English using AI} # College Student Grade Control ## Introduction -This project aims to practice "code smells" related to unit tests using an application that simulates a real business scenario. +This project aims to practice architecture skills using an application that simulates a real business scenario. The code will be developed using the TDD technique in the Detroit style, hence the name of the repository. The application simulates the control of grades for university students. +# Architecture +[text](architecture.odp) + # Setup and Test ``` python3.11 -m venv venv @@ -37,6 +40,7 @@ python cli.py --name any --cpf 123.456.789-10 --course-identifier any ``` +# Application specification Below is the specification of the application: Definition of Done: diff --git a/architecture.odp b/architecture.odp new file mode 100644 index 0000000000000000000000000000000000000000..7d9115341977cf0e30a5e3e8d7f34a30a2550e52 GIT binary patch literal 25327 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ⅈqZ03 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 literal 0 HcmV?d00001 diff --git a/coverage.sh b/coverage.sh index 13fd930..a6e11af 100755 --- a/coverage.sh +++ b/coverage.sh @@ -1,3 +1,3 @@ -coverage run --include='main.py' --source='src' -m pytest +coverage run --include='cli.py' --source='src' -m pytest coverage report coverage html \ No newline at end of file diff --git a/manual_test.sh b/manual_smoke_test.sh similarity index 100% rename from manual_test.sh rename to manual_smoke_test.sh diff --git a/run.sh b/run.sh deleted file mode 100755 index 8d17153..0000000 --- a/run.sh +++ /dev/null @@ -1 +0,0 @@ -pytest --html=report.html --self-contained-html From e82147a8196d8aab1cc58f5d7556bfd090d2d7ee Mon Sep 17 00:00:00 2001 From: Douglas Cardoso Date: Tue, 16 Apr 2024 00:00:49 -0300 Subject: [PATCH 19/44] Add take subject command --- README.md | 14 ++++++++++-- architecture.odp | Bin 25327 -> 24333 bytes cli.py | 21 +++++++++++++++++ for_admin.py | 4 +++- manual_smoke_test.sh | 8 +++---- src/cli_helper.py | 16 +++++++++++++ src/database.py | 4 +++- src/services/enrollment_validator.py | 6 ++++- src/services/student_handler.py | 33 +++++++++++++++------------ src/utils.py | 6 +++++ tests/test_grade_calculator.py | 28 +++++++++-------------- tests/test_student.py | 16 +++++-------- 12 files changed, 106 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index a2518a9..72590eb 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ Definition of Done: 3. Data is being saved in the database. # Deliverables Construction of the basic functions of the system -1. Each student will have a grade control called "grade point average" (GPA). +1. DONE Each student will have a grade control called "grade point average" (GPA). 2. The GPA is the average of the student's grades in the ~~courses~~ subjects already taken. 3. The student is considered approved at the university if their GPA is above or equal to 7 (seven) at the end of the course. 4. If a student takes the same subject more than once, the highest grade will be considered in the GPA calculation. @@ -104,4 +104,14 @@ Construction of the basic functions of the system 47. The student has 10 semesters to graduate. 48. If the student exceeds the 10 semesters, they are automatically failed. 49. The coordinator can only coordinate a maximum of 3 courses. -50. The general coordinator cannot be a coordinator of courses. \ No newline at end of file +50. The general coordinator cannot be a coordinator of courses. + +# Features add after architecture analisys +These features were introduced after analysis in architecture and specifications. Some features does not make sense without them: +51. The teacher sets thes grade for all students of his/her subjects +52. Each teacher may teach in 3 subjects at maximum +53. The general coordinator is responsible to open and close the semesters +54. The general coordinator is responible to add students to enrollment list after manual analysis of thier documentation +55. The corse coordinator is responsible to add new subjects to his/her course +56. The general coordinator is responsible to add new courses to the university +57. The student, teacher and coordinators need to authenticate with valid credentials before perfom any action in the system \ No newline at end of file diff --git a/architecture.odp b/architecture.odp index 7d9115341977cf0e30a5e3e8d7f34a30a2550e52..4a2b4b372bd2b3310de8b54b4902f5486f496d0f 100644 GIT binary patch delta 20764 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 aJJEkPjpTn>uF#SYbjX(F+K}0A_WuC6(nd%C delta 21751 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 diff --git a/cli.py b/cli.py index c7ec3ae..49fb94f 100644 --- a/cli.py +++ b/cli.py @@ -9,6 +9,7 @@ datefmt="%Y-%m-%d %H:%M:%S", format="%(asctime)s - %(levelname)s: [%(filename)s:%(lineno)s - %(funcName)20s() ] %(message)s", filemode="a", + level="DEBUG", ) @@ -71,7 +72,27 @@ def calculate_student_gpa(student_identifier): logging.error(str(e)) +@click.command() +@click.option( + "--student-identifier", + prompt="Student identifier", + help="Student identifier number.", +) +@click.option( + "--subject-name", + prompt="Subject name", + help="The name of the subject the student wants to take.", +) +def take_subject(student_identifier, subject_name): + try: + database = Database() + cli_helper.take_subject(database, student_identifier, subject_name) + except Exception as e: + logging.error(str(e)) + + cli.add_command(enroll_student) +cli.add_command(take_subject) cli.add_command(calculate_student_gpa) cli.add_command(activate_course) cli.add_command(deactivate_course) diff --git a/for_admin.py b/for_admin.py index ed88fef..862b43a 100644 --- a/for_admin.py +++ b/for_admin.py @@ -5,7 +5,9 @@ db.enrollment.populate("douglas", "098.765.432.12", "adm") db.enrollment.populate("maria", "028.745.462.18", "mat") db.enrollment.populate("joana", "038.745.452.19", "port") -db.enrollment.populate("any", "123.456.789-10", "any") +db.enrollment.populate( + "any", "123.456.789-10", "any" +) # 290f2113c2e6579c8bb6ec395ea56572 db.course.populate("adm") db.course.populate("mat") diff --git a/manual_smoke_test.sh b/manual_smoke_test.sh index 372116b..5ee9c02 100755 --- a/manual_smoke_test.sh +++ b/manual_smoke_test.sh @@ -2,9 +2,9 @@ rm university.db python for_admin.py python cli.py enroll-student --name any --cpf 123.456.789-10 --course-name any -python cli.py enroll-student --name douglas --cpf 098.765.432.12 --course-name adm -python cli.py enroll-student --name any --cpf 123.456.789-10 --course-name invalid +python cli.py take-subject --student-identifier 290f2113c2e6579c8bb6ec395ea56572 --subject-name any1 +python cli.py calculate-student-gpa --student-identifier 290f2113c2e6579c8bb6ec395ea56572 + python cli.py activate-course --name deact python cli.py deactivate-course --name act -python cli.py cancel-course --name any -python cli.py calculate-student-gpa --student-identifier 290f2113c2e6579c8bb6ec395ea56572 +python cli.py cancel-course --name any \ No newline at end of file diff --git a/src/cli_helper.py b/src/cli_helper.py index e45d8e2..08880ab 100644 --- a/src/cli_helper.py +++ b/src/cli_helper.py @@ -69,6 +69,22 @@ def calculate_student_gpa(database, student_identifier): return False +def take_subject(database, student_identifier, subject_name): + try: + student_handler = StudentHandler(database) + student_handler.identifier = student_identifier + student_handler.take_subject(subject_name) + print(f"Student '{student_identifier}' toke subject '{subject_name}'.") + return True + except NonValidStudent as e: + logging.error(str(e)) + print(f"Student '{student_identifier}' is not valid'") + except Exception as e: + logging.error(str(e)) + print(UNEXPECTED_ERROR) + return False + + def enroll_student(database, name, cpf, course_name): try: student = StudentHandler(database) diff --git a/src/database.py b/src/database.py index 2bd806e..b721ef1 100644 --- a/src/database.py +++ b/src/database.py @@ -234,7 +234,9 @@ def load(self, subject_identifier): f"SELECT * FROM {self.TABLE} WHERE identifier = '{subject_identifier}'" ).fetchone() if not result: - raise NotFoundError() + raise NotFoundError( + f"Subject '{subject_identifier}' not found in table '{self.TABLE}'" + ) self.name = result[0] self.state = result[1] diff --git a/src/services/enrollment_validator.py b/src/services/enrollment_validator.py index c1f15a8..b0bd064 100644 --- a/src/services/enrollment_validator.py +++ b/src/services/enrollment_validator.py @@ -5,7 +5,11 @@ class EnrollmentValidator: def __init__(self, database): self.__database = database - def validate_student(self, name, cpf, course_name): + def validate_student_by_data(self, name, cpf, course_name): # the valid students are predefined as the list of approved person in the given course student_identifier = utils.generate_student_identifier(name, cpf, course_name) + return self.validate_student_by_identifier(student_identifier) + + def validate_student_by_identifier(self, student_identifier): + # the valid students are predefined as the list of approved person in the given course return self.__database.enrollment.select(student_identifier) diff --git a/src/services/student_handler.py b/src/services/student_handler.py index 308d291..34fd67d 100644 --- a/src/services/student_handler.py +++ b/src/services/student_handler.py @@ -61,9 +61,10 @@ def __is_locked(self): return self.__state == self.LOCKED def __is_enrolled_student(self, course_name): - return EnrollmentValidator(self.__database).validate_student( + enrollment_validator = EnrollmentValidator(self.__database) + return enrollment_validator.validate_student_by_data( self.name, self.cpf, course_name - ) + ) or enrollment_validator.validate_student_by_identifier(self.identifier) def __save(self): self.__database.student.name = self.name @@ -91,12 +92,16 @@ def __add_to_grade_calculator(self): self.__database.grade_calculator.grade = 0 self.__database.grade_calculator.add() - def update_grade_to_subject(self, grade, subject_identifier): + def update_grade_to_subject(self, grade, subject_name): if grade < 0 or grade > 10: raise NonValidGrade("Grade must be between '0' and '10'") + + subject_identifier = utils.generate_subject_identifier( + self.__course, subject_name + ) if not self.__is_valid_subject(subject_identifier): return NonValidSubject( - f"The student is not enrolled to this subject '{subject_identifier}'" + f"The student is not enrolled to this subject '{subject_name}'" ) class Subject: @@ -168,24 +173,24 @@ def enroll_to_course(self, course_name): logging.error(str(e)) raise - def take_subject(self, subject_identifier): - """ - subject_identifier (str): The unique identifier of the subject. It follows the pattern - - - """ - is_valid_student = self.__is_enrolled_student(self.__course) - if not is_valid_student: - raise NonValidStudent() + def take_subject(self, subject_name): + if not self.__is_enrolled_student(self.__course): + raise NonValidStudent(f"Student '{self.identifier}' is not valid.") + + self.load_from_database(self.identifier) if self.__is_locked(): - raise NonValidStudent() + raise NonValidStudent(f"Student '{self.identifier}' is locked.") + subject_identifier = utils.generate_subject_identifier( + self.__course, subject_name + ) subject_handler = SubjectHandler(self.__database) try: subject_handler.load_from_database(subject_identifier) except Exception as e: logging.error(str(e)) - raise NonValidSubject() + raise NonValidSubject(f"Subject '{subject_identifier}' not found.") if subject_handler.course != self.__course: raise NonValidSubject() diff --git a/src/utils.py b/src/utils.py index c4a7370..067f6b5 100644 --- a/src/utils.py +++ b/src/utils.py @@ -1,13 +1,19 @@ import uuid +import logging def generate_course_identifier(name): + logging.info(f"Generate identifier for course '{name}'") return uuid.uuid5(uuid.NAMESPACE_URL, str(f"{name}")).hex def generate_student_identifier(name, cpf, course_name): + logging.info( + f"Generate identifier for student: NAME '{name}', CPF '{cpf}', COURSE NAME '{course_name}'" + ) return uuid.uuid5(uuid.NAMESPACE_URL, str(f"{name}{cpf}{course_name}")).hex def generate_subject_identifier(course, name): + logging.info(f"Generate identifier for subject '{name}'") return uuid.uuid5(uuid.NAMESPACE_URL, str(f"{name}{course}")).hex diff --git a/tests/test_grade_calculator.py b/tests/test_grade_calculator.py index a7a62b6..3e3d982 100644 --- a/tests/test_grade_calculator.py +++ b/tests/test_grade_calculator.py @@ -23,23 +23,17 @@ def test_calculate_student_gpa_when_subjects_have_grades( student_handler.cpf = "123.456.789-10" student_handler.enroll_to_course(course_name) - subject_identifier1 = utils.generate_subject_identifier(course_name, "any1") - subject_identifier2 = utils.generate_subject_identifier(course_name, "any2") - subject_identifier3 = utils.generate_subject_identifier(course_name, "any3") + subject_name1 = "any1" + subject_name2 = "any2" + subject_name3 = "any3" - student_handler.take_subject(subject_identifier1) - student_handler.take_subject(subject_identifier2) - student_handler.take_subject(subject_identifier3) + student_handler.take_subject("any1") + student_handler.take_subject("any2") + student_handler.take_subject("any3") - student_handler.update_grade_to_subject( - grade=grade1, subject_identifier=subject_identifier1 - ) - student_handler.update_grade_to_subject( - grade=grade2, subject_identifier=subject_identifier2 - ) - student_handler.update_grade_to_subject( - grade=grade3, subject_identifier=subject_identifier3 - ) + student_handler.update_grade_to_subject(grade=grade1, subject_name=subject_name1) + student_handler.update_grade_to_subject(grade=grade2, subject_name=subject_name2) + student_handler.update_grade_to_subject(grade=grade3, subject_name=subject_name3) assert ( grade_calculator.calculate_gpa_for_student(student_handler.identifier) @@ -56,6 +50,6 @@ def test_calculate_student_gpa_when_no_grades(set_in_memory_database): student_handler.cpf = "123.456.789-10" student_handler.enroll_to_course(course_name) - student_handler.take_subject(utils.generate_subject_identifier(course_name, "any1")) - student_handler.take_subject(utils.generate_subject_identifier(course_name, "any2")) + student_handler.take_subject("any1") + student_handler.take_subject("any2") assert grade_calculator.calculate_gpa_for_student(student_handler.identifier) == 0 diff --git a/tests/test_student.py b/tests/test_student.py index 0ceb7c1..f99cc78 100644 --- a/tests/test_student.py +++ b/tests/test_student.py @@ -25,14 +25,12 @@ def test_calculate_student_gpa_when_subjects_have_invalid_grades( student_handler.cpf = "123.456.789-10" student_handler.enroll_to_course(course_name) - subject_identifier1 = utils.generate_subject_identifier(course_name, "any1") + subject_name = "any1" - student_handler.take_subject(subject_identifier1) + student_handler.take_subject(subject_name) with pytest.raises(NonValidGrade): - student_handler.update_grade_to_subject( - grade=grade, subject_identifier=subject_identifier1 - ) + student_handler.update_grade_to_subject(grade=grade, subject_name=subject_name) def test_unlock_course(set_in_memory_database): @@ -62,7 +60,7 @@ def test_take_subject_from_course_when_locked_student_return_error( student = StudentHandler(set_in_memory_database) student.name = "any" student.cpf = "123.456.789-10" - subject = "any" + subject = "any1" student.enroll_to_course("any") student.lock_course() @@ -97,11 +95,9 @@ def test_take_subject_from_course(set_in_memory_database): student.name = "any" student.cpf = "123.456.789-10" course = "any" - student.enroll_to_course("any") - subject_identifier = utils.generate_subject_identifier(course, "any1") - student.enroll_to_course(course) - assert student.take_subject(subject_identifier) is True + + assert student.take_subject("any1") is True def test_enroll_invalid_student_to_course_retunr_error(set_in_memory_database): From cbddc77e32aa1c7269bc42f669e913031b655a29 Mon Sep 17 00:00:00 2001 From: Douglas Cardoso Date: Tue, 16 Apr 2024 00:46:37 -0300 Subject: [PATCH 20/44] Add update grade command --- cli.py | 26 +++++++++++++++++++++++++ manual_smoke_test.sh | 1 + src/cli_helper.py | 33 ++++++++++++++++++++++++++------ src/services/grade_calculator.py | 20 +++++++++++++------ src/services/student_handler.py | 29 ++++++---------------------- 5 files changed, 74 insertions(+), 35 deletions(-) diff --git a/cli.py b/cli.py index 49fb94f..7641a32 100644 --- a/cli.py +++ b/cli.py @@ -72,6 +72,31 @@ def calculate_student_gpa(student_identifier): logging.error(str(e)) +@click.command() +@click.option( + "--student-identifier", + prompt="Student identifier", + help="Student identifier number.", +) +@click.option( + "--subject-name", + prompt="Subject name", + help="The name of the subject the student wants to take.", +) +@click.option( + "--grade", + type=int, + prompt="Subject name", + help="The name of the subject the student wants to take.", +) +def update_grade(student_identifier, subject_name, grade): + try: + database = Database() + cli_helper.update_grade(database, student_identifier, subject_name, grade) + except Exception as e: + logging.error(str(e)) + + @click.command() @click.option( "--student-identifier", @@ -93,6 +118,7 @@ def take_subject(student_identifier, subject_name): cli.add_command(enroll_student) cli.add_command(take_subject) +cli.add_command(update_grade) cli.add_command(calculate_student_gpa) cli.add_command(activate_course) cli.add_command(deactivate_course) diff --git a/manual_smoke_test.sh b/manual_smoke_test.sh index 5ee9c02..29fed8b 100755 --- a/manual_smoke_test.sh +++ b/manual_smoke_test.sh @@ -3,6 +3,7 @@ rm university.db python for_admin.py python cli.py enroll-student --name any --cpf 123.456.789-10 --course-name any python cli.py take-subject --student-identifier 290f2113c2e6579c8bb6ec395ea56572 --subject-name any1 +python cli.py update-grade --student-identifier 290f2113c2e6579c8bb6ec395ea56572 --subject-name any1 --grade 7 python cli.py calculate-student-gpa --student-identifier 290f2113c2e6579c8bb6ec395ea56572 python cli.py activate-course --name deact diff --git a/src/cli_helper.py b/src/cli_helper.py index 08880ab..bb3f3ad 100644 --- a/src/cli_helper.py +++ b/src/cli_helper.py @@ -1,7 +1,12 @@ import logging -from src.services.student_handler import StudentHandler, NonValidStudent +from src.services.student_handler import ( + StudentHandler, + NonValidStudent, + NonValidGrade, + NonValidSubject, +) from src.services.course_handler import CourseHandler, NonValidCourse -from src.services.grade_calculator import GradeCalculator +from src.services.grade_calculator import GradeCalculator, NonValidGradeOperation UNEXPECTED_ERROR = "Unexpected error. Consult the system adminstrator." @@ -60,9 +65,9 @@ def calculate_student_gpa(database, student_identifier): gpa = grade_calculator.calculate_gpa_for_student(student_identifier) print(f"GPA of student '{student_identifier}' is '{gpa}'.") return True - except NonValidStudent as e: + except (NonValidStudent, NonValidGradeOperation) as e: logging.error(str(e)) - print(f"Student '{student_identifier}' is not valid'") + print(str(e)) except Exception as e: logging.error(str(e)) print(UNEXPECTED_ERROR) @@ -71,8 +76,7 @@ def calculate_student_gpa(database, student_identifier): def take_subject(database, student_identifier, subject_name): try: - student_handler = StudentHandler(database) - student_handler.identifier = student_identifier + student_handler = StudentHandler(database, student_identifier) student_handler.take_subject(subject_name) print(f"Student '{student_identifier}' toke subject '{subject_name}'.") return True @@ -85,6 +89,23 @@ def take_subject(database, student_identifier, subject_name): return False +def update_grade(database, student_identifier, subject_name, grade): + try: + student_handler = StudentHandler(database, student_identifier) + student_handler.update_grade_to_subject(grade, subject_name) + print( + f"Student '{student_identifier}' updated grade of subject '{subject_name}' to '{grade}'." + ) + return True + except (NonValidStudent, NonValidSubject, NonValidGrade) as e: + logging.error(str(e)) + print(str(e)) + except Exception as e: + logging.error(str(e)) + print(UNEXPECTED_ERROR) + return False + + def enroll_student(database, name, cpf, course_name): try: student = StudentHandler(database) diff --git a/src/services/grade_calculator.py b/src/services/grade_calculator.py index 0322f02..653e3fa 100644 --- a/src/services/grade_calculator.py +++ b/src/services/grade_calculator.py @@ -36,11 +36,6 @@ def load_from_database(self, student_identifier, subject_identifier): self.__subject_identifier = self.__databse.grade_calculator.subject_identifier self.__grade = self.__databse.grade_calculator.grade - def __load_all_from_database_by(self, student_identifier): - return self.__databse.grade_calculator.load_all_by_student_identifier( - student_identifier - ) - def add(self, student_identifier, subject_identifier, grade): self.__databse.grade_calculator.student_identifier = student_identifier self.__databse.grade_calculator.subject_identifier = subject_identifier @@ -54,9 +49,22 @@ def save(self): self.__databse.grade_calculator.save() def calculate_gpa_for_student(self, student_identifier): - self.__rows = self.__load_all_from_database_by(student_identifier) + try: + self.__rows = ( + self.__databse.grade_calculator.load_all_by_student_identifier( + student_identifier + ) + ) + except Exception: + raise NonValidGradeOperation( + f"Student '{student_identifier}' not enrolled to any subject." + ) total = 0 for row in self.__rows: total += row.grade return total / len(self.__rows) + + +class NonValidGradeOperation(Exception): + pass diff --git a/src/services/student_handler.py b/src/services/student_handler.py index 34fd67d..3b5af6f 100644 --- a/src/services/student_handler.py +++ b/src/services/student_handler.py @@ -10,8 +10,8 @@ class StudentHandler: LOCKED = "locked" ENROLLED = "enrolled" - def __init__(self, database): - self.__identifier = None + def __init__(self, database, identifier=None): + self.__identifier = identifier self.__state = None self.__gpa = 0 self.__subjects = [] @@ -25,10 +25,6 @@ def __init__(self, database): def identifier(self): return self.__identifier - @identifier.setter - def identifier(self, value): - self.__identifier = value - @property def state(self): return self.__state @@ -86,21 +82,17 @@ def __add(self): self.__database.student.course = self.__course self.__database.student.add() - def __add_to_grade_calculator(self): - self.__database.grade_calculator.student_identifier = self.identifier - self.__database.grade_calculator.subject_identifier = -1 - self.__database.grade_calculator.grade = 0 - self.__database.grade_calculator.add() - def update_grade_to_subject(self, grade, subject_name): if grade < 0 or grade > 10: raise NonValidGrade("Grade must be between '0' and '10'") + self.load_from_database(self.identifier) + subject_identifier = utils.generate_subject_identifier( self.__course, subject_name ) if not self.__is_valid_subject(subject_identifier): - return NonValidSubject( + raise NonValidSubject( f"The student is not enrolled to this subject '{subject_name}'" ) @@ -125,6 +117,7 @@ class Subject: assert grade_calculator.subject_identifier in [ s.identifier for s in self.__subjects_2 ] + assert grade_calculator.grade == grade def __is_valid_subject(self, subject_identifier): return subject_identifier in self.subjects @@ -150,7 +143,6 @@ def enroll_to_course(self, course_name): course.enroll_student(self.identifier) self.__state = self.ENROLLED self.__add() - self.__add_to_grade_calculator() # post condition self.__database.student.load(self.identifier) @@ -159,15 +151,6 @@ def enroll_to_course(self, course_name): assert self.__database.student.course == self.__course assert self.identifier in course.enrolled_students - result = self.__database.grade_calculator.load_all_by_student_identifier( - self.identifier - ) - for row in result: - assert row.student_identifier == self.identifier - # assert row.subject_identifier - # assert self.__database.grade_calculator.course == self.__course - # assert self.identifier in course.enrolled_students - return self.identifier except Exception as e: logging.error(str(e)) From b9510476b2012298b2fd1414868da6bfd0dce3ec Mon Sep 17 00:00:00 2001 From: Douglas Cardoso Date: Tue, 16 Apr 2024 00:57:32 -0300 Subject: [PATCH 21/44] Fix GPA calculation --- README.md | 6 +++--- for_admin.py | 1 + manual_smoke_test.sh | 2 ++ src/database.py | 7 +++---- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 72590eb..21ef959 100644 --- a/README.md +++ b/README.md @@ -50,13 +50,13 @@ Definition of Done: # Deliverables Construction of the basic functions of the system 1. DONE Each student will have a grade control called "grade point average" (GPA). -2. The GPA is the average of the student's grades in the ~~courses~~ subjects already taken. +2. DONE The GPA is the average of the student's grades in the ~~courses~~ subjects already taken. 3. The student is considered approved at the university if their GPA is above or equal to 7 (seven) at the end of the course. 4. If a student takes the same subject more than once, the highest grade will be considered in the GPA calculation. 5. Initially, the university will have 3 courses with 3 subjects each. -6. Subjects in each course may have the same names but will be differentiated by a unique identifier (niu). -7. The system must calculate the student's situation taking into account the subjects taken and the total number of subjects in each course. +6. DONE Subjects in each course may have the same names but will be differentiated by a unique identifier (niu). +7. DONE The system must calculate the student's situation taking into account the subjects taken and the total number of subjects in each course. 8. The student can only take subjects from their course. 9. Courses must have a unique identifier and name. 10. ~~Course~~ Subject names can be the same, but the unique identifier for each ~~course~~ subject must be different. diff --git a/for_admin.py b/for_admin.py index 862b43a..b261334 100644 --- a/for_admin.py +++ b/for_admin.py @@ -21,3 +21,4 @@ db.subject.populate("any", "any2") # 283631d2292c54879b9aa72e27a1b4ff db.subject.populate("any", "any3") # 0eaaeb1a39ed5d04a62b31cd951f34ce db.subject.populate("any", "any4", 0) # ef15a071407953bd858cfca59ad99056 +db.subject.populate("adm", "any1") diff --git a/manual_smoke_test.sh b/manual_smoke_test.sh index 29fed8b..d5ddae6 100755 --- a/manual_smoke_test.sh +++ b/manual_smoke_test.sh @@ -3,7 +3,9 @@ rm university.db python for_admin.py python cli.py enroll-student --name any --cpf 123.456.789-10 --course-name any python cli.py take-subject --student-identifier 290f2113c2e6579c8bb6ec395ea56572 --subject-name any1 +python cli.py take-subject --student-identifier 290f2113c2e6579c8bb6ec395ea56572 --subject-name any2 python cli.py update-grade --student-identifier 290f2113c2e6579c8bb6ec395ea56572 --subject-name any1 --grade 7 +python cli.py update-grade --student-identifier 290f2113c2e6579c8bb6ec395ea56572 --subject-name any2 --grade 9 python cli.py calculate-student-gpa --student-identifier 290f2113c2e6579c8bb6ec395ea56572 python cli.py activate-course --name deact diff --git a/src/database.py b/src/database.py index b721ef1..aed30e8 100644 --- a/src/database.py +++ b/src/database.py @@ -342,10 +342,9 @@ def save(self): try: cmd = f""" UPDATE {self.TABLE} - SET student_identifier = '{self.student_identifier}', - subject_identifier = '{self.subject_identifier}', - grade = {self.grade} - WHERE student_identifier = '{self.student_identifier}'; + SET grade = {self.grade} + WHERE student_identifier = '{self.student_identifier}' + AND subject_identifier = '{self.subject_identifier}'; """ self.cur.execute(cmd) From 728dfb49f0f8961119daea914e705aa815afc523 Mon Sep 17 00:00:00 2001 From: Douglas Cardoso Date: Tue, 16 Apr 2024 01:11:16 -0300 Subject: [PATCH 22/44] Add lock student command --- README.md | 24 ++++++++++++++---------- cli.py | 15 +++++++++++++++ manual_smoke_test.sh | 1 + src/cli_helper.py | 19 +++++++++++++++++-- src/services/student_handler.py | 3 +++ 5 files changed, 50 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 21ef959..f270ca2 100644 --- a/README.md +++ b/README.md @@ -49,21 +49,25 @@ Definition of Done: 3. Data is being saved in the database. # Deliverables Construction of the basic functions of the system -1. DONE Each student will have a grade control called "grade point average" (GPA). -2. DONE The GPA is the average of the student's grades in the ~~courses~~ subjects already taken. +1. **DONE** Each student will have a grade control called "grade point average" (GPA). +2. **DONE** The GPA is the average of the student's grades in the ~~courses~~ subjects already taken. 3. The student is considered approved at the university if their GPA is above or equal to 7 (seven) at the end of the course. 4. If a student takes the same subject more than once, the highest grade will be considered in the GPA calculation. 5. Initially, the university will have 3 courses with 3 subjects each. -6. DONE Subjects in each course may have the same names but will be differentiated by a unique identifier (niu). -7. DONE The system must calculate the student's situation taking into account the subjects taken and the total number of subjects in each course. -8. The student can only take subjects from their course. +6. **DONE** Subjects in each course may have the same names but will be differentiated by a unique identifier (niu). +7. **DONE** The system must calculate the student's situation taking into account the subjects taken and the total number of subjects in each course. +8. **DONE** The student can only take subjects from their course. 9. Courses must have a unique identifier and name. -10. ~~Course~~ Subject names can be the same, but the unique identifier for each ~~course~~ subject must be different. -11. A course cannot have two subjects with the same name, even if the niu is different. -12. The maximum grade for a student in a subject is 10. -13. The minimum grade for a student in a subject is 0. -14. The student can lock the course, and in this case, they cannot update their grades or the subjects taken. + + +12. **DONE** The maximum grade for a student in a subject is 10. +13. **DONE** The minimum grade for a student in a subject is 0. +14. **DONE** The student can lock the course, and in this case, they cannot update their grades or the subjects taken. 15. The course coordinator can list the students, grades in each subject, and GPAs of the students. 16. The student can unlock the course, and their situation returns to the previous state. 17. Students must have names. diff --git a/cli.py b/cli.py index 7641a32..9b7eab8 100644 --- a/cli.py +++ b/cli.py @@ -116,10 +116,25 @@ def take_subject(student_identifier, subject_name): logging.error(str(e)) +@click.command() +@click.option( + "--student-identifier", + prompt="Student identifier", + help="Student identifier number.", +) +def lock_course(student_identifier): + try: + database = Database() + cli_helper.lock_course(database, student_identifier) + except Exception as e: + logging.error(str(e)) + + cli.add_command(enroll_student) cli.add_command(take_subject) cli.add_command(update_grade) cli.add_command(calculate_student_gpa) +cli.add_command(lock_course) cli.add_command(activate_course) cli.add_command(deactivate_course) cli.add_command(cancel_course) diff --git a/manual_smoke_test.sh b/manual_smoke_test.sh index d5ddae6..96f42e2 100755 --- a/manual_smoke_test.sh +++ b/manual_smoke_test.sh @@ -7,6 +7,7 @@ python cli.py take-subject --student-identifier 290f2113c2e6579c8bb6ec395ea56572 python cli.py update-grade --student-identifier 290f2113c2e6579c8bb6ec395ea56572 --subject-name any1 --grade 7 python cli.py update-grade --student-identifier 290f2113c2e6579c8bb6ec395ea56572 --subject-name any2 --grade 9 python cli.py calculate-student-gpa --student-identifier 290f2113c2e6579c8bb6ec395ea56572 +python cli.py lock-course --student-identifier 290f2113c2e6579c8bb6ec395ea56572 python cli.py activate-course --name deact python cli.py deactivate-course --name act diff --git a/src/cli_helper.py b/src/cli_helper.py index bb3f3ad..c90bb4d 100644 --- a/src/cli_helper.py +++ b/src/cli_helper.py @@ -80,9 +80,24 @@ def take_subject(database, student_identifier, subject_name): student_handler.take_subject(subject_name) print(f"Student '{student_identifier}' toke subject '{subject_name}'.") return True - except NonValidStudent as e: + except (NonValidStudent, NonValidSubject, NonValidGrade) as e: + logging.error(str(e)) + print(str(e)) + except Exception as e: logging.error(str(e)) - print(f"Student '{student_identifier}' is not valid'") + print(UNEXPECTED_ERROR) + return False + + +def lock_course(database, student_identifier): + try: + student_handler = StudentHandler(database, student_identifier) + student_handler.lock_course() + print(f"Student '{student_identifier}' locked the course.") + return True + except (NonValidStudent, NonValidSubject, NonValidGrade) as e: + logging.error(str(e)) + print(str(e)) except Exception as e: logging.error(str(e)) print(UNEXPECTED_ERROR) diff --git a/src/services/student_handler.py b/src/services/student_handler.py index 3b5af6f..0ef5dce 100644 --- a/src/services/student_handler.py +++ b/src/services/student_handler.py @@ -88,6 +88,9 @@ def update_grade_to_subject(self, grade, subject_name): self.load_from_database(self.identifier) + if self.__is_locked(): + raise NonValidStudent(f"Student '{self.identifier}' is locked.") + subject_identifier = utils.generate_subject_identifier( self.__course, subject_name ) From 2b4e0413a68d89ec55b961f127c6f87cc5a3becd Mon Sep 17 00:00:00 2001 From: Douglas Cardoso Date: Tue, 16 Apr 2024 01:18:36 -0300 Subject: [PATCH 23/44] Add unlock command --- README.md | 2 +- cli.py | 15 +++++++++++++++ manual_smoke_test.sh | 1 + src/cli_helper.py | 15 +++++++++++++++ src/services/grade_calculator.py | 2 +- src/services/student_handler.py | 20 ++++++++++---------- 6 files changed, 43 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index f270ca2..03cf9e7 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ Same as requirement 6 13. **DONE** The minimum grade for a student in a subject is 0. 14. **DONE** The student can lock the course, and in this case, they cannot update their grades or the subjects taken. 15. The course coordinator can list the students, grades in each subject, and GPAs of the students. -16. The student can unlock the course, and their situation returns to the previous state. +16. **DONE** The student can unlock the course, and their situation returns to the previous state. 17. Students must have names. 18. The general coordinator can list all courses, students, grades in each subject, and GPAs of each student. 19. The course coordinator can remove subjects, and in this case, students cannot update their grades in that subject. diff --git a/cli.py b/cli.py index 9b7eab8..103c8e0 100644 --- a/cli.py +++ b/cli.py @@ -130,11 +130,26 @@ def lock_course(student_identifier): logging.error(str(e)) +@click.command() +@click.option( + "--student-identifier", + prompt="Student identifier", + help="Student identifier number.", +) +def unlock_course(student_identifier): + try: + database = Database() + cli_helper.unlock_course(database, student_identifier) + except Exception as e: + logging.error(str(e)) + + cli.add_command(enroll_student) cli.add_command(take_subject) cli.add_command(update_grade) cli.add_command(calculate_student_gpa) cli.add_command(lock_course) +cli.add_command(unlock_course) cli.add_command(activate_course) cli.add_command(deactivate_course) cli.add_command(cancel_course) diff --git a/manual_smoke_test.sh b/manual_smoke_test.sh index 96f42e2..97a23a8 100755 --- a/manual_smoke_test.sh +++ b/manual_smoke_test.sh @@ -8,6 +8,7 @@ python cli.py update-grade --student-identifier 290f2113c2e6579c8bb6ec395ea56572 python cli.py update-grade --student-identifier 290f2113c2e6579c8bb6ec395ea56572 --subject-name any2 --grade 9 python cli.py calculate-student-gpa --student-identifier 290f2113c2e6579c8bb6ec395ea56572 python cli.py lock-course --student-identifier 290f2113c2e6579c8bb6ec395ea56572 +python cli.py unlock-course --student-identifier 290f2113c2e6579c8bb6ec395ea56572 python cli.py activate-course --name deact python cli.py deactivate-course --name act diff --git a/src/cli_helper.py b/src/cli_helper.py index c90bb4d..3755cb0 100644 --- a/src/cli_helper.py +++ b/src/cli_helper.py @@ -104,6 +104,21 @@ def lock_course(database, student_identifier): return False +def unlock_course(database, student_identifier): + try: + student_handler = StudentHandler(database, student_identifier) + student_handler.unlock_course() + print(f"Student '{student_identifier}' unlocked the course.") + return True + except (NonValidStudent, NonValidSubject, NonValidGrade) as e: + logging.error(str(e)) + print(str(e)) + except Exception as e: + logging.error(str(e)) + print(UNEXPECTED_ERROR) + return False + + def update_grade(database, student_identifier, subject_name, grade): try: student_handler = StudentHandler(database, student_identifier) diff --git a/src/services/grade_calculator.py b/src/services/grade_calculator.py index 653e3fa..fc51d78 100644 --- a/src/services/grade_calculator.py +++ b/src/services/grade_calculator.py @@ -63,7 +63,7 @@ def calculate_gpa_for_student(self, student_identifier): for row in self.__rows: total += row.grade - return total / len(self.__rows) + return round(total / len(self.__rows), 1) class NonValidGradeOperation(Exception): diff --git a/src/services/student_handler.py b/src/services/student_handler.py index 0ef5dce..bb7b069 100644 --- a/src/services/student_handler.py +++ b/src/services/student_handler.py @@ -208,18 +208,18 @@ def take_subject(self, subject_name): return True def unlock_course(self): - if self.__is_enrolled_student(self.__course): - self.__state = None - self.__save() - return self.state - raise NonValidStudent() + if not self.__is_enrolled_student(self.__course): + raise NonValidStudent(f"Student is not not enrolled in any course.") + self.__state = None + self.__save() + return self.state def lock_course(self): - if self.__is_enrolled_student(self.__course): - self.__state = self.LOCKED - self.__save() - return self.state - raise NonValidStudent() + if not self.__is_enrolled_student(self.__course): + raise NonValidStudent(f"Student is not not enrolled in any course.") + self.__state = self.LOCKED + self.__save() + return self.state def load_from_database(self, student_identifier): try: From 14ae2f2bb24fd4289e600a5f0ba5677d8b1e963b Mon Sep 17 00:00:00 2001 From: Douglas Cardoso Date: Tue, 16 Apr 2024 02:00:49 -0300 Subject: [PATCH 24/44] Add 'remove subject' command --- .gitignore | 3 ++- README.md | 2 +- cli.py | 10 +++++++++ for_admin.py | 8 +++++-- manual_smoke_test.sh | 19 +++++++++-------- src/cli_helper.py | 18 +++++++++++++++- src/constants.py | 1 + src/services/course_handler.py | 3 ++- src/services/subject_handler.py | 38 +++++++++++++++++++++++++++------ tests/test_subject.py | 4 ++-- 10 files changed, 83 insertions(+), 23 deletions(-) create mode 100644 src/constants.py diff --git a/.gitignore b/.gitignore index a35bca1..3bd724e 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ htmlcov/ *.db *.pyc *.log -.coverage \ No newline at end of file +.coverage +.~lock.architecture.odp# \ No newline at end of file diff --git a/README.md b/README.md index 03cf9e7..efb0fdf 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ Same as requirement 6 14. **DONE** The student can lock the course, and in this case, they cannot update their grades or the subjects taken. 15. The course coordinator can list the students, grades in each subject, and GPAs of the students. 16. **DONE** The student can unlock the course, and their situation returns to the previous state. -17. Students must have names. +17. **DONE** Students must have names. 18. The general coordinator can list all courses, students, grades in each subject, and GPAs of each student. 19. The course coordinator can remove subjects, and in this case, students cannot update their grades in that subject. 20. Students can only update their grades in the subjects they are enrolled in. diff --git a/cli.py b/cli.py index 103c8e0..53e7d75 100644 --- a/cli.py +++ b/cli.py @@ -19,6 +19,14 @@ def cli(): pass +@click.command() +@click.option("--course-name", prompt="Course name", help="Name of the course.") +@click.option("--subject-name", prompt="Subject name", help="Name of the subject.") +def remove_subject(course_name, subject_name): + database = Database() + cli_helper.remove_subject(database, course_name, subject_name) + + @click.command() @click.option("--name", prompt="Course name", help="Name of the course.") def cancel_course(name): @@ -150,9 +158,11 @@ def unlock_course(student_identifier): cli.add_command(calculate_student_gpa) cli.add_command(lock_course) cli.add_command(unlock_course) + cli.add_command(activate_course) cli.add_command(deactivate_course) cli.add_command(cancel_course) +cli.add_command(remove_subject) if __name__ == "__main__": cli() diff --git a/for_admin.py b/for_admin.py index b261334..6de9e12 100644 --- a/for_admin.py +++ b/for_admin.py @@ -2,7 +2,7 @@ db = Database() # TODO need to check if the courses are available -db.enrollment.populate("douglas", "098.765.432.12", "adm") +db.enrollment.populate("douglas", "098.765.432.12", "mat") db.enrollment.populate("maria", "028.745.462.18", "mat") db.enrollment.populate("joana", "038.745.452.19", "port") db.enrollment.populate( @@ -21,4 +21,8 @@ db.subject.populate("any", "any2") # 283631d2292c54879b9aa72e27a1b4ff db.subject.populate("any", "any3") # 0eaaeb1a39ed5d04a62b31cd951f34ce db.subject.populate("any", "any4", 0) # ef15a071407953bd858cfca59ad99056 -db.subject.populate("adm", "any1") +db.subject.populate("adm", "management") +db.subject.populate("mat", "calculus") +db.subject.populate("mat", "algebra") +db.subject.populate("mat", "analysis") +db.subject.populate("mat", "geometry") diff --git a/manual_smoke_test.sh b/manual_smoke_test.sh index 97a23a8..9d81126 100755 --- a/manual_smoke_test.sh +++ b/manual_smoke_test.sh @@ -1,15 +1,16 @@ # set -x rm university.db python for_admin.py -python cli.py enroll-student --name any --cpf 123.456.789-10 --course-name any -python cli.py take-subject --student-identifier 290f2113c2e6579c8bb6ec395ea56572 --subject-name any1 -python cli.py take-subject --student-identifier 290f2113c2e6579c8bb6ec395ea56572 --subject-name any2 -python cli.py update-grade --student-identifier 290f2113c2e6579c8bb6ec395ea56572 --subject-name any1 --grade 7 -python cli.py update-grade --student-identifier 290f2113c2e6579c8bb6ec395ea56572 --subject-name any2 --grade 9 -python cli.py calculate-student-gpa --student-identifier 290f2113c2e6579c8bb6ec395ea56572 -python cli.py lock-course --student-identifier 290f2113c2e6579c8bb6ec395ea56572 -python cli.py unlock-course --student-identifier 290f2113c2e6579c8bb6ec395ea56572 +python cli.py enroll-student --name douglas --cpf 098.765.432.12 --course-name mat +python cli.py take-subject --student-identifier 25a5a5c24a5252968097e5d5c80e6352 --subject-name calculus +python cli.py take-subject --student-identifier 25a5a5c24a5252968097e5d5c80e6352 --subject-name geometry +python cli.py update-grade --student-identifier 25a5a5c24a5252968097e5d5c80e6352 --subject-name calculus --grade 7 +python cli.py update-grade --student-identifier 25a5a5c24a5252968097e5d5c80e6352 --subject-name geometry --grade 9 +python cli.py calculate-student-gpa --student-identifier 25a5a5c24a5252968097e5d5c80e6352 +python cli.py lock-course --student-identifier 25a5a5c24a5252968097e5d5c80e6352 +python cli.py unlock-course --student-identifier 25a5a5c24a5252968097e5d5c80e6352 +python cli.py remove-subject --course-name adm --subject-name management python cli.py activate-course --name deact python cli.py deactivate-course --name act -python cli.py cancel-course --name any \ No newline at end of file +python cli.py cancel-course --name adm \ No newline at end of file diff --git a/src/cli_helper.py b/src/cli_helper.py index 3755cb0..85ddd89 100644 --- a/src/cli_helper.py +++ b/src/cli_helper.py @@ -3,14 +3,30 @@ StudentHandler, NonValidStudent, NonValidGrade, - NonValidSubject, ) from src.services.course_handler import CourseHandler, NonValidCourse from src.services.grade_calculator import GradeCalculator, NonValidGradeOperation +from src.services.subject_handler import SubjectHandler, NonValidSubject UNEXPECTED_ERROR = "Unexpected error. Consult the system adminstrator." +def remove_subject(database, course_name, subject_name): + try: + subject_handler = SubjectHandler(database, course=course_name) + subject_handler.name = subject_name + subject_handler.remove() + print(f"Subject removed from course.") + return True + except NonValidSubject as e: + logging.error(str(e)) + print(str(e)) + except Exception as e: + logging.error(str(e)) + print(UNEXPECTED_ERROR) + return False + + def cancel_course(database, name): try: course_handler = CourseHandler(database) diff --git a/src/constants.py b/src/constants.py new file mode 100644 index 0000000..a8ba7f6 --- /dev/null +++ b/src/constants.py @@ -0,0 +1 @@ +DUMMY_IDENTIFIER = -1 diff --git a/src/services/course_handler.py b/src/services/course_handler.py index 2d7b6ab..3c7b111 100644 --- a/src/services/course_handler.py +++ b/src/services/course_handler.py @@ -1,5 +1,6 @@ import uuid import logging +from src.constants import DUMMY_IDENTIFIER class CourseHandler: @@ -8,7 +9,7 @@ class CourseHandler: CANCELLED = "cancelled" def __init__(self, database) -> None: - self.__identifier = -1 + self.__identifier = DUMMY_IDENTIFIER self.__name = None self.__state = self.INACTIVE # TODO use enum self.__enrolled_students = [] diff --git a/src/services/subject_handler.py b/src/services/subject_handler.py index e7262b5..48e1d31 100644 --- a/src/services/subject_handler.py +++ b/src/services/subject_handler.py @@ -1,4 +1,6 @@ import logging +from src import utils +from src.constants import DUMMY_IDENTIFIER class SubjectHandler: @@ -6,12 +8,14 @@ class SubjectHandler: REMOVED = "removed" ACTIVE = "active" - def __init__(self, database, subject_identifier=-1) -> None: + def __init__( + self, database, subject_identifier=DUMMY_IDENTIFIER, course=None + ) -> None: self.__database = database self.__identifier = subject_identifier self.__state = None self.__enrolled_students = [] - self.__course = None + self.__course = course self.__max_enrollment = 0 self.__name = None @@ -54,11 +58,13 @@ def is_active(self): return self.__state == self.ACTIVE def activate(self): - if not self.identifier: - raise NonValidSubject() + if self.identifier == DUMMY_IDENTIFIER: + raise NonValidSubject(f"Subject not found.'") if self.state == self.REMOVED: - raise NonValidSubject() + raise NonValidSubject( + f"Subject '{self.identifier}' is removed and can not be activated." + ) self.__state = self.ACTIVE self.save() @@ -69,8 +75,18 @@ def activate(self): return self.__state def remove(self): + self.__generate_identifier() + + try: + self.load_from_database(self.identifier) + except Exception as e: + logging.error(str(e)) + raise NonValidSubject( + f"Subject '{self.name}' not found in course '{self.course}'.'" + ) + if not self.state == self.ACTIVE: - raise NonValidSubject() + raise NonValidSubject(f"Subject '{self.identifier} is not active.'") self.__state = self.REMOVED self.save() @@ -80,6 +96,16 @@ def remove(self): assert self.__database.subject.state == self.REMOVED return self.__state + def __generate_identifier(self): + if self.identifier != DUMMY_IDENTIFIER: + return + if not self.name: + raise NonValidSubject("Need to set a name to subject.") + if not self.course: + raise NonValidSubject("Need to set a course to subject.") + + self.__identifier = utils.generate_subject_identifier(self.course, self.name) + def save(self): self.__database.subject.enrolled_students = ",".join(self.__enrolled_students) self.__database.subject.max_enrollment = self.__max_enrollment diff --git a/tests/test_subject.py b/tests/test_subject.py index 3adb54b..231f357 100644 --- a/tests/test_subject.py +++ b/tests/test_subject.py @@ -11,8 +11,8 @@ def test_remove_invalid_subject_return_error(set_in_memory_database): def test_remove(set_in_memory_database): - subject_handler = SubjectHandler(set_in_memory_database) - subject_handler.load_from_database(utils.generate_subject_identifier("any", "any1")) + subject_handler = SubjectHandler(set_in_memory_database, course="any") + subject_handler.name = "any1" assert subject_handler.remove() == "removed" From 34e12a56bc751f4fda7d9081b5bcf61f102f34be Mon Sep 17 00:00:00 2001 From: Douglas Cardoso Date: Tue, 16 Apr 2024 02:20:32 -0300 Subject: [PATCH 25/44] Add cpf validator --- README.md | 12 +++++++----- for_admin.py | 2 +- manual_smoke_test.sh | 2 +- src/cli_helper.py | 4 +--- src/services/cpf_validator.py | 8 ++++++++ src/services/student_handler.py | 9 ++++----- 6 files changed, 22 insertions(+), 15 deletions(-) create mode 100644 src/services/cpf_validator.py diff --git a/README.md b/README.md index efb0fdf..c3c7e2d 100644 --- a/README.md +++ b/README.md @@ -72,9 +72,10 @@ Same as requirement 6 16. **DONE** The student can unlock the course, and their situation returns to the previous state. 17. **DONE** Students must have names. 18. The general coordinator can list all courses, students, grades in each subject, and GPAs of each student. -19. The course coordinator can remove subjects, and in this case, students cannot update their grades in that subject. -20. Students can only update their grades in the subjects they are enrolled in. +19. **DONE** The course coordinator can remove subjects, and in this case, students cannot update their grades in that subject. +20. **DONE** Students can only update their grades in the subjects they are enrolled in. 21. Course and subject names must have a maximum of 10 letters. + 22. Courses can have the same names if they are from different units. 23. The student can only enroll in one course. @@ -82,7 +83,7 @@ Same as requirement 6 25. The coordinator can list the students, subjects, grades, and GPAs of all their courses (coordinator of more than one course). -26. The course can be canceled by the general cordinator. +26. **DONE** The course can be canceled by the general cordinator. 27. Canceled courses cannot accept student enrollments. 28. Canceled courses cannot have coordinators. @@ -90,10 +91,11 @@ Same as requirement 6 30. The student must enroll in a minimum of 3 subjects. 31. If the number of subjects missing for a student is less than 3, they can enroll in 1 subject. 32. If the student does not enroll in the minimum number of subjects per semester, they will be automatically failed. -33. The student must have a validated CPF (Brazilian Social Security Number) in the external CPF validation system (government system). +33. **DONE** The student must have a validated CPF (Brazilian Social Security Number) in the external CPF validation system (government system). 34. Add the course name to the coordinator's reports. 35. The student is only approved if they achieve the minimum grade in all course subjects, even if their GPA is above the minimum. -36. The ~~user~~ student (person) must be able to create students with basic information. + +36. **DONE** The ~~user~~ student (person) must be able to create students with basic information. 37. ~~The user must be able to enroll the student in a course.~~ 38. The ~~user~~ general coordinator must be able to create courses with the minimum number of subjects. diff --git a/for_admin.py b/for_admin.py index 6de9e12..dfc5f35 100644 --- a/for_admin.py +++ b/for_admin.py @@ -3,7 +3,7 @@ db = Database() # TODO need to check if the courses are available db.enrollment.populate("douglas", "098.765.432.12", "mat") -db.enrollment.populate("maria", "028.745.462.18", "mat") +db.enrollment.populate("maria", "028.745.462.18", "adm") db.enrollment.populate("joana", "038.745.452.19", "port") db.enrollment.populate( "any", "123.456.789-10", "any" diff --git a/manual_smoke_test.sh b/manual_smoke_test.sh index 9d81126..074a5c1 100755 --- a/manual_smoke_test.sh +++ b/manual_smoke_test.sh @@ -13,4 +13,4 @@ python cli.py unlock-course --student-identifier 25a5a5c24a5252968097e5d5c80e635 python cli.py remove-subject --course-name adm --subject-name management python cli.py activate-course --name deact python cli.py deactivate-course --name act -python cli.py cancel-course --name adm \ No newline at end of file +python cli.py cancel-course --name adm diff --git a/src/cli_helper.py b/src/cli_helper.py index 85ddd89..e356f0c 100644 --- a/src/cli_helper.py +++ b/src/cli_helper.py @@ -162,9 +162,7 @@ def enroll_student(database, name, cpf, course_name): return True except NonValidStudent as e: logging.error(str(e)) - print( - f"Student '{name}' with CPF '{cpf}' is not valid in course '{course_name}'" - ) + print(str(e)) except Exception as e: logging.error(str(e)) print(UNEXPECTED_ERROR) diff --git a/src/services/cpf_validator.py b/src/services/cpf_validator.py new file mode 100644 index 0000000..aedde56 --- /dev/null +++ b/src/services/cpf_validator.py @@ -0,0 +1,8 @@ +# External service +def is_valide_cpf(cpf): + return cpf in [ + "123.456.789-10", + "028.745.462.18", + "038.745.452.19", + "098.765.432.12", + ] diff --git a/src/services/student_handler.py b/src/services/student_handler.py index bb7b069..72a4e1e 100644 --- a/src/services/student_handler.py +++ b/src/services/student_handler.py @@ -1,8 +1,9 @@ import logging from src.services.enrollment_validator import EnrollmentValidator from src.services.course_handler import CourseHandler -from src.services.subject_handler import SubjectHandler +from src.services.subject_handler import SubjectHandler, NonValidSubject from src.services.grade_calculator import GradeCalculator +from src.services.cpf_validator import is_valide_cpf from src import utils @@ -51,6 +52,8 @@ def cpf(self): @cpf.setter def cpf(self, value): + if not is_valide_cpf(value): + raise NonValidStudent(f"CPF {value} is not valid.") self.__cpf = value def __is_locked(self): @@ -242,9 +245,5 @@ class NonValidStudent(Exception): pass -class NonValidSubject(Exception): - pass - - class NonValidGrade(Exception): pass From a12c37c3b4893d4dc54e8aea49044c7d90cb7096 Mon Sep 17 00:00:00 2001 From: Douglas Cardoso Date: Wed, 17 Apr 2024 01:03:01 -0300 Subject: [PATCH 26/44] Add list student, subjects and GPA --- cli.py | 15 ++++ for_admin.py | 3 +- manual_smoke_test.sh | 4 ++ src/cli_helper.py | 21 +++++- src/database.py | 98 +++++++++++++++++++++----- src/services/course_handler.py | 20 ++++++ src/services/grade_calculator.py | 39 +++++++---- src/services/student_handler.py | 117 ++++++++++++++++++------------- tests/conftest.py | 2 + tests/test_course.py | 28 ++++++++ tests/test_student.py | 4 +- 11 files changed, 268 insertions(+), 83 deletions(-) diff --git a/cli.py b/cli.py index 53e7d75..78eddf0 100644 --- a/cli.py +++ b/cli.py @@ -19,6 +19,20 @@ def cli(): pass +@click.command() +@click.option( + "--course-name", + prompt="Course name", + help="Course name.", +) +def list_students(course_name): + try: + database = Database() + cli_helper.list_students(database, course_name) + except Exception as e: + logging.error(str(e)) + + @click.command() @click.option("--course-name", prompt="Course name", help="Name of the course.") @click.option("--subject-name", prompt="Subject name", help="Name of the subject.") @@ -163,6 +177,7 @@ def unlock_course(student_identifier): cli.add_command(deactivate_course) cli.add_command(cancel_course) cli.add_command(remove_subject) +cli.add_command(list_students) if __name__ == "__main__": cli() diff --git a/for_admin.py b/for_admin.py index dfc5f35..b95c486 100644 --- a/for_admin.py +++ b/for_admin.py @@ -3,7 +3,8 @@ db = Database() # TODO need to check if the courses are available db.enrollment.populate("douglas", "098.765.432.12", "mat") -db.enrollment.populate("maria", "028.745.462.18", "adm") +db.enrollment.populate("maria", "028.745.462.18", "mat") +db.enrollment.populate("aline", "028.745.462.18", "adm") db.enrollment.populate("joana", "038.745.452.19", "port") db.enrollment.populate( "any", "123.456.789-10", "any" diff --git a/manual_smoke_test.sh b/manual_smoke_test.sh index 074a5c1..ed4886e 100755 --- a/manual_smoke_test.sh +++ b/manual_smoke_test.sh @@ -10,6 +10,10 @@ python cli.py calculate-student-gpa --student-identifier 25a5a5c24a5252968097e5d python cli.py lock-course --student-identifier 25a5a5c24a5252968097e5d5c80e6352 python cli.py unlock-course --student-identifier 25a5a5c24a5252968097e5d5c80e6352 +python cli.py enroll-student --name maria --cpf 028.745.462.18 --course-name mat +python cli.py list-students --course-name mat + + python cli.py remove-subject --course-name adm --subject-name management python cli.py activate-course --name deact python cli.py deactivate-course --name act diff --git a/src/cli_helper.py b/src/cli_helper.py index e356f0c..b9fa05d 100644 --- a/src/cli_helper.py +++ b/src/cli_helper.py @@ -158,7 +158,26 @@ def enroll_student(database, name, cpf, course_name): student.name = name student.cpf = cpf identifier = student.enroll_to_course(course_name) - print(f"Student enrolled with identifier '{identifier}'.") + print(f"Student '{name}' enrolled with identifier '{identifier}'.") + return True + except NonValidStudent as e: + logging.error(str(e)) + print(str(e)) + except Exception as e: + logging.error(str(e)) + print(UNEXPECTED_ERROR) + return False + + +def list_students(database, course_name): + try: + course_handler = CourseHandler(database) + course_handler.name = course_name + students = course_handler.list_students() + print(f"List of students:") + print(f"Student identifier, Student name, Subjects, GPA") + for student in students: + print(f" {student}") return True except NonValidStudent as e: logging.error(str(e)) diff --git a/src/database.py b/src/database.py index aed30e8..eb2c45e 100644 --- a/src/database.py +++ b/src/database.py @@ -3,6 +3,12 @@ from src import utils +def convert_list_with_empty_string_to_empty_list(the_list): + if len(the_list[0]) == 0: + the_list = [] + return the_list + + # TODO test concurrency class Database: @@ -23,7 +29,7 @@ class DbStudent: cpf = None identifier = None gpa = None - subjects = None + subjects = [] course = None def __init__(self, con, cur): @@ -34,6 +40,9 @@ def __init__(self, con, cur): f"CREATE TABLE IF NOT EXISTS {self.TABLE} (name, state, cpf, identifier, gpa, subjects, course)" ) + def __convert_subjects_to_csv(self): + return ",".join(set(self.subjects)) + def add(self): try: cmd = f""" @@ -42,8 +51,8 @@ def add(self): '{self.state}', '{self.cpf}', '{self.identifier}', - '{self.gpa}', - '{self.subjects}', + {self.gpa}, + '{self.__convert_subjects_to_csv()}', '{self.course}') """ self.cur.execute(cmd) @@ -57,8 +66,8 @@ def save(self): cmd = f""" UPDATE {self.TABLE} SET state = '{self.state}', - gpa = '{self.gpa}', - subjects = '{self.subjects}' + gpa = {self.gpa}, + subjects = '{self.__convert_subjects_to_csv()}' WHERE identifier = '{self.identifier}'; """ self.cur.execute(cmd) @@ -99,8 +108,10 @@ def load(self, identifier): self.state = result[1] self.cpf = result[2] self.identifier = result[3] - self.gpq = result[4] - self.subjects = result[5].split(",") + self.gpa = result[4] + self.subjects = convert_list_with_empty_string_to_empty_list( + result[5].split(",") + ) self.course = result[6] except Exception as e: logging.error(str(e)) @@ -167,6 +178,19 @@ def populate(self, name): self.con.commit() def save(self): + try: + cmd = f""" + UPDATE {self.TABLE} + SET enrolled_students = '{self.enrolled_students}' + WHERE identifier = '{self.identifier}'; + """ + self.cur.execute(cmd) + self.con.commit() + except Exception as e: + logging.error(str(e)) + raise + + def add(self): try: self.cur.execute( f""" @@ -191,9 +215,13 @@ def load_from_database(self, name): self.name = result[0] self.state = result[1] self.identifier = result[2] - self.enrolled_students = result[3].split(",") + self.enrolled_students = convert_list_with_empty_string_to_empty_list( + the_list=result[3].split(",") + ) self.max_enrollment = result[4] - self.subjects = result[5].split(",") + self.subjects = convert_list_with_empty_string_to_empty_list( + result[5].split(",") + ) class DbSubject: TABLE = "subject" @@ -265,6 +293,11 @@ def save(self): raise class DbGradeCalculator: + class GradeCalculatorRow: + student_identifier = None + subject_identifier = None + grade = None + TABLE = "grade_calculator" student_identifier = None subject_identifier = None @@ -288,14 +321,9 @@ def load_all_by_student_identifier(self, student_identifier): f"Student '{student_identifier}' not found in table '{self.TABLE}'" ) - class GradeCalculatorRow: - student_identifier = None - subject_identifier = None - grade = None - grade_calculators = [] for row in result: - grade_calculator_row = GradeCalculatorRow() + grade_calculator_row = self.GradeCalculatorRow() grade_calculator_row.student_identifier = row[0] grade_calculator_row.subject_identifier = row[1] grade_calculator_row.grade = row[2] @@ -305,6 +333,27 @@ class GradeCalculatorRow: logging.error(str(e)) raise + def search(self, student_identifier, subject_identifier): + try: + result = self.cur.execute( + f"""SELECT * FROM {self.TABLE} + WHERE subject_identifier = '{subject_identifier}' + AND student_identifier = '{student_identifier}' + """ + ).fetchone() + if not result: + return + + grade_calculator_row = self.GradeCalculatorRow() + grade_calculator_row.student_identifier = result[0] + grade_calculator_row.subject_identifier = result[1] + grade_calculator_row.grade = result[2] + + return grade_calculator_row + except Exception as e: + logging.error(str(e)) + raise + def load(self, student_identifier, subject_identifier): try: result = self.cur.execute( @@ -314,7 +363,10 @@ def load(self, student_identifier, subject_identifier): """ ).fetchone() if not result: - raise NotFoundError() + raise NotFoundError( + f"Student '{student_identifier}' and subject '{subject_identifier}'" + f" not found in table '{self.TABLE}'." + ) self.student_identifier = result[0] self.subject_identifier = result[1] @@ -353,6 +405,20 @@ def save(self): logging.error(str(e)) raise + def remove(self, student_identifier, subject_identifier): + try: + cmd = f""" + DELETE FROM {self.TABLE} + WHERE student_identifier = '{student_identifier}' + AND subject_identifier = '{subject_identifier}'; + """ + self.cur.execute(cmd) + + self.con.commit() + except Exception as e: + logging.error(str(e)) + raise + class DbSemester: TABLE = "semester" identifier = None diff --git a/src/services/course_handler.py b/src/services/course_handler.py index 3c7b111..526a605 100644 --- a/src/services/course_handler.py +++ b/src/services/course_handler.py @@ -1,6 +1,7 @@ import uuid import logging from src.constants import DUMMY_IDENTIFIER +from src.database import NotFoundError class CourseHandler: @@ -73,6 +74,25 @@ def load_from_database(self, name): logging.error(str(e)) raise NonValidCourse("Course not found.") + def list_students(self): + self.load_from_database(self.name) + enrolled_students = self.__database.course.enrolled_students + result = [] + for student_identifier in enrolled_students: + try: + + self.__database.student.load(student_identifier) + student_identifer = self.__database.student.identifier + student_name = self.__database.student.name + subjects = self.__database.student.subjects + gpa = self.__database.student.gpa + + result.append([student_identifer, student_name, subjects, gpa]) + except Exception as e: + logging.error(str(e)) + raise + return result + def enroll_student(self, student_identifier): if not self.state == self.ACTIVE: raise NonValidCourse("Course is not active.") diff --git a/src/services/grade_calculator.py b/src/services/grade_calculator.py index fc51d78..1d54407 100644 --- a/src/services/grade_calculator.py +++ b/src/services/grade_calculator.py @@ -4,7 +4,7 @@ def __init__(self, database) -> None: self.__subject_identifier = None self.__grade = None self.__rows = None - self.__databse = database + self.__database = database @property def student_identifier(self): @@ -31,27 +31,37 @@ def grade(self, value): self.__grade = value def load_from_database(self, student_identifier, subject_identifier): - self.__databse.grade_calculator.load(student_identifier, subject_identifier) - self.__student_identifier = self.__databse.grade_calculator.student_identifier - self.__subject_identifier = self.__databse.grade_calculator.subject_identifier - self.__grade = self.__databse.grade_calculator.grade + self.__database.grade_calculator.load(student_identifier, subject_identifier) + self.__student_identifier = self.__database.grade_calculator.student_identifier + self.__subject_identifier = self.__database.grade_calculator.subject_identifier + self.__grade = self.__database.grade_calculator.grade + + def search(self, student_identifier, subject_identifier): + return self.__database.grade_calculator.search( + student_identifier, subject_identifier + ) + + def remove(self, student_identifier, subject_identifier): + return self.__database.grade_calculator.remove( + student_identifier, subject_identifier + ) def add(self, student_identifier, subject_identifier, grade): - self.__databse.grade_calculator.student_identifier = student_identifier - self.__databse.grade_calculator.subject_identifier = subject_identifier - self.__databse.grade_calculator.grade = grade - self.__databse.grade_calculator.add() + self.__database.grade_calculator.student_identifier = student_identifier + self.__database.grade_calculator.subject_identifier = subject_identifier + self.__database.grade_calculator.grade = grade + self.__database.grade_calculator.add() def save(self): - self.__databse.grade_calculator.student_identifier = self.student_identifier - self.__databse.grade_calculator.subject_identifier = self.subject_identifier - self.__databse.grade_calculator.grade = self.grade - self.__databse.grade_calculator.save() + self.__database.grade_calculator.student_identifier = self.student_identifier + self.__database.grade_calculator.subject_identifier = self.subject_identifier + self.__database.grade_calculator.grade = self.grade + self.__database.grade_calculator.save() def calculate_gpa_for_student(self, student_identifier): try: self.__rows = ( - self.__databse.grade_calculator.load_all_by_student_identifier( + self.__database.grade_calculator.load_all_by_student_identifier( student_identifier ) ) @@ -63,6 +73,7 @@ def calculate_gpa_for_student(self, student_identifier): for row in self.__rows: total += row.grade + # When the return round(total / len(self.__rows), 1) diff --git a/src/services/student_handler.py b/src/services/student_handler.py index 72a4e1e..37e9238 100644 --- a/src/services/student_handler.py +++ b/src/services/student_handler.py @@ -5,21 +5,26 @@ from src.services.grade_calculator import GradeCalculator from src.services.cpf_validator import is_valide_cpf from src import utils +from src.database import NotFoundError +from src.services.grade_calculator import GradeCalculator +from src.constants import DUMMY_IDENTIFIER class StudentHandler: - LOCKED = "locked" - ENROLLED = "enrolled" + class Subject: + identifier = None + grade = None def __init__(self, database, identifier=None): + self.__LOCKED = "locked" + self.__ENROLLED = "enrolled" self.__identifier = identifier self.__state = None self.__gpa = 0 - self.__subjects = [] self.__course = None self.__name = None self.__cpf = None - self.__subjects_2 = [] + self.__subject_identifiers = [] self.__database = database @property @@ -36,7 +41,7 @@ def gpa(self): @property def subjects(self): - return self.__subjects + return self.__subject_identifiers @property def name(self): @@ -57,7 +62,7 @@ def cpf(self, value): self.__cpf = value def __is_locked(self): - return self.__state == self.LOCKED + return self.__state == self.__LOCKED def __is_enrolled_student(self, course_name): enrollment_validator = EnrollmentValidator(self.__database) @@ -66,28 +71,24 @@ def __is_enrolled_student(self, course_name): ) or enrollment_validator.validate_student_by_identifier(self.identifier) def __save(self): - self.__database.student.name = self.name - self.__database.student.state = self.state - self.__database.student.cpf = self.cpf - self.__database.student.identifier = self.identifier - self.__database.student.gpa = self.gpa - self.__database.student.subjects = ",".join(self.subjects) - self.__database.student.course = self.__course - self.__database.student.save() - - def __add(self): - self.__database.student.name = self.name - self.__database.student.state = self.state - self.__database.student.cpf = self.cpf - self.__database.student.identifier = self.identifier - self.__database.student.gpa = self.gpa - self.__database.student.subjects = ",".join(self.subjects) - self.__database.student.course = self.__course - self.__database.student.add() + try: + self.__database.student.name = self.name + self.__database.student.state = self.state + self.__database.student.cpf = self.cpf + self.__database.student.identifier = self.identifier + self.__database.student.gpa = GradeCalculator( + self.__database + ).calculate_gpa_for_student(self.identifier) + self.__database.student.subjects.extend(self.subjects) + self.__database.student.course = self.__course + self.__database.student.save() + except Exception as e: + logging.error(str(e)) + raise def update_grade_to_subject(self, grade, subject_name): if grade < 0 or grade > 10: - raise NonValidGrade("Grade must be between '0' and '10'") + raise NonValidGrade("Grade must be between '0' and '10'.") self.load_from_database(self.identifier) @@ -99,17 +100,10 @@ def update_grade_to_subject(self, grade, subject_name): ) if not self.__is_valid_subject(subject_identifier): raise NonValidSubject( - f"The student is not enrolled to this subject '{subject_name}'" + f"The student '{self.identifier}' is not enrolled to this subject '{subject_name}'" ) - class Subject: - identifier = None - grade = None - - subject = Subject() - subject.identifier = subject_identifier - subject.grade = grade - self.__subjects_2.append(subject) + self.__subject_identifiers.append(subject_identifier) grade_calculator = GradeCalculator(self.__database) grade_calculator.student_identifier = self.identifier @@ -120,9 +114,7 @@ class Subject: # post condition assert grade_calculator.student_identifier == self.identifier - assert grade_calculator.subject_identifier in [ - s.identifier for s in self.__subjects_2 - ] + assert grade_calculator.subject_identifier in self.__subject_identifiers assert grade_calculator.grade == grade def __is_valid_subject(self, subject_identifier): @@ -141,20 +133,32 @@ def enroll_to_course(self, course_name): course = CourseHandler(self.__database) course.load_from_database(course_name) - self.__identifier = utils.generate_student_identifier( self.name, self.cpf, course_name ) self.__course = course_name course.enroll_student(self.identifier) - self.__state = self.ENROLLED - self.__add() + + self.__state = self.__ENROLLED + self.__database.student.name = self.name + self.__database.student.state = self.state + self.__database.student.cpf = self.cpf + self.__database.student.identifier = self.identifier + self.__database.student.gpa = 0 + self.__database.student.subjects.extend(self.subjects) + self.__database.student.course = self.__course + self.__database.student.add() + + grade_calculator = GradeCalculator(self.__database) + grade_calculator.add(self.identifier, DUMMY_IDENTIFIER, grade=0) # post condition self.__database.student.load(self.identifier) + assert self.__database.student.identifier == self.identifier - assert self.__database.student.state == self.ENROLLED + assert self.__database.student.state == self.__ENROLLED assert self.__database.student.course == self.__course + assert self.__database.student.gpa == 0 assert self.identifier in course.enrolled_students return self.identifier @@ -177,23 +181,32 @@ def take_subject(self, subject_name): subject_handler = SubjectHandler(self.__database) try: subject_handler.load_from_database(subject_identifier) - except Exception as e: + except NotFoundError as e: logging.error(str(e)) raise NonValidSubject(f"Subject '{subject_identifier}' not found.") + except Exception as e: + logging.error(str(e)) + raise if subject_handler.course != self.__course: - raise NonValidSubject() + raise NonValidSubject( + f"The subject '{subject_handler.identifier}' is not part of course '{self.__course}'." + ) if not subject_handler.is_available() or not subject_handler.is_active(): - raise NonValidSubject() + raise NonValidSubject( + f"Subject '{subject_handler.identifier}' is not available or is not active." + ) - self.subjects.append(subject_identifier) + self.__subject_identifiers.append(subject_identifier) self.__save() subject_handler.enrolled_students.append(self.identifier) subject_handler.save() grade_calculator = GradeCalculator(self.__database) + if grade_calculator.search(self.identifier, DUMMY_IDENTIFIER): + grade_calculator.remove(self.identifier, DUMMY_IDENTIFIER) grade_calculator.add(self.identifier, subject_identifier, grade=0) # post condition @@ -213,14 +226,18 @@ def take_subject(self, subject_name): def unlock_course(self): if not self.__is_enrolled_student(self.__course): raise NonValidStudent(f"Student is not not enrolled in any course.") - self.__state = None + + self.load_from_database(self.identifier) + self.__state = self.__ENROLLED self.__save() return self.state def lock_course(self): if not self.__is_enrolled_student(self.__course): raise NonValidStudent(f"Student is not not enrolled in any course.") - self.__state = self.LOCKED + + self.load_from_database(self.identifier) + self.__state = self.__LOCKED self.__save() return self.state @@ -233,12 +250,14 @@ def load_from_database(self, student_identifier): self.__cpf = self.__database.student.cpf self.__identifier = self.__database.student.identifier self.__gpa = self.__database.student.gpa - self.__subjects = self.__database.student.subjects + self.__subject_identifiers.extend(self.__database.student.subjects) self.__course = self.__database.student.course + except NotFoundError as e: + raise NonValidStudent(str(e)) except Exception as e: logging.error(str(e)) - raise NonValidStudent("Student not found.") + raise class NonValidStudent(Exception): diff --git a/tests/conftest.py b/tests/conftest.py index 492b976..db0887f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,6 +10,8 @@ def set_in_memory_database(): db.enrollment.populate("maria", "028.745.462.18", "mat") db.enrollment.populate("joana", "038.745.452.19", "port") db.enrollment.populate("any", "123.456.789-10", "any") + db.enrollment.populate("other", "123.456.789-10", "any") + db.enrollment.populate("another", "123.456.789-10", "any") db.course.populate("adm") db.course.populate("mat") diff --git a/tests/test_course.py b/tests/test_course.py index 412915a..5ce600a 100644 --- a/tests/test_course.py +++ b/tests/test_course.py @@ -1,5 +1,33 @@ import pytest from src.services.course_handler import CourseHandler, NonValidCourse +from src.services.student_handler import StudentHandler +from src import utils + + +def test_list_empty_when_no_enrolled_students(set_in_memory_database): + course_handler = CourseHandler(set_in_memory_database) + course_handler.name = "any" + + actual = course_handler.list_students() + + assert len(actual) == 0 + + +def test_list_enrolled_students(set_in_memory_database): + name = "any" + cpf = "123.456.789-10" + course = "any" + student_handler = StudentHandler(set_in_memory_database) + student_handler.name = name + student_handler.cpf = cpf + student_handler.enroll_to_course(course) + course_handler = CourseHandler(set_in_memory_database) + course_handler.name = course + + actual = course_handler.list_students() + + assert utils.generate_student_identifier(name, cpf, course) in actual[0] + assert len(actual) == 1 def test_enroll_student_to_inactive_course_return_error(set_in_memory_database): diff --git a/tests/test_student.py b/tests/test_student.py index f99cc78..0378d34 100644 --- a/tests/test_student.py +++ b/tests/test_student.py @@ -40,7 +40,7 @@ def test_unlock_course(set_in_memory_database): student.enroll_to_course("any") student.unlock_course() - assert student.state == None + assert student.state == "enrolled" def test_lock_course(set_in_memory_database): @@ -63,7 +63,7 @@ def test_take_subject_from_course_when_locked_student_return_error( subject = "any1" student.enroll_to_course("any") - student.lock_course() + x = student.lock_course() with pytest.raises(NonValidStudent): student.take_subject(subject) From 1e1e76c21b0eebab80eeceb3e3ce6cc00428665e Mon Sep 17 00:00:00 2001 From: Douglas Cardoso Date: Wed, 17 Apr 2024 01:47:44 -0300 Subject: [PATCH 27/44] List the student details --- README.md | 2 +- src/cli_helper.py | 11 ++++++----- src/services/course_handler.py | 32 +++++++++++++++++--------------- tests/test_course.py | 6 +++--- 4 files changed, 27 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index c3c7e2d..8e49085 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ Same as requirement 6 12. **DONE** The maximum grade for a student in a subject is 10. 13. **DONE** The minimum grade for a student in a subject is 0. 14. **DONE** The student can lock the course, and in this case, they cannot update their grades or the subjects taken. -15. The course coordinator can list the students, grades in each subject, and GPAs of the students. +15. **DONE** The course coordinator can list the students, grades in each subject, and GPAs of the students. 16. **DONE** The student can unlock the course, and their situation returns to the previous state. 17. **DONE** Students must have names. 18. The general coordinator can list all courses, students, grades in each subject, and GPAs of each student. diff --git a/src/cli_helper.py b/src/cli_helper.py index b9fa05d..5f5cf47 100644 --- a/src/cli_helper.py +++ b/src/cli_helper.py @@ -1,4 +1,5 @@ import logging +import json from src.services.student_handler import ( StudentHandler, NonValidStudent, @@ -158,7 +159,9 @@ def enroll_student(database, name, cpf, course_name): student.name = name student.cpf = cpf identifier = student.enroll_to_course(course_name) - print(f"Student '{name}' enrolled with identifier '{identifier}'.") + print( + f"Student '{name}' enrolled in course '{course_name}' with identifier '{identifier}'." + ) return True except NonValidStudent as e: logging.error(str(e)) @@ -173,11 +176,9 @@ def list_students(database, course_name): try: course_handler = CourseHandler(database) course_handler.name = course_name - students = course_handler.list_students() + students = course_handler.list_student_details() print(f"List of students:") - print(f"Student identifier, Student name, Subjects, GPA") - for student in students: - print(f" {student}") + print(json.dumps(students, sort_keys=True, indent=4)) return True except NonValidStudent as e: logging.error(str(e)) diff --git a/src/services/course_handler.py b/src/services/course_handler.py index 526a605..e4a9dbc 100644 --- a/src/services/course_handler.py +++ b/src/services/course_handler.py @@ -1,7 +1,7 @@ import uuid import logging from src.constants import DUMMY_IDENTIFIER -from src.database import NotFoundError +from src.services.grade_calculator import GradeCalculator class CourseHandler: @@ -74,23 +74,25 @@ def load_from_database(self, name): logging.error(str(e)) raise NonValidCourse("Course not found.") - def list_students(self): + def list_student_details(self): self.load_from_database(self.name) enrolled_students = self.__database.course.enrolled_students - result = [] + result = {} for student_identifier in enrolled_students: - try: - - self.__database.student.load(student_identifier) - student_identifer = self.__database.student.identifier - student_name = self.__database.student.name - subjects = self.__database.student.subjects - gpa = self.__database.student.gpa - - result.append([student_identifer, student_name, subjects, gpa]) - except Exception as e: - logging.error(str(e)) - raise + self.__database.student.load(student_identifier) + result[self.__database.student.identifier] = { + "name": self.__database.student.name, + "gpa": self.__database.student.gpa, + } + + for student_identifier in enrolled_students: + self.__database.student.load(student_identifier) + for subject_identifier in self.__database.student.subjects: + grade_calculator = GradeCalculator(self.__database) + grade_calculator.load_from_database( + student_identifier, subject_identifier + ) + result[student_identifier][subject_identifier] = grade_calculator.grade return result def enroll_student(self, student_identifier): diff --git a/tests/test_course.py b/tests/test_course.py index 5ce600a..957d778 100644 --- a/tests/test_course.py +++ b/tests/test_course.py @@ -8,7 +8,7 @@ def test_list_empty_when_no_enrolled_students(set_in_memory_database): course_handler = CourseHandler(set_in_memory_database) course_handler.name = "any" - actual = course_handler.list_students() + actual = course_handler.list_student_details() assert len(actual) == 0 @@ -24,9 +24,9 @@ def test_list_enrolled_students(set_in_memory_database): course_handler = CourseHandler(set_in_memory_database) course_handler.name = course - actual = course_handler.list_students() + actual = course_handler.list_student_details() - assert utils.generate_student_identifier(name, cpf, course) in actual[0] + assert utils.generate_student_identifier(name, cpf, course) in actual assert len(actual) == 1 From 0c5a743a9554dd1ff1dbddab45d068337433883c Mon Sep 17 00:00:00 2001 From: Douglas Cardoso Date: Wed, 17 Apr 2024 02:20:28 -0300 Subject: [PATCH 28/44] Add list course command --- README.md | 4 ++-- cli.py | 12 +++++++++++- manual_smoke_test.sh | 1 + src/cli_helper.py | 18 +++++++++++++++++- src/database.py | 29 +++++++++++++++++++++++++++++ src/services/course_handler.py | 12 ++++++++++++ src/services/subject_handler.py | 4 ++++ tests/test_course.py | 18 +++++++++++++++++- tests/test_student.py | 2 +- 9 files changed, 94 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 8e49085..f78b430 100644 --- a/README.md +++ b/README.md @@ -71,10 +71,10 @@ Same as requirement 6 15. **DONE** The course coordinator can list the students, grades in each subject, and GPAs of the students. 16. **DONE** The student can unlock the course, and their situation returns to the previous state. 17. **DONE** Students must have names. -18. The general coordinator can list all courses, students, grades in each subject, and GPAs of each student. +18. **DONE** The general coordinator can list all courses, students, grades in each subject, and GPAs of each student. 19. **DONE** The course coordinator can remove subjects, and in this case, students cannot update their grades in that subject. 20. **DONE** Students can only update their grades in the subjects they are enrolled in. -21. Course and subject names must have a maximum of 10 letters. +21. **DONE** Course and subject names must have a maximum of 10 letters. 22. Courses can have the same names if they are from different units. diff --git a/cli.py b/cli.py index 78eddf0..d0b5fba 100644 --- a/cli.py +++ b/cli.py @@ -19,6 +19,15 @@ def cli(): pass +@click.command() +def list_courses(): + try: + database = Database() + cli_helper.list_all_course_details(database) + except Exception as e: + logging.error(str(e)) + + @click.command() @click.option( "--course-name", @@ -28,7 +37,7 @@ def cli(): def list_students(course_name): try: database = Database() - cli_helper.list_students(database, course_name) + cli_helper.list_student_details(database, course_name) except Exception as e: logging.error(str(e)) @@ -178,6 +187,7 @@ def unlock_course(student_identifier): cli.add_command(cancel_course) cli.add_command(remove_subject) cli.add_command(list_students) +cli.add_command(list_courses) if __name__ == "__main__": cli() diff --git a/manual_smoke_test.sh b/manual_smoke_test.sh index ed4886e..1ebe2bd 100755 --- a/manual_smoke_test.sh +++ b/manual_smoke_test.sh @@ -11,6 +11,7 @@ python cli.py lock-course --student-identifier 25a5a5c24a5252968097e5d5c80e6352 python cli.py unlock-course --student-identifier 25a5a5c24a5252968097e5d5c80e6352 python cli.py enroll-student --name maria --cpf 028.745.462.18 --course-name mat +python cli.py enroll-student --name aline --cpf 028.745.462.18 --course-name adm python cli.py list-students --course-name mat diff --git a/src/cli_helper.py b/src/cli_helper.py index 5f5cf47..ec387f8 100644 --- a/src/cli_helper.py +++ b/src/cli_helper.py @@ -172,7 +172,7 @@ def enroll_student(database, name, cpf, course_name): return False -def list_students(database, course_name): +def list_student_details(database, course_name): try: course_handler = CourseHandler(database) course_handler.name = course_name @@ -187,3 +187,19 @@ def list_students(database, course_name): logging.error(str(e)) print(UNEXPECTED_ERROR) return False + + +def list_all_course_details(database): + try: + course_handler = CourseHandler(database) + courses = course_handler.list_all_courses_with_details() + print(f"List of courses:") + print(json.dumps(courses, sort_keys=True, indent=4)) + return True + except NonValidStudent as e: + logging.error(str(e)) + print(str(e)) + except Exception as e: + logging.error(str(e)) + print(UNEXPECTED_ERROR) + return False diff --git a/src/database.py b/src/database.py index eb2c45e..afd8ddb 100644 --- a/src/database.py +++ b/src/database.py @@ -208,6 +208,35 @@ def add(self): logging.error(str(e)) raise + def search_all(self): + result = self.cur.execute(f"SELECT * FROM {self.TABLE}").fetchall() + + class CourseRow: + name = None + state = None + identifier = None + enrolled_students = None + max_enrollment = None + subjects = None + + courses = [] + for row in result: + course_row = CourseRow() + course_row.name = row[0] + course_row.state = row[1] + course_row.identifier = row[2] + course_row.enrolled_students = ( + convert_list_with_empty_string_to_empty_list( + the_list=row[3].split(",") + ) + ) + course_row.max_enrollment = row[4] + course_row.subjects = convert_list_with_empty_string_to_empty_list( + row[5].split(",") + ) + courses.append(course_row) + return courses + def load_from_database(self, name): result = self.cur.execute( f"SELECT * FROM {self.TABLE} WHERE name = '{name}'" diff --git a/src/services/course_handler.py b/src/services/course_handler.py index e4a9dbc..95210d2 100644 --- a/src/services/course_handler.py +++ b/src/services/course_handler.py @@ -40,6 +40,10 @@ def name(self): @name.setter def name(self, value): + if len(value) > 10: + raise NonValidCourse( + f"The maximum number of characters to course's name is '10'. Set with '{len(value)}'." + ) self.__name = value @property @@ -83,6 +87,7 @@ def list_student_details(self): result[self.__database.student.identifier] = { "name": self.__database.student.name, "gpa": self.__database.student.gpa, + "course": self.__database.student.course, } for student_identifier in enrolled_students: @@ -95,6 +100,13 @@ def list_student_details(self): result[student_identifier][subject_identifier] = grade_calculator.grade return result + def list_all_courses_with_details(self): + all_details = {} + for course in self.__database.course.search_all(): + self.name = course.name + all_details[self.name] = self.list_student_details() + return all_details + def enroll_student(self, student_identifier): if not self.state == self.ACTIVE: raise NonValidCourse("Course is not active.") diff --git a/src/services/subject_handler.py b/src/services/subject_handler.py index 48e1d31..62d6f42 100644 --- a/src/services/subject_handler.py +++ b/src/services/subject_handler.py @@ -41,6 +41,10 @@ def name(self): @name.setter def name(self, value): + if len(value) > 10: + raise NonValidSubject( + f"The maximum number of characters to subject's name is '10'. Set with '{len(value)}'." + ) self.__name = value @property diff --git a/tests/test_course.py b/tests/test_course.py index 957d778..93cfe24 100644 --- a/tests/test_course.py +++ b/tests/test_course.py @@ -4,6 +4,22 @@ from src import utils +def test_list_all_courses(set_in_memory_database): + name = "any" + cpf = "123.456.789-10" + course = "any" + student_handler = StudentHandler(set_in_memory_database) + student_handler.name = name + student_handler.cpf = cpf + student_handler.enroll_to_course(course) + course_handler = CourseHandler(set_in_memory_database) + + actual = course_handler.list_all_courses_with_details() + + assert len(actual) > 0 + assert "mat" in actual + + def test_list_empty_when_no_enrolled_students(set_in_memory_database): course_handler = CourseHandler(set_in_memory_database) course_handler.name = "any" @@ -13,7 +29,7 @@ def test_list_empty_when_no_enrolled_students(set_in_memory_database): assert len(actual) == 0 -def test_list_enrolled_students(set_in_memory_database): +def test_list_enrolled_students_in_specific_course(set_in_memory_database): name = "any" cpf = "123.456.789-10" course = "any" diff --git a/tests/test_student.py b/tests/test_student.py index 0378d34..7d95dbb 100644 --- a/tests/test_student.py +++ b/tests/test_student.py @@ -63,7 +63,7 @@ def test_take_subject_from_course_when_locked_student_return_error( subject = "any1" student.enroll_to_course("any") - x = student.lock_course() + student.lock_course() with pytest.raises(NonValidStudent): student.take_subject(subject) From 77c1d7f04e816ce6cd066f91c648fc55d8144099 Mon Sep 17 00:00:00 2001 From: Douglas Cardoso Date: Wed, 17 Apr 2024 13:38:13 -0300 Subject: [PATCH 29/44] Allow creation of course and subjects --- README.md | 24 ++++---- architecture.odp | Bin 24333 -> 24714 bytes cli.py | 23 ++++++++ for_admin.py | 2 +- manual_smoke_test.sh | 5 +- src/cli_helper.py | 47 ++++++++++++++-- src/database.py | 99 ++++++++++++++++++--------------- src/services/course_handler.py | 48 ++++++++++++---- tests/conftest.py | 1 + tests/test_course.py | 64 +++++++++++++++++++-- tests/test_models.py | 12 ++-- 11 files changed, 238 insertions(+), 87 deletions(-) diff --git a/README.md b/README.md index f78b430..15b80b9 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ Construction of the basic functions of the system 6. **DONE** Subjects in each course may have the same names but will be differentiated by a unique identifier (niu). 7. **DONE** The system must calculate the student's situation taking into account the subjects taken and the total number of subjects in each course. 8. **DONE** The student can only take subjects from their course. -9. Courses must have a unique identifier and name. +9. **DONE** Courses must have a unique identifier and name. @@ -77,28 +77,28 @@ Same as requirement 6 21. **DONE** Course and subject names must have a maximum of 10 letters. 22. Courses can have the same names if they are from different units. - -23. The student can only enroll in one course. + +23. **DONE** The student can only enroll in one course. 24. The coordinator can coordinate a maximum of three courses. -25. The coordinator can list the students, subjects, grades, and GPAs of all their courses (coordinator of more than one course). +25. **DONE** The coordinator can list the students, subjects, grades, and GPAs of all their courses (coordinator of more than one course). 26. **DONE** The course can be canceled by the general cordinator. -27. Canceled courses cannot accept student enrollments. +27. **DONE** Canceled courses cannot accept student enrollments. 28. Canceled courses cannot have coordinators. -29. Each subject can have a maximum of 30 enrolled students. +29. **DONE** Each subject can have a maximum of 30 enrolled students. 30. The student must enroll in a minimum of 3 subjects. 31. If the number of subjects missing for a student is less than 3, they can enroll in 1 subject. -32. If the student does not enroll in the minimum number of subjects per semester, they will be automatically failed. +32. If the student does not enroll in the minimum number of subjects per semester, they will be automatically ~~failed~~ locked. 33. **DONE** The student must have a validated CPF (Brazilian Social Security Number) in the external CPF validation system (government system). -34. Add the course name to the coordinator's reports. -35. The student is only approved if they achieve the minimum grade in all course subjects, even if their GPA is above the minimum. +34. **DONE** Add the course name to the coordinator's reports. +35. The students are only approved if they achieve the minimum grade in all course subjects, even if their GPA is above the minimum. 36. **DONE** The ~~user~~ student (person) must be able to create students with basic information. 37. ~~The user must be able to enroll the student in a course.~~ -38. The ~~user~~ general coordinator must be able to create courses with the minimum number of subjects. +38. **DONE** The ~~user~~ general coordinator must be able to create courses with the minimum number of subjects. 39. The administrator, and only the administrator, must be able to list all students with detailed information (all available information). 40. The administrator, and only the administrator, must be able to list all courses with all available information. 41. The administrator, and only the administrator, must be able to list the list of students per course. @@ -120,4 +120,6 @@ These features were introduced after analysis in architecture and specifications 54. The general coordinator is responible to add students to enrollment list after manual analysis of thier documentation 55. The corse coordinator is responsible to add new subjects to his/her course 56. The general coordinator is responsible to add new courses to the university -57. The student, teacher and coordinators need to authenticate with valid credentials before perfom any action in the system \ No newline at end of file +57. The student, teacher and coordinators need to authenticate with valid credentials before perfom any action in the system +56. The course need a minimum enrollment of 100 students +56. The subject need a minimum enrollment of 10 students \ No newline at end of file diff --git a/architecture.odp b/architecture.odp index 4a2b4b372bd2b3310de8b54b4902f5486f496d0f..faad21a568e9747e8007d72f93272b21836261d2 100644 GIT binary patch delta 19232 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 Ct%8F9 diff --git a/cli.py b/cli.py index d0b5fba..8f3bba4 100644 --- a/cli.py +++ b/cli.py @@ -64,6 +64,26 @@ def deactivate_course(name): cli_helper.deactivate_course(database, name) +@click.command() +@click.option("--name", prompt="Course name", help="Name of the course.") +@click.option( + "--max-enrollment", + prompt="Course maximum number of students", + help="The maximum number of students in a course.", +) +def create_course(name, max_enrollment): + database = Database() + cli_helper.create_course(database, name, max_enrollment) + + +@click.command() +@click.option("--course-name", prompt="Course name", help="Name of the course.") +@click.option("--subject-name", prompt="Subject name", help="Name of the subject.") +def add_subject(course_name, subject_name): + database = Database() + cli_helper.add_subject_to_course(database, course_name, subject_name) + + @click.command() @click.option("--name", prompt="Course name", help="Name of the course.") def activate_course(name): @@ -188,6 +208,9 @@ def unlock_course(student_identifier): cli.add_command(remove_subject) cli.add_command(list_students) cli.add_command(list_courses) +cli.add_command(create_course) +cli.add_command(add_subject) + if __name__ == "__main__": cli() diff --git a/for_admin.py b/for_admin.py index b95c486..f898203 100644 --- a/for_admin.py +++ b/for_admin.py @@ -21,7 +21,7 @@ db.subject.populate("any", "any1") # e4c858cd917f518194c9d93c9d13def8 db.subject.populate("any", "any2") # 283631d2292c54879b9aa72e27a1b4ff db.subject.populate("any", "any3") # 0eaaeb1a39ed5d04a62b31cd951f34ce -db.subject.populate("any", "any4", 0) # ef15a071407953bd858cfca59ad99056 +db.subject.populate("any", "any4", 30) # ef15a071407953bd858cfca59ad99056 db.subject.populate("adm", "management") db.subject.populate("mat", "calculus") db.subject.populate("mat", "algebra") diff --git a/manual_smoke_test.sh b/manual_smoke_test.sh index 1ebe2bd..d3d3b0b 100755 --- a/manual_smoke_test.sh +++ b/manual_smoke_test.sh @@ -11,11 +11,14 @@ python cli.py lock-course --student-identifier 25a5a5c24a5252968097e5d5c80e6352 python cli.py unlock-course --student-identifier 25a5a5c24a5252968097e5d5c80e6352 python cli.py enroll-student --name maria --cpf 028.745.462.18 --course-name mat -python cli.py enroll-student --name aline --cpf 028.745.462.18 --course-name adm +# python cli.py enroll-student --name aline --cpf 028.745.462.18 --course-name adm python cli.py list-students --course-name mat +python cli.py list-courses python cli.py remove-subject --course-name adm --subject-name management python cli.py activate-course --name deact python cli.py deactivate-course --name act python cli.py cancel-course --name adm +python cli.py create-course --name geography --max-enrollment 11 +python cli.py add-subject --course-name geography --subject-name minerals \ No newline at end of file diff --git a/src/cli_helper.py b/src/cli_helper.py index ec387f8..730ba9b 100644 --- a/src/cli_helper.py +++ b/src/cli_helper.py @@ -5,7 +5,11 @@ NonValidStudent, NonValidGrade, ) -from src.services.course_handler import CourseHandler, NonValidCourse +from src.services.course_handler import ( + CourseHandler, + NonValidCourse, + NonMinimunSubjects, +) from src.services.grade_calculator import GradeCalculator, NonValidGradeOperation from src.services.subject_handler import SubjectHandler, NonValidSubject @@ -33,7 +37,7 @@ def cancel_course(database, name): course_handler = CourseHandler(database) course_handler.load_from_database(name) course_handler.cancel() - print(f"Course cancelled.") + print(f"Course '{name}' cancelled.") return True except NonValidCourse as e: logging.error(str(e)) @@ -49,7 +53,7 @@ def deactivate_course(database, name): course_handler = CourseHandler(database) course_handler.load_from_database(name) course_handler.deactivate() - print(f"Course deactivated.") + print(f"Course '{name}' deactivated.") return True except NonValidCourse as e: logging.error(str(e)) @@ -65,7 +69,22 @@ def activate_course(database, name): course_handler = CourseHandler(database) course_handler.load_from_database(name) course_handler.activate() - print(f"Course activated.") + print(f"Course '{name}' activated.") + return True + except (NonValidCourse, NonMinimunSubjects) as e: + logging.error(str(e)) + print(str(e)) + except Exception as e: + logging.error(str(e)) + print(UNEXPECTED_ERROR) + return False + + +def create_course(database, name, max_enrollment): + try: + course_handler = CourseHandler(database) + course_handler.create(name, max_enrollment) + print(f"Course '{name}' created.") return True except NonValidCourse as e: logging.error(str(e)) @@ -76,6 +95,22 @@ def activate_course(database, name): return False +def add_subject_to_course(database, course_name, subject_name): + try: + course_handler = CourseHandler(database) + course_handler.name = course_name + course_handler.add_subject(subject_name) + print(f"Subject '{subject_name}' added to course '{course_name}'.") + return True + except NonValidCourse as e: + logging.error(str(e)) + print(f"Course '{course_name}' is not valid.") + except Exception as e: + logging.error(str(e)) + print(UNEXPECTED_ERROR) + return False + + def calculate_student_gpa(database, student_identifier): try: grade_calculator = GradeCalculator(database) @@ -163,7 +198,7 @@ def enroll_student(database, name, cpf, course_name): f"Student '{name}' enrolled in course '{course_name}' with identifier '{identifier}'." ) return True - except NonValidStudent as e: + except (NonValidStudent, NonValidCourse) as e: logging.error(str(e)) print(str(e)) except Exception as e: @@ -177,7 +212,7 @@ def list_student_details(database, course_name): course_handler = CourseHandler(database) course_handler.name = course_name students = course_handler.list_student_details() - print(f"List of students:") + print(f"List of students in course {course_name}:") print(json.dumps(students, sort_keys=True, indent=4)) return True except NonValidStudent as e: diff --git a/src/database.py b/src/database.py index afd8ddb..3758e25 100644 --- a/src/database.py +++ b/src/database.py @@ -3,10 +3,14 @@ from src import utils -def convert_list_with_empty_string_to_empty_list(the_list): - if len(the_list[0]) == 0: - the_list = [] - return the_list +def convert_csv_to_list(the_csv): + if len(the_csv) == 0 or the_csv is None: + return [] + return the_csv.split(",") + + +def convert_list_to_csv(the_list): + return ",".join(set(the_list)) # TODO test concurrency @@ -40,9 +44,6 @@ def __init__(self, con, cur): f"CREATE TABLE IF NOT EXISTS {self.TABLE} (name, state, cpf, identifier, gpa, subjects, course)" ) - def __convert_subjects_to_csv(self): - return ",".join(set(self.subjects)) - def add(self): try: cmd = f""" @@ -52,7 +53,7 @@ def add(self): '{self.cpf}', '{self.identifier}', {self.gpa}, - '{self.__convert_subjects_to_csv()}', + '{convert_list_to_csv(self.subjects)}', '{self.course}') """ self.cur.execute(cmd) @@ -67,7 +68,7 @@ def save(self): UPDATE {self.TABLE} SET state = '{self.state}', gpa = {self.gpa}, - subjects = '{self.__convert_subjects_to_csv()}' + subjects = '{convert_list_to_csv(self.subjects)}' WHERE identifier = '{self.identifier}'; """ self.cur.execute(cmd) @@ -109,9 +110,7 @@ def load(self, identifier): self.cpf = result[2] self.identifier = result[3] self.gpa = result[4] - self.subjects = convert_list_with_empty_string_to_empty_list( - result[5].split(",") - ) + self.subjects = convert_csv_to_list(result[5]) self.course = result[6] except Exception as e: logging.error(str(e)) @@ -124,7 +123,7 @@ def __init__(self, con, cur): self.cur = cur self.con = con self.cur.execute( - f"CREATE TABLE IF NOT EXISTS {self.TABLE} (student_identifier)" + f"CREATE TABLE IF NOT EXISTS {self.TABLE} (student_identifier TEXT NOT NULL UNIQUE)" ) # Just for admin. The university has a predefined list of approved students to each course. @@ -151,28 +150,34 @@ class DbCourse: identifier = None enrolled_students = None max_enrollment = None - subjects = None + subjects = [] def __init__(self, con, cur): self.con = con self.cur = cur self.cur.execute( - f"CREATE TABLE IF NOT EXISTS {self.TABLE} (name, state, identifier, enrolled_students, max_enrollment, subjects)" + f"CREATE TABLE IF NOT EXISTS {self.TABLE}" + " (name TEXT NOT NULL UNIQUE," + " state TEXT NOT NULL," + " identifier TEXT NOT NULL UNIQUE," + " enrolled_students TEXT," + " max_enrollment INTEGER NOT NULL," + " subjects TEXT)" ) # Just for admin. Necessary because there is not a user story to create courses # TODO create a public funtion - def populate(self, name): + def populate(self, name, state="active", subjects="any1,any2,any3"): identifier = utils.generate_course_identifier(name) self.cur.execute( f""" INSERT INTO {self.TABLE} VALUES ('{name}', - 'active', + '{state}', '{identifier}', '', - '10', - 'any1,any2,any3') + 10, + '{subjects}') """ ) self.con.commit() @@ -181,7 +186,10 @@ def save(self): try: cmd = f""" UPDATE {self.TABLE} - SET enrolled_students = '{self.enrolled_students}' + SET enrolled_students = '{convert_list_to_csv(self.enrolled_students)}', + state = '{self.state}', + max_enrollment = '{self.max_enrollment}', + subjects = '{convert_list_to_csv(self.subjects)}' WHERE identifier = '{self.identifier}'; """ self.cur.execute(cmd) @@ -198,9 +206,9 @@ def add(self): ('{self.name}', '{self.state}', '{self.identifier}', - '{self.enrolled_students}', - '{self.max_enrollment}', - '{self.subjects}') + '{convert_list_to_csv(self.enrolled_students)}', + {self.max_enrollment}, + '{convert_list_to_csv(self.subjects)}') """ ) self.con.commit() @@ -225,32 +233,30 @@ class CourseRow: course_row.name = row[0] course_row.state = row[1] course_row.identifier = row[2] - course_row.enrolled_students = ( - convert_list_with_empty_string_to_empty_list( - the_list=row[3].split(",") - ) - ) + course_row.enrolled_students = convert_csv_to_list(the_csv=row[3]) course_row.max_enrollment = row[4] - course_row.subjects = convert_list_with_empty_string_to_empty_list( - row[5].split(",") - ) + course_row.subjects = convert_csv_to_list(row[5]) courses.append(course_row) return courses def load_from_database(self, name): - result = self.cur.execute( - f"SELECT * FROM {self.TABLE} WHERE name = '{name}'" - ).fetchone() - self.name = result[0] - self.state = result[1] - self.identifier = result[2] - self.enrolled_students = convert_list_with_empty_string_to_empty_list( - the_list=result[3].split(",") - ) - self.max_enrollment = result[4] - self.subjects = convert_list_with_empty_string_to_empty_list( - result[5].split(",") - ) + try: + result = self.cur.execute( + f"SELECT * FROM {self.TABLE} WHERE name = '{name}'" + ).fetchone() + if not result: + raise NotFoundError( + f"Course '{name}' not found in table {self.TABLE}." + ) + self.name = result[0] + self.state = result[1] + self.identifier = result[2] + self.enrolled_students = convert_csv_to_list(the_csv=result[3]) + self.max_enrollment = result[4] + self.subjects = convert_csv_to_list(result[5]) + except Exception as e: + logging.error(str(e)) + raise class DbSubject: TABLE = "subject" @@ -260,12 +266,15 @@ class DbSubject: max_enrollment = None identifier = None course = None + MAX_ENROLLMENT = 30 def __init__(self, con, cur): self.cur = cur self.con = con cur.execute( - f"CREATE TABLE IF NOT EXISTS {self.TABLE} (name, state, identifier, enrolled_students, max_enrollment, course)" + f"CREATE TABLE IF NOT EXISTS {self.TABLE}" + " (name, state, identifier, enrolled_students," + f" max_enrollment CHECK (max_enrollment <= {self.MAX_ENROLLMENT}) , course)" ) # Just for admin. The university has a predefined list of approved students to each course. diff --git a/src/services/course_handler.py b/src/services/course_handler.py index 95210d2..38bf0af 100644 --- a/src/services/course_handler.py +++ b/src/services/course_handler.py @@ -2,6 +2,8 @@ import logging from src.constants import DUMMY_IDENTIFIER from src.services.grade_calculator import GradeCalculator +from src.database import Database, NotFoundError +from src import utils class CourseHandler: @@ -9,7 +11,7 @@ class CourseHandler: INACTIVE = "inactive" CANCELLED = "cancelled" - def __init__(self, database) -> None: + def __init__(self, database: Database) -> None: self.__identifier = DUMMY_IDENTIFIER self.__name = None self.__state = self.INACTIVE # TODO use enum @@ -44,6 +46,7 @@ def name(self, value): raise NonValidCourse( f"The maximum number of characters to course's name is '10'. Set with '{len(value)}'." ) + self.__identifier = utils.generate_course_identifier(value) self.__name = value @property @@ -58,15 +61,14 @@ def save(self): self.__database.course.name = self.name self.__database.course.state = self.state self.__database.course.identifier = self.identifier - self.__database.course.enrolled_students = ",".join(self.enrolled_students) + self.__database.course.enrolled_students = self.enrolled_students self.__database.course.max_enrollment = self.max_enrollment - self.__database.course.subjects = ",".join(self.subjects) + self.__database.course.subjects.extend(self.subjects) self.__database.course.save() def load_from_database(self, name): try: self.__database.course.load_from_database(name) - self.name = self.__database.course.name self.__state = self.__database.course.state self.__identifier = self.__database.course.identifier @@ -74,9 +76,12 @@ def load_from_database(self, name): self.max_enrollment = self.__database.course.max_enrollment self.__subjects = self.__database.course.subjects + except NotFoundError as e: + logging.error(str(e)) + raise except Exception as e: logging.error(str(e)) - raise NonValidCourse("Course not found.") + raise def list_student_details(self): self.load_from_database(self.name) @@ -109,18 +114,22 @@ def list_all_courses_with_details(self): def enroll_student(self, student_identifier): if not self.state == self.ACTIVE: - raise NonValidCourse("Course is not active.") + raise NonValidCourse(f"Course '{self.name}' is not active.") self.__enrolled_students.append(student_identifier) self.save() return True def add_subject(self, subject): - self.subjects.append(subject) + self.load_from_database(self.name) + self.__subjects.append(subject) self.save() + return True def cancel(self): if not self.name: raise NonValidCourse("No name set to course.") + + self.load_from_database(self.name) self.__state = self.CANCELLED self.save() return self.__state @@ -129,26 +138,41 @@ def deactivate(self): if not self.name: raise NonValidCourse("No name set to course.") - if self.state == self.ACTIVE: - self.__state = self.INACTIVE - self.save() + self.load_from_database(self.name) + self.__state = self.INACTIVE + self.save() return self.__state def activate(self): if not self.name: raise NonValidCourse("No name set to course.") + self.load_from_database(self.name) MINIMUM = 3 if not len(self.subjects) >= MINIMUM: - raise NonValidCourse( + raise NonMinimunSubjects( f"Need '{MINIMUM}' subjects. Set '{len(self.subjects)}'" ) - self.__identifier = uuid.uuid5(uuid.NAMESPACE_URL, f"{self.name}").hex self.__state = self.ACTIVE self.save() return self.__state + def create(self, course_name, max_enrollmet): + self.name = course_name + self.__max_enrollment = max_enrollmet + self.__database.course.identifier = self.identifier + self.__database.course.name = course_name + self.__database.course.state = self.state + self.__database.course.enrolled_students = self.enrolled_students + self.__database.course.max_enrollment = self.max_enrollment + self.__database.course.add() + return True + class NonValidCourse(Exception): pass + + +class NonMinimunSubjects(Exception): + pass diff --git a/tests/conftest.py b/tests/conftest.py index db0887f..528dcae 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,6 +20,7 @@ def set_in_memory_database(): db.course.populate("noise") db.course.populate("deact") db.course.populate("act") + db.course.populate("nosubjects", state="", subjects="") db.subject.populate("any", "any1") # e4c858cd917f518194c9d93c9d13def8 db.subject.populate("any", "any2") # 283631d2292c54879b9aa72e27a1b4ff diff --git a/tests/test_course.py b/tests/test_course.py index 93cfe24..4cb32e5 100644 --- a/tests/test_course.py +++ b/tests/test_course.py @@ -1,9 +1,41 @@ import pytest -from src.services.course_handler import CourseHandler, NonValidCourse +from src.services.course_handler import ( + CourseHandler, + NonValidCourse, + NonMinimunSubjects, +) from src.services.student_handler import StudentHandler from src import utils +def test_add_subject_to_new_course(set_in_memory_database): + course = "newcourse" + max_enrollment = 9 + subject = "newsubject" + database = set_in_memory_database + course_handler = CourseHandler(database) + course_handler.create(course, max_enrollment) + assert course_handler.add_subject(subject) is True + + # post conditions + course_handler.load_from_database(course) + assert subject in database.course.subjects + assert database.course.max_enrollment == max_enrollment + + +def test_create_courses_without_subjects(set_in_memory_database): + name = "newcourse" + max_enrollment = 9 + database = set_in_memory_database + course_handler = CourseHandler(database) + assert course_handler.create(name, max_enrollment) is True + + # post conditions + course_handler.load_from_database(name) + assert database.course.name == name + assert database.course.max_enrollment == max_enrollment + + def test_list_all_courses(set_in_memory_database): name = "any" cpf = "123.456.789-10" @@ -46,6 +78,19 @@ def test_list_enrolled_students_in_specific_course(set_in_memory_database): assert len(actual) == 1 +def test_enroll_student_to_cancelled_course_return_error(set_in_memory_database): + course_handler = CourseHandler(set_in_memory_database) + course_handler.name = "adm" + course_handler.add_subject("any1") + course_handler.add_subject("any2") + course_handler.add_subject("any3") + course_handler.activate() + course_handler.cancel() + + with pytest.raises(NonValidCourse): + course_handler.enroll_student("any") + + def test_enroll_student_to_inactive_course_return_error(set_in_memory_database): course_handler = CourseHandler(set_in_memory_database) course_handler.name = "adm" @@ -69,7 +114,8 @@ def test_enroll_student_to_active_course(set_in_memory_database): assert course_handler.enroll_student("any") == True - assert database.course.enrolled_students == "any" + course_handler.load_from_database("adm") + assert database.course.enrolled_students == ["any"] def test_cancel_inactive_course(set_in_memory_database): @@ -82,6 +128,7 @@ def test_cancel_inactive_course(set_in_memory_database): course_handler.deactivate() assert course_handler.cancel() == "cancelled" + course_handler.load_from_database("adm") assert database.course.state == "cancelled" @@ -93,8 +140,9 @@ def test_cancel_active_course(set_in_memory_database): course_handler.add_subject("any2") course_handler.add_subject("any3") course_handler.activate() - assert course_handler.cancel() == "cancelled" + assert course_handler.cancel() == "cancelled" + course_handler.load_from_database("adm") assert database.course.state == "cancelled" @@ -104,6 +152,7 @@ def test_deactivate_non_active_course_return_error(set_in_memory_database): with pytest.raises(NonValidCourse): course_handler.deactivate() + course_handler.load_from_database("adm") assert database.course.state != "inactive" @@ -117,15 +166,19 @@ def test_deactivate_course(set_in_memory_database): course_handler.activate() assert course_handler.deactivate() == "inactive" + course_handler.load_from_database("adm") assert database.course.state == "inactive" def test_activate_course_without_minimum_subjects_return_error(set_in_memory_database): database = set_in_memory_database + name = "nosubjects" course_handler = CourseHandler(database) - course_handler.name = "any" - with pytest.raises(NonValidCourse): + course_handler.name = name + with pytest.raises(NonMinimunSubjects): course_handler.activate() + + course_handler.load_from_database(name) assert database.course.state != "active" @@ -134,7 +187,6 @@ def test_activate_course_without_name_return_error(set_in_memory_database): course_handler = CourseHandler(database) with pytest.raises(NonValidCourse): course_handler.activate() - assert database.course.state != "active" def test_activate_course(set_in_memory_database): diff --git a/tests/test_models.py b/tests/test_models.py index c2902d9..914a25a 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -25,24 +25,26 @@ def test_subject_model(set_in_memory_database): def test_course_model(set_in_memory_database): + course = "any" database = set_in_memory_database course_handler = CourseHandler(database) - course_handler.name = "any_name" + course_handler.name = course course_handler.save() - assert course_handler.name == "any_name" + assert course_handler.name == course assert course_handler.identifier is not None assert course_handler.state == "inactive" assert course_handler.enrolled_students == [] assert course_handler.max_enrollment == 0 assert course_handler.subjects == [] - assert database.course.name == "any_name" + course_handler.load_from_database(course) + assert database.course.name == course assert database.course.identifier is not None assert database.course.state == "inactive" - assert database.course.enrolled_students == "" + assert database.course.enrolled_students == [] assert database.course.max_enrollment == 0 - assert database.course.subjects == "" + assert database.course.subjects == [] def test_student_model(set_in_memory_database): From ea82725329da34652ae452f2460f94c1059036bd Mon Sep 17 00:00:00 2001 From: Douglas Cardoso Date: Wed, 17 Apr 2024 13:42:50 -0300 Subject: [PATCH 30/44] Fix readme --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 15b80b9..59fcb82 100644 --- a/README.md +++ b/README.md @@ -103,13 +103,15 @@ Same as requirement 6 40. The administrator, and only the administrator, must be able to list all courses with all available information. 41. The administrator, and only the administrator, must be able to list the list of students per course. 42. The administrator, and only the administrator, must be able to list the list of subjects per student. -43. The student must be able to list all subjects only from their course. -44. The student must be able to list all subjects they have taken. -45. The student must be able to list the missing subjects. +43. The students must be able to list all subjects only from their course. +44. The students must be able to list all subjects they have taken. +45. The students must be able to list the missing subjects. + 46. The administrator must be able to list all course coordinators with available information. 47. The student has 10 semesters to graduate. 48. If the student exceeds the 10 semesters, they are automatically failed. -49. The coordinator can only coordinate a maximum of 3 courses. + +49. ~~The coordinator can only coordinate a maximum of 3 courses~~. 50. The general coordinator cannot be a coordinator of courses. # Features add after architecture analisys From 74bfdb4275b0108fa1c378baea1c3282163a1fd3 Mon Sep 17 00:00:00 2001 From: Douglas Cardoso Date: Wed, 17 Apr 2024 17:31:31 -0300 Subject: [PATCH 31/44] Calculate gpa and subject situation when update grades --- architecture.odp | Bin 24714 -> 25211 bytes src/constants.py | 1 + src/database.py | 97 ++++++++++++++++++++++++++++--- src/services/grade_calculator.py | 28 ++++++++- src/services/semester_monitor.py | 29 ++++++--- src/services/student_handler.py | 68 +++++++++++++++++----- tests/test_models.py | 10 ++-- tests/test_semester.py | 35 ++++++++++- tests/test_student.py | 35 ++++++++++- 9 files changed, 262 insertions(+), 41 deletions(-) diff --git a/architecture.odp b/architecture.odp index faad21a568e9747e8007d72f93272b21836261d2..9799c925953de65f548b55197e26d7a6bc7d794f 100644 GIT binary patch delta 18920 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 C4CmGW delta 18414 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 diff --git a/src/constants.py b/src/constants.py index a8ba7f6..b04810c 100644 --- a/src/constants.py +++ b/src/constants.py @@ -1 +1,2 @@ DUMMY_IDENTIFIER = -1 +SUBJECT_IN_PROGRESS = "inprogress" diff --git a/src/database.py b/src/database.py index 3758e25..70dbb0c 100644 --- a/src/database.py +++ b/src/database.py @@ -35,13 +35,21 @@ class DbStudent: gpa = None subjects = [] course = None + semester_counter = None def __init__(self, con, cur): self.cur = cur self.con = con # TODO move to installation file cur.execute( - f"CREATE TABLE IF NOT EXISTS {self.TABLE} (name, state, cpf, identifier, gpa, subjects, course)" + f"CREATE TABLE IF NOT EXISTS {self.TABLE} (name," + " state," + " cpf," + " identifier," + " gpa," + " subjects," + " course," + " semester_counter)" ) def add(self): @@ -54,7 +62,8 @@ def add(self): '{self.identifier}', {self.gpa}, '{convert_list_to_csv(self.subjects)}', - '{self.course}') + '{self.course}', + {self.semester_counter}) """ self.cur.execute(cmd) @@ -68,6 +77,7 @@ def save(self): UPDATE {self.TABLE} SET state = '{self.state}', gpa = {self.gpa}, + semester_counter = {self.semester_counter}, subjects = '{convert_list_to_csv(self.subjects)}' WHERE identifier = '{self.identifier}'; """ @@ -92,11 +102,45 @@ def populate( '{student_identifier}', '{gpa}', '{subject}', - '{course_identifier}') + '{course_identifier}', + 0) """ ) self.con.commit() + def search_all(self): + class StudentRow: + name = None + state = None + cpf = None + identifier = None + gpa = None + subjects = None + course = None + semester_counter = None + + try: + cmd = f"SELECT * FROM {self.TABLE}" + result = self.cur.execute(cmd).fetchall() + if not result: + return [] + student_rows = [] + for row in result: + student_row = StudentRow() + student_row.name = row[0] + student_row.state = row[1] + student_row.cpf = row[2] + student_row.identifier = row[3] + student_row.gpa = row[4] + student_row.subjects = convert_csv_to_list(row[5]) + student_row.course = row[6] + student_row.semester_counter = row[7] + student_rows.append(student_row) + return student_rows + except Exception as e: + logging.error(str(e)) + raise + def load(self, identifier): try: cmd = f"SELECT * FROM {self.TABLE} WHERE identifier = '{identifier}'" @@ -112,6 +156,7 @@ def load(self, identifier): self.gpa = result[4] self.subjects = convert_csv_to_list(result[5]) self.course = result[6] + self.semester_counter = result[7] except Exception as e: logging.error(str(e)) raise @@ -335,17 +380,22 @@ class GradeCalculatorRow: student_identifier = None subject_identifier = None grade = None + subject_situation = None TABLE = "grade_calculator" student_identifier = None subject_identifier = None grade = None + subject_situation = None def __init__(self, con, cur): self.con = con self.cur = cur cur.execute( - f"CREATE TABLE IF NOT EXISTS {self.TABLE} (student_identifier, subject_identifier, grade)" + f"CREATE TABLE IF NOT EXISTS {self.TABLE} (student_identifier," + " subject_identifier," + " grade INTEGER," + " subject_situation)" ) def load_all_by_student_identifier(self, student_identifier): @@ -365,6 +415,7 @@ def load_all_by_student_identifier(self, student_identifier): grade_calculator_row.student_identifier = row[0] grade_calculator_row.subject_identifier = row[1] grade_calculator_row.grade = row[2] + grade_calculator_row.subject_situation = row[3] grade_calculators.append(grade_calculator_row) return grade_calculators except Exception as e: @@ -386,12 +437,33 @@ def search(self, student_identifier, subject_identifier): grade_calculator_row.student_identifier = result[0] grade_calculator_row.subject_identifier = result[1] grade_calculator_row.grade = result[2] + grade_calculator_row.subject_situation = result[3] return grade_calculator_row except Exception as e: logging.error(str(e)) raise + def search_all(self): + try: + result = self.cur.execute(f"""SELECT * FROM {self.TABLE}""").fetchall() + if not result: + return + + grade_calculators = [] + for row in result: + grade_calculator_row = self.GradeCalculatorRow() + grade_calculator_row.student_identifier = row[0] + grade_calculator_row.subject_identifier = row[1] + grade_calculator_row.grade = row[2] + grade_calculator_row.subject_situation = row[3] + grade_calculators.append(grade_calculator_row) + + return grade_calculators + except Exception as e: + logging.error(str(e)) + raise + def load(self, student_identifier, subject_identifier): try: result = self.cur.execute( @@ -409,6 +481,7 @@ def load(self, student_identifier, subject_identifier): self.student_identifier = result[0] self.subject_identifier = result[1] self.grade = result[2] + self.subject_situation = result[3] except Exception as e: logging.error(str(e)) raise @@ -419,7 +492,8 @@ def add(self): INSERT INTO {self.TABLE} VALUES ('{self.student_identifier}', '{self.subject_identifier}', - {self.grade}) + {self.grade}, + '{self.subject_situation}') """ self.cur.execute(cmd) @@ -432,7 +506,8 @@ def save(self): try: cmd = f""" UPDATE {self.TABLE} - SET grade = {self.grade} + SET grade = {self.grade}, + subject_situation = '{self.subject_situation}' WHERE student_identifier = '{self.student_identifier}' AND subject_identifier = '{self.subject_identifier}'; """ @@ -478,8 +553,7 @@ def populate(self, identifier, state): """ ) self.con.commit() - - x = self.cur.execute(f"select * from {self.TABLE}").fetchall() + self.cur.execute(f"select * from {self.TABLE}").fetchall() def save(self): try: @@ -488,7 +562,12 @@ def save(self): SET state = '{self.state}' WHERE identifier = '{self.identifier}'; """ - self.cur.execute(cmd) + result = self.cur.execute(cmd) + + if not result: + raise NotFoundError( + f"Semester '{self.identifier}' not found in table '{self.TABLE}'" + ) self.con.commit() except Exception as e: diff --git a/src/services/grade_calculator.py b/src/services/grade_calculator.py index 1d54407..006ab68 100644 --- a/src/services/grade_calculator.py +++ b/src/services/grade_calculator.py @@ -1,9 +1,15 @@ +import logging +from src.database import Database, NotFoundError +from src.constants import SUBJECT_IN_PROGRESS + + class GradeCalculator: - def __init__(self, database) -> None: + def __init__(self, database: Database) -> None: self.__student_identifier = None self.__subject_identifier = None self.__grade = None self.__rows = None + self.__subject_situation = SUBJECT_IN_PROGRESS self.__database = database @property @@ -22,6 +28,14 @@ def subject_identifier(self): def subject_identifier(self, value): self.__subject_identifier = value + @property + def subject_situation(self): + return self.__subject_situation + + @subject_situation.setter + def subject_situation(self, value): + self.__subject_situation = value + @property def grade(self): return self.__grade @@ -35,12 +49,16 @@ def load_from_database(self, student_identifier, subject_identifier): self.__student_identifier = self.__database.grade_calculator.student_identifier self.__subject_identifier = self.__database.grade_calculator.subject_identifier self.__grade = self.__database.grade_calculator.grade + self.__subject_situation = self.__database.grade_calculator.subject_situation def search(self, student_identifier, subject_identifier): return self.__database.grade_calculator.search( student_identifier, subject_identifier ) + def search_all(self): + return self.__database.grade_calculator.search_all() + def remove(self, student_identifier, subject_identifier): return self.__database.grade_calculator.remove( student_identifier, subject_identifier @@ -50,12 +68,14 @@ def add(self, student_identifier, subject_identifier, grade): self.__database.grade_calculator.student_identifier = student_identifier self.__database.grade_calculator.subject_identifier = subject_identifier self.__database.grade_calculator.grade = grade + self.__database.grade_calculator.subject_situation = self.subject_situation self.__database.grade_calculator.add() def save(self): self.__database.grade_calculator.student_identifier = self.student_identifier self.__database.grade_calculator.subject_identifier = self.subject_identifier self.__database.grade_calculator.grade = self.grade + self.__database.grade_calculator.subject_situation = self.subject_situation self.__database.grade_calculator.save() def calculate_gpa_for_student(self, student_identifier): @@ -65,15 +85,17 @@ def calculate_gpa_for_student(self, student_identifier): student_identifier ) ) - except Exception: + except NotFoundError as e: raise NonValidGradeOperation( f"Student '{student_identifier}' not enrolled to any subject." ) + except Exception: + logging.error(str(e)) + raise total = 0 for row in self.__rows: total += row.grade - # When the return round(total / len(self.__rows), 1) diff --git a/src/services/semester_monitor.py b/src/services/semester_monitor.py index a0b99ac..e43799d 100644 --- a/src/services/semester_monitor.py +++ b/src/services/semester_monitor.py @@ -1,9 +1,12 @@ import logging +from src.database import Database, NotFoundError +from src.services.grade_calculator import GradeCalculator +from src.services.student_handler import StudentHandler class SemesterMonitor: - def __init__(self, database, identifier) -> None: + def __init__(self, database: Database, identifier) -> None: self.__CLOSED = "closed" self.__OPEN = "open" self.__identifier = identifier # TODO get next from database @@ -48,19 +51,27 @@ def close(self): if not self.identifier: raise NonValidSemester("Need to set the semester identifier.") - self.__state = self.__CLOSED self.__database.semester.identifier = self.identifier - self.__database.semester.state = self.state - self.__database.semester.save() - - # post condition try: self.__database.semester.load_by_identifier() + except NotFoundError as e: + logging.error(str(e)) + raise NonValidOperation(f"Semester '{self.identifier}' not found") except Exception as e: logging.error(str(e)) - raise NonValidOperation(f"Semester '{self.identifier}' is not valid.") - assert self.identifier == self.__database.semester.identifier - assert self.__state == self.__database.semester.state + raise + self.__state = self.__CLOSED + self.__database.semester.state = self.state + self.__database.semester.save() + + student_handler = StudentHandler(self.__database) + student_rows = student_handler.search_all() + + for row in student_rows: + student_handler = StudentHandler(self.__database, row.identifier) + student_handler.calculate_gpa() + student_handler.increment_semester() + return self.__state diff --git a/src/services/student_handler.py b/src/services/student_handler.py index 37e9238..9f17f6c 100644 --- a/src/services/student_handler.py +++ b/src/services/student_handler.py @@ -5,9 +5,9 @@ from src.services.grade_calculator import GradeCalculator from src.services.cpf_validator import is_valide_cpf from src import utils -from src.database import NotFoundError -from src.services.grade_calculator import GradeCalculator -from src.constants import DUMMY_IDENTIFIER +from src.database import Database, NotFoundError +from src.services.grade_calculator import GradeCalculator, NonValidGradeOperation +from src.constants import DUMMY_IDENTIFIER, SUBJECT_IN_PROGRESS class StudentHandler: @@ -15,28 +15,43 @@ class Subject: identifier = None grade = None - def __init__(self, database, identifier=None): + def __init__(self, database: Database, identifier=None): self.__LOCKED = "locked" self.__ENROLLED = "enrolled" - self.__identifier = identifier self.__state = None self.__gpa = 0 self.__course = None self.__name = None self.__cpf = None + self.__semester_counter = 0 self.__subject_identifiers = [] self.__database = database + self.__identifier = None + if identifier: + self.__identifier = identifier + self.load_from_database(identifier) + + def __generate_identifier_when_student_ready(self): + if self.name and self.cpf and self.__course: + self.__identifier = utils.generate_student_identifier( + self.name, self.cpf, self.__course + ) @property def identifier(self): return self.__identifier + @property + def semester_counter(self): + return self.__semester_counter + @property def state(self): return self.__state @property def gpa(self): + self.calculate_gpa() return self.__gpa @property @@ -50,6 +65,7 @@ def name(self): @name.setter def name(self, value): self.__name = value + self.__generate_identifier_when_student_ready() @property def cpf(self): @@ -60,6 +76,7 @@ def cpf(self, value): if not is_valide_cpf(value): raise NonValidStudent(f"CPF {value} is not valid.") self.__cpf = value + self.__generate_identifier_when_student_ready() def __is_locked(self): return self.__state == self.__LOCKED @@ -81,11 +98,30 @@ def __save(self): ).calculate_gpa_for_student(self.identifier) self.__database.student.subjects.extend(self.subjects) self.__database.student.course = self.__course + self.__database.student.semester_counter = self.__semester_counter self.__database.student.save() except Exception as e: logging.error(str(e)) raise + def calculate_gpa(self): + try: + self.__gpa = GradeCalculator(self.__database).calculate_gpa_for_student( + self.identifier + ) + except NonValidGradeOperation as e: + raise NonValidGrade( + f"Student '{self.identifier}' may not be enrolled to any subject." + ) + except Exception as e: + logging.error(str(e)) + raise + + def increment_semester(self): + self.load_from_database(self.identifier) + self.__semester_counter += 1 + self.__save() + def update_grade_to_subject(self, grade, subject_name): if grade < 0 or grade > 10: raise NonValidGrade("Grade must be between '0' and '10'.") @@ -109,13 +145,15 @@ def update_grade_to_subject(self, grade, subject_name): grade_calculator.student_identifier = self.identifier grade_calculator.subject_identifier = subject_identifier grade_calculator.grade = grade + + subject_situation = self.__return_subject_situation(grade) + grade_calculator.subject_situation = subject_situation grade_calculator.save() - grade_calculator.load_from_database(self.identifier, subject_identifier) - # post condition - assert grade_calculator.student_identifier == self.identifier - assert grade_calculator.subject_identifier in self.__subject_identifiers - assert grade_calculator.grade == grade + def __return_subject_situation(self, grade): + if grade < 7: + return "failed" + return "passed" def __is_valid_subject(self, subject_identifier): return subject_identifier in self.subjects @@ -133,10 +171,8 @@ def enroll_to_course(self, course_name): course = CourseHandler(self.__database) course.load_from_database(course_name) - self.__identifier = utils.generate_student_identifier( - self.name, self.cpf, course_name - ) self.__course = course_name + self.__generate_identifier_when_student_ready() course.enroll_student(self.identifier) self.__state = self.__ENROLLED @@ -147,6 +183,7 @@ def enroll_to_course(self, course_name): self.__database.student.gpa = 0 self.__database.student.subjects.extend(self.subjects) self.__database.student.course = self.__course + self.__database.student.semester_counter = self.__semester_counter self.__database.student.add() grade_calculator = GradeCalculator(self.__database) @@ -207,6 +244,7 @@ def take_subject(self, subject_name): grade_calculator = GradeCalculator(self.__database) if grade_calculator.search(self.identifier, DUMMY_IDENTIFIER): grade_calculator.remove(self.identifier, DUMMY_IDENTIFIER) + grade_calculator.subject_situation = SUBJECT_IN_PROGRESS grade_calculator.add(self.identifier, subject_identifier, grade=0) # post condition @@ -241,6 +279,9 @@ def lock_course(self): self.__save() return self.state + def search_all(self): + return self.__database.student.search_all() + def load_from_database(self, student_identifier): try: self.__database.student.load(student_identifier) @@ -252,6 +293,7 @@ def load_from_database(self, student_identifier): self.__gpa = self.__database.student.gpa self.__subject_identifiers.extend(self.__database.student.subjects) self.__course = self.__database.student.course + self.__semester_counter = self.__database.student.semester_counter except NotFoundError as e: raise NonValidStudent(str(e)) diff --git a/tests/test_models.py b/tests/test_models.py index 914a25a..df21248 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,4 +1,5 @@ -from src.services.student_handler import StudentHandler +import pytest +from src.services.student_handler import StudentHandler, NonValidGrade from src.services.course_handler import CourseHandler from src.services.subject_handler import SubjectHandler from src.services.semester_monitor import SemesterMonitor @@ -50,12 +51,13 @@ def test_course_model(set_in_memory_database): def test_student_model(set_in_memory_database): database = set_in_memory_database student = StudentHandler(database) - student.name = "any_name" + student.name = "any" student.cpf = "123.456.789-10" - assert student.name == "any_name" + assert student.name == "any" assert student.cpf == "123.456.789-10" assert student.identifier is None assert student.state == None - assert student.gpa == 0 + with pytest.raises(NonValidGrade): + assert student.gpa == 0 assert student.subjects == [] diff --git a/tests/test_semester.py b/tests/test_semester.py index 8817682..992d3cc 100644 --- a/tests/test_semester.py +++ b/tests/test_semester.py @@ -3,6 +3,32 @@ SemesterMonitor, NonValidOperation, ) +from src.services.student_handler import StudentHandler + + +def test_calculate_gpa_of_all_enrolled_students_when_semester_closes( + set_in_memory_database, +): + student = "any" + cpf = "123.456.789-10" + course = "any" + semester = "2024-1" + database = set_in_memory_database + student_handler = StudentHandler(database) + student_handler.name = student + student_handler.cpf = cpf + student_handler.enroll_to_course(course) + student_handler.take_subject("any1") + student_handler.take_subject("any2") + student_handler.take_subject("any3") + student_handler.update_grade_to_subject(9, "any1") + + semester_monitor = SemesterMonitor(database, semester) + semester_monitor.close() + + student_handler.load_from_database(student_handler.identifier) + assert student_handler.gpa > 0 + assert student_handler.semester_counter > 0 def test_return_error_when_close_invalid_semester(set_in_memory_database): @@ -33,8 +59,15 @@ def test_open_semester(set_in_memory_database): assert semester_monitor.open() == "open" -def test_close_semester(set_in_memory_database): +def test_close_semester_when_no_students(set_in_memory_database): identifier = "2024-1" + database = set_in_memory_database semester_monitor = SemesterMonitor(set_in_memory_database, identifier) assert semester_monitor.close() == "closed" + + # post conditions + database.semester.load_by_identifier() + + assert identifier == database.semester.identifier + assert semester_monitor.state == database.semester.state diff --git a/tests/test_student.py b/tests/test_student.py index 7d95dbb..ef8cd0e 100644 --- a/tests/test_student.py +++ b/tests/test_student.py @@ -6,6 +6,37 @@ NonValidGrade, ) from src import utils +from src.services.grade_calculator import GradeCalculator + + +@pytest.mark.parametrize( + "grade, expected", + [ + (6.99, "failed"), + (7.01, "passed"), + ], +) +def test_subject_situation_after_upgrade_grades( + set_in_memory_database, grade, expected +): + course_name = "any" + subject_name = "any1" + database = set_in_memory_database + student_handler = StudentHandler(database) + student_handler.name = "any" + student_handler.cpf = "123.456.789-10" + student_handler.enroll_to_course(course_name) + student_handler.take_subject(subject_name) + student_handler.update_grade_to_subject(grade=grade, subject_name=subject_name) + + # post condition + grade_calculator = GradeCalculator(database) + subject_identifier = utils.generate_subject_identifier(course_name, subject_name) + grade_calculator.load_from_database(student_handler.identifier, subject_identifier) + assert grade_calculator.student_identifier == student_handler.identifier + assert grade_calculator.subject_identifier in student_handler.subjects + assert grade_calculator.grade == grade + assert grade_calculator.subject_situation == expected @pytest.mark.parametrize( @@ -100,7 +131,7 @@ def test_take_subject_from_course(set_in_memory_database): assert student.take_subject("any1") is True -def test_enroll_invalid_student_to_course_retunr_error(set_in_memory_database): +def test_enroll_invalid_student_to_course_return_error(set_in_memory_database): student = StudentHandler(set_in_memory_database) student.name = "invalid" student.cpf = "123.456.789-10" @@ -109,7 +140,7 @@ def test_enroll_invalid_student_to_course_retunr_error(set_in_memory_database): student.enroll_to_course("any") -def test_enroll_student_to_course(set_in_memory_database): +def test_enroll_student_to_course_x(set_in_memory_database): student = StudentHandler(set_in_memory_database) student.name = "any" student.cpf = "123.456.789-10" From 365baae85cbad2990ea52c660c39fa2cc4455405 Mon Sep 17 00:00:00 2001 From: Douglas Cardoso Date: Wed, 17 Apr 2024 17:43:10 -0300 Subject: [PATCH 32/44] Add 'close semester' command --- cli.py | 16 ++++++++++++++++ for_admin.py | 2 ++ manual_smoke_test.sh | 9 +++++---- src/cli_helper.py | 21 +++++++++++++++++++++ src/services/semester_monitor.py | 1 - 5 files changed, 44 insertions(+), 5 deletions(-) diff --git a/cli.py b/cli.py index 8f3bba4..c3b8bf3 100644 --- a/cli.py +++ b/cli.py @@ -19,6 +19,20 @@ def cli(): pass +@click.command() +@click.option( + "--identifier", + prompt="Semester identifier", + help="Semester identifier. E.g. '2024-1'.", +) +def close_semester(identifier): + try: + database = Database() + cli_helper.close_semester(database, identifier) + except Exception as e: + logging.error(str(e)) + + @click.command() def list_courses(): try: @@ -211,6 +225,8 @@ def unlock_course(student_identifier): cli.add_command(create_course) cli.add_command(add_subject) +cli.add_command(close_semester) + if __name__ == "__main__": cli() diff --git a/for_admin.py b/for_admin.py index f898203..77f0e9d 100644 --- a/for_admin.py +++ b/for_admin.py @@ -27,3 +27,5 @@ db.subject.populate("mat", "algebra") db.subject.populate("mat", "analysis") db.subject.populate("mat", "geometry") + +db.semester.populate("2024-1", "open") diff --git a/manual_smoke_test.sh b/manual_smoke_test.sh index d3d3b0b..7fab6fc 100755 --- a/manual_smoke_test.sh +++ b/manual_smoke_test.sh @@ -1,7 +1,7 @@ # set -x rm university.db python for_admin.py -python cli.py enroll-student --name douglas --cpf 098.765.432.12 --course-name mat +python cli.py enroll-student --name douglas --cpf 098.765.432.12 --course-name mat # 25a5a5c24a5252968097e5d5c80e6352 python cli.py take-subject --student-identifier 25a5a5c24a5252968097e5d5c80e6352 --subject-name calculus python cli.py take-subject --student-identifier 25a5a5c24a5252968097e5d5c80e6352 --subject-name geometry python cli.py update-grade --student-identifier 25a5a5c24a5252968097e5d5c80e6352 --subject-name calculus --grade 7 @@ -13,12 +13,13 @@ python cli.py unlock-course --student-identifier 25a5a5c24a5252968097e5d5c80e635 python cli.py enroll-student --name maria --cpf 028.745.462.18 --course-name mat # python cli.py enroll-student --name aline --cpf 028.745.462.18 --course-name adm python cli.py list-students --course-name mat -python cli.py list-courses - python cli.py remove-subject --course-name adm --subject-name management python cli.py activate-course --name deact python cli.py deactivate-course --name act python cli.py cancel-course --name adm python cli.py create-course --name geography --max-enrollment 11 -python cli.py add-subject --course-name geography --subject-name minerals \ No newline at end of file +python cli.py add-subject --course-name geography --subject-name minerals + +python cli.py close-semester --identifier 2024-1 +python cli.py list-courses \ No newline at end of file diff --git a/src/cli_helper.py b/src/cli_helper.py index 730ba9b..7f27bc2 100644 --- a/src/cli_helper.py +++ b/src/cli_helper.py @@ -12,10 +12,31 @@ ) from src.services.grade_calculator import GradeCalculator, NonValidGradeOperation from src.services.subject_handler import SubjectHandler, NonValidSubject +from src.services.semester_monitor import ( + SemesterMonitor, + NonValidOperation, + NonValidSemester, +) + UNEXPECTED_ERROR = "Unexpected error. Consult the system adminstrator." +def close_semester(database, identifier): + try: + course_handler = SemesterMonitor(database, identifier) + course_handler.close() + print(f"Semester '{identifier}' closed.") + return True + except (NonValidOperation, NonValidSemester) as e: + logging.error(str(e)) + print(str(e)) + except Exception as e: + logging.error(str(e)) + print(UNEXPECTED_ERROR) + return False + + def remove_subject(database, course_name, subject_name): try: subject_handler = SubjectHandler(database, course=course_name) diff --git a/src/services/semester_monitor.py b/src/services/semester_monitor.py index e43799d..60b62e7 100644 --- a/src/services/semester_monitor.py +++ b/src/services/semester_monitor.py @@ -1,6 +1,5 @@ import logging from src.database import Database, NotFoundError -from src.services.grade_calculator import GradeCalculator from src.services.student_handler import StudentHandler From 9384bb8a3094efafe1ee48c8a72ac45de3770867 Mon Sep 17 00:00:00 2001 From: Douglas Cardoso Date: Wed, 17 Apr 2024 18:39:59 -0300 Subject: [PATCH 33/44] Check the student status when finish course --- src/constants.py | 4 ++++ src/database.py | 7 ++++++- src/services/grade_calculator.py | 22 ++++++++++++++++++- src/services/semester_monitor.py | 18 ++++++++++++++++ src/services/student_handler.py | 36 +++++++++++++++++++++++++++++--- tests/test_semester.py | 1 + 6 files changed, 83 insertions(+), 5 deletions(-) diff --git a/src/constants.py b/src/constants.py index b04810c..4edce7c 100644 --- a/src/constants.py +++ b/src/constants.py @@ -1,2 +1,6 @@ DUMMY_IDENTIFIER = -1 SUBJECT_IN_PROGRESS = "inprogress" +SUBJECT_FAILED = "failed" +SUBJECT_PASSED = "passed" +STUDENT_APPROVED = "approved" +STUDENT_FAILED = "failed" diff --git a/src/database.py b/src/database.py index 70dbb0c..ae5be60 100644 --- a/src/database.py +++ b/src/database.py @@ -214,6 +214,11 @@ def __init__(self, con, cur): # TODO create a public funtion def populate(self, name, state="active", subjects="any1,any2,any3"): identifier = utils.generate_course_identifier(name) + subjects = subjects.split(",") + list_of_subjects = [] + for subject in subjects: + subject_identifier = utils.generate_subject_identifier(name, subject) + list_of_subjects.append(subject_identifier) self.cur.execute( f""" INSERT INTO {self.TABLE} VALUES @@ -222,7 +227,7 @@ def populate(self, name, state="active", subjects="any1,any2,any3"): '{identifier}', '', 10, - '{subjects}') + '{",".join(list_of_subjects)}') """ ) self.con.commit() diff --git a/src/services/grade_calculator.py b/src/services/grade_calculator.py index 006ab68..2bfd150 100644 --- a/src/services/grade_calculator.py +++ b/src/services/grade_calculator.py @@ -1,6 +1,6 @@ import logging from src.database import Database, NotFoundError -from src.constants import SUBJECT_IN_PROGRESS +from src.constants import SUBJECT_IN_PROGRESS, SUBJECT_FAILED class GradeCalculator: @@ -78,6 +78,26 @@ def save(self): self.__database.grade_calculator.subject_situation = self.subject_situation self.__database.grade_calculator.save() + def is_approved(self, student_identifier): + try: + self.__rows = ( + self.__database.grade_calculator.load_all_by_student_identifier( + student_identifier + ) + ) + except NotFoundError as e: + raise NonValidGradeOperation( + f"Student '{student_identifier}' not enrolled to any subject." + ) + except Exception: + logging.error(str(e)) + raise + + for row in self.__rows: + if row.subject_situation == SUBJECT_FAILED: + return False + return True + def calculate_gpa_for_student(self, student_identifier): try: self.__rows = ( diff --git a/src/services/semester_monitor.py b/src/services/semester_monitor.py index 60b62e7..3625b5e 100644 --- a/src/services/semester_monitor.py +++ b/src/services/semester_monitor.py @@ -1,6 +1,9 @@ import logging from src.database import Database, NotFoundError from src.services.student_handler import StudentHandler +from src.services.course_handler import CourseHandler +from src.services.grade_calculator import GradeCalculator +from src.constants import STUDENT_APPROVED, STUDENT_FAILED class SemesterMonitor: @@ -71,8 +74,23 @@ def close(self): student_handler.calculate_gpa() student_handler.increment_semester() + course_handler = CourseHandler(self.__database) + course_handler.load_from_database(student_handler.course) + course_handler.name = student_handler.course + if self.__is_course_completed(student_handler, course_handler): + grade_calculator = GradeCalculator(self.__database) + if grade_calculator.is_approved(student_handler.identifier): + student_handler.state = STUDENT_APPROVED + else: + student_handler.state = STUDENT_FAILED + return self.__state + def __is_course_completed(self, student_handler, course_handler): + return set(student_handler.subjects).intersection( + course_handler.subjects + ) == set(student_handler.subjects) + class NonValidOperation(Exception): pass diff --git a/src/services/student_handler.py b/src/services/student_handler.py index 9f17f6c..d390549 100644 --- a/src/services/student_handler.py +++ b/src/services/student_handler.py @@ -7,7 +7,12 @@ from src import utils from src.database import Database, NotFoundError from src.services.grade_calculator import GradeCalculator, NonValidGradeOperation -from src.constants import DUMMY_IDENTIFIER, SUBJECT_IN_PROGRESS +from src.constants import ( + DUMMY_IDENTIFIER, + SUBJECT_IN_PROGRESS, + STUDENT_APPROVED, + STUDENT_FAILED, +) class StudentHandler: @@ -49,6 +54,12 @@ def semester_counter(self): def state(self): return self.__state + @state.setter + def state(self, value): + self.load_from_database(self.identifier) + self.__state = value + self.__save() + @property def gpa(self): self.calculate_gpa() @@ -71,6 +82,10 @@ def name(self, value): def cpf(self): return self.__cpf + @property + def course(self): + return self.__course + @cpf.setter def cpf(self, value): if not is_valide_cpf(value): @@ -168,6 +183,10 @@ def enroll_to_course(self, course_name): raise NonValidStudent( f"Student '{self.identifier}' does not appears in enrollment list." ) + if self.state == STUDENT_APPROVED or self.state == STUDENT_FAILED: + raise NonValidStudent( + f"Can not perform the operation. The student is '{self.state}' in course '{self.course}'" + ) course = CourseHandler(self.__database) course.load_from_database(course_name) @@ -207,6 +226,11 @@ def take_subject(self, subject_name): if not self.__is_enrolled_student(self.__course): raise NonValidStudent(f"Student '{self.identifier}' is not valid.") + if self.state == STUDENT_APPROVED or self.state == STUDENT_FAILED: + raise NonValidStudent( + f"Can not perform the operation. The student is '{self.state}' in course '{self.course}'" + ) + self.load_from_database(self.identifier) if self.__is_locked(): @@ -264,7 +288,10 @@ def take_subject(self, subject_name): def unlock_course(self): if not self.__is_enrolled_student(self.__course): raise NonValidStudent(f"Student is not not enrolled in any course.") - + if self.state == STUDENT_APPROVED or self.state == STUDENT_FAILED: + raise NonValidStudent( + f"Can not perform the operation. The student is '{self.state}' in course '{self.course}'" + ) self.load_from_database(self.identifier) self.__state = self.__ENROLLED self.__save() @@ -273,7 +300,10 @@ def unlock_course(self): def lock_course(self): if not self.__is_enrolled_student(self.__course): raise NonValidStudent(f"Student is not not enrolled in any course.") - + if self.state == STUDENT_APPROVED or self.state == STUDENT_FAILED: + raise NonValidStudent( + f"Can not perform the operation. The student is '{self.state}' in course '{self.course}'" + ) self.load_from_database(self.identifier) self.__state = self.__LOCKED self.__save() diff --git a/tests/test_semester.py b/tests/test_semester.py index 992d3cc..b0c3789 100644 --- a/tests/test_semester.py +++ b/tests/test_semester.py @@ -29,6 +29,7 @@ def test_calculate_gpa_of_all_enrolled_students_when_semester_closes( student_handler.load_from_database(student_handler.identifier) assert student_handler.gpa > 0 assert student_handler.semester_counter > 0 + assert student_handler.state == "approved" def test_return_error_when_close_invalid_semester(set_in_memory_database): From 34edb43af26a9540723da9867b740fa0c27efff6 Mon Sep 17 00:00:00 2001 From: Douglas Cardoso Date: Thu, 18 Apr 2024 00:50:31 -0300 Subject: [PATCH 34/44] Add test 'test_student_finishes_course' --- README.md | 4 +- src/constants.py | 1 + src/database.py | 28 ++++++- src/services/course_handler.py | 67 +++++++++++----- src/services/semester_monitor.py | 25 ++++-- src/services/student_handler.py | 16 +++- src/services/subject_handler.py | 83 +++++++++++++------- src/utils.py | 3 +- tests/conftest.py | 13 +++- tests/test_course.py | 4 +- tests/test_grade_calculator.py | 1 - tests/test_integration.py | 128 +++++++++++++++++++++++++++++++ tests/test_models.py | 5 +- tests/test_semester.py | 2 +- 14 files changed, 309 insertions(+), 71 deletions(-) create mode 100644 tests/test_integration.py diff --git a/README.md b/README.md index 59fcb82..c24b278 100644 --- a/README.md +++ b/README.md @@ -108,8 +108,8 @@ Same as requirement 6 45. The students must be able to list the missing subjects. 46. The administrator must be able to list all course coordinators with available information. -47. The student has 10 semesters to graduate. -48. If the student exceeds the 10 semesters, they are automatically failed. +47. **DONE** The student has 10 semesters to graduate. +48. **DONE** If the student exceeds the 10 semesters, they are automatically failed. 49. ~~The coordinator can only coordinate a maximum of 3 courses~~. 50. The general coordinator cannot be a coordinator of courses. diff --git a/src/constants.py b/src/constants.py index 4edce7c..b04c3cc 100644 --- a/src/constants.py +++ b/src/constants.py @@ -4,3 +4,4 @@ SUBJECT_PASSED = "passed" STUDENT_APPROVED = "approved" STUDENT_FAILED = "failed" +MAX_SEMESTERS_TO_FINISH_COURSE = 10 diff --git a/src/database.py b/src/database.py index ae5be60..10084c2 100644 --- a/src/database.py +++ b/src/database.py @@ -214,7 +214,10 @@ def __init__(self, con, cur): # TODO create a public funtion def populate(self, name, state="active", subjects="any1,any2,any3"): identifier = utils.generate_course_identifier(name) - subjects = subjects.split(",") + if len(subjects) == 0: + subjects = [] + else: + subjects = subjects.split(",") list_of_subjects = [] for subject in subjects: subject_identifier = utils.generate_subject_identifier(name, subject) @@ -346,9 +349,8 @@ def populate(self, course, name, max_enrollment=10, state="active"): def load(self, subject_identifier): try: - result = self.cur.execute( - f"SELECT * FROM {self.TABLE} WHERE identifier = '{subject_identifier}'" - ).fetchone() + cmd = f"SELECT * FROM {self.TABLE} WHERE identifier = '{subject_identifier}'" + result = self.cur.execute(cmd).fetchone() if not result: raise NotFoundError( f"Subject '{subject_identifier}' not found in table '{self.TABLE}'" @@ -364,6 +366,24 @@ def load(self, subject_identifier): logging.error(str(e)) raise + def add(self): + try: + cmd = f""" + INSERT INTO {self.TABLE} VALUES + ('{self.name}', + '{self.state}', + '{self.identifier}', + '{self.enrolled_students}', + {self.max_enrollment}, + '{self.course}') + """ + self.cur.execute(cmd) + + self.con.commit() + except Exception as e: + logging.error(str(e)) + raise + def save(self): try: cmd = f""" diff --git a/src/services/course_handler.py b/src/services/course_handler.py index 38bf0af..c29769f 100644 --- a/src/services/course_handler.py +++ b/src/services/course_handler.py @@ -3,6 +3,7 @@ from src.constants import DUMMY_IDENTIFIER from src.services.grade_calculator import GradeCalculator from src.database import Database, NotFoundError +from src.services.subject_handler import SubjectHandler from src import utils @@ -34,6 +35,7 @@ def enrolled_students(self): @property def subjects(self): + self.__subjects = list(set(self.__subjects)) return self.__subjects @property @@ -42,12 +44,38 @@ def name(self): @name.setter def name(self, value): + self.__check_name_lenght(value) + self.__identifier = utils.generate_course_identifier(value) + self.__name = value + + def __check_name_lenght(self, value): if len(value) > 10: raise NonValidCourse( - f"The maximum number of characters to course's name is '10'. Set with '{len(value)}'." + f"The maximum number of characters to course's " + "name is '10'. Set with '{len(value)}'." + ) + + def __check_name(self): + if not self.name: + raise NonValidCourse("Need to set the name.") + + def __check_active(self): + if not self.state == self.ACTIVE: + raise NonValidCourse(f"Course '{self.name}' is not active.") + + def __check_minimum_number_of_subjects(self): + MINIMUM = 3 + if not len(self.subjects) >= MINIMUM: + raise NonMinimunSubjects( + f"Need '{MINIMUM}' subjects. Set '{len(self.subjects)}'" + ) + + def __check_maximum_enrollment(self): + if len(self.__database.course.subjects) > self.max_enrollment: + raise NonValidCourse( + f"Exceeded the maximum number of subjects." + f" Expected '{self.max_enrollment}. Set '{len(self.subjects)}'." ) - self.__identifier = utils.generate_course_identifier(value) - self.__name = value @property def max_enrollment(self): @@ -63,7 +91,8 @@ def save(self): self.__database.course.identifier = self.identifier self.__database.course.enrolled_students = self.enrolled_students self.__database.course.max_enrollment = self.max_enrollment - self.__database.course.subjects.extend(self.subjects) + self.__database.course.subjects = self.subjects + self.__check_maximum_enrollment() self.__database.course.save() def load_from_database(self, name): @@ -113,46 +142,42 @@ def list_all_courses_with_details(self): return all_details def enroll_student(self, student_identifier): - if not self.state == self.ACTIVE: - raise NonValidCourse(f"Course '{self.name}' is not active.") + self.__check_active() self.__enrolled_students.append(student_identifier) self.save() return True def add_subject(self, subject): + self.__check_name() self.load_from_database(self.name) - self.__subjects.append(subject) + subject_identifier = utils.generate_subject_identifier(self.name, subject) + self.__subjects.append(subject_identifier) self.save() + + subject_handler = SubjectHandler(self.__database) + subject_handler.name = subject + subject_handler.course = self.name + subject_handler.add() return True def cancel(self): - if not self.name: - raise NonValidCourse("No name set to course.") - + self.__check_name() self.load_from_database(self.name) self.__state = self.CANCELLED self.save() return self.__state def deactivate(self): - if not self.name: - raise NonValidCourse("No name set to course.") - + self.__check_name() self.load_from_database(self.name) self.__state = self.INACTIVE self.save() return self.__state def activate(self): - if not self.name: - raise NonValidCourse("No name set to course.") - + self.__check_name() self.load_from_database(self.name) - MINIMUM = 3 - if not len(self.subjects) >= MINIMUM: - raise NonMinimunSubjects( - f"Need '{MINIMUM}' subjects. Set '{len(self.subjects)}'" - ) + self.__check_minimum_number_of_subjects() self.__state = self.ACTIVE self.save() diff --git a/src/services/semester_monitor.py b/src/services/semester_monitor.py index 3625b5e..50950b4 100644 --- a/src/services/semester_monitor.py +++ b/src/services/semester_monitor.py @@ -3,7 +3,11 @@ from src.services.student_handler import StudentHandler from src.services.course_handler import CourseHandler from src.services.grade_calculator import GradeCalculator -from src.constants import STUDENT_APPROVED, STUDENT_FAILED +from src.constants import ( + STUDENT_APPROVED, + STUDENT_FAILED, + MAX_SEMESTERS_TO_FINISH_COURSE, +) class SemesterMonitor: @@ -15,6 +19,17 @@ def __init__(self, database: Database, identifier) -> None: self.__state = self.__OPEN self.__database = database + def __check_identifier(self): + if not self.identifier: + raise NonValidSemester("Need to set the semester identifier.") + + def __is_course_completed(self, student_handler, course_handler): + return ( + set(student_handler.subjects).intersection(course_handler.subjects) + == set(student_handler.subjects) + and student_handler.semester_counter > MAX_SEMESTERS_TO_FINISH_COURSE + ) + @property def identifier(self): return self.__identifier @@ -50,8 +65,7 @@ def open(self): return self.__state def close(self): - if not self.identifier: - raise NonValidSemester("Need to set the semester identifier.") + self.__check_identifier() self.__database.semester.identifier = self.identifier try: @@ -86,11 +100,6 @@ def close(self): return self.__state - def __is_course_completed(self, student_handler, course_handler): - return set(student_handler.subjects).intersection( - course_handler.subjects - ) == set(student_handler.subjects) - class NonValidOperation(Exception): pass diff --git a/src/services/student_handler.py b/src/services/student_handler.py index d390549..ad38569 100644 --- a/src/services/student_handler.py +++ b/src/services/student_handler.py @@ -12,6 +12,7 @@ SUBJECT_IN_PROGRESS, STUDENT_APPROVED, STUDENT_FAILED, + MAX_SEMESTERS_TO_FINISH_COURSE, ) @@ -48,6 +49,7 @@ def identifier(self): @property def semester_counter(self): + self.load_from_database(self.identifier) return self.__semester_counter @property @@ -135,6 +137,8 @@ def calculate_gpa(self): def increment_semester(self): self.load_from_database(self.identifier) self.__semester_counter += 1 + if self.__semester_counter > MAX_SEMESTERS_TO_FINISH_COURSE: + self.__state = STUDENT_FAILED self.__save() def update_grade_to_subject(self, grade, subject_name): @@ -228,7 +232,8 @@ def take_subject(self, subject_name): if self.state == STUDENT_APPROVED or self.state == STUDENT_FAILED: raise NonValidStudent( - f"Can not perform the operation. The student is '{self.state}' in course '{self.course}'" + f"Can not perform the operation." + f" The student is '{self.state}' in course '{self.course}'" ) self.load_from_database(self.identifier) @@ -254,9 +259,14 @@ def take_subject(self, subject_name): f"The subject '{subject_handler.identifier}' is not part of course '{self.__course}'." ) - if not subject_handler.is_available() or not subject_handler.is_active(): + if not subject_handler.is_available(): + raise NonValidSubject( + f"Subject '{subject_handler.identifier}' is not available." + ) + + if not subject_handler.is_active(): raise NonValidSubject( - f"Subject '{subject_handler.identifier}' is not available or is not active." + f"Subject '{subject_handler.identifier}' is not active." ) self.__subject_identifiers.append(subject_identifier) diff --git a/src/services/subject_handler.py b/src/services/subject_handler.py index 62d6f42..6da211f 100644 --- a/src/services/subject_handler.py +++ b/src/services/subject_handler.py @@ -1,6 +1,7 @@ import logging from src import utils from src.constants import DUMMY_IDENTIFIER +from src.database import Database class SubjectHandler: @@ -9,16 +10,46 @@ class SubjectHandler: ACTIVE = "active" def __init__( - self, database, subject_identifier=DUMMY_IDENTIFIER, course=None + self, database: Database, identifier=DUMMY_IDENTIFIER, course=None ) -> None: self.__database = database - self.__identifier = subject_identifier + self.__identifier = identifier + if identifier != DUMMY_IDENTIFIER: + self.load_from_database(identifier) self.__state = None self.__enrolled_students = [] self.__course = course self.__max_enrollment = 0 self.__name = None + def __check_identifier(self): + if self.identifier != DUMMY_IDENTIFIER: + return + if not self.name: + raise NonValidSubject("Need to set a name to subject.") + if not self.course: + raise NonValidSubject("Need to set a course to subject.") + + def __check_removed(self): + if self.state == self.REMOVED: + raise NonValidSubject( + f"Subject '{self.identifier}' is removed and can not be activated." + ) + + def __generate_identifier_when_subject_ready(self): + if self.name and self.__course: + self.__identifier = utils.generate_subject_identifier( + self.__course, + self.name, + ) + + def __check_name_leght(self, value): + if len(value) > 10: + raise NonValidSubject( + f"The maximum number of characters to subject's name is '10'." + f" Set with '{len(value)}'." + ) + @property def identifier(self): return self.__identifier @@ -35,17 +66,20 @@ def enrolled_students(self): def course(self): return self.__course + @course.setter + def course(self, value): + self.__course = value + self.__generate_identifier_when_subject_ready() + @property def name(self): return self.__name @name.setter def name(self, value): - if len(value) > 10: - raise NonValidSubject( - f"The maximum number of characters to subject's name is '10'. Set with '{len(value)}'." - ) + self.__check_name_leght(value) self.__name = value + self.__generate_identifier_when_subject_ready() @property def max_enrollment(self): @@ -53,7 +87,10 @@ def max_enrollment(self): @max_enrollment.setter def max_enrollment(self, value): + self.__check_identifier() + self.load_from_database(self.identifier) self.__max_enrollment = value + self.save() def is_available(self): return len(self.enrolled_students) < self.__max_enrollment @@ -62,14 +99,9 @@ def is_active(self): return self.__state == self.ACTIVE def activate(self): - if self.identifier == DUMMY_IDENTIFIER: - raise NonValidSubject(f"Subject not found.'") - - if self.state == self.REMOVED: - raise NonValidSubject( - f"Subject '{self.identifier}' is removed and can not be activated." - ) - + self.__check_identifier() + self.load_from_database(self.identifier) + self.__check_removed() self.__state = self.ACTIVE self.save() @@ -79,14 +111,14 @@ def activate(self): return self.__state def remove(self): - self.__generate_identifier() + self.__generate_identifier_when_subject_ready() try: self.load_from_database(self.identifier) except Exception as e: logging.error(str(e)) raise NonValidSubject( - f"Subject '{self.name}' not found in course '{self.course}'.'" + f"Subject '{self.identifier}' not found in course '{self.course}'.'" ) if not self.state == self.ACTIVE: @@ -100,22 +132,21 @@ def remove(self): assert self.__database.subject.state == self.REMOVED return self.__state - def __generate_identifier(self): - if self.identifier != DUMMY_IDENTIFIER: - return - if not self.name: - raise NonValidSubject("Need to set a name to subject.") - if not self.course: - raise NonValidSubject("Need to set a course to subject.") - - self.__identifier = utils.generate_subject_identifier(self.course, self.name) - def save(self): self.__database.subject.enrolled_students = ",".join(self.__enrolled_students) self.__database.subject.max_enrollment = self.__max_enrollment self.__database.subject.state = self.__state self.__database.subject.save() + def add(self): + self.__database.subject.name = self.name + self.__database.subject.state = self.state + self.__database.subject.identifier = self.identifier + self.__database.subject.enrolled_students = self.__enrolled_students + self.__database.subject.max_enrollment = self.__max_enrollment + self.__database.subject.course = self.course + self.__database.subject.add() + def load_from_database(self, subject_identifier): try: self.__database.subject.load(subject_identifier) diff --git a/src/utils.py b/src/utils.py index 067f6b5..e5d0c5b 100644 --- a/src/utils.py +++ b/src/utils.py @@ -16,4 +16,5 @@ def generate_student_identifier(name, cpf, course_name): def generate_subject_identifier(course, name): logging.info(f"Generate identifier for subject '{name}'") - return uuid.uuid5(uuid.NAMESPACE_URL, str(f"{name}{course}")).hex + # return uuid.uuid5(uuid.NAMESPACE_URL, str(f"{name}{course}")).hex + return f"{course}_{name}" diff --git a/tests/conftest.py b/tests/conftest.py index 528dcae..5c4946e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,7 +13,7 @@ def set_in_memory_database(): db.enrollment.populate("other", "123.456.789-10", "any") db.enrollment.populate("another", "123.456.789-10", "any") - db.course.populate("adm") + db.course.populate("adm", subjects="") db.course.populate("mat") db.course.populate("port") db.course.populate("any") @@ -34,5 +34,16 @@ def set_in_memory_database(): db.semester.populate("2023-2", "closed") db.semester.populate("2024-1", "open") + db.semester.populate("1234-1", "open") + db.semester.populate("1234-2", "open") + db.semester.populate("1234-3", "open") + db.semester.populate("1234-4", "open") + db.semester.populate("1234-5", "open") + db.semester.populate("1234-6", "open") + db.semester.populate("1234-7", "open") + db.semester.populate("1234-8", "open") + db.semester.populate("1234-9", "open") + db.semester.populate("1234-10", "open") + db.semester.populate("1234-11", "open") yield db diff --git a/tests/test_course.py b/tests/test_course.py index 4cb32e5..b875568 100644 --- a/tests/test_course.py +++ b/tests/test_course.py @@ -19,7 +19,9 @@ def test_add_subject_to_new_course(set_in_memory_database): # post conditions course_handler.load_from_database(course) - assert subject in database.course.subjects + assert ( + utils.generate_subject_identifier(course, subject) in database.course.subjects + ) assert database.course.max_enrollment == max_enrollment diff --git a/tests/test_grade_calculator.py b/tests/test_grade_calculator.py index 3e3d982..488e50e 100644 --- a/tests/test_grade_calculator.py +++ b/tests/test_grade_calculator.py @@ -1,7 +1,6 @@ import pytest from src.services.grade_calculator import GradeCalculator from src.services.student_handler import StudentHandler -from src import utils @pytest.mark.parametrize( diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100644 index 0000000..5cd616a --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,128 @@ +import pytest +from src.services.student_handler import StudentHandler +from src.services.course_handler import CourseHandler +from src.services.subject_handler import SubjectHandler +from src.services.semester_monitor import SemesterMonitor +from src.constants import MAX_SEMESTERS_TO_FINISH_COURSE + + +def test_student_locked_by_minimun_subjects_per_semester(): + pass + + +def test_student_can_enroll_to_course_after_fail_it_losing_all_history(): + pass + + +def test_student_cannot_do_anythin_after_pass_or_fail_course(): + pass + + +def test_coordinator_cancel_course_before_studend_conclude_it_not_affecting_grades(): + pass + + +def test_coordinator_cancel_course_before_studend_conclude_it_not_affecting_student_situation(): + pass + + +def test_student_locks_course_and_forget_and_fail_by_maximum_semesters(): + pass + + +def test_student_failed_by_maximum_semesters(set_in_memory_database): + return + course = "adm" + grade = 9 + situation = "failed" + subjects = [ + "mgmt", + "people1", + "people2", + "people3", + "strategy1", + "strategy2", + "strategy3", + "business1", + "business2", + "business3", + ] + database = set_in_memory_database + course_handler = CourseHandler(database) + course_handler.name = course + for subject in subjects: + course_handler.add_subject(subject) + subject_handler = SubjectHandler(database) + subject_handler.name = subject + subject_handler.course = course + subject_handler.max_enrollment = 10 + subject_handler.activate() + course_handler.activate() + + student_handler = StudentHandler(database) + student_handler.name = "douglas" + student_handler.cpf = "098.765.432.12" + student_handler.enroll_to_course(course) + + for subject in subjects: + student_handler.take_subject(subject) + student_handler.update_grade_to_subject(grade, subject) + + for i in range(MAX_SEMESTERS_TO_FINISH_COURSE + 1): + semester_monitor = SemesterMonitor(database, f"1234-{i+1}") + semester_monitor.close() + + assert student_handler.semester_counter == MAX_SEMESTERS_TO_FINISH_COURSE + 1 + assert student_handler.gpa == grade + assert student_handler.state == situation + + +@pytest.mark.parametrize( + "grade,situation", + [ + (7, "approved"), + (5, "failed"), + ], +) +def test_student_finishes_course(set_in_memory_database, grade, situation): + course = "adm" + subjects = [ + "mgmt", + "people1", + "people2", + "people3", + "strategy1", + "strategy2", + "strategy3", + "business1", + "business2", + "business3", + ] + database = set_in_memory_database + course_handler = CourseHandler(database) + course_handler.name = course + for subject in subjects: + course_handler.add_subject(subject) + subject_handler = SubjectHandler(database) + subject_handler.name = subject + subject_handler.course = course + subject_handler.max_enrollment = 10 + subject_handler.activate() + course_handler.activate() + + student_handler = StudentHandler(database) + student_handler.name = "douglas" + student_handler.cpf = "098.765.432.12" + student_handler.enroll_to_course(course) + + for subject in subjects: + student_handler.take_subject(subject) + student_handler.update_grade_to_subject(grade, subject) + + for i in range(MAX_SEMESTERS_TO_FINISH_COURSE + 1): + semester_monitor = SemesterMonitor(database, f"1234-{i+1}") + semester_monitor.close() + + assert student_handler.semester_counter == MAX_SEMESTERS_TO_FINISH_COURSE + 1 + assert student_handler.gpa == grade + assert student_handler.state == situation diff --git a/tests/test_models.py b/tests/test_models.py index df21248..25c4f57 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -16,13 +16,14 @@ def test_subject_model(set_in_memory_database): database = set_in_memory_database subject_handler = SubjectHandler(database) subject_handler.name = "any_name" + subject_handler.course = "any" assert subject_handler.name == "any_name" - assert subject_handler.identifier is -1 + assert subject_handler.identifier is not -1 assert subject_handler.state == None assert subject_handler.enrolled_students == [] assert subject_handler.max_enrollment == 0 - assert subject_handler.course == None + assert subject_handler.course == "any" def test_course_model(set_in_memory_database): diff --git a/tests/test_semester.py b/tests/test_semester.py index b0c3789..8dc324c 100644 --- a/tests/test_semester.py +++ b/tests/test_semester.py @@ -29,7 +29,7 @@ def test_calculate_gpa_of_all_enrolled_students_when_semester_closes( student_handler.load_from_database(student_handler.identifier) assert student_handler.gpa > 0 assert student_handler.semester_counter > 0 - assert student_handler.state == "approved" + assert student_handler.state == "enrolled" def test_return_error_when_close_invalid_semester(set_in_memory_database): From fb3a5748d068546fc6cfbeacfd6740bfd798388d Mon Sep 17 00:00:00 2001 From: Douglas Cardoso Date: Thu, 18 Apr 2024 01:20:23 -0300 Subject: [PATCH 35/44] Add test 'test_student_failed_by_maximum_semesters' --- src/services/grade_calculator.py | 20 ----- src/services/semester_monitor.py | 24 +++++- tests/test_integration.py | 135 +++++++++++++++---------------- 3 files changed, 88 insertions(+), 91 deletions(-) diff --git a/src/services/grade_calculator.py b/src/services/grade_calculator.py index 2bfd150..dcfde35 100644 --- a/src/services/grade_calculator.py +++ b/src/services/grade_calculator.py @@ -78,26 +78,6 @@ def save(self): self.__database.grade_calculator.subject_situation = self.subject_situation self.__database.grade_calculator.save() - def is_approved(self, student_identifier): - try: - self.__rows = ( - self.__database.grade_calculator.load_all_by_student_identifier( - student_identifier - ) - ) - except NotFoundError as e: - raise NonValidGradeOperation( - f"Student '{student_identifier}' not enrolled to any subject." - ) - except Exception: - logging.error(str(e)) - raise - - for row in self.__rows: - if row.subject_situation == SUBJECT_FAILED: - return False - return True - def calculate_gpa_for_student(self, student_identifier): try: self.__rows = ( diff --git a/src/services/semester_monitor.py b/src/services/semester_monitor.py index 50950b4..8ca6baa 100644 --- a/src/services/semester_monitor.py +++ b/src/services/semester_monitor.py @@ -7,6 +7,8 @@ STUDENT_APPROVED, STUDENT_FAILED, MAX_SEMESTERS_TO_FINISH_COURSE, + SUBJECT_FAILED, + STUDENT_APPROVED, ) @@ -30,6 +32,25 @@ def __is_course_completed(self, student_handler, course_handler): and student_handler.semester_counter > MAX_SEMESTERS_TO_FINISH_COURSE ) + def __is_approved(self, student_identifier): + self.__database.student.load(student_identifier) + self.__database.course.load_from_database(self.__database.student.course) + + if not self.__is_student_finished_all_subjects(): + return False + + for row in self.__database.grade_calculator.load_all_by_student_identifier( + student_identifier + ): + if row.subject_situation == SUBJECT_FAILED: + return False + return True + + def __is_student_finished_all_subjects(self): + return len(self.__database.student.subjects) == len( + self.__database.course.subjects + ) + @property def identifier(self): return self.__identifier @@ -92,8 +113,7 @@ def close(self): course_handler.load_from_database(student_handler.course) course_handler.name = student_handler.course if self.__is_course_completed(student_handler, course_handler): - grade_calculator = GradeCalculator(self.__database) - if grade_calculator.is_approved(student_handler.identifier): + if self.__is_approved(student_handler.identifier): student_handler.state = STUDENT_APPROVED else: student_handler.state = STUDENT_FAILED diff --git a/tests/test_integration.py b/tests/test_integration.py index 5cd616a..bf8ccd9 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -6,6 +6,62 @@ from src.constants import MAX_SEMESTERS_TO_FINISH_COURSE +def __get_subjects(): + subjects = [ + "mgmt", + "people1", + "people2", + "people3", + "strategy1", + "strategy2", + "strategy3", + "business1", + "business2", + "business3", + ] + + return subjects + + +def __update_grade_of_3_subjects_only(grade, subjects, student_handler): + for i in range(3): + student_handler.take_subject(subjects[i]) + student_handler.update_grade_to_subject(grade, subjects[i]) + + +def __close_maximum_semesters(database): + for i in range(MAX_SEMESTERS_TO_FINISH_COURSE + 1): + semester_monitor = SemesterMonitor(database, f"1234-{i+1}") + semester_monitor.close() + + +def __update_grade_of_all_subjects(grade, subjects, student_handler): + for subject in subjects: + student_handler.take_subject(subject) + student_handler.update_grade_to_subject(grade, subject) + + +def __enroll_student_to_course(course, database): + student_handler = StudentHandler(database) + student_handler.name = "douglas" + student_handler.cpf = "098.765.432.12" + student_handler.enroll_to_course(course) + return student_handler + + +def __add_all_subjects_to_course(course, subjects, database): + course_handler = CourseHandler(database) + course_handler.name = course + for subject in subjects: + course_handler.add_subject(subject) + subject_handler = SubjectHandler(database) + subject_handler.name = subject + subject_handler.course = course + subject_handler.max_enrollment = 5 + subject_handler.activate() + course_handler.activate() + + def test_student_locked_by_minimun_subjects_per_semester(): pass @@ -31,46 +87,16 @@ def test_student_locks_course_and_forget_and_fail_by_maximum_semesters(): def test_student_failed_by_maximum_semesters(set_in_memory_database): - return course = "adm" grade = 9 situation = "failed" - subjects = [ - "mgmt", - "people1", - "people2", - "people3", - "strategy1", - "strategy2", - "strategy3", - "business1", - "business2", - "business3", - ] + subjects = __get_subjects() database = set_in_memory_database - course_handler = CourseHandler(database) - course_handler.name = course - for subject in subjects: - course_handler.add_subject(subject) - subject_handler = SubjectHandler(database) - subject_handler.name = subject - subject_handler.course = course - subject_handler.max_enrollment = 10 - subject_handler.activate() - course_handler.activate() - - student_handler = StudentHandler(database) - student_handler.name = "douglas" - student_handler.cpf = "098.765.432.12" - student_handler.enroll_to_course(course) - for subject in subjects: - student_handler.take_subject(subject) - student_handler.update_grade_to_subject(grade, subject) - - for i in range(MAX_SEMESTERS_TO_FINISH_COURSE + 1): - semester_monitor = SemesterMonitor(database, f"1234-{i+1}") - semester_monitor.close() + __add_all_subjects_to_course(course, subjects, database) + student_handler = __enroll_student_to_course(course, database) + __update_grade_of_3_subjects_only(grade, subjects, student_handler) + __close_maximum_semesters(database) assert student_handler.semester_counter == MAX_SEMESTERS_TO_FINISH_COURSE + 1 assert student_handler.gpa == grade @@ -86,42 +112,13 @@ def test_student_failed_by_maximum_semesters(set_in_memory_database): ) def test_student_finishes_course(set_in_memory_database, grade, situation): course = "adm" - subjects = [ - "mgmt", - "people1", - "people2", - "people3", - "strategy1", - "strategy2", - "strategy3", - "business1", - "business2", - "business3", - ] + subjects = __get_subjects() database = set_in_memory_database - course_handler = CourseHandler(database) - course_handler.name = course - for subject in subjects: - course_handler.add_subject(subject) - subject_handler = SubjectHandler(database) - subject_handler.name = subject - subject_handler.course = course - subject_handler.max_enrollment = 10 - subject_handler.activate() - course_handler.activate() - student_handler = StudentHandler(database) - student_handler.name = "douglas" - student_handler.cpf = "098.765.432.12" - student_handler.enroll_to_course(course) - - for subject in subjects: - student_handler.take_subject(subject) - student_handler.update_grade_to_subject(grade, subject) - - for i in range(MAX_SEMESTERS_TO_FINISH_COURSE + 1): - semester_monitor = SemesterMonitor(database, f"1234-{i+1}") - semester_monitor.close() + __add_all_subjects_to_course(course, subjects, database) + student_handler = __enroll_student_to_course(course, database) + __update_grade_of_all_subjects(grade, subjects, student_handler) + __close_maximum_semesters(database) assert student_handler.semester_counter == MAX_SEMESTERS_TO_FINISH_COURSE + 1 assert student_handler.gpa == grade From 4bcdb649c7129281dfcd4f38bbeab1ad48f458fe Mon Sep 17 00:00:00 2001 From: Douglas Cardoso Date: Thu, 18 Apr 2024 01:21:35 -0300 Subject: [PATCH 36/44] Small fix in tests --- tests/test_integration.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_integration.py b/tests/test_integration.py index bf8ccd9..d747678 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -90,9 +90,9 @@ def test_student_failed_by_maximum_semesters(set_in_memory_database): course = "adm" grade = 9 situation = "failed" - subjects = __get_subjects() database = set_in_memory_database + subjects = __get_subjects() __add_all_subjects_to_course(course, subjects, database) student_handler = __enroll_student_to_course(course, database) __update_grade_of_3_subjects_only(grade, subjects, student_handler) @@ -112,9 +112,9 @@ def test_student_failed_by_maximum_semesters(set_in_memory_database): ) def test_student_finishes_course(set_in_memory_database, grade, situation): course = "adm" - subjects = __get_subjects() database = set_in_memory_database + subjects = __get_subjects() __add_all_subjects_to_course(course, subjects, database) student_handler = __enroll_student_to_course(course, database) __update_grade_of_all_subjects(grade, subjects, student_handler) From 9e4bf83ee532e1eccb5973dcbd27d882f36d9f07 Mon Sep 17 00:00:00 2001 From: Douglas Cardoso Date: Thu, 18 Apr 2024 01:38:44 -0300 Subject: [PATCH 37/44] Add check to name and enrollment --- src/services/student_handler.py | 92 ++++++++++++++++++--------------- 1 file changed, 51 insertions(+), 41 deletions(-) diff --git a/src/services/student_handler.py b/src/services/student_handler.py index ad38569..5ba8a47 100644 --- a/src/services/student_handler.py +++ b/src/services/student_handler.py @@ -90,19 +90,44 @@ def course(self): @cpf.setter def cpf(self, value): - if not is_valide_cpf(value): - raise NonValidStudent(f"CPF {value} is not valid.") + self.__check_cpf(value) self.__cpf = value self.__generate_identifier_when_student_ready() - def __is_locked(self): - return self.__state == self.__LOCKED + def __check_cpf(self, value): + if not is_valide_cpf(value): + raise NonValidStudent(f"CPF {value} is not valid.") + + def __check_locked(self): + if self.__state == self.__LOCKED: + raise NonValidStudent(f"Student '{self.identifier}' is locked.") - def __is_enrolled_student(self, course_name): + def __check_enrolled_student(self, course_name): enrollment_validator = EnrollmentValidator(self.__database) - return enrollment_validator.validate_student_by_data( - self.name, self.cpf, course_name - ) or enrollment_validator.validate_student_by_identifier(self.identifier) + if not ( + enrollment_validator.validate_student_by_data( + self.name, self.cpf, course_name + ) + or enrollment_validator.validate_student_by_identifier(self.identifier) + ): + raise NonValidStudent( + f"Student '{self.identifier}' does not appears in enrollment list." + ) + + def __check_grade_range(self, grade): + if grade < 0 or grade > 10: + raise NonValidGrade("Grade must be between '0' and '10'.") + + def __return_subject_situation(self, grade): + if grade < 7: + return "failed" + return "passed" + + def __check_valid_subject(self, subject_identifier): + if not subject_identifier in self.subjects: + raise NonValidSubject( + f"The student '{self.identifier}' is not enrolled to this subject '{subject_identifier}'" + ) def __save(self): try: @@ -137,26 +162,21 @@ def calculate_gpa(self): def increment_semester(self): self.load_from_database(self.identifier) self.__semester_counter += 1 + self.__fail_course_if_exceed_max_semester() + self.__save() + + def __fail_course_if_exceed_max_semester(self): if self.__semester_counter > MAX_SEMESTERS_TO_FINISH_COURSE: self.__state = STUDENT_FAILED - self.__save() def update_grade_to_subject(self, grade, subject_name): - if grade < 0 or grade > 10: - raise NonValidGrade("Grade must be between '0' and '10'.") - + self.__check_grade_range(grade) self.load_from_database(self.identifier) - - if self.__is_locked(): - raise NonValidStudent(f"Student '{self.identifier}' is locked.") - + self.__check_locked() subject_identifier = utils.generate_subject_identifier( self.__course, subject_name ) - if not self.__is_valid_subject(subject_identifier): - raise NonValidSubject( - f"The student '{self.identifier}' is not enrolled to this subject '{subject_name}'" - ) + self.__check_valid_subject(subject_identifier) self.__subject_identifiers.append(subject_identifier) @@ -169,24 +189,13 @@ def update_grade_to_subject(self, grade, subject_name): grade_calculator.subject_situation = subject_situation grade_calculator.save() - def __return_subject_situation(self, grade): - if grade < 7: - return "failed" - return "passed" - - def __is_valid_subject(self, subject_identifier): - return subject_identifier in self.subjects - def enroll_to_course(self, course_name): try: - if not self.__name: - raise NonValidStudent("Need to set the student's name.") + self.__check_name() if not self.__cpf: raise NonValidStudent("Need to set the student's CPF.") - if not self.__is_enrolled_student(course_name): - raise NonValidStudent( - f"Student '{self.identifier}' does not appears in enrollment list." - ) + self.__check_enrolled_student(course_name) + if self.state == STUDENT_APPROVED or self.state == STUDENT_FAILED: raise NonValidStudent( f"Can not perform the operation. The student is '{self.state}' in course '{self.course}'" @@ -226,9 +235,12 @@ def enroll_to_course(self, course_name): logging.error(str(e)) raise + def __check_name(self): + if not self.__name: + raise NonValidStudent("Need to set the student's name.") + def take_subject(self, subject_name): - if not self.__is_enrolled_student(self.__course): - raise NonValidStudent(f"Student '{self.identifier}' is not valid.") + self.__check_enrolled_student(self.__course) if self.state == STUDENT_APPROVED or self.state == STUDENT_FAILED: raise NonValidStudent( @@ -238,7 +250,7 @@ def take_subject(self, subject_name): self.load_from_database(self.identifier) - if self.__is_locked(): + if self.__check_locked(): raise NonValidStudent(f"Student '{self.identifier}' is locked.") subject_identifier = utils.generate_subject_identifier( @@ -296,8 +308,7 @@ def take_subject(self, subject_name): return True def unlock_course(self): - if not self.__is_enrolled_student(self.__course): - raise NonValidStudent(f"Student is not not enrolled in any course.") + self.__check_enrolled_student(self.__course) if self.state == STUDENT_APPROVED or self.state == STUDENT_FAILED: raise NonValidStudent( f"Can not perform the operation. The student is '{self.state}' in course '{self.course}'" @@ -308,8 +319,7 @@ def unlock_course(self): return self.state def lock_course(self): - if not self.__is_enrolled_student(self.__course): - raise NonValidStudent(f"Student is not not enrolled in any course.") + self.__check_enrolled_student(self.__course) if self.state == STUDENT_APPROVED or self.state == STUDENT_FAILED: raise NonValidStudent( f"Can not perform the operation. The student is '{self.state}' in course '{self.course}'" From 81347dc2ffce4f538056366910b9cae5c6737a70 Mon Sep 17 00:00:00 2001 From: Douglas Cardoso Date: Thu, 18 Apr 2024 01:49:13 -0300 Subject: [PATCH 38/44] Organize StudentHander code --- src/services/student_handler.py | 206 ++++++++++++++++---------------- 1 file changed, 103 insertions(+), 103 deletions(-) diff --git a/src/services/student_handler.py b/src/services/student_handler.py index 5ba8a47..8a2b8c1 100644 --- a/src/services/student_handler.py +++ b/src/services/student_handler.py @@ -32,9 +32,8 @@ def __init__(self, database: Database, identifier=None): self.__semester_counter = 0 self.__subject_identifiers = [] self.__database = database - self.__identifier = None + self.__identifier = identifier if identifier: - self.__identifier = identifier self.load_from_database(identifier) def __generate_identifier_when_student_ready(self): @@ -43,61 +42,14 @@ def __generate_identifier_when_student_ready(self): self.name, self.cpf, self.__course ) - @property - def identifier(self): - return self.__identifier - - @property - def semester_counter(self): - self.load_from_database(self.identifier) - return self.__semester_counter - - @property - def state(self): - return self.__state - - @state.setter - def state(self, value): - self.load_from_database(self.identifier) - self.__state = value - self.__save() - - @property - def gpa(self): - self.calculate_gpa() - return self.__gpa - - @property - def subjects(self): - return self.__subject_identifiers - - @property - def name(self): - return self.__name - - @name.setter - def name(self, value): - self.__name = value - self.__generate_identifier_when_student_ready() - - @property - def cpf(self): - return self.__cpf - - @property - def course(self): - return self.__course - - @cpf.setter - def cpf(self, value): - self.__check_cpf(value) - self.__cpf = value - self.__generate_identifier_when_student_ready() - - def __check_cpf(self, value): + def __check_cpf_validity(self, value): if not is_valide_cpf(value): raise NonValidStudent(f"CPF {value} is not valid.") + def __check_cpf(self): + if not self.__cpf: + raise NonValidStudent("Need to set the student's CPF.") + def __check_locked(self): if self.__state == self.__LOCKED: raise NonValidStudent(f"Student '{self.identifier}' is locked.") @@ -118,6 +70,28 @@ def __check_grade_range(self, grade): if grade < 0 or grade > 10: raise NonValidGrade("Grade must be between '0' and '10'.") + def __remove_dummy_subject(self, grade_calculator): + if grade_calculator.search(self.identifier, DUMMY_IDENTIFIER): + grade_calculator.remove(self.identifier, DUMMY_IDENTIFIER) + + def __check_subject_activation(self, subject_handler): + if not subject_handler.is_active(): + raise NonValidSubject( + f"Subject '{subject_handler.identifier}' is not active." + ) + + def __check_subject_availability(self, subject_handler): + if not subject_handler.is_available(): + raise NonValidSubject( + f"Subject '{subject_handler.identifier}' is not available." + ) + + def __check_course_is_same_of_subject(self, subject_handler): + if subject_handler.course != self.__course: + raise NonValidSubject( + f"The subject '{subject_handler.identifier}' is not part of course '{self.__course}'." + ) + def __return_subject_situation(self, grade): if grade < 7: return "failed" @@ -129,6 +103,20 @@ def __check_valid_subject(self, subject_identifier): f"The student '{self.identifier}' is not enrolled to this subject '{subject_identifier}'" ) + def __check_finished_course(self): + if self.state == STUDENT_APPROVED or self.state == STUDENT_FAILED: + raise NonValidStudent( + f"Can not perform the operation. The student is '{self.state}' in course '{self.course}'" + ) + + def __check_name(self): + if not self.__name: + raise NonValidStudent("Need to set the student's name.") + + def __fail_course_if_exceed_max_semester(self): + if self.__semester_counter > MAX_SEMESTERS_TO_FINISH_COURSE: + self.__state = STUDENT_FAILED + def __save(self): try: self.__database.student.name = self.name @@ -146,6 +134,57 @@ def __save(self): logging.error(str(e)) raise + @property + def identifier(self): + return self.__identifier + + @property + def semester_counter(self): + self.load_from_database(self.identifier) + return self.__semester_counter + + @property + def state(self): + return self.__state + + @state.setter + def state(self, value): + self.load_from_database(self.identifier) + self.__state = value + self.__save() + + @property + def gpa(self): + self.calculate_gpa() + return self.__gpa + + @property + def subjects(self): + return self.__subject_identifiers + + @property + def name(self): + return self.__name + + @name.setter + def name(self, value): + self.__name = value + self.__generate_identifier_when_student_ready() + + @property + def cpf(self): + return self.__cpf + + @property + def course(self): + return self.__course + + @cpf.setter + def cpf(self, value): + self.__check_cpf_validity(value) + self.__cpf = value + self.__generate_identifier_when_student_ready() + def calculate_gpa(self): try: self.__gpa = GradeCalculator(self.__database).calculate_gpa_for_student( @@ -165,10 +204,6 @@ def increment_semester(self): self.__fail_course_if_exceed_max_semester() self.__save() - def __fail_course_if_exceed_max_semester(self): - if self.__semester_counter > MAX_SEMESTERS_TO_FINISH_COURSE: - self.__state = STUDENT_FAILED - def update_grade_to_subject(self, grade, subject_name): self.__check_grade_range(grade) self.load_from_database(self.identifier) @@ -192,14 +227,9 @@ def update_grade_to_subject(self, grade, subject_name): def enroll_to_course(self, course_name): try: self.__check_name() - if not self.__cpf: - raise NonValidStudent("Need to set the student's CPF.") + self.__check_cpf() self.__check_enrolled_student(course_name) - - if self.state == STUDENT_APPROVED or self.state == STUDENT_FAILED: - raise NonValidStudent( - f"Can not perform the operation. The student is '{self.state}' in course '{self.course}'" - ) + self.__check_finished_course() course = CourseHandler(self.__database) course.load_from_database(course_name) @@ -235,23 +265,11 @@ def enroll_to_course(self, course_name): logging.error(str(e)) raise - def __check_name(self): - if not self.__name: - raise NonValidStudent("Need to set the student's name.") - def take_subject(self, subject_name): self.__check_enrolled_student(self.__course) - - if self.state == STUDENT_APPROVED or self.state == STUDENT_FAILED: - raise NonValidStudent( - f"Can not perform the operation." - f" The student is '{self.state}' in course '{self.course}'" - ) - + self.__check_finished_course() self.load_from_database(self.identifier) - - if self.__check_locked(): - raise NonValidStudent(f"Student '{self.identifier}' is locked.") + self.__check_locked() subject_identifier = utils.generate_subject_identifier( self.__course, subject_name @@ -266,20 +284,9 @@ def take_subject(self, subject_name): logging.error(str(e)) raise - if subject_handler.course != self.__course: - raise NonValidSubject( - f"The subject '{subject_handler.identifier}' is not part of course '{self.__course}'." - ) - - if not subject_handler.is_available(): - raise NonValidSubject( - f"Subject '{subject_handler.identifier}' is not available." - ) - - if not subject_handler.is_active(): - raise NonValidSubject( - f"Subject '{subject_handler.identifier}' is not active." - ) + self.__check_course_is_same_of_subject(subject_handler) + self.__check_subject_availability(subject_handler) + self.__check_subject_activation(subject_handler) self.__subject_identifiers.append(subject_identifier) self.__save() @@ -288,8 +295,7 @@ def take_subject(self, subject_name): subject_handler.save() grade_calculator = GradeCalculator(self.__database) - if grade_calculator.search(self.identifier, DUMMY_IDENTIFIER): - grade_calculator.remove(self.identifier, DUMMY_IDENTIFIER) + self.__remove_dummy_subject(grade_calculator) grade_calculator.subject_situation = SUBJECT_IN_PROGRESS grade_calculator.add(self.identifier, subject_identifier, grade=0) @@ -309,10 +315,7 @@ def take_subject(self, subject_name): def unlock_course(self): self.__check_enrolled_student(self.__course) - if self.state == STUDENT_APPROVED or self.state == STUDENT_FAILED: - raise NonValidStudent( - f"Can not perform the operation. The student is '{self.state}' in course '{self.course}'" - ) + self.__check_finished_course() self.load_from_database(self.identifier) self.__state = self.__ENROLLED self.__save() @@ -320,10 +323,7 @@ def unlock_course(self): def lock_course(self): self.__check_enrolled_student(self.__course) - if self.state == STUDENT_APPROVED or self.state == STUDENT_FAILED: - raise NonValidStudent( - f"Can not perform the operation. The student is '{self.state}' in course '{self.course}'" - ) + self.__check_finished_course() self.load_from_database(self.identifier) self.__state = self.__LOCKED self.__save() From d3cc5ef0be3a76a15e28ed1ca20838c2ea5d9d80 Mon Sep 17 00:00:00 2001 From: Douglas Cardoso Date: Thu, 18 Apr 2024 17:08:35 -0300 Subject: [PATCH 39/44] Added tests to check the relation of student and cancelled course --- architecture.odp | Bin 25211 -> 24262 bytes src/services/course_handler.py | 74 +++++++++++++++------------ src/services/enrollment_validator.py | 5 ++ src/services/semester_monitor.py | 34 +++++++----- src/services/student_handler.py | 3 ++ tests/test_course.py | 3 ++ tests/test_integration.py | 73 ++++++++++++++++++++++++-- tests/test_models.py | 2 - 8 files changed, 140 insertions(+), 54 deletions(-) diff --git a/architecture.odp b/architecture.odp index 9799c925953de65f548b55197e26d7a6bc7d794f..8cd3c01f2d7e8f9abf9b18ec8bb1b8a85fc9995f 100644 GIT binary patch delta 17772 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 NlOvL3*;=DOq5v{mq0s;U delta 18766 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
XJZm+YwkuxMK00ES{#n$eq6%4E^lw|@A>EZ z`#!(t_xU`}^XKz@z7)X0bv@GfN5X|aL!1OH?(e2uJ}Pvp!Cb;{bIm3`cMS%2qH&5v zk)pS{5Hony11bW30z@DToJaDPrRG)2m1|-aBC(qkcqBP8v&#V_enuLh9k}_b5dV0* zb^ya_sKmmy^u0lno&7?D>7Kn4_V}Y3(cvrM8Ud043+u_c40 zbmCkvZ?V)^pww#igmr^oW_mc3bl_aMeumaKO*y^}ZvCr=f9)l)qRv_!{#n>RAo zVM627jYB1r*7ye%Gp&fk^9j0?ogti3Gxu3%qa_=Gjc`Z3w(EP*qP^)zg#6M(Lv50I zx)Om#tFG8%n?1<&@d!p|tKkM+HP!EG&OdY}0P~pf>FO4Vw9#giZoJ{beVx$f!ILdH z%J`dYQAN~=5|t~vTQ{XvXP%5a9bgB$xyZ8)bfADI=eFltFJbYHn$*seHpuiJ&|>n2 zULH8xa!brwGGcAwBSC+&Jy$YQWvBBp_t7Wyl_^hH427f4vK!5k?VVnZUD|)d5S(zO z-_(uh7_{cH-~yH8UE+Q^K zRYP;kNCTN}6U$mS;5{Q?0O@)9X-_E@_2K&_ySuE>Y8@e}ZN$A9E=tlwltI{rHjB+q zJ&!sOeA1bW%Lc(sDz|0$3tbrIf?A~F-0h%Qqu*jnpjp-6Vwmoov#Qi3%}=uK(I|#2 z$}tI#6)vA$k#U4eP~K||-Aj9>9EYEgQ+G3N4a1n*7M0A#itjKW%y34iH3f#=01A^- z0T)L<5xaG+(%jQ#6ih~{Bh_ZjjLJn8=#lKJXw^qun~x8wFpV16TWl4JZ6~RPQ z1$F60!jjljIWEZ0P`ki^vVGhy1Fui#(2ZhRL6 z5*hN>3=`=o*e--tM)$e@{?N+g^i&V0iKgmrM8N1}3$l`VIkqM8)Vi?M3fe73vHwnM zc#|uV7WF2*BFu>2t1>1e? zc`f`yZDSRs<@ZttxbX*~*IA6~OVSZmiKB>j%kIOqEj+a(?*kh#XZGLoOE)hqKOaGM zJ09s))OpCfG*w!*x;sZjS?D^}%!V!(B_foj_P3*up# z#llfEqL=XQ`2c&9x9geF@pNax?ua>sSB475o{_Mf6oh97uVRR}mUb}**wi=i(od30 zu~J{x+WEJD{i?3D?_WuzGL;u>NL7(av_r8WHL4wK2tvauyka(%=IDIOVha&g}O>H4Yw4+Y>f)%#5`dmoc)lV;|9cNc@I!^CYW6;8PKsttrQ0z8J z#_42AAhp4*tmrogsNXjLmsWoW_i-!S0!2mMs_eJ3i|k@UxIO7jtBP7b8E(z@Z}Yq z%LfyM8OHY|R6PrAVOF9IZ+41WaHZP#g9il`v8l0a9@pZzHms)~`$YH7z+n8!8fuC| zS)Q}xh84}cKfz2ewFjK0Khsh+h&`!sBidt@TYj6L8pKX=(Y^|G>b;jgW%dW^?%)V$ zb&@|+IhP{ij1>gz^QthxKA)EcKkRuZh3E<_pJ^*R6T%{et11w%@s{o?p2q zp15ywT68@x_!sQrWwKH3_~Z87J%P^fGJ<_({^yzikpIj6G3x0OFMZd~H9yL`<&H%^ z#0@wAaK@kPgNQy8`rnOxlYYA$^Vdz*m5ah#fdIg^AJzZ>*Aetnggw@uTmN`6>;JE4 g1qlEU!$@TL3)a`}Ai3)U005SceR7$(IpK5mAK1B#82|tP diff --git a/src/services/course_handler.py b/src/services/course_handler.py index c29769f..f5b2bc4 100644 --- a/src/services/course_handler.py +++ b/src/services/course_handler.py @@ -8,11 +8,10 @@ class CourseHandler: - ACTIVE = "active" - INACTIVE = "inactive" - CANCELLED = "cancelled" - def __init__(self, database: Database) -> None: + self.ACTIVE = "active" + self.INACTIVE = "inactive" + self.CANCELLED = "cancelled" self.__identifier = DUMMY_IDENTIFIER self.__name = None self.__state = self.INACTIVE # TODO use enum @@ -21,6 +20,38 @@ def __init__(self, database: Database) -> None: self.__max_enrollment = 0 self.__database = database + def __check_name_lenght(self, value): + if not value or len(value) > 10: + raise NonValidCourse( + f"The maximum number of characters to course's " f"name is '10'." + ) + + def __check_name(self): + if not self.name: + raise NonValidCourse("Need to set the name.") + + def __check_active(self): + if not self.state == self.ACTIVE: + raise NonValidCourse(f"Course '{self.name}' is not active.") + + def __check_cancelled(self): + if self.state == self.CANCELLED: + raise NonValidCourse(f"Course '{self.name}' is cancelled.") + + def __check_minimum_number_of_subjects(self): + MINIMUM = 3 + if not len(self.subjects) >= MINIMUM: + raise NonMinimunSubjects( + f"Need '{MINIMUM}' subjects. Set '{len(self.subjects)}'" + ) + + def __check_maximum_enrollment(self): + if len(self.__database.course.subjects) > self.max_enrollment: + raise NonValidCourse( + f"Exceeded the maximum number of subjects." + f" Expected '{self.max_enrollment}. Set '{len(self.subjects)}'." + ) + @property def identifier(self): return self.__identifier @@ -48,35 +79,6 @@ def name(self, value): self.__identifier = utils.generate_course_identifier(value) self.__name = value - def __check_name_lenght(self, value): - if len(value) > 10: - raise NonValidCourse( - f"The maximum number of characters to course's " - "name is '10'. Set with '{len(value)}'." - ) - - def __check_name(self): - if not self.name: - raise NonValidCourse("Need to set the name.") - - def __check_active(self): - if not self.state == self.ACTIVE: - raise NonValidCourse(f"Course '{self.name}' is not active.") - - def __check_minimum_number_of_subjects(self): - MINIMUM = 3 - if not len(self.subjects) >= MINIMUM: - raise NonMinimunSubjects( - f"Need '{MINIMUM}' subjects. Set '{len(self.subjects)}'" - ) - - def __check_maximum_enrollment(self): - if len(self.__database.course.subjects) > self.max_enrollment: - raise NonValidCourse( - f"Exceeded the maximum number of subjects." - f" Expected '{self.max_enrollment}. Set '{len(self.subjects)}'." - ) - @property def max_enrollment(self): return self.__max_enrollment @@ -85,6 +87,10 @@ def max_enrollment(self): def max_enrollment(self, value): self.__max_enrollment = value + def is_active(self): + self.load_from_database(self.name) + return self.state == self.ACTIVE + def save(self): self.__database.course.name = self.name self.__database.course.state = self.state @@ -143,6 +149,7 @@ def list_all_courses_with_details(self): def enroll_student(self, student_identifier): self.__check_active() + self.load_from_database(self.name) self.__enrolled_students.append(student_identifier) self.save() return True @@ -150,6 +157,7 @@ def enroll_student(self, student_identifier): def add_subject(self, subject): self.__check_name() self.load_from_database(self.name) + self.__check_cancelled() subject_identifier = utils.generate_subject_identifier(self.name, subject) self.__subjects.append(subject_identifier) self.save() diff --git a/src/services/enrollment_validator.py b/src/services/enrollment_validator.py index b0bd064..aca7b40 100644 --- a/src/services/enrollment_validator.py +++ b/src/services/enrollment_validator.py @@ -1,4 +1,5 @@ from src import utils +from src.services.course_handler import CourseHandler, NonValidCourse class EnrollmentValidator: @@ -6,6 +7,10 @@ def __init__(self, database): self.__database = database def validate_student_by_data(self, name, cpf, course_name): + courser_handler = CourseHandler(self.__database) + courser_handler.name = course_name + if not courser_handler.is_active(): + raise NonValidCourse(f"The course '{course_name}' is not active.") # the valid students are predefined as the list of approved person in the given course student_identifier = utils.generate_student_identifier(name, cpf, course_name) return self.validate_student_by_identifier(student_identifier) diff --git a/src/services/semester_monitor.py b/src/services/semester_monitor.py index 8ca6baa..cd93222 100644 --- a/src/services/semester_monitor.py +++ b/src/services/semester_monitor.py @@ -1,8 +1,7 @@ import logging from src.database import Database, NotFoundError -from src.services.student_handler import StudentHandler +from src.services.student_handler import StudentHandler, NonValidStudent from src.services.course_handler import CourseHandler -from src.services.grade_calculator import GradeCalculator from src.constants import ( STUDENT_APPROVED, STUDENT_FAILED, @@ -105,18 +104,25 @@ def close(self): student_rows = student_handler.search_all() for row in student_rows: - student_handler = StudentHandler(self.__database, row.identifier) - student_handler.calculate_gpa() - student_handler.increment_semester() - - course_handler = CourseHandler(self.__database) - course_handler.load_from_database(student_handler.course) - course_handler.name = student_handler.course - if self.__is_course_completed(student_handler, course_handler): - if self.__is_approved(student_handler.identifier): - student_handler.state = STUDENT_APPROVED - else: - student_handler.state = STUDENT_FAILED + try: + student_handler = StudentHandler(self.__database, row.identifier) + student_handler.increment_semester() + student_handler.calculate_gpa() + + course_handler = CourseHandler(self.__database) + course_handler.load_from_database(student_handler.course) + course_handler.name = student_handler.course + if self.__is_course_completed(student_handler, course_handler): + if self.__is_approved(student_handler.identifier): + student_handler.state = STUDENT_APPROVED + else: + student_handler.state = STUDENT_FAILED + except NonValidStudent as e: + logging.error(str(e)) + continue + except Exception as e: + logging.error(str(e)) + raise return self.__state diff --git a/src/services/student_handler.py b/src/services/student_handler.py index 8a2b8c1..613a9eb 100644 --- a/src/services/student_handler.py +++ b/src/services/student_handler.py @@ -186,6 +186,9 @@ def cpf(self, value): self.__generate_identifier_when_student_ready() def calculate_gpa(self): + self.__check_enrolled_student(self.course) + self.load_from_database(self.identifier) + self.__check_locked() try: self.__gpa = GradeCalculator(self.__database).calculate_gpa_for_student( self.identifier diff --git a/tests/test_course.py b/tests/test_course.py index b875568..33f2869 100644 --- a/tests/test_course.py +++ b/tests/test_course.py @@ -15,6 +15,9 @@ def test_add_subject_to_new_course(set_in_memory_database): database = set_in_memory_database course_handler = CourseHandler(database) course_handler.create(course, max_enrollment) + for i in range(3): + course_handler.add_subject(f"subject{i}") + course_handler.activate() assert course_handler.add_subject(subject) is True # post conditions diff --git a/tests/test_integration.py b/tests/test_integration.py index d747678..8b2abb8 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -1,6 +1,6 @@ import pytest from src.services.student_handler import StudentHandler -from src.services.course_handler import CourseHandler +from src.services.course_handler import CourseHandler, NonValidCourse from src.services.subject_handler import SubjectHandler from src.services.semester_monitor import SemesterMonitor from src.constants import MAX_SEMESTERS_TO_FINISH_COURSE @@ -60,6 +60,7 @@ def __add_all_subjects_to_course(course, subjects, database): subject_handler.max_enrollment = 5 subject_handler.activate() course_handler.activate() + return course_handler def test_student_locked_by_minimun_subjects_per_semester(): @@ -78,12 +79,74 @@ def test_coordinator_cancel_course_before_studend_conclude_it_not_affecting_grad pass -def test_coordinator_cancel_course_before_studend_conclude_it_not_affecting_student_situation(): - pass +def test_coordinator_cancel_course_before_studend_conclude_it_not_affecting_student_situation( + set_in_memory_database, +): + course = "adm" + grade = 9 + situation = "approved" + database = set_in_memory_database + subjects = __get_subjects() + course_handler = __add_all_subjects_to_course(course, subjects, database) + student_handler = __enroll_student_to_course(course, database) + __add_all_subjects_to_course(course, subjects, database) + __update_grade_of_all_subjects(grade, subjects, student_handler) + course_handler.cancel() + course_handler.activate() + __close_maximum_semesters(database) -def test_student_locks_course_and_forget_and_fail_by_maximum_semesters(): - pass + assert student_handler.semester_counter == MAX_SEMESTERS_TO_FINISH_COURSE + 1 + assert student_handler.gpa == grade + assert student_handler.state == situation + + +def test_coordinator_cancel_course_and_not_allow_any_student_operation( + set_in_memory_database, +): + course = "adm" + grade = 0 + situation = "failed" + database = set_in_memory_database + + subjects = __get_subjects() + course_handler = __add_all_subjects_to_course(course, subjects, database) + student_handler = __enroll_student_to_course(course, database) + course_handler.cancel() + with pytest.raises(NonValidCourse): + __update_grade_of_3_subjects_only(grade, subjects, student_handler) + with pytest.raises(NonValidCourse): + student_handler.lock_course() + with pytest.raises(NonValidCourse): + student_handler.unlock_course() + with pytest.raises(NonValidCourse): + student_handler.calculate_gpa() + course_handler.activate() + __close_maximum_semesters(database) + + assert student_handler.semester_counter == MAX_SEMESTERS_TO_FINISH_COURSE + 1 + assert student_handler.gpa == grade + assert student_handler.state == situation + + +def test_student_locks_course_and_forget_and_fail_by_maximum_semesters( + set_in_memory_database, +): + course = "adm" + grade = 9 + situation = "failed" + database = set_in_memory_database + + subjects = __get_subjects() + __add_all_subjects_to_course(course, subjects, database) + student_handler = __enroll_student_to_course(course, database) + __update_grade_of_3_subjects_only(grade, subjects, student_handler) + student_handler.lock_course() + __close_maximum_semesters(database) + + assert student_handler.semester_counter == MAX_SEMESTERS_TO_FINISH_COURSE + 1 + assert student_handler.gpa == grade + assert student_handler.state == situation def test_student_failed_by_maximum_semesters(set_in_memory_database): diff --git a/tests/test_models.py b/tests/test_models.py index 25c4f57..b0427c3 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -59,6 +59,4 @@ def test_student_model(set_in_memory_database): assert student.cpf == "123.456.789-10" assert student.identifier is None assert student.state == None - with pytest.raises(NonValidGrade): - assert student.gpa == 0 assert student.subjects == [] From f13ca8fd8bea3efb042ccc974748e0ad965ef879 Mon Sep 17 00:00:00 2001 From: Douglas Cardoso Date: Thu, 18 Apr 2024 18:26:29 -0300 Subject: [PATCH 40/44] Add test 'test_student_cannot_do_any_operation_after_pass_or_fail_course' --- src/services/semester_monitor.py | 2 +- src/services/student_handler.py | 115 +++++++++++++++++-------------- tests/test_integration.py | 41 ++++++++--- tests/test_models.py | 20 +++--- tests/test_student.py | 12 ++-- 5 files changed, 113 insertions(+), 77 deletions(-) diff --git a/src/services/semester_monitor.py b/src/services/semester_monitor.py index cd93222..03d1ae1 100644 --- a/src/services/semester_monitor.py +++ b/src/services/semester_monitor.py @@ -107,7 +107,7 @@ def close(self): try: student_handler = StudentHandler(self.__database, row.identifier) student_handler.increment_semester() - student_handler.calculate_gpa() + student_handler.gpa course_handler = CourseHandler(self.__database) course_handler.load_from_database(student_handler.course) diff --git a/src/services/student_handler.py b/src/services/student_handler.py index 613a9eb..7189ed9 100644 --- a/src/services/student_handler.py +++ b/src/services/student_handler.py @@ -30,16 +30,16 @@ def __init__(self, database: Database, identifier=None): self.__name = None self.__cpf = None self.__semester_counter = 0 - self.__subject_identifiers = [] + self.__subjects = [] self.__database = database self.__identifier = identifier if identifier: self.load_from_database(identifier) def __generate_identifier_when_student_ready(self): - if self.name and self.cpf and self.__course: + if self.__name and self.__cpf and self.__course: self.__identifier = utils.generate_student_identifier( - self.name, self.cpf, self.__course + self.__name, self.__cpf, self.__course ) def __check_cpf_validity(self, value): @@ -52,18 +52,18 @@ def __check_cpf(self): def __check_locked(self): if self.__state == self.__LOCKED: - raise NonValidStudent(f"Student '{self.identifier}' is locked.") + raise NonValidStudent(f"Student '{self.__identifier}' is locked.") def __check_enrolled_student(self, course_name): enrollment_validator = EnrollmentValidator(self.__database) if not ( enrollment_validator.validate_student_by_data( - self.name, self.cpf, course_name + self.__name, self.__cpf, course_name ) - or enrollment_validator.validate_student_by_identifier(self.identifier) + or enrollment_validator.validate_student_by_identifier(self.__identifier) ): raise NonValidStudent( - f"Student '{self.identifier}' does not appears in enrollment list." + f"Student '{self.__identifier}' does not appears in enrollment list." ) def __check_grade_range(self, grade): @@ -71,8 +71,8 @@ def __check_grade_range(self, grade): raise NonValidGrade("Grade must be between '0' and '10'.") def __remove_dummy_subject(self, grade_calculator): - if grade_calculator.search(self.identifier, DUMMY_IDENTIFIER): - grade_calculator.remove(self.identifier, DUMMY_IDENTIFIER) + if grade_calculator.search(self.__identifier, DUMMY_IDENTIFIER): + grade_calculator.remove(self.__identifier, DUMMY_IDENTIFIER) def __check_subject_activation(self, subject_handler): if not subject_handler.is_active(): @@ -100,13 +100,13 @@ def __return_subject_situation(self, grade): def __check_valid_subject(self, subject_identifier): if not subject_identifier in self.subjects: raise NonValidSubject( - f"The student '{self.identifier}' is not enrolled to this subject '{subject_identifier}'" + f"The student '{self.__identifier}' is not enrolled to this subject '{subject_identifier}'" ) def __check_finished_course(self): - if self.state == STUDENT_APPROVED or self.state == STUDENT_FAILED: + if self.__state == STUDENT_APPROVED or self.__state == STUDENT_FAILED: raise NonValidStudent( - f"Can not perform the operation. The student is '{self.state}' in course '{self.course}'" + f"Can not perform the operation. The student is '{self.__state}' in course '{self.__course}'" ) def __check_name(self): @@ -118,15 +118,17 @@ def __fail_course_if_exceed_max_semester(self): self.__state = STUDENT_FAILED def __save(self): + if not self.__identifier: + return try: - self.__database.student.name = self.name - self.__database.student.state = self.state - self.__database.student.cpf = self.cpf - self.__database.student.identifier = self.identifier + self.__database.student.name = self.__name + self.__database.student.state = self.__state + self.__database.student.cpf = self.__cpf + self.__database.student.identifier = self.__identifier self.__database.student.gpa = GradeCalculator( self.__database - ).calculate_gpa_for_student(self.identifier) - self.__database.student.subjects.extend(self.subjects) + ).calculate_gpa_for_student(self.__identifier) + self.__database.student.subjects.extend(self.__subjects) self.__database.student.course = self.__course self.__database.student.semester_counter = self.__semester_counter self.__database.student.save() @@ -140,43 +142,49 @@ def identifier(self): @property def semester_counter(self): - self.load_from_database(self.identifier) + self.load_from_database(self.__identifier) return self.__semester_counter @property def state(self): + self.load_from_database(self.__identifier) return self.__state @state.setter def state(self, value): - self.load_from_database(self.identifier) + self.load_from_database(self.__identifier) self.__state = value self.__save() @property def gpa(self): - self.calculate_gpa() + self.__calculate_gpa() return self.__gpa @property def subjects(self): - return self.__subject_identifiers + self.load_from_database(self.__identifier) + return self.__subjects @property def name(self): + self.load_from_database(self.__identifier) return self.__name @name.setter def name(self, value): self.__name = value self.__generate_identifier_when_student_ready() + self.__save() @property def cpf(self): + self.load_from_database(self.__identifier) return self.__cpf @property def course(self): + self.load_from_database(self.__identifier) return self.__course @cpf.setter @@ -184,42 +192,45 @@ def cpf(self, value): self.__check_cpf_validity(value) self.__cpf = value self.__generate_identifier_when_student_ready() + self.__save() - def calculate_gpa(self): + def __calculate_gpa(self): self.__check_enrolled_student(self.course) - self.load_from_database(self.identifier) + self.load_from_database(self.__identifier) self.__check_locked() try: self.__gpa = GradeCalculator(self.__database).calculate_gpa_for_student( - self.identifier + self.__identifier ) except NonValidGradeOperation as e: raise NonValidGrade( - f"Student '{self.identifier}' may not be enrolled to any subject." + f"Student '{self.__identifier}' may not be enrolled to any subject." ) except Exception as e: logging.error(str(e)) raise def increment_semester(self): - self.load_from_database(self.identifier) + self.load_from_database(self.__identifier) + self.__check_finished_course() self.__semester_counter += 1 self.__fail_course_if_exceed_max_semester() self.__save() def update_grade_to_subject(self, grade, subject_name): self.__check_grade_range(grade) - self.load_from_database(self.identifier) + self.__check_finished_course() + self.load_from_database(self.__identifier) self.__check_locked() subject_identifier = utils.generate_subject_identifier( self.__course, subject_name ) self.__check_valid_subject(subject_identifier) - self.__subject_identifiers.append(subject_identifier) + self.__subjects.append(subject_identifier) grade_calculator = GradeCalculator(self.__database) - grade_calculator.student_identifier = self.identifier + grade_calculator.student_identifier = self.__identifier grade_calculator.subject_identifier = subject_identifier grade_calculator.grade = grade @@ -238,32 +249,32 @@ def enroll_to_course(self, course_name): course.load_from_database(course_name) self.__course = course_name self.__generate_identifier_when_student_ready() - course.enroll_student(self.identifier) + course.enroll_student(self.__identifier) self.__state = self.__ENROLLED - self.__database.student.name = self.name - self.__database.student.state = self.state - self.__database.student.cpf = self.cpf - self.__database.student.identifier = self.identifier + self.__database.student.name = self.__name + self.__database.student.state = self.__state + self.__database.student.cpf = self.__cpf + self.__database.student.identifier = self.__identifier self.__database.student.gpa = 0 - self.__database.student.subjects.extend(self.subjects) + self.__database.student.subjects.extend(self.__subjects) self.__database.student.course = self.__course self.__database.student.semester_counter = self.__semester_counter self.__database.student.add() grade_calculator = GradeCalculator(self.__database) - grade_calculator.add(self.identifier, DUMMY_IDENTIFIER, grade=0) + grade_calculator.add(self.__identifier, DUMMY_IDENTIFIER, grade=0) # post condition - self.__database.student.load(self.identifier) + self.__database.student.load(self.__identifier) - assert self.__database.student.identifier == self.identifier + assert self.__database.student.identifier == self.__identifier assert self.__database.student.state == self.__ENROLLED assert self.__database.student.course == self.__course assert self.__database.student.gpa == 0 - assert self.identifier in course.enrolled_students + assert self.__identifier in course.enrolled_students - return self.identifier + return self.__identifier except Exception as e: logging.error(str(e)) raise @@ -271,7 +282,7 @@ def enroll_to_course(self, course_name): def take_subject(self, subject_name): self.__check_enrolled_student(self.__course) self.__check_finished_course() - self.load_from_database(self.identifier) + self.load_from_database(self.__identifier) self.__check_locked() subject_identifier = utils.generate_subject_identifier( @@ -291,26 +302,26 @@ def take_subject(self, subject_name): self.__check_subject_availability(subject_handler) self.__check_subject_activation(subject_handler) - self.__subject_identifiers.append(subject_identifier) + self.__subjects.append(subject_identifier) self.__save() - subject_handler.enrolled_students.append(self.identifier) + subject_handler.enrolled_students.append(self.__identifier) subject_handler.save() grade_calculator = GradeCalculator(self.__database) self.__remove_dummy_subject(grade_calculator) grade_calculator.subject_situation = SUBJECT_IN_PROGRESS - grade_calculator.add(self.identifier, subject_identifier, grade=0) + grade_calculator.add(self.__identifier, subject_identifier, grade=0) # post condition subject_handler.load_from_database(subject_identifier) - assert self.identifier in subject_handler.enrolled_students + assert self.__identifier in subject_handler.enrolled_students - self.load_from_database(self.identifier) + self.load_from_database(self.__identifier) assert subject_identifier in self.subjects - grade_calculator.load_from_database(self.identifier, subject_identifier) - assert self.identifier in grade_calculator.student_identifier + grade_calculator.load_from_database(self.__identifier, subject_identifier) + assert self.__identifier in grade_calculator.student_identifier assert subject_identifier in grade_calculator.subject_identifier assert grade_calculator.grade == 0 @@ -319,7 +330,7 @@ def take_subject(self, subject_name): def unlock_course(self): self.__check_enrolled_student(self.__course) self.__check_finished_course() - self.load_from_database(self.identifier) + self.load_from_database(self.__identifier) self.__state = self.__ENROLLED self.__save() return self.state @@ -327,7 +338,7 @@ def unlock_course(self): def lock_course(self): self.__check_enrolled_student(self.__course) self.__check_finished_course() - self.load_from_database(self.identifier) + self.load_from_database(self.__identifier) self.__state = self.__LOCKED self.__save() return self.state @@ -344,7 +355,7 @@ def load_from_database(self, student_identifier): self.__cpf = self.__database.student.cpf self.__identifier = self.__database.student.identifier self.__gpa = self.__database.student.gpa - self.__subject_identifiers.extend(self.__database.student.subjects) + self.__subjects.extend(self.__database.student.subjects) self.__course = self.__database.student.course self.__semester_counter = self.__database.student.semester_counter diff --git a/tests/test_integration.py b/tests/test_integration.py index 8b2abb8..9f41e75 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -1,5 +1,5 @@ import pytest -from src.services.student_handler import StudentHandler +from src.services.student_handler import StudentHandler, NonValidStudent from src.services.course_handler import CourseHandler, NonValidCourse from src.services.subject_handler import SubjectHandler from src.services.semester_monitor import SemesterMonitor @@ -71,15 +71,37 @@ def test_student_can_enroll_to_course_after_fail_it_losing_all_history(): pass -def test_student_cannot_do_anythin_after_pass_or_fail_course(): - pass +def test_student_cannot_do_any_operation_after_pass_or_fail_course( + set_in_memory_database, +): + course = "adm" + grade = 9 + situation = "approved" + database = set_in_memory_database + + subjects = __get_subjects() + __add_all_subjects_to_course(course, subjects, database) + student_handler = __enroll_student_to_course(course, database) + __update_grade_of_all_subjects(grade, subjects, student_handler) + __close_maximum_semesters(database) + assert student_handler.state == situation -def test_coordinator_cancel_course_before_studend_conclude_it_not_affecting_grades(): - pass + with pytest.raises(NonValidStudent): + student_handler.lock_course() + with pytest.raises(NonValidStudent): + student_handler.unlock_course() + with pytest.raises(NonValidStudent): + student_handler.enroll_to_course(course) + with pytest.raises(NonValidStudent): + student_handler.increment_semester() + with pytest.raises(NonValidStudent): + student_handler.take_subject(subjects[0]) + with pytest.raises(NonValidStudent): + student_handler.update_grade_to_subject(1, subjects[0]) -def test_coordinator_cancel_course_before_studend_conclude_it_not_affecting_student_situation( +def test_coordinator_cancel_course_before_studend_conclude_it_not_affecting_student_situation_or_grades( set_in_memory_database, ): course = "adm" @@ -90,7 +112,6 @@ def test_coordinator_cancel_course_before_studend_conclude_it_not_affecting_stud subjects = __get_subjects() course_handler = __add_all_subjects_to_course(course, subjects, database) student_handler = __enroll_student_to_course(course, database) - __add_all_subjects_to_course(course, subjects, database) __update_grade_of_all_subjects(grade, subjects, student_handler) course_handler.cancel() course_handler.activate() @@ -120,7 +141,7 @@ def test_coordinator_cancel_course_and_not_allow_any_student_operation( with pytest.raises(NonValidCourse): student_handler.unlock_course() with pytest.raises(NonValidCourse): - student_handler.calculate_gpa() + student_handler.gpa course_handler.activate() __close_maximum_semesters(database) @@ -181,8 +202,10 @@ def test_student_finishes_course(set_in_memory_database, grade, situation): __add_all_subjects_to_course(course, subjects, database) student_handler = __enroll_student_to_course(course, database) __update_grade_of_all_subjects(grade, subjects, student_handler) + + assert student_handler.gpa == grade + __close_maximum_semesters(database) assert student_handler.semester_counter == MAX_SEMESTERS_TO_FINISH_COURSE + 1 - assert student_handler.gpa == grade assert student_handler.state == situation diff --git a/tests/test_models.py b/tests/test_models.py index b0427c3..a2f349e 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -51,12 +51,14 @@ def test_course_model(set_in_memory_database): def test_student_model(set_in_memory_database): database = set_in_memory_database - student = StudentHandler(database) - student.name = "any" - student.cpf = "123.456.789-10" - - assert student.name == "any" - assert student.cpf == "123.456.789-10" - assert student.identifier is None - assert student.state == None - assert student.subjects == [] + student_handler = StudentHandler(database) + student_handler.name = "any" + student_handler.cpf = "123.456.789-10" + student_handler.enroll_to_course("any") + + assert student_handler.name == "any" + assert student_handler.cpf == "123.456.789-10" + assert student_handler.identifier is not None + assert student_handler.state == "enrolled" + assert student_handler.gpa == 0 + assert student_handler.subjects == [] diff --git a/tests/test_student.py b/tests/test_student.py index ef8cd0e..faaf009 100644 --- a/tests/test_student.py +++ b/tests/test_student.py @@ -140,13 +140,13 @@ def test_enroll_invalid_student_to_course_return_error(set_in_memory_database): student.enroll_to_course("any") -def test_enroll_student_to_course_x(set_in_memory_database): +def test_enroll_student_to_course(set_in_memory_database): + name = "any" + cpf = "123.456.789-10" student = StudentHandler(set_in_memory_database) - student.name = "any" - student.cpf = "123.456.789-10" + student.name = name + student.cpf = cpf course_name = "any" - identifier = utils.generate_student_identifier( - student.name, student.cpf, course_name - ) + identifier = utils.generate_student_identifier(name, cpf, course_name) assert student.enroll_to_course(course_name) == identifier From 323dbe00d6ef0266938acfbcde7e8762cdfa7c98 Mon Sep 17 00:00:00 2001 From: Douglas Cardoso Date: Thu, 18 Apr 2024 18:43:40 -0300 Subject: [PATCH 41/44] Add test 'test_student_failed_in_a_course_fails_even_if_gpa_is_above_the_minimum' --- README.md | 12 +++++++----- tests/test_integration.py | 32 ++++++++++++++++++++++++++++++-- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index c24b278..6f2853f 100644 --- a/README.md +++ b/README.md @@ -51,10 +51,10 @@ Definition of Done: Construction of the basic functions of the system 1. **DONE** Each student will have a grade control called "grade point average" (GPA). 2. **DONE** The GPA is the average of the student's grades in the ~~courses~~ subjects already taken. -3. The student is considered approved at the university if their GPA is above or equal to 7 (seven) at the end of the course. +3. **DONE** The student is considered approved at the university if their GPA is above or equal to 7 (seven) at the end of the course. 4. If a student takes the same subject more than once, the highest grade will be considered in the GPA calculation. -5. Initially, the university will have 3 courses with 3 subjects each. +5. **DONE** Initially, the university will have 3 courses with 3 subjects each. 6. **DONE** Subjects in each course may have the same names but will be differentiated by a unique identifier (niu). 7. **DONE** The system must calculate the student's situation taking into account the subjects taken and the total number of subjects in each course. 8. **DONE** The student can only take subjects from their course. @@ -88,12 +88,13 @@ Same as requirement 6 28. Canceled courses cannot have coordinators. 29. **DONE** Each subject can have a maximum of 30 enrolled students. + 30. The student must enroll in a minimum of 3 subjects. 31. If the number of subjects missing for a student is less than 3, they can enroll in 1 subject. 32. If the student does not enroll in the minimum number of subjects per semester, they will be automatically ~~failed~~ locked. 33. **DONE** The student must have a validated CPF (Brazilian Social Security Number) in the external CPF validation system (government system). 34. **DONE** Add the course name to the coordinator's reports. -35. The students are only approved if they achieve the minimum grade in all course subjects, even if their GPA is above the minimum. +35. **DONE** The students are only approved if they achieve the minimum grade in all course subjects, even if their GPA is above the minimum. 36. **DONE** The ~~user~~ student (person) must be able to create students with basic information. @@ -123,5 +124,6 @@ These features were introduced after analysis in architecture and specifications 55. The corse coordinator is responsible to add new subjects to his/her course 56. The general coordinator is responsible to add new courses to the university 57. The student, teacher and coordinators need to authenticate with valid credentials before perfom any action in the system -56. The course need a minimum enrollment of 100 students -56. The subject need a minimum enrollment of 10 students \ No newline at end of file +58. The course need a minimum enrollment of 100 students +59. The subject need a minimum enrollment of 10 students +60. Student can enroll to course again after fail it losing all his/her history \ No newline at end of file diff --git a/tests/test_integration.py b/tests/test_integration.py index 9f41e75..af22dd4 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -41,6 +41,17 @@ def __update_grade_of_all_subjects(grade, subjects, student_handler): student_handler.update_grade_to_subject(grade, subject) +def __pass_all_subjects_but_fails_one(subjects, student_handler): + grade1 = 10 + grade2 = 6 + for subject in subjects: + student_handler.take_subject(subject) + if subject == "mgmt": + student_handler.update_grade_to_subject(grade2, subject) + continue + student_handler.update_grade_to_subject(grade1, subject) + + def __enroll_student_to_course(course, database): student_handler = StudentHandler(database) student_handler.name = "douglas" @@ -67,8 +78,25 @@ def test_student_locked_by_minimun_subjects_per_semester(): pass -def test_student_can_enroll_to_course_after_fail_it_losing_all_history(): - pass +def test_student_failed_in_a_course_fails_even_if_gpa_is_above_the_minimum( + set_in_memory_database, +): + course = "adm" + minimum_gpa = 7 + situation = "failed" + database = set_in_memory_database + + subjects = __get_subjects() + __add_all_subjects_to_course(course, subjects, database) + student_handler = __enroll_student_to_course(course, database) + __pass_all_subjects_but_fails_one(subjects, student_handler) + + assert student_handler.gpa > minimum_gpa + + __close_maximum_semesters(database) + + assert student_handler.semester_counter == MAX_SEMESTERS_TO_FINISH_COURSE + 1 + assert student_handler.state == situation def test_student_cannot_do_any_operation_after_pass_or_fail_course( From 7533f5ae12636062ad2e4565dde97db9dccc9ef2 Mon Sep 17 00:00:00 2001 From: Douglas Cardoso Date: Thu, 18 Apr 2024 18:49:27 -0300 Subject: [PATCH 42/44] Change test names to fit the name of the services --- tests/test_course.py | 206 ----------------------------------------- tests/test_models.py | 64 ------------- tests/test_semester.py | 74 --------------- tests/test_student.py | 152 ------------------------------ tests/test_subject.py | 44 --------- 5 files changed, 540 deletions(-) delete mode 100644 tests/test_course.py delete mode 100644 tests/test_models.py delete mode 100644 tests/test_semester.py delete mode 100644 tests/test_student.py delete mode 100644 tests/test_subject.py diff --git a/tests/test_course.py b/tests/test_course.py deleted file mode 100644 index 33f2869..0000000 --- a/tests/test_course.py +++ /dev/null @@ -1,206 +0,0 @@ -import pytest -from src.services.course_handler import ( - CourseHandler, - NonValidCourse, - NonMinimunSubjects, -) -from src.services.student_handler import StudentHandler -from src import utils - - -def test_add_subject_to_new_course(set_in_memory_database): - course = "newcourse" - max_enrollment = 9 - subject = "newsubject" - database = set_in_memory_database - course_handler = CourseHandler(database) - course_handler.create(course, max_enrollment) - for i in range(3): - course_handler.add_subject(f"subject{i}") - course_handler.activate() - assert course_handler.add_subject(subject) is True - - # post conditions - course_handler.load_from_database(course) - assert ( - utils.generate_subject_identifier(course, subject) in database.course.subjects - ) - assert database.course.max_enrollment == max_enrollment - - -def test_create_courses_without_subjects(set_in_memory_database): - name = "newcourse" - max_enrollment = 9 - database = set_in_memory_database - course_handler = CourseHandler(database) - assert course_handler.create(name, max_enrollment) is True - - # post conditions - course_handler.load_from_database(name) - assert database.course.name == name - assert database.course.max_enrollment == max_enrollment - - -def test_list_all_courses(set_in_memory_database): - name = "any" - cpf = "123.456.789-10" - course = "any" - student_handler = StudentHandler(set_in_memory_database) - student_handler.name = name - student_handler.cpf = cpf - student_handler.enroll_to_course(course) - course_handler = CourseHandler(set_in_memory_database) - - actual = course_handler.list_all_courses_with_details() - - assert len(actual) > 0 - assert "mat" in actual - - -def test_list_empty_when_no_enrolled_students(set_in_memory_database): - course_handler = CourseHandler(set_in_memory_database) - course_handler.name = "any" - - actual = course_handler.list_student_details() - - assert len(actual) == 0 - - -def test_list_enrolled_students_in_specific_course(set_in_memory_database): - name = "any" - cpf = "123.456.789-10" - course = "any" - student_handler = StudentHandler(set_in_memory_database) - student_handler.name = name - student_handler.cpf = cpf - student_handler.enroll_to_course(course) - course_handler = CourseHandler(set_in_memory_database) - course_handler.name = course - - actual = course_handler.list_student_details() - - assert utils.generate_student_identifier(name, cpf, course) in actual - assert len(actual) == 1 - - -def test_enroll_student_to_cancelled_course_return_error(set_in_memory_database): - course_handler = CourseHandler(set_in_memory_database) - course_handler.name = "adm" - course_handler.add_subject("any1") - course_handler.add_subject("any2") - course_handler.add_subject("any3") - course_handler.activate() - course_handler.cancel() - - with pytest.raises(NonValidCourse): - course_handler.enroll_student("any") - - -def test_enroll_student_to_inactive_course_return_error(set_in_memory_database): - course_handler = CourseHandler(set_in_memory_database) - course_handler.name = "adm" - course_handler.add_subject("any1") - course_handler.add_subject("any2") - course_handler.add_subject("any3") - course_handler.deactivate() - - with pytest.raises(NonValidCourse): - course_handler.enroll_student("any") - - -def test_enroll_student_to_active_course(set_in_memory_database): - database = set_in_memory_database - course_handler = CourseHandler(database) - course_handler.name = "adm" - course_handler.add_subject("any1") - course_handler.add_subject("any2") - course_handler.add_subject("any3") - course_handler.activate() - - assert course_handler.enroll_student("any") == True - - course_handler.load_from_database("adm") - assert database.course.enrolled_students == ["any"] - - -def test_cancel_inactive_course(set_in_memory_database): - database = set_in_memory_database - course_handler = CourseHandler(database) - course_handler.name = "adm" - course_handler.add_subject("any1") - course_handler.add_subject("any2") - course_handler.add_subject("any3") - course_handler.deactivate() - assert course_handler.cancel() == "cancelled" - - course_handler.load_from_database("adm") - assert database.course.state == "cancelled" - - -def test_cancel_active_course(set_in_memory_database): - database = set_in_memory_database - course_handler = CourseHandler(database) - course_handler.name = "adm" - course_handler.add_subject("any1") - course_handler.add_subject("any2") - course_handler.add_subject("any3") - course_handler.activate() - - assert course_handler.cancel() == "cancelled" - course_handler.load_from_database("adm") - assert database.course.state == "cancelled" - - -def test_deactivate_non_active_course_return_error(set_in_memory_database): - database = set_in_memory_database - course_handler = CourseHandler(database) - with pytest.raises(NonValidCourse): - course_handler.deactivate() - - course_handler.load_from_database("adm") - assert database.course.state != "inactive" - - -def test_deactivate_course(set_in_memory_database): - database = set_in_memory_database - course_handler = CourseHandler(database) - course_handler.name = "adm" - course_handler.add_subject("any1") - course_handler.add_subject("any2") - course_handler.add_subject("any3") - course_handler.activate() - assert course_handler.deactivate() == "inactive" - - course_handler.load_from_database("adm") - assert database.course.state == "inactive" - - -def test_activate_course_without_minimum_subjects_return_error(set_in_memory_database): - database = set_in_memory_database - name = "nosubjects" - course_handler = CourseHandler(database) - course_handler.name = name - with pytest.raises(NonMinimunSubjects): - course_handler.activate() - - course_handler.load_from_database(name) - assert database.course.state != "active" - - -def test_activate_course_without_name_return_error(set_in_memory_database): - database = set_in_memory_database - course_handler = CourseHandler(database) - with pytest.raises(NonValidCourse): - course_handler.activate() - - -def test_activate_course(set_in_memory_database): - database = set_in_memory_database - course_handler = CourseHandler(database) - course_handler.name = "adm" - course_handler.add_subject("any1") - course_handler.add_subject("any2") - course_handler.add_subject("any3") - assert course_handler.activate() == "active" - - assert database.course.state == "active" diff --git a/tests/test_models.py b/tests/test_models.py deleted file mode 100644 index a2f349e..0000000 --- a/tests/test_models.py +++ /dev/null @@ -1,64 +0,0 @@ -import pytest -from src.services.student_handler import StudentHandler, NonValidGrade -from src.services.course_handler import CourseHandler -from src.services.subject_handler import SubjectHandler -from src.services.semester_monitor import SemesterMonitor - - -def test_semester_model(set_in_memory_database): - semester = SemesterMonitor(set_in_memory_database, "2024-1") - - assert semester.identifier == "2024-1" - assert semester.state == "open" - - -def test_subject_model(set_in_memory_database): - database = set_in_memory_database - subject_handler = SubjectHandler(database) - subject_handler.name = "any_name" - subject_handler.course = "any" - - assert subject_handler.name == "any_name" - assert subject_handler.identifier is not -1 - assert subject_handler.state == None - assert subject_handler.enrolled_students == [] - assert subject_handler.max_enrollment == 0 - assert subject_handler.course == "any" - - -def test_course_model(set_in_memory_database): - course = "any" - database = set_in_memory_database - course_handler = CourseHandler(database) - course_handler.name = course - course_handler.save() - - assert course_handler.name == course - assert course_handler.identifier is not None - assert course_handler.state == "inactive" - assert course_handler.enrolled_students == [] - assert course_handler.max_enrollment == 0 - assert course_handler.subjects == [] - - course_handler.load_from_database(course) - assert database.course.name == course - assert database.course.identifier is not None - assert database.course.state == "inactive" - assert database.course.enrolled_students == [] - assert database.course.max_enrollment == 0 - assert database.course.subjects == [] - - -def test_student_model(set_in_memory_database): - database = set_in_memory_database - student_handler = StudentHandler(database) - student_handler.name = "any" - student_handler.cpf = "123.456.789-10" - student_handler.enroll_to_course("any") - - assert student_handler.name == "any" - assert student_handler.cpf == "123.456.789-10" - assert student_handler.identifier is not None - assert student_handler.state == "enrolled" - assert student_handler.gpa == 0 - assert student_handler.subjects == [] diff --git a/tests/test_semester.py b/tests/test_semester.py deleted file mode 100644 index 8dc324c..0000000 --- a/tests/test_semester.py +++ /dev/null @@ -1,74 +0,0 @@ -import pytest -from src.services.semester_monitor import ( - SemesterMonitor, - NonValidOperation, -) -from src.services.student_handler import StudentHandler - - -def test_calculate_gpa_of_all_enrolled_students_when_semester_closes( - set_in_memory_database, -): - student = "any" - cpf = "123.456.789-10" - course = "any" - semester = "2024-1" - database = set_in_memory_database - student_handler = StudentHandler(database) - student_handler.name = student - student_handler.cpf = cpf - student_handler.enroll_to_course(course) - student_handler.take_subject("any1") - student_handler.take_subject("any2") - student_handler.take_subject("any3") - student_handler.update_grade_to_subject(9, "any1") - - semester_monitor = SemesterMonitor(database, semester) - semester_monitor.close() - - student_handler.load_from_database(student_handler.identifier) - assert student_handler.gpa > 0 - assert student_handler.semester_counter > 0 - assert student_handler.state == "enrolled" - - -def test_return_error_when_close_invalid_semester(set_in_memory_database): - identifier = "3024-1" - semester_monitor = SemesterMonitor(set_in_memory_database, identifier) - with pytest.raises(NonValidOperation): - semester_monitor.close() - - -def test_return_error_when_open_invalid_semester(set_in_memory_database): - identifier = "3024-1" - semester_monitor = SemesterMonitor(set_in_memory_database, identifier) - with pytest.raises(NonValidOperation): - semester_monitor.open() - - -def test_open_closed_semester_return_error(set_in_memory_database): - identifier = "2024-1" - semester_monitor = SemesterMonitor(set_in_memory_database, identifier) - semester_monitor.close() - with pytest.raises(NonValidOperation): - semester_monitor.open() - - -def test_open_semester(set_in_memory_database): - identifier = "2024-1" - semester_monitor = SemesterMonitor(set_in_memory_database, identifier) - assert semester_monitor.open() == "open" - - -def test_close_semester_when_no_students(set_in_memory_database): - identifier = "2024-1" - database = set_in_memory_database - semester_monitor = SemesterMonitor(set_in_memory_database, identifier) - - assert semester_monitor.close() == "closed" - - # post conditions - database.semester.load_by_identifier() - - assert identifier == database.semester.identifier - assert semester_monitor.state == database.semester.state diff --git a/tests/test_student.py b/tests/test_student.py deleted file mode 100644 index faaf009..0000000 --- a/tests/test_student.py +++ /dev/null @@ -1,152 +0,0 @@ -import pytest -from src.services.student_handler import ( - StudentHandler, - NonValidStudent, - NonValidSubject, - NonValidGrade, -) -from src import utils -from src.services.grade_calculator import GradeCalculator - - -@pytest.mark.parametrize( - "grade, expected", - [ - (6.99, "failed"), - (7.01, "passed"), - ], -) -def test_subject_situation_after_upgrade_grades( - set_in_memory_database, grade, expected -): - course_name = "any" - subject_name = "any1" - database = set_in_memory_database - student_handler = StudentHandler(database) - student_handler.name = "any" - student_handler.cpf = "123.456.789-10" - student_handler.enroll_to_course(course_name) - student_handler.take_subject(subject_name) - student_handler.update_grade_to_subject(grade=grade, subject_name=subject_name) - - # post condition - grade_calculator = GradeCalculator(database) - subject_identifier = utils.generate_subject_identifier(course_name, subject_name) - grade_calculator.load_from_database(student_handler.identifier, subject_identifier) - assert grade_calculator.student_identifier == student_handler.identifier - assert grade_calculator.subject_identifier in student_handler.subjects - assert grade_calculator.grade == grade - assert grade_calculator.subject_situation == expected - - -@pytest.mark.parametrize( - "grade", - [ - (-1), - (11), - ], -) -def test_calculate_student_gpa_when_subjects_have_invalid_grades( - set_in_memory_database, grade -): - course_name = "any" - database = set_in_memory_database - student_handler = StudentHandler(database) - student_handler.name = "any" - student_handler.cpf = "123.456.789-10" - student_handler.enroll_to_course(course_name) - - subject_name = "any1" - - student_handler.take_subject(subject_name) - - with pytest.raises(NonValidGrade): - student_handler.update_grade_to_subject(grade=grade, subject_name=subject_name) - - -def test_unlock_course(set_in_memory_database): - student = StudentHandler(set_in_memory_database) - student.name = "any" - student.cpf = "123.456.789-10" - student.enroll_to_course("any") - - student.unlock_course() - assert student.state == "enrolled" - - -def test_lock_course(set_in_memory_database): - database = set_in_memory_database - student = StudentHandler(database) - student.name = "any" - student.cpf = "123.456.789-10" - student.enroll_to_course("any") - - assert student.lock_course() == "locked" - assert database.student.state == "locked" - - -def test_take_subject_from_course_when_locked_student_return_error( - set_in_memory_database, -): - student = StudentHandler(set_in_memory_database) - student.name = "any" - student.cpf = "123.456.789-10" - subject = "any1" - - student.enroll_to_course("any") - student.lock_course() - with pytest.raises(NonValidStudent): - student.take_subject(subject) - - -def test_take_full_subject_from_course_return_error(set_in_memory_database): - student = StudentHandler(set_in_memory_database) - student.name = "any" - student.cpf = "123.456.789-10" - subject = utils.generate_subject_identifier("course1", "subject_full") - - student.enroll_to_course("any") - with pytest.raises(NonValidSubject): - student.take_subject(subject) - - -def test_take_invalid_subject_from_course_return_error(set_in_memory_database): - student = StudentHandler(set_in_memory_database) - student.name = "any" - student.cpf = "123.456.789-10" - subject = "invalid" - - student.enroll_to_course("any") - with pytest.raises(NonValidSubject): - student.take_subject(subject) - - -def test_take_subject_from_course(set_in_memory_database): - student = StudentHandler(set_in_memory_database) - student.name = "any" - student.cpf = "123.456.789-10" - course = "any" - student.enroll_to_course(course) - - assert student.take_subject("any1") is True - - -def test_enroll_invalid_student_to_course_return_error(set_in_memory_database): - student = StudentHandler(set_in_memory_database) - student.name = "invalid" - student.cpf = "123.456.789-10" - - with pytest.raises(NonValidStudent): - student.enroll_to_course("any") - - -def test_enroll_student_to_course(set_in_memory_database): - name = "any" - cpf = "123.456.789-10" - student = StudentHandler(set_in_memory_database) - student.name = name - student.cpf = cpf - course_name = "any" - identifier = utils.generate_student_identifier(name, cpf, course_name) - - assert student.enroll_to_course(course_name) == identifier diff --git a/tests/test_subject.py b/tests/test_subject.py deleted file mode 100644 index 231f357..0000000 --- a/tests/test_subject.py +++ /dev/null @@ -1,44 +0,0 @@ -import pytest -from src.services.subject_handler import SubjectHandler, NonValidSubject -from src import utils - - -def test_remove_invalid_subject_return_error(set_in_memory_database): - subject_handler = SubjectHandler(set_in_memory_database) - - with pytest.raises(NonValidSubject): - subject_handler.remove() - - -def test_remove(set_in_memory_database): - subject_handler = SubjectHandler(set_in_memory_database, course="any") - subject_handler.name = "any1" - assert subject_handler.remove() == "removed" - - -def test_activate_removed_subject_return_error(set_in_memory_database): - subject_handler = SubjectHandler( - set_in_memory_database, utils.generate_subject_identifier("any", "any1") - ) - subject_handler.activate() - subject_handler.remove() - - with pytest.raises(NonValidSubject): - subject_handler.activate() - - -def test_activate_invalid_subject_return_error(set_in_memory_database): - subject_handler = SubjectHandler(set_in_memory_database) - subject_handler.load_from_database( - utils.generate_subject_identifier("course1", "subject_removed") - ) - - with pytest.raises(NonValidSubject): - subject_handler.activate() - - -def test_activate(set_in_memory_database): - subject_identifier = utils.generate_subject_identifier("any", "any1") - subject_handler = SubjectHandler(set_in_memory_database, subject_identifier) - - assert subject_handler.activate() == "active" From 51c0be738c94130ad7ca207935efeeff25b00cad Mon Sep 17 00:00:00 2001 From: Douglas Cardoso Date: Thu, 18 Apr 2024 22:58:18 -0300 Subject: [PATCH 43/44] Changed test names to fit service names enhancements in exception handler of cli.py --- cli.py | 36 ++--- src/cli_helper.py | 35 +++-- src/database.py | 2 +- src/services/enrollment_validator.py | 10 +- src/services/student_handler.py | 21 +-- tests/test_course_handler.py | 206 +++++++++++++++++++++++++++ tests/test_models_of_services.py | 64 +++++++++ tests/test_roles.py | 7 + tests/test_semester_monitor.py | 74 ++++++++++ tests/test_student_handler.py | 170 ++++++++++++++++++++++ tests/test_subject_handler.py | 44 ++++++ 11 files changed, 614 insertions(+), 55 deletions(-) create mode 100644 tests/test_course_handler.py create mode 100644 tests/test_models_of_services.py create mode 100644 tests/test_roles.py create mode 100644 tests/test_semester_monitor.py create mode 100644 tests/test_student_handler.py create mode 100644 tests/test_subject_handler.py diff --git a/cli.py b/cli.py index c3b8bf3..ad4f899 100644 --- a/cli.py +++ b/cli.py @@ -29,8 +29,8 @@ def close_semester(identifier): try: database = Database() cli_helper.close_semester(database, identifier) - except Exception as e: - logging.error(str(e)) + except Exception: + raise @click.command() @@ -38,8 +38,8 @@ def list_courses(): try: database = Database() cli_helper.list_all_course_details(database) - except Exception as e: - logging.error(str(e)) + except Exception: + raise @click.command() @@ -52,8 +52,8 @@ def list_students(course_name): try: database = Database() cli_helper.list_student_details(database, course_name) - except Exception as e: - logging.error(str(e)) + except Exception: + raise @click.command() @@ -119,8 +119,8 @@ def enroll_student(name, cpf, course_name): try: database = Database() cli_helper.enroll_student(database, name, cpf, course_name) - except Exception as e: - logging.error(str(e)) + except Exception: + raise @click.command() @@ -133,8 +133,8 @@ def calculate_student_gpa(student_identifier): try: database = Database() cli_helper.calculate_student_gpa(database, student_identifier) - except Exception as e: - logging.error(str(e)) + except Exception: + raise @click.command() @@ -158,8 +158,8 @@ def update_grade(student_identifier, subject_name, grade): try: database = Database() cli_helper.update_grade(database, student_identifier, subject_name, grade) - except Exception as e: - logging.error(str(e)) + except Exception: + raise @click.command() @@ -177,8 +177,8 @@ def take_subject(student_identifier, subject_name): try: database = Database() cli_helper.take_subject(database, student_identifier, subject_name) - except Exception as e: - logging.error(str(e)) + except Exception: + raise @click.command() @@ -191,8 +191,8 @@ def lock_course(student_identifier): try: database = Database() cli_helper.lock_course(database, student_identifier) - except Exception as e: - logging.error(str(e)) + except Exception: + raise @click.command() @@ -205,8 +205,8 @@ def unlock_course(student_identifier): try: database = Database() cli_helper.unlock_course(database, student_identifier) - except Exception as e: - logging.error(str(e)) + except Exception: + raise cli.add_command(enroll_student) diff --git a/src/cli_helper.py b/src/cli_helper.py index 7f27bc2..0334523 100644 --- a/src/cli_helper.py +++ b/src/cli_helper.py @@ -33,7 +33,7 @@ def close_semester(database, identifier): print(str(e)) except Exception as e: logging.error(str(e)) - print(UNEXPECTED_ERROR) + raise CommandError(UNEXPECTED_ERROR) return False @@ -49,7 +49,7 @@ def remove_subject(database, course_name, subject_name): print(str(e)) except Exception as e: logging.error(str(e)) - print(UNEXPECTED_ERROR) + raise CommandError(UNEXPECTED_ERROR) return False @@ -65,7 +65,7 @@ def cancel_course(database, name): print(f"Course '{name}' is not valid.") except Exception as e: logging.error(str(e)) - print(UNEXPECTED_ERROR) + raise CommandError(UNEXPECTED_ERROR) return False @@ -81,7 +81,7 @@ def deactivate_course(database, name): print(f"Course '{name}' is not valid.") except Exception as e: logging.error(str(e)) - print(UNEXPECTED_ERROR) + raise CommandError(UNEXPECTED_ERROR) return False @@ -97,7 +97,7 @@ def activate_course(database, name): print(str(e)) except Exception as e: logging.error(str(e)) - print(UNEXPECTED_ERROR) + raise CommandError(UNEXPECTED_ERROR) return False @@ -112,7 +112,7 @@ def create_course(database, name, max_enrollment): print(f"Course '{name}' is not valid.") except Exception as e: logging.error(str(e)) - print(UNEXPECTED_ERROR) + raise CommandError(UNEXPECTED_ERROR) return False @@ -128,7 +128,7 @@ def add_subject_to_course(database, course_name, subject_name): print(f"Course '{course_name}' is not valid.") except Exception as e: logging.error(str(e)) - print(UNEXPECTED_ERROR) + raise CommandError(UNEXPECTED_ERROR) return False @@ -143,7 +143,7 @@ def calculate_student_gpa(database, student_identifier): print(str(e)) except Exception as e: logging.error(str(e)) - print(UNEXPECTED_ERROR) + raise CommandError(UNEXPECTED_ERROR) return False @@ -158,7 +158,8 @@ def take_subject(database, student_identifier, subject_name): print(str(e)) except Exception as e: logging.error(str(e)) - print(UNEXPECTED_ERROR) + raise CommandError(UNEXPECTED_ERROR) + raise return False @@ -173,7 +174,7 @@ def lock_course(database, student_identifier): print(str(e)) except Exception as e: logging.error(str(e)) - print(UNEXPECTED_ERROR) + raise CommandError(UNEXPECTED_ERROR) return False @@ -188,7 +189,7 @@ def unlock_course(database, student_identifier): print(str(e)) except Exception as e: logging.error(str(e)) - print(UNEXPECTED_ERROR) + raise CommandError(UNEXPECTED_ERROR) return False @@ -205,7 +206,7 @@ def update_grade(database, student_identifier, subject_name, grade): print(str(e)) except Exception as e: logging.error(str(e)) - print(UNEXPECTED_ERROR) + raise CommandError(UNEXPECTED_ERROR) return False @@ -224,7 +225,7 @@ def enroll_student(database, name, cpf, course_name): print(str(e)) except Exception as e: logging.error(str(e)) - print(UNEXPECTED_ERROR) + raise CommandError(UNEXPECTED_ERROR) return False @@ -241,7 +242,7 @@ def list_student_details(database, course_name): print(str(e)) except Exception as e: logging.error(str(e)) - print(UNEXPECTED_ERROR) + raise CommandError(UNEXPECTED_ERROR) return False @@ -257,5 +258,9 @@ def list_all_course_details(database): print(str(e)) except Exception as e: logging.error(str(e)) - print(UNEXPECTED_ERROR) + raise CommandError(UNEXPECTED_ERROR) return False + + +class CommandError(Exception): + pass diff --git a/src/database.py b/src/database.py index 10084c2..185eba8 100644 --- a/src/database.py +++ b/src/database.py @@ -186,7 +186,7 @@ def populate(self, name, cpf, course_name): def select(self, student_identifier): cmd = f"SELECT * FROM {self.TABLE} WHERE student_identifier = '{student_identifier}'" - return self.cur.execute(cmd).fetchone() is not None + return self.cur.execute(cmd).fetchone() class DbCourse: TABLE = "course" diff --git a/src/services/enrollment_validator.py b/src/services/enrollment_validator.py index aca7b40..93daaae 100644 --- a/src/services/enrollment_validator.py +++ b/src/services/enrollment_validator.py @@ -1,20 +1,16 @@ from src import utils -from src.services.course_handler import CourseHandler, NonValidCourse +from src.database import Database class EnrollmentValidator: - def __init__(self, database): + def __init__(self, database: Database): self.__database = database def validate_student_by_data(self, name, cpf, course_name): - courser_handler = CourseHandler(self.__database) - courser_handler.name = course_name - if not courser_handler.is_active(): - raise NonValidCourse(f"The course '{course_name}' is not active.") # the valid students are predefined as the list of approved person in the given course student_identifier = utils.generate_student_identifier(name, cpf, course_name) return self.validate_student_by_identifier(student_identifier) def validate_student_by_identifier(self, student_identifier): # the valid students are predefined as the list of approved person in the given course - return self.__database.enrollment.select(student_identifier) + return self.__database.enrollment.select(student_identifier) is not None diff --git a/src/services/student_handler.py b/src/services/student_handler.py index 7189ed9..425cc7a 100644 --- a/src/services/student_handler.py +++ b/src/services/student_handler.py @@ -1,6 +1,6 @@ import logging from src.services.enrollment_validator import EnrollmentValidator -from src.services.course_handler import CourseHandler +from src.services.course_handler import CourseHandler, NonValidCourse from src.services.subject_handler import SubjectHandler, NonValidSubject from src.services.grade_calculator import GradeCalculator from src.services.cpf_validator import is_valide_cpf @@ -66,6 +66,11 @@ def __check_enrolled_student(self, course_name): f"Student '{self.__identifier}' does not appears in enrollment list." ) + courser_handler = CourseHandler(self.__database) + courser_handler.name = course_name + if not courser_handler.is_active(): + raise NonValidCourse(f"The course '{course_name}' is not active.") + def __check_grade_range(self, grade): if grade < 0 or grade > 10: raise NonValidGrade("Grade must be between '0' and '10'.") @@ -313,18 +318,6 @@ def take_subject(self, subject_name): grade_calculator.subject_situation = SUBJECT_IN_PROGRESS grade_calculator.add(self.__identifier, subject_identifier, grade=0) - # post condition - subject_handler.load_from_database(subject_identifier) - assert self.__identifier in subject_handler.enrolled_students - - self.load_from_database(self.__identifier) - assert subject_identifier in self.subjects - - grade_calculator.load_from_database(self.__identifier, subject_identifier) - assert self.__identifier in grade_calculator.student_identifier - assert subject_identifier in grade_calculator.subject_identifier - assert grade_calculator.grade == 0 - return True def unlock_course(self): @@ -360,7 +353,7 @@ def load_from_database(self, student_identifier): self.__semester_counter = self.__database.student.semester_counter except NotFoundError as e: - raise NonValidStudent(str(e)) + raise NonValidStudent(f"Student '{self.__identifier}' not found.") except Exception as e: logging.error(str(e)) raise diff --git a/tests/test_course_handler.py b/tests/test_course_handler.py new file mode 100644 index 0000000..33f2869 --- /dev/null +++ b/tests/test_course_handler.py @@ -0,0 +1,206 @@ +import pytest +from src.services.course_handler import ( + CourseHandler, + NonValidCourse, + NonMinimunSubjects, +) +from src.services.student_handler import StudentHandler +from src import utils + + +def test_add_subject_to_new_course(set_in_memory_database): + course = "newcourse" + max_enrollment = 9 + subject = "newsubject" + database = set_in_memory_database + course_handler = CourseHandler(database) + course_handler.create(course, max_enrollment) + for i in range(3): + course_handler.add_subject(f"subject{i}") + course_handler.activate() + assert course_handler.add_subject(subject) is True + + # post conditions + course_handler.load_from_database(course) + assert ( + utils.generate_subject_identifier(course, subject) in database.course.subjects + ) + assert database.course.max_enrollment == max_enrollment + + +def test_create_courses_without_subjects(set_in_memory_database): + name = "newcourse" + max_enrollment = 9 + database = set_in_memory_database + course_handler = CourseHandler(database) + assert course_handler.create(name, max_enrollment) is True + + # post conditions + course_handler.load_from_database(name) + assert database.course.name == name + assert database.course.max_enrollment == max_enrollment + + +def test_list_all_courses(set_in_memory_database): + name = "any" + cpf = "123.456.789-10" + course = "any" + student_handler = StudentHandler(set_in_memory_database) + student_handler.name = name + student_handler.cpf = cpf + student_handler.enroll_to_course(course) + course_handler = CourseHandler(set_in_memory_database) + + actual = course_handler.list_all_courses_with_details() + + assert len(actual) > 0 + assert "mat" in actual + + +def test_list_empty_when_no_enrolled_students(set_in_memory_database): + course_handler = CourseHandler(set_in_memory_database) + course_handler.name = "any" + + actual = course_handler.list_student_details() + + assert len(actual) == 0 + + +def test_list_enrolled_students_in_specific_course(set_in_memory_database): + name = "any" + cpf = "123.456.789-10" + course = "any" + student_handler = StudentHandler(set_in_memory_database) + student_handler.name = name + student_handler.cpf = cpf + student_handler.enroll_to_course(course) + course_handler = CourseHandler(set_in_memory_database) + course_handler.name = course + + actual = course_handler.list_student_details() + + assert utils.generate_student_identifier(name, cpf, course) in actual + assert len(actual) == 1 + + +def test_enroll_student_to_cancelled_course_return_error(set_in_memory_database): + course_handler = CourseHandler(set_in_memory_database) + course_handler.name = "adm" + course_handler.add_subject("any1") + course_handler.add_subject("any2") + course_handler.add_subject("any3") + course_handler.activate() + course_handler.cancel() + + with pytest.raises(NonValidCourse): + course_handler.enroll_student("any") + + +def test_enroll_student_to_inactive_course_return_error(set_in_memory_database): + course_handler = CourseHandler(set_in_memory_database) + course_handler.name = "adm" + course_handler.add_subject("any1") + course_handler.add_subject("any2") + course_handler.add_subject("any3") + course_handler.deactivate() + + with pytest.raises(NonValidCourse): + course_handler.enroll_student("any") + + +def test_enroll_student_to_active_course(set_in_memory_database): + database = set_in_memory_database + course_handler = CourseHandler(database) + course_handler.name = "adm" + course_handler.add_subject("any1") + course_handler.add_subject("any2") + course_handler.add_subject("any3") + course_handler.activate() + + assert course_handler.enroll_student("any") == True + + course_handler.load_from_database("adm") + assert database.course.enrolled_students == ["any"] + + +def test_cancel_inactive_course(set_in_memory_database): + database = set_in_memory_database + course_handler = CourseHandler(database) + course_handler.name = "adm" + course_handler.add_subject("any1") + course_handler.add_subject("any2") + course_handler.add_subject("any3") + course_handler.deactivate() + assert course_handler.cancel() == "cancelled" + + course_handler.load_from_database("adm") + assert database.course.state == "cancelled" + + +def test_cancel_active_course(set_in_memory_database): + database = set_in_memory_database + course_handler = CourseHandler(database) + course_handler.name = "adm" + course_handler.add_subject("any1") + course_handler.add_subject("any2") + course_handler.add_subject("any3") + course_handler.activate() + + assert course_handler.cancel() == "cancelled" + course_handler.load_from_database("adm") + assert database.course.state == "cancelled" + + +def test_deactivate_non_active_course_return_error(set_in_memory_database): + database = set_in_memory_database + course_handler = CourseHandler(database) + with pytest.raises(NonValidCourse): + course_handler.deactivate() + + course_handler.load_from_database("adm") + assert database.course.state != "inactive" + + +def test_deactivate_course(set_in_memory_database): + database = set_in_memory_database + course_handler = CourseHandler(database) + course_handler.name = "adm" + course_handler.add_subject("any1") + course_handler.add_subject("any2") + course_handler.add_subject("any3") + course_handler.activate() + assert course_handler.deactivate() == "inactive" + + course_handler.load_from_database("adm") + assert database.course.state == "inactive" + + +def test_activate_course_without_minimum_subjects_return_error(set_in_memory_database): + database = set_in_memory_database + name = "nosubjects" + course_handler = CourseHandler(database) + course_handler.name = name + with pytest.raises(NonMinimunSubjects): + course_handler.activate() + + course_handler.load_from_database(name) + assert database.course.state != "active" + + +def test_activate_course_without_name_return_error(set_in_memory_database): + database = set_in_memory_database + course_handler = CourseHandler(database) + with pytest.raises(NonValidCourse): + course_handler.activate() + + +def test_activate_course(set_in_memory_database): + database = set_in_memory_database + course_handler = CourseHandler(database) + course_handler.name = "adm" + course_handler.add_subject("any1") + course_handler.add_subject("any2") + course_handler.add_subject("any3") + assert course_handler.activate() == "active" + + assert database.course.state == "active" diff --git a/tests/test_models_of_services.py b/tests/test_models_of_services.py new file mode 100644 index 0000000..a2f349e --- /dev/null +++ b/tests/test_models_of_services.py @@ -0,0 +1,64 @@ +import pytest +from src.services.student_handler import StudentHandler, NonValidGrade +from src.services.course_handler import CourseHandler +from src.services.subject_handler import SubjectHandler +from src.services.semester_monitor import SemesterMonitor + + +def test_semester_model(set_in_memory_database): + semester = SemesterMonitor(set_in_memory_database, "2024-1") + + assert semester.identifier == "2024-1" + assert semester.state == "open" + + +def test_subject_model(set_in_memory_database): + database = set_in_memory_database + subject_handler = SubjectHandler(database) + subject_handler.name = "any_name" + subject_handler.course = "any" + + assert subject_handler.name == "any_name" + assert subject_handler.identifier is not -1 + assert subject_handler.state == None + assert subject_handler.enrolled_students == [] + assert subject_handler.max_enrollment == 0 + assert subject_handler.course == "any" + + +def test_course_model(set_in_memory_database): + course = "any" + database = set_in_memory_database + course_handler = CourseHandler(database) + course_handler.name = course + course_handler.save() + + assert course_handler.name == course + assert course_handler.identifier is not None + assert course_handler.state == "inactive" + assert course_handler.enrolled_students == [] + assert course_handler.max_enrollment == 0 + assert course_handler.subjects == [] + + course_handler.load_from_database(course) + assert database.course.name == course + assert database.course.identifier is not None + assert database.course.state == "inactive" + assert database.course.enrolled_students == [] + assert database.course.max_enrollment == 0 + assert database.course.subjects == [] + + +def test_student_model(set_in_memory_database): + database = set_in_memory_database + student_handler = StudentHandler(database) + student_handler.name = "any" + student_handler.cpf = "123.456.789-10" + student_handler.enroll_to_course("any") + + assert student_handler.name == "any" + assert student_handler.cpf == "123.456.789-10" + assert student_handler.identifier is not None + assert student_handler.state == "enrolled" + assert student_handler.gpa == 0 + assert student_handler.subjects == [] diff --git a/tests/test_roles.py b/tests/test_roles.py new file mode 100644 index 0000000..fcf2169 --- /dev/null +++ b/tests/test_roles.py @@ -0,0 +1,7 @@ +def test_just_student_role_can_take_subjects_to_course(): + class StudentRole: + def take_subject(self, subject): + return True + + role = StudentRole() + assert role.take_subject("subject1") == True diff --git a/tests/test_semester_monitor.py b/tests/test_semester_monitor.py new file mode 100644 index 0000000..8dc324c --- /dev/null +++ b/tests/test_semester_monitor.py @@ -0,0 +1,74 @@ +import pytest +from src.services.semester_monitor import ( + SemesterMonitor, + NonValidOperation, +) +from src.services.student_handler import StudentHandler + + +def test_calculate_gpa_of_all_enrolled_students_when_semester_closes( + set_in_memory_database, +): + student = "any" + cpf = "123.456.789-10" + course = "any" + semester = "2024-1" + database = set_in_memory_database + student_handler = StudentHandler(database) + student_handler.name = student + student_handler.cpf = cpf + student_handler.enroll_to_course(course) + student_handler.take_subject("any1") + student_handler.take_subject("any2") + student_handler.take_subject("any3") + student_handler.update_grade_to_subject(9, "any1") + + semester_monitor = SemesterMonitor(database, semester) + semester_monitor.close() + + student_handler.load_from_database(student_handler.identifier) + assert student_handler.gpa > 0 + assert student_handler.semester_counter > 0 + assert student_handler.state == "enrolled" + + +def test_return_error_when_close_invalid_semester(set_in_memory_database): + identifier = "3024-1" + semester_monitor = SemesterMonitor(set_in_memory_database, identifier) + with pytest.raises(NonValidOperation): + semester_monitor.close() + + +def test_return_error_when_open_invalid_semester(set_in_memory_database): + identifier = "3024-1" + semester_monitor = SemesterMonitor(set_in_memory_database, identifier) + with pytest.raises(NonValidOperation): + semester_monitor.open() + + +def test_open_closed_semester_return_error(set_in_memory_database): + identifier = "2024-1" + semester_monitor = SemesterMonitor(set_in_memory_database, identifier) + semester_monitor.close() + with pytest.raises(NonValidOperation): + semester_monitor.open() + + +def test_open_semester(set_in_memory_database): + identifier = "2024-1" + semester_monitor = SemesterMonitor(set_in_memory_database, identifier) + assert semester_monitor.open() == "open" + + +def test_close_semester_when_no_students(set_in_memory_database): + identifier = "2024-1" + database = set_in_memory_database + semester_monitor = SemesterMonitor(set_in_memory_database, identifier) + + assert semester_monitor.close() == "closed" + + # post conditions + database.semester.load_by_identifier() + + assert identifier == database.semester.identifier + assert semester_monitor.state == database.semester.state diff --git a/tests/test_student_handler.py b/tests/test_student_handler.py new file mode 100644 index 0000000..96ce407 --- /dev/null +++ b/tests/test_student_handler.py @@ -0,0 +1,170 @@ +import pytest +from src.services.student_handler import ( + StudentHandler, + NonValidStudent, + NonValidSubject, + NonValidGrade, +) +from src import utils +from src.services.grade_calculator import GradeCalculator +from src.services.subject_handler import SubjectHandler + + +@pytest.mark.parametrize( + "grade, expected", + [ + (6.99, "failed"), + (7.01, "passed"), + ], +) +def test_subject_situation_after_upgrade_grades( + set_in_memory_database, grade, expected +): + course_name = "any" + subject_name = "any1" + database = set_in_memory_database + student_handler = StudentHandler(database) + student_handler.name = "any" + student_handler.cpf = "123.456.789-10" + student_handler.enroll_to_course(course_name) + student_handler.take_subject(subject_name) + student_handler.update_grade_to_subject(grade=grade, subject_name=subject_name) + + # post condition + grade_calculator = GradeCalculator(database) + subject_identifier = utils.generate_subject_identifier(course_name, subject_name) + grade_calculator.load_from_database(student_handler.identifier, subject_identifier) + assert grade_calculator.student_identifier == student_handler.identifier + assert grade_calculator.subject_identifier in student_handler.subjects + assert grade_calculator.grade == grade + assert grade_calculator.subject_situation == expected + + +@pytest.mark.parametrize( + "grade", + [ + (-1), + (11), + ], +) +def test_calculate_student_gpa_when_subjects_have_invalid_grades( + set_in_memory_database, grade +): + course_name = "any" + database = set_in_memory_database + student_handler = StudentHandler(database) + student_handler.name = "any" + student_handler.cpf = "123.456.789-10" + student_handler.enroll_to_course(course_name) + + subject_name = "any1" + + student_handler.take_subject(subject_name) + + with pytest.raises(NonValidGrade): + student_handler.update_grade_to_subject(grade=grade, subject_name=subject_name) + + +def test_unlock_course(set_in_memory_database): + student = StudentHandler(set_in_memory_database) + student.name = "any" + student.cpf = "123.456.789-10" + student.enroll_to_course("any") + + student.unlock_course() + assert student.state == "enrolled" + + +def test_lock_course(set_in_memory_database): + database = set_in_memory_database + student = StudentHandler(database) + student.name = "any" + student.cpf = "123.456.789-10" + student.enroll_to_course("any") + + assert student.lock_course() == "locked" + assert database.student.state == "locked" + + +def test_take_subject_from_course_when_locked_student_return_error( + set_in_memory_database, +): + student = StudentHandler(set_in_memory_database) + student.name = "any" + student.cpf = "123.456.789-10" + subject = "any1" + + student.enroll_to_course("any") + student.lock_course() + with pytest.raises(NonValidStudent): + student.take_subject(subject) + + +def test_take_full_subject_from_course_return_error(set_in_memory_database): + student = StudentHandler(set_in_memory_database) + student.name = "any" + student.cpf = "123.456.789-10" + subject = utils.generate_subject_identifier("course1", "subject_full") + + student.enroll_to_course("any") + with pytest.raises(NonValidSubject): + student.take_subject(subject) + + +def test_take_invalid_subject_from_course_return_error(set_in_memory_database): + student = StudentHandler(set_in_memory_database) + student.name = "any" + student.cpf = "123.456.789-10" + subject = "invalid" + + student.enroll_to_course("any") + with pytest.raises(NonValidSubject): + student.take_subject(subject) + + +def test_take_subject_from_course(set_in_memory_database): + course = "any" + subject = "any1" + database = set_in_memory_database + student_handler = StudentHandler(database) + student_handler.name = "any" + student_handler.cpf = "123.456.789-10" + student_handler.enroll_to_course(course) + + assert student_handler.take_subject(subject) is True + + # post condition + subject_identifier = utils.generate_subject_identifier(course, subject) + subject_handler = SubjectHandler(database) + subject_handler.load_from_database(subject_identifier) + assert student_handler.identifier in subject_handler.enrolled_students + + student_handler.load_from_database(student_handler.identifier) + assert subject_identifier in student_handler.subjects + + grade_calculator = GradeCalculator(database) + grade_calculator.load_from_database(student_handler.identifier, subject_identifier) + assert student_handler.identifier in grade_calculator.student_identifier + assert subject_identifier in grade_calculator.subject_identifier + assert grade_calculator.grade == 0 + + +def test_enroll_invalid_student_to_course_return_error(set_in_memory_database): + student = StudentHandler(set_in_memory_database) + student.name = "invalid" + student.cpf = "123.456.789-10" + + with pytest.raises(NonValidStudent): + student.enroll_to_course("any") + + +def test_enroll_student_to_course(set_in_memory_database): + name = "any" + cpf = "123.456.789-10" + student = StudentHandler(set_in_memory_database) + student.name = name + student.cpf = cpf + course_name = "any" + identifier = utils.generate_student_identifier(name, cpf, course_name) + + assert student.enroll_to_course(course_name) == identifier diff --git a/tests/test_subject_handler.py b/tests/test_subject_handler.py new file mode 100644 index 0000000..231f357 --- /dev/null +++ b/tests/test_subject_handler.py @@ -0,0 +1,44 @@ +import pytest +from src.services.subject_handler import SubjectHandler, NonValidSubject +from src import utils + + +def test_remove_invalid_subject_return_error(set_in_memory_database): + subject_handler = SubjectHandler(set_in_memory_database) + + with pytest.raises(NonValidSubject): + subject_handler.remove() + + +def test_remove(set_in_memory_database): + subject_handler = SubjectHandler(set_in_memory_database, course="any") + subject_handler.name = "any1" + assert subject_handler.remove() == "removed" + + +def test_activate_removed_subject_return_error(set_in_memory_database): + subject_handler = SubjectHandler( + set_in_memory_database, utils.generate_subject_identifier("any", "any1") + ) + subject_handler.activate() + subject_handler.remove() + + with pytest.raises(NonValidSubject): + subject_handler.activate() + + +def test_activate_invalid_subject_return_error(set_in_memory_database): + subject_handler = SubjectHandler(set_in_memory_database) + subject_handler.load_from_database( + utils.generate_subject_identifier("course1", "subject_removed") + ) + + with pytest.raises(NonValidSubject): + subject_handler.activate() + + +def test_activate(set_in_memory_database): + subject_identifier = utils.generate_subject_identifier("any", "any1") + subject_handler = SubjectHandler(set_in_memory_database, subject_identifier) + + assert subject_handler.activate() == "active" From 13dc9cdd200c3de08e349b8257e269f25baa1c7b Mon Sep 17 00:00:00 2001 From: Douglas Cardoso Date: Fri, 19 Apr 2024 16:03:54 -0300 Subject: [PATCH 44/44] Add tests for CLI --- cli.py | 13 +- src/cli_helper.py | 134 +++++++-------------- src/database.py | 47 ++++++-- src/services/course_handler.py | 66 ++++++++--- src/services/grade_calculator.py | 4 +- src/services/student_handler.py | 27 ++--- src/services/subject_handler.py | 7 +- src/utils.py | 3 +- tests/test_cli.py | 197 +++++++++++++++++++++++++++---- tests/test_course_handler.py | 4 +- tests/test_student_handler.py | 21 +++- 11 files changed, 341 insertions(+), 182 deletions(-) diff --git a/cli.py b/cli.py index ad4f899..e00bac5 100644 --- a/cli.py +++ b/cli.py @@ -9,7 +9,7 @@ datefmt="%Y-%m-%d %H:%M:%S", format="%(asctime)s - %(levelname)s: [%(filename)s:%(lineno)s - %(funcName)20s() ] %(message)s", filemode="a", - level="DEBUG", + level="ERROR", ) @@ -128,8 +128,9 @@ def enroll_student(name, cpf, course_name): "--student-identifier", prompt="Student identifier", help="Student identifier number.", + hide_input=True, ) -def calculate_student_gpa(student_identifier): +def calculate_gpa(student_identifier): try: database = Database() cli_helper.calculate_student_gpa(database, student_identifier) @@ -142,6 +143,7 @@ def calculate_student_gpa(student_identifier): "--student-identifier", prompt="Student identifier", help="Student identifier number.", + hide_input=True, ) @click.option( "--subject-name", @@ -150,7 +152,7 @@ def calculate_student_gpa(student_identifier): ) @click.option( "--grade", - type=int, + type=float, prompt="Subject name", help="The name of the subject the student wants to take.", ) @@ -167,6 +169,7 @@ def update_grade(student_identifier, subject_name, grade): "--student-identifier", prompt="Student identifier", help="Student identifier number.", + hide_input=True, ) @click.option( "--subject-name", @@ -186,6 +189,7 @@ def take_subject(student_identifier, subject_name): "--student-identifier", prompt="Student identifier", help="Student identifier number.", + hide_input=True, ) def lock_course(student_identifier): try: @@ -200,6 +204,7 @@ def lock_course(student_identifier): "--student-identifier", prompt="Student identifier", help="Student identifier number.", + hide_input=True, ) def unlock_course(student_identifier): try: @@ -212,7 +217,7 @@ def unlock_course(student_identifier): cli.add_command(enroll_student) cli.add_command(take_subject) cli.add_command(update_grade) -cli.add_command(calculate_student_gpa) +cli.add_command(calculate_gpa) cli.add_command(lock_course) cli.add_command(unlock_course) diff --git a/src/cli_helper.py b/src/cli_helper.py index 0334523..a136deb 100644 --- a/src/cli_helper.py +++ b/src/cli_helper.py @@ -2,39 +2,27 @@ import json from src.services.student_handler import ( StudentHandler, - NonValidStudent, - NonValidGrade, ) from src.services.course_handler import ( CourseHandler, - NonValidCourse, - NonMinimunSubjects, ) -from src.services.grade_calculator import GradeCalculator, NonValidGradeOperation -from src.services.subject_handler import SubjectHandler, NonValidSubject +from src.services.grade_calculator import GradeCalculator +from src.services.subject_handler import SubjectHandler from src.services.semester_monitor import ( SemesterMonitor, - NonValidOperation, - NonValidSemester, ) -UNEXPECTED_ERROR = "Unexpected error. Consult the system adminstrator." - - def close_semester(database, identifier): try: course_handler = SemesterMonitor(database, identifier) course_handler.close() print(f"Semester '{identifier}' closed.") return True - except (NonValidOperation, NonValidSemester) as e: - logging.error(str(e)) - print(str(e)) except Exception as e: logging.error(str(e)) - raise CommandError(UNEXPECTED_ERROR) - return False + print(str(e)) + return False def remove_subject(database, course_name, subject_name): @@ -44,13 +32,10 @@ def remove_subject(database, course_name, subject_name): subject_handler.remove() print(f"Subject removed from course.") return True - except NonValidSubject as e: - logging.error(str(e)) - print(str(e)) except Exception as e: logging.error(str(e)) - raise CommandError(UNEXPECTED_ERROR) - return False + print(str(e)) + return False def cancel_course(database, name): @@ -60,13 +45,10 @@ def cancel_course(database, name): course_handler.cancel() print(f"Course '{name}' cancelled.") return True - except NonValidCourse as e: - logging.error(str(e)) - print(f"Course '{name}' is not valid.") except Exception as e: logging.error(str(e)) - raise CommandError(UNEXPECTED_ERROR) - return False + print(str(e)) + return False def deactivate_course(database, name): @@ -76,13 +58,10 @@ def deactivate_course(database, name): course_handler.deactivate() print(f"Course '{name}' deactivated.") return True - except NonValidCourse as e: - logging.error(str(e)) - print(f"Course '{name}' is not valid.") except Exception as e: logging.error(str(e)) - raise CommandError(UNEXPECTED_ERROR) - return False + print(str(e)) + return False def activate_course(database, name): @@ -92,13 +71,10 @@ def activate_course(database, name): course_handler.activate() print(f"Course '{name}' activated.") return True - except (NonValidCourse, NonMinimunSubjects) as e: - logging.error(str(e)) - print(str(e)) except Exception as e: logging.error(str(e)) - raise CommandError(UNEXPECTED_ERROR) - return False + print(str(e)) + return False def create_course(database, name, max_enrollment): @@ -107,13 +83,10 @@ def create_course(database, name, max_enrollment): course_handler.create(name, max_enrollment) print(f"Course '{name}' created.") return True - except NonValidCourse as e: - logging.error(str(e)) - print(f"Course '{name}' is not valid.") except Exception as e: logging.error(str(e)) - raise CommandError(UNEXPECTED_ERROR) - return False + print(str(e)) + return False def add_subject_to_course(database, course_name, subject_name): @@ -123,91 +96,70 @@ def add_subject_to_course(database, course_name, subject_name): course_handler.add_subject(subject_name) print(f"Subject '{subject_name}' added to course '{course_name}'.") return True - except NonValidCourse as e: - logging.error(str(e)) - print(f"Course '{course_name}' is not valid.") except Exception as e: logging.error(str(e)) - raise CommandError(UNEXPECTED_ERROR) - return False + print(str(e)) + return False def calculate_student_gpa(database, student_identifier): try: grade_calculator = GradeCalculator(database) gpa = grade_calculator.calculate_gpa_for_student(student_identifier) - print(f"GPA of student '{student_identifier}' is '{gpa}'.") + print(f"GPA of student is '{gpa}'.") return True - except (NonValidStudent, NonValidGradeOperation) as e: - logging.error(str(e)) - print(str(e)) except Exception as e: logging.error(str(e)) - raise CommandError(UNEXPECTED_ERROR) - return False + print(str(e)) + return False def take_subject(database, student_identifier, subject_name): try: student_handler = StudentHandler(database, student_identifier) student_handler.take_subject(subject_name) - print(f"Student '{student_identifier}' toke subject '{subject_name}'.") + print(f"Student toke subject '{subject_name}'.") return True - except (NonValidStudent, NonValidSubject, NonValidGrade) as e: - logging.error(str(e)) - print(str(e)) except Exception as e: logging.error(str(e)) - raise CommandError(UNEXPECTED_ERROR) - raise - return False + print(str(e)) + return False def lock_course(database, student_identifier): try: student_handler = StudentHandler(database, student_identifier) student_handler.lock_course() - print(f"Student '{student_identifier}' locked the course.") + print(f"Student locked the course.") return True - except (NonValidStudent, NonValidSubject, NonValidGrade) as e: - logging.error(str(e)) - print(str(e)) except Exception as e: logging.error(str(e)) - raise CommandError(UNEXPECTED_ERROR) - return False + print(str(e)) + return False def unlock_course(database, student_identifier): try: student_handler = StudentHandler(database, student_identifier) student_handler.unlock_course() - print(f"Student '{student_identifier}' unlocked the course.") + print(f"Student unlocked the course.") return True - except (NonValidStudent, NonValidSubject, NonValidGrade) as e: - logging.error(str(e)) - print(str(e)) except Exception as e: logging.error(str(e)) - raise CommandError(UNEXPECTED_ERROR) - return False + print(str(e)) + return False def update_grade(database, student_identifier, subject_name, grade): try: student_handler = StudentHandler(database, student_identifier) student_handler.update_grade_to_subject(grade, subject_name) - print( - f"Student '{student_identifier}' updated grade of subject '{subject_name}' to '{grade}'." - ) + print(f"Student updated grade of subject '{subject_name}' to '{grade}'.") return True - except (NonValidStudent, NonValidSubject, NonValidGrade) as e: - logging.error(str(e)) - print(str(e)) except Exception as e: logging.error(str(e)) - raise CommandError(UNEXPECTED_ERROR) - return False + print(str(e)) + return False def enroll_student(database, name, cpf, course_name): @@ -218,15 +170,15 @@ def enroll_student(database, name, cpf, course_name): identifier = student.enroll_to_course(course_name) print( f"Student '{name}' enrolled in course '{course_name}' with identifier '{identifier}'." + f" Save the identifier. It is necessary for next operations." ) return True - except (NonValidStudent, NonValidCourse) as e: - logging.error(str(e)) - print(str(e)) except Exception as e: logging.error(str(e)) - raise CommandError(UNEXPECTED_ERROR) - return False + print(str(e)) + return False + # print(str(e)) + return False def list_student_details(database, course_name): @@ -237,13 +189,10 @@ def list_student_details(database, course_name): print(f"List of students in course {course_name}:") print(json.dumps(students, sort_keys=True, indent=4)) return True - except NonValidStudent as e: - logging.error(str(e)) - print(str(e)) except Exception as e: logging.error(str(e)) - raise CommandError(UNEXPECTED_ERROR) - return False + print(str(e)) + return False def list_all_course_details(database): @@ -253,13 +202,10 @@ def list_all_course_details(database): print(f"List of courses:") print(json.dumps(courses, sort_keys=True, indent=4)) return True - except NonValidStudent as e: - logging.error(str(e)) - print(str(e)) except Exception as e: logging.error(str(e)) - raise CommandError(UNEXPECTED_ERROR) - return False + print(str(e)) + return False class CommandError(Exception): diff --git a/src/database.py b/src/database.py index 185eba8..fe05548 100644 --- a/src/database.py +++ b/src/database.py @@ -27,7 +27,7 @@ def __init__(self, database="university.db"): self.semester = self.DbSemester(con, cur) class DbStudent: - TABLE = "student" + TABLE = "students" name = None state = None cpf = None @@ -146,9 +146,7 @@ def load(self, identifier): cmd = f"SELECT * FROM {self.TABLE} WHERE identifier = '{identifier}'" result = self.cur.execute(cmd).fetchone() if not result: - raise NotFoundError( - f"Student '{self.identifier}' not found in table '{self.TABLE}'." - ) + raise NotFoundError(f"Student not found in table '{self.TABLE}'.") self.name = result[0] self.state = result[1] self.cpf = result[2] @@ -162,7 +160,7 @@ def load(self, identifier): raise class DbEnrollment: - TABLE = "enrollment" + TABLE = "enrollments" def __init__(self, con, cur): self.cur = cur @@ -189,7 +187,7 @@ def select(self, student_identifier): return self.cur.execute(cmd).fetchone() class DbCourse: - TABLE = "course" + TABLE = "courses" name = None state = None identifier = None @@ -312,7 +310,7 @@ def load_from_database(self, name): raise class DbSubject: - TABLE = "subject" + TABLE = "subjects" name = None state = None enrolled_students = None @@ -400,6 +398,31 @@ def save(self): logging.error(str(e)) raise + def search_all_by_course(self, course_name): + result = self.cur.execute( + f"SELECT * FROM {self.TABLE} WHERE course = '{course_name}'" + ).fetchall() + + class SubjectRow: + name = None + state = None + identifier = None + enrolled_students = None + max_enrollment = None + course = None + + subjects = [] + for row in result: + subject_row = SubjectRow() + subject_row.name = row[0] + subject_row.state = row[1] + subject_row.identifier = row[2] + subject_row.enrolled_students = convert_csv_to_list(the_csv=row[3]) + subject_row.max_enrollment = row[4] + subject_row.course = row[5] + subjects.append(subject_row) + return subjects + class DbGradeCalculator: class GradeCalculatorRow: student_identifier = None @@ -407,7 +430,7 @@ class GradeCalculatorRow: grade = None subject_situation = None - TABLE = "grade_calculator" + TABLE = "grade_calculators" student_identifier = None subject_identifier = None grade = None @@ -430,9 +453,7 @@ def load_all_by_student_identifier(self, student_identifier): """ result = self.cur.execute(cmd).fetchall() if not result: - raise NotFoundError( - f"Student '{student_identifier}' not found in table '{self.TABLE}'" - ) + raise NotFoundError(f"Student not found in table '{self.TABLE}'") grade_calculators = [] for row in result: @@ -499,7 +520,7 @@ def load(self, student_identifier, subject_identifier): ).fetchone() if not result: raise NotFoundError( - f"Student '{student_identifier}' and subject '{subject_identifier}'" + f"Student and subject '{subject_identifier}'" f" not found in table '{self.TABLE}'." ) @@ -558,7 +579,7 @@ def remove(self, student_identifier, subject_identifier): raise class DbSemester: - TABLE = "semester" + TABLE = "semesters" identifier = None state = None diff --git a/src/services/course_handler.py b/src/services/course_handler.py index f5b2bc4..29d8d8b 100644 --- a/src/services/course_handler.py +++ b/src/services/course_handler.py @@ -1,4 +1,4 @@ -import uuid +import sqlite3 import logging from src.constants import DUMMY_IDENTIFIER from src.services.grade_calculator import GradeCalculator @@ -113,7 +113,7 @@ def load_from_database(self, name): except NotFoundError as e: logging.error(str(e)) - raise + raise NonValidCourse(f"Course '{name}' not found.") except Exception as e: logging.error(str(e)) raise @@ -122,22 +122,28 @@ def list_student_details(self): self.load_from_database(self.name) enrolled_students = self.__database.course.enrolled_students result = {} - for student_identifier in enrolled_students: - self.__database.student.load(student_identifier) - result[self.__database.student.identifier] = { - "name": self.__database.student.name, - "gpa": self.__database.student.gpa, - "course": self.__database.student.course, - } + students_data = [] for student_identifier in enrolled_students: self.__database.student.load(student_identifier) + subjects_data = [] + for subject_identifier in self.__database.student.subjects: grade_calculator = GradeCalculator(self.__database) grade_calculator.load_from_database( student_identifier, subject_identifier ) - result[student_identifier][subject_identifier] = grade_calculator.grade + subjects_data.append({subject_identifier: grade_calculator.grade}) + + students_data.append( + { + "name": self.__database.student.name, + "gpa": self.__database.student.gpa, + "grades": subjects_data, + } + ) + result["students"] = students_data + return result def list_all_courses_with_details(self): @@ -145,6 +151,9 @@ def list_all_courses_with_details(self): for course in self.__database.course.search_all(): self.name = course.name all_details[self.name] = self.list_student_details() + all_details[self.name]["subjects"] = [ + s.name for s in self.__database.subject.search_all_by_course(self.name) + ] return all_details def enroll_student(self, student_identifier): @@ -159,6 +168,10 @@ def add_subject(self, subject): self.load_from_database(self.name) self.__check_cancelled() subject_identifier = utils.generate_subject_identifier(self.name, subject) + if subject_identifier in self.__subjects: + raise NonValidSubject( + f"Subject '{subject}' already exists in course '{self.__name}'" + ) self.__subjects.append(subject_identifier) self.save() @@ -192,15 +205,26 @@ def activate(self): return self.__state def create(self, course_name, max_enrollmet): - self.name = course_name - self.__max_enrollment = max_enrollmet - self.__database.course.identifier = self.identifier - self.__database.course.name = course_name - self.__database.course.state = self.state - self.__database.course.enrolled_students = self.enrolled_students - self.__database.course.max_enrollment = self.max_enrollment - self.__database.course.add() - return True + if max_enrollmet < 1: + raise NonValidCourse( + f"The max enrollment '{max_enrollmet}' is not valid. Need to set a number bigger than '0'." + ) + try: + self.name = course_name + self.__max_enrollment = max_enrollmet + self.__database.course.identifier = self.identifier + self.__database.course.name = course_name + self.__database.course.state = self.state + self.__database.course.enrolled_students = self.enrolled_students + self.__database.course.max_enrollment = self.max_enrollment + self.__database.course.add() + return True + except sqlite3.IntegrityError as e: + raise NonValidCourse(f"Course '{course_name}' already exists.") + except Exception as e: + raise NonValidCourse( + f"Not able to create the course '{course_name}'. Check with system adminstrator." + ) class NonValidCourse(Exception): @@ -209,3 +233,7 @@ class NonValidCourse(Exception): class NonMinimunSubjects(Exception): pass + + +class NonValidSubject(Exception): + pass diff --git a/src/services/grade_calculator.py b/src/services/grade_calculator.py index dcfde35..b9c7c07 100644 --- a/src/services/grade_calculator.py +++ b/src/services/grade_calculator.py @@ -86,9 +86,7 @@ def calculate_gpa_for_student(self, student_identifier): ) ) except NotFoundError as e: - raise NonValidGradeOperation( - f"Student '{student_identifier}' not enrolled to any subject." - ) + raise NonValidGradeOperation(f"Student not enrolled to any subject.") except Exception: logging.error(str(e)) raise diff --git a/src/services/student_handler.py b/src/services/student_handler.py index 425cc7a..75c7b56 100644 --- a/src/services/student_handler.py +++ b/src/services/student_handler.py @@ -44,7 +44,7 @@ def __generate_identifier_when_student_ready(self): def __check_cpf_validity(self, value): if not is_valide_cpf(value): - raise NonValidStudent(f"CPF {value} is not valid.") + raise NonValidStudent(f"CPF '{value}' is not valid.") def __check_cpf(self): if not self.__cpf: @@ -52,7 +52,7 @@ def __check_cpf(self): def __check_locked(self): if self.__state == self.__LOCKED: - raise NonValidStudent(f"Student '{self.__identifier}' is locked.") + raise NonValidStudent(f"Student is locked.") def __check_enrolled_student(self, course_name): enrollment_validator = EnrollmentValidator(self.__database) @@ -63,7 +63,7 @@ def __check_enrolled_student(self, course_name): or enrollment_validator.validate_student_by_identifier(self.__identifier) ): raise NonValidStudent( - f"Student '{self.__identifier}' does not appears in enrollment list." + f"Student '{self.__name}' does not appears in enrollment list." ) courser_handler = CourseHandler(self.__database) @@ -105,7 +105,7 @@ def __return_subject_situation(self, grade): def __check_valid_subject(self, subject_identifier): if not subject_identifier in self.subjects: raise NonValidSubject( - f"The student '{self.__identifier}' is not enrolled to this subject '{subject_identifier}'" + f"The student is not enrolled to this subject '{subject_identifier}'" ) def __check_finished_course(self): @@ -208,9 +208,7 @@ def __calculate_gpa(self): self.__identifier ) except NonValidGradeOperation as e: - raise NonValidGrade( - f"Student '{self.__identifier}' may not be enrolled to any subject." - ) + raise NonValidGrade(f"Student may not be enrolled to any subject.") except Exception as e: logging.error(str(e)) raise @@ -270,15 +268,6 @@ def enroll_to_course(self, course_name): grade_calculator = GradeCalculator(self.__database) grade_calculator.add(self.__identifier, DUMMY_IDENTIFIER, grade=0) - # post condition - self.__database.student.load(self.__identifier) - - assert self.__database.student.identifier == self.__identifier - assert self.__database.student.state == self.__ENROLLED - assert self.__database.student.course == self.__course - assert self.__database.student.gpa == 0 - assert self.__identifier in course.enrolled_students - return self.__identifier except Exception as e: logging.error(str(e)) @@ -293,6 +282,10 @@ def take_subject(self, subject_name): subject_identifier = utils.generate_subject_identifier( self.__course, subject_name ) + if subject_identifier in self.__subjects: + raise NonValidSubject( + f"The student already toke the subject '{subject_name}'." + ) subject_handler = SubjectHandler(self.__database) try: subject_handler.load_from_database(subject_identifier) @@ -353,7 +346,7 @@ def load_from_database(self, student_identifier): self.__semester_counter = self.__database.student.semester_counter except NotFoundError as e: - raise NonValidStudent(f"Student '{self.__identifier}' not found.") + raise NonValidStudent(f"Student not found.") except Exception as e: logging.error(str(e)) raise diff --git a/src/services/subject_handler.py b/src/services/subject_handler.py index 6da211f..e774598 100644 --- a/src/services/subject_handler.py +++ b/src/services/subject_handler.py @@ -118,7 +118,7 @@ def remove(self): except Exception as e: logging.error(str(e)) raise NonValidSubject( - f"Subject '{self.identifier}' not found in course '{self.course}'.'" + f"Subject '{self.__name}' not found in course '{self.course}'." ) if not self.state == self.ACTIVE: @@ -160,7 +160,10 @@ def load_from_database(self, subject_identifier): except Exception as e: logging.error(str(e)) - raise NonValidSubject("Subject not found.") + identifier = subject_identifier + if self.__name: + identifier = self.__name + raise NonValidSubject(f"Subject '{identifier}' not found.") class NonValidSubject(Exception): diff --git a/src/utils.py b/src/utils.py index e5d0c5b..067f6b5 100644 --- a/src/utils.py +++ b/src/utils.py @@ -16,5 +16,4 @@ def generate_student_identifier(name, cpf, course_name): def generate_subject_identifier(course, name): logging.info(f"Generate identifier for subject '{name}'") - # return uuid.uuid5(uuid.NAMESPACE_URL, str(f"{name}{course}")).hex - return f"{course}_{name}" + return uuid.uuid5(uuid.NAMESPACE_URL, str(f"{name}{course}")).hex diff --git a/tests/test_cli.py b/tests/test_cli.py index 41acb7e..b851cc8 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,36 +1,191 @@ +import pytest from src import cli_helper +from src import utils -def test_cancel_course_by_cli(set_in_memory_database): - name = "any" - assert cli_helper.cancel_course(set_in_memory_database, name) == True +@pytest.mark.parametrize( + "identifier,expected", + [ + ("2024-1", True), + ("invalid", False), + ], +) +def test_close_semester_by_cli(set_in_memory_database, identifier, expected): + assert cli_helper.close_semester(set_in_memory_database, identifier) == expected -def test_deactivate_course_by_cli(set_in_memory_database): - name = "act" - assert cli_helper.deactivate_course(set_in_memory_database, name) == True +@pytest.mark.parametrize( + "course_name,subject_name,expected", + [ + ("any", "any1", True), + ("invalid", "any1", False), + ("any", "invalid", False), + ], +) +def test_remove_subject_by_cli( + set_in_memory_database, course_name, subject_name, expected +): + assert ( + cli_helper.remove_subject(set_in_memory_database, course_name, subject_name) + == expected + ) + + +@pytest.mark.parametrize( + "name,max_enrollment,expected", + [ + ("new", 1, True), + ("any", 1, False), # duplicated + ("new", -1, False), + ], +) +def test_create_course_by_cli(set_in_memory_database, name, max_enrollment, expected): + assert ( + cli_helper.create_course(set_in_memory_database, name, max_enrollment) + == expected + ) + + +@pytest.mark.parametrize( + "couse_name,subject_name,expected", + [ + ("any", "new", True), + ("any", "any1", False), # duplicated + ("invalid", "new", False), + ], +) +def test_add_subject_to_course_by_cli( + set_in_memory_database, couse_name, subject_name, expected +): + assert ( + cli_helper.add_subject_to_course( + set_in_memory_database, couse_name, subject_name + ) + == expected + ) + + +@pytest.mark.parametrize( + "name,cpf,course_name,expected", + [ + ("any", "123.456.789-10", "any", True), + ("invalid", "123.456.789-10", "any", False), + ], +) +def test_calculate_student_gpa_by_cli( + set_in_memory_database, name, cpf, course_name, expected +): + database = set_in_memory_database + student_identifier = utils.generate_student_identifier(name, cpf, course_name) + cli_helper.enroll_student(database, name, cpf, course_name) + + assert cli_helper.calculate_student_gpa(database, student_identifier) == expected + + +def test_calculate_student_gpa_by_cli_when_student_is_valid_but_not_enrolled( + set_in_memory_database, +): + database = set_in_memory_database + student_identifier = utils.generate_student_identifier( + "any", "123.456.789-10", "any" + ) + assert cli_helper.calculate_student_gpa(database, student_identifier) == False -def test_activate_course_cli(set_in_memory_database): - name = "deact" - assert cli_helper.activate_course(set_in_memory_database, name) == True +@pytest.mark.parametrize( + "name,cpf,course_name,subject_name,expected", + [ + ("any", "123.456.789-10", "any", "any1", True), + ("any", "123.456.789-10", "any", "invalid", False), + ], +) +def test_take_subject_by_cli( + set_in_memory_database, name, cpf, course_name, subject_name, expected +): + database = set_in_memory_database + student_identifier = utils.generate_student_identifier(name, cpf, course_name) + cli_helper.enroll_student(database, name, cpf, course_name) -def test_enroll_student_to_invalid_course(set_in_memory_database): - name = "any" - cpf = "123.456.789-10" - course_identifier = "invalid" assert ( - cli_helper.enroll_student(set_in_memory_database, name, cpf, course_identifier) - == False + cli_helper.take_subject(database, student_identifier, subject_name) == expected ) -def test_enroll_student_to_course_by_cli(set_in_memory_database): - name = "any" - cpf = "123.456.789-10" - course_identifier = "any" +def test_take_subject_by_cli_when_student_is_valid_but_not_enrolled( + set_in_memory_database, +): + database = set_in_memory_database + student_identifier = utils.generate_student_identifier( + "any", "123.456.789-10", "any" + ) + + assert cli_helper.take_subject(database, student_identifier, "any1") == False + + +@pytest.mark.parametrize( + "course_name,expected", + [ + ("any", True), + ("invalid", False), + ], +) +def test_list_student_details_by_cli(set_in_memory_database, course_name, expected): + assert ( + cli_helper.list_student_details(set_in_memory_database, course_name) == expected + ) + + +def test_list_all_course_details_by_cli(set_in_memory_database): + assert cli_helper.list_all_course_details(set_in_memory_database) == True + + +@pytest.mark.parametrize( + "name,expected", + [ + ("act", True), + ("invalid", False), + ], +) +def test_cancel_course_by_cli(set_in_memory_database, name, expected): + assert cli_helper.cancel_course(set_in_memory_database, name) == expected + + +@pytest.mark.parametrize( + "name,expected", + [ + ("act", True), + ("invalid", False), + ], +) +def test_deactivate_course_by_cli(set_in_memory_database, name, expected): + assert cli_helper.deactivate_course(set_in_memory_database, name) == expected + + +@pytest.mark.parametrize( + "name,expected", + [ + ("deact", True), + ("invalid", False), + ], +) +def test_activate_course_cli(set_in_memory_database, name, expected): + assert cli_helper.activate_course(set_in_memory_database, name) == expected + + +@pytest.mark.parametrize( + "name,cpf,course_name, expected", + [ + ("any", "123.456.789-10", "any", True), + ("invalid", "123.456.789-10", "any", False), + ("any", "invalid", "any", False), + ("any", "123.456.789-10", "invalid", False), + ], +) +def test_enroll_student_to_course_by_cli( + set_in_memory_database, name, cpf, course_name, expected +): assert ( - cli_helper.enroll_student(set_in_memory_database, name, cpf, course_identifier) - == True + cli_helper.enroll_student(set_in_memory_database, name, cpf, course_name) + == expected ) diff --git a/tests/test_course_handler.py b/tests/test_course_handler.py index 33f2869..f3bbfa9 100644 --- a/tests/test_course_handler.py +++ b/tests/test_course_handler.py @@ -62,8 +62,7 @@ def test_list_empty_when_no_enrolled_students(set_in_memory_database): course_handler.name = "any" actual = course_handler.list_student_details() - - assert len(actual) == 0 + assert len(actual["students"]) == 0 def test_list_enrolled_students_in_specific_course(set_in_memory_database): @@ -79,7 +78,6 @@ def test_list_enrolled_students_in_specific_course(set_in_memory_database): actual = course_handler.list_student_details() - assert utils.generate_student_identifier(name, cpf, course) in actual assert len(actual) == 1 diff --git a/tests/test_student_handler.py b/tests/test_student_handler.py index 96ce407..4f4404b 100644 --- a/tests/test_student_handler.py +++ b/tests/test_student_handler.py @@ -8,6 +8,7 @@ from src import utils from src.services.grade_calculator import GradeCalculator from src.services.subject_handler import SubjectHandler +from src.services.course_handler import CourseHandler @pytest.mark.parametrize( @@ -159,12 +160,24 @@ def test_enroll_invalid_student_to_course_return_error(set_in_memory_database): def test_enroll_student_to_course(set_in_memory_database): + database = set_in_memory_database name = "any" cpf = "123.456.789-10" - student = StudentHandler(set_in_memory_database) - student.name = name - student.cpf = cpf + student_handler = StudentHandler(database) + student_handler.name = name + student_handler.cpf = cpf course_name = "any" identifier = utils.generate_student_identifier(name, cpf, course_name) - assert student.enroll_to_course(course_name) == identifier + assert student_handler.enroll_to_course(course_name) == identifier + + # post condition + database.student.load(student_handler.identifier) + assert database.student.identifier == student_handler.identifier + assert database.student.state == "enrolled" + assert database.student.course == course_name + assert database.student.gpa == 0 + + course = CourseHandler(database) + course.load_from_database(course_name) + assert student_handler.identifier in course.enrolled_students