diff --git a/.gitignore b/.gitignore index 46d1927..dc96dc5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,9 @@ unit_test/ -venv/ +venv*/ tdd_detroid/ htmlcov/ *.db *.pyc +*.log .coverage -*.whl -*.tar.gz +.~lock.architecture.odp# diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 12766da..0000000 --- a/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ - -FROM python:3.6 - -WORKDIR /webapp -COPY . /webapp -RUN chmod -R 777 /webapp/utils -RUN /webapp/utils/remove_files.sh -RUN /webapp/utils/setup.sh -CMD /webapp/utils/start.sh \ No newline at end of file diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 335ea9d..0000000 --- a/LICENSE +++ /dev/null @@ -1,19 +0,0 @@ -Copyright (c) 2018 The Python Packaging Authority - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 5ed8293..ea94b6d 100644 --- a/README.md +++ b/README.md @@ -1,223 +1,129 @@ -# Control of grades of college students +{Translated from Portuguese to English using AI} +# College Student Grade Control ## Introduction -This project aims to exercise the "code smells" related to unit tests using an application that simulates a real business. -The code will be developed with the Detroit-style TDD technique, hence the name of the repository. -The application simulates the control of students' grades at a university. Below is the specification of the application: -Definition of Done: -1. Unit tests are covering the functionality -2. The functionality is designed to be used via CLI -3. Data is being saved to the database -# How to use -## Option 1: docker-compose -Run the commnands and access "http://localhost:5000" -``` -chmod +x utils/build_container.sh -chmod +x utils/build_dist.sh -./utils/build_container.sh -./utils/build_dist.sh -docker-compose up -d -docker exec -it tdd-detroid python /webapp/cli.py init-bd -``` -## Option 2: Kubernetes -Cosidering the Minikube and Virtual Box are installed, [Push the image](#publish-image) to Docker Hub and run the commands -``` -minikube start --driver=virtualbox -``` -### Declarative way -``` -kubectl create -f kubernetes/deployments-aio-pod.yaml -``` - -Add the $(minikube ip) to /etc/hosts like "minikube" DNS +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. -### Or Imperative way -``` -kubectl create deployment tdd-detroid --image=douglasdcm/tdd-detroid -kubectl get pods -``` -Create services -``` -kubectl expose deployment tdd-detroid --type=NodePort --port=5000 -kubectl get services -``` +# Architecture +[text](architecture.odp) -Install database -``` -kubectl exec -it -c tdd-detroid -- python /webapp/cli.py init-b -``` -### Check access +# Setup and Test ``` -curl $(minikube ip): -``` -and navigate to ```http://$(minikube ip):``` -## Option 3: locally by Live Server -1. At the root of the project, run the commands below: -``` -python3.6 -m venv venv +python3.11 -m venv venv source venv/bin/activate -pip install --upgrade pip pip install -r requirements.txt -python -m pytest -python -m build -cp -r dist/ src/ui/ -``` -2. This project was developed using VSCode, so install [Live Server](https://github.com/ritwickdey/vscode-live-server-plus-plus) on VSCode -3. Navigate to the index.html file and start the Live Server as per the documentation -4. Fill in the form and confirm to see the records created -## Option 4: locally by Flask -1. Run the commands above to setup the application -2. Start the aplication running the commnad bellow and access "http://localhost:5000" -``` -python app.py -``` -## Option 5: by CLI -5. It is possible to interact with the application using the command line. See more details in the help menu. -``` -python cli. init-bd -python cli.py --help -``` -# Config database manually +pytest --random-order ``` -python cli.py init-bd -docker exec -it postgres psql -U postgres -create schema api; -create role web_anon nologin; - -grant usage on schema api to web_anon; -grant select on api.alunos to web_anon; -grant insert on api.alunos to web_anon; -grant delete on api.alunos to web_anon; - -grant select on api.courses to web_anon; -grant insert on api.courses to web_anon; -grant delete on api.courses to web_anon; - -GRANT ALL ON TABLE api.alunos TO postgres; -GRANT ALL ON TABLE api.alunos TO web_anon; - -GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA api TO web_anon; - -create role authenticator noinherit login password 'mysecretpassword'; -grant web_anon to authenticator; +# Installation ``` - -# Technologies -This project uses pure Python in the backend, html for the screens, sqlite to store the records, PyScript to create the elements of the screens, Liver Server, docker-compose or kubernetes as options to bring the application up. -# Limitations -When used by the graphical interface, the inserted records are lost every time the screen is rendered. Persistence is only possible when used from the command line -# Publish image -Run the commands +# create database +sqlite3 university.db +sqlite> .quit ``` -docker login -u someuser -p somepassword -docker push douglasdcm/tdd-detroid:latest -``` -# Notes about Kubernetes -``` -app > docker/containerd > deployment() > k8s pod > k8s node > k8s cluster - -if app down > k8s creats a new - -if node down > k8s find other - -* Notes -k8s pods: 1 or more containers - - https://kubernetes.io/docs/concepts/workloads/pods/ - -k8s node: virtual/phisical, has a container server to run pods - - https://kubernetes.io/docs/concepts/architecture/nodes/ - -k8s cluster: control plane/client [kubectl] -``` -# Debug -Access a container in a pod +# CLI Usage ``` -kubectl exec -it tdd-detroid-57fbdd679-4sj87 -c tdd-detroid bash -``` -# Links -## K8s -Minikube: https://kubernetes.io/docs/tutorials/hello-minikube/
-Concepts: https://kubernetes.io/docs/concepts/architecture/
-API: https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#PodSpec
-Cluster Networking: https://kubernetes.io/docs/concepts/cluster-administration/networking/ - -## HTML -Attributes: https://www.w3schools.com/html/html_attributes.asp
-Tags: https://www.w3schools.com/tags/tag_pre.asp
-CSS: https://www.w3schools.com/css/css_howto.asp +python cli.py --help +Usage: cli.py [OPTIONS] -## PyScript -Home: https://pyscript.net
-Issues: https://github.com/pyscript/pyscript/issues?q=websocket +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. -## Pyodide -FAQ: https://pyodide.org/en/stable/usage/faq.html#how-can-i-send-a-python-object-from-my-server-to-pyodide -## PostgreSQL -Doc: https://docs.sqlalchemy.org/en/14/dialects/postgresql.html
-Doc: https://docs.sqlalchemy.org/en/14/core/pooling.html#pool-disconnects
-Erros: https://docs.sqlalchemy.org/en/14/errors.html#error-f405 +# enroll student to course +python cli.py --name any --cpf 123.456.789-10 --course-identifier any -## Websocket -Doc: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API +``` -## PostgREST -Home: https://postgrest.org/en/stable/
-Quick Start https://postgrest.org/en/stable/tutorials/tut0.html
-Operations: https://postgrest.org/en/stable/api.html#insertions +# Application specification -# Deliveries +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 "performance coefficient" (CR)~~ -2. ~~The CR is the average of the student's grades in the subjects already taken~~ -3. The student is considered approved at the university if his CR is above or equal to 7 (seven) at the end of the course -4. ~~If the student takes the same subject more than once, the highest grade will be considered in the calculation of the CR~~ -5. ~~The faculty will initially have 3 courses with 3 subjects each~~ -6. ~~The subjects of each course may have the same names, but they will be differentiated by the unique identifier number (niu)~~ -7. The system should calculate the student's situation taking into account the subjects taken and the total number of subjects in each course -8. The student will only be able to attend subjects of his/her course -9. ~~The courses must have a unique identifier and name~~ -10. ~~The course names can be the same, but the unique identifier of each course must be different~~ -11. ~~A course cannot have two subjects with the same name, even if the niu is different~~ -12. ~~A student's maximum grade in a subject is 10~~ -13. ~~A student's minimum grade in a subject is 0~~ -14. The student can lock the course and in this case he cannot update his grades or subjects taken -15. The course coordinator can list the students, grades in each subject and students' credits. -16. The Student can unlock the course and their situation reverts to the previous one -17. ~~Students must have names~~ -18. The general coordinator can list the students and grades in each subject and credit of each student from all courses -19. The course coordinator can eliminate subjects and in this case students cannot update their grades in this subject -20. Students can only update their grades in the subjects they are enrolled in -21. The name of the courses and subjects 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 more than one course -25. The coordinator can list the students, subjects and grades, and credits of all your courses (coordinator of more than one course) -26. The course can be canceled -27. Canceled courses cannot accept student applications -28. Canceled courses cannot have coordinators -29. Each subject can have a maximum of 30 students enrolled -30. ~~The student must enroll in at least 3 subjects~~ -31. If the number of missing subjects of a student is less than 3, he can enroll in 1 subject -32. If the student does not enroll in the minimum number of subjects per semester, he will automatically fail -33. The student must have a CPF validated in the external CPF validation system (government system) -34. Add the name of the course in the coordinators' reports -35. The Student is only approved if he obtains the minimum grade in all subjects of the course, even if his credit is above the minimum -36. User must be able to create students with basic information -37. User must be able to enroll student in a course -38. The user must be able to create courses with the minimum number of subjects -39. The admin and only the admin should 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 he must be able to list the list of students by course -42. The administrator and only he should be able to list the list of subjects per student -43. The student must be able to list all the subjects of his course only -44. The student must be able to list all of the subjects studied -45. The student must be able to list the missing materials -46. ​​The administrator should be able to list all course coordinators with available informationop -47. The student has 10 semesters to graduate -48. If the student exceeds 10 semesters, he is automatically failed -49. The coordinator can only be the coordinator of a maximum of 3 courses -50. The general coordinator cannot be a course coordinator \ No newline at end of file +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. **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. **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. +9. **DONE** Courses must have a unique identifier and name. + + +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. **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. **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. **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. **DONE** The student can only enroll in one course. +24. The coordinator can coordinate a maximum of three courses. +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. **DONE** Canceled courses cannot accept student enrollments. + +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. **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. + +37. ~~The user must be able to enroll the student in a course.~~ +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. +42. The administrator, and only the administrator, must be able to list the list of subjects per student. +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. **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. + +# 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 +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 diff --git a/alembic.ini b/alembic.ini deleted file mode 100644 index d1db428..0000000 --- a/alembic.ini +++ /dev/null @@ -1,104 +0,0 @@ -# A generic, single database configuration. - -[alembic] -# path to migration scripts -script_location = alembic - -# template used to generate migration files -# file_template = %%(rev)s_%%(slug)s - -# sys.path path, will be prepended to sys.path if present. -# defaults to the current working directory. -prepend_sys_path = . - -# timezone to use when rendering the date within the migration file -# as well as the filename. -# If specified, requires the python-dateutil library that can be -# installed by adding `alembic[tz]` to the pip requirements -# string value is passed to dateutil.tz.gettz() -# leave blank for localtime -# timezone = - -# max length of characters to apply to the -# "slug" field -# truncate_slug_length = 40 - -# set to 'true' to run the environment during -# the 'revision' command, regardless of autogenerate -# revision_environment = false - -# set to 'true' to allow .pyc and .pyo files without -# a source .py file to be detected as revisions in the -# versions/ directory -# sourceless = false - -# version location specification; This defaults -# to alembic/versions. When using multiple version -# directories, initial revisions must be specified with --version-path. -# The path separator used here should be the separator specified by "version_path_separator" below. -# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions - -# version path separator; As mentioned above, this is the character used to split -# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. -# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. -# Valid values for version_path_separator are: -# -# version_path_separator = : -# version_path_separator = ; -# version_path_separator = space -version_path_separator = os # Use os.pathsep. Default configuration used for new projects. - -# the output encoding used when revision files -# are written from script.py.mako -# output_encoding = utf-8 - -; sqlalchemy.url = driver://user:pass@localhost/dbname -sqlalchemy.url = sqlite:///production.db -; sqlalchemy.url = sqlite:///production.db - - -[post_write_hooks] -# post_write_hooks defines scripts or Python functions that are run -# on newly generated revision scripts. See the documentation for further -# detail and examples - -# format using "black" - use the console_scripts runner, against the "black" entrypoint -# hooks = black -# black.type = console_scripts -# black.entrypoint = black -# black.options = -l 79 REVISION_SCRIPT_FILENAME - -# Logging configuration -[loggers] -keys = root,sqlalchemy,alembic - -[handlers] -keys = console - -[formatters] -keys = generic - -[logger_root] -level = WARN -handlers = console -qualname = - -[logger_sqlalchemy] -level = WARN -handlers = -qualname = sqlalchemy.engine - -[logger_alembic] -level = INFO -handlers = -qualname = alembic - -[handler_console] -class = StreamHandler -args = (sys.stderr,) -level = NOTSET -formatter = generic - -[formatter_generic] -format = %(levelname)-5.5s [%(name)s] %(message)s -datefmt = %H:%M:%S diff --git a/alembic/README b/alembic/README deleted file mode 100644 index 98e4f9c..0000000 --- a/alembic/README +++ /dev/null @@ -1 +0,0 @@ -Generic single-database configuration. \ No newline at end of file diff --git a/alembic/env.py b/alembic/env.py deleted file mode 100644 index 646f960..0000000 --- a/alembic/env.py +++ /dev/null @@ -1,78 +0,0 @@ -from logging.config import fileConfig - -from sqlalchemy import engine_from_config -from sqlalchemy import pool - -from alembic import context - -# this is the Alembic Config object, which provides -# access to the values within the .ini file in use. -config = context.config - -# Interpret the config file for Python logging. -# This line sets up loggers basically. -if config.config_file_name is not None: - fileConfig(config.config_file_name) - -# add your model's MetaData object here -# for 'autogenerate' support -# from myapp import mymodel -# target_metadata = mymodel.Base.metadata -from src.utils.sql_client import Base - -target_metadata = Base.metadata - -# other values from the config, defined by the needs of env.py, -# can be acquired: -# my_important_option = config.get_main_option("my_important_option") -# ... etc. - - -def run_migrations_offline(): - """Run migrations in 'offline' mode. - - This configures the context with just a URL - and not an Engine, though an Engine is acceptable - here as well. By skipping the Engine creation - we don't even need a DBAPI to be available. - - Calls to context.execute() here emit the given string to the - script output. - - """ - url = config.get_main_option("sqlalchemy.url") - context.configure( - url=url, - target_metadata=target_metadata, - literal_binds=True, - dialect_opts={"paramstyle": "named"}, - ) - - with context.begin_transaction(): - context.run_migrations() - - -def run_migrations_online(): - """Run migrations in 'online' mode. - - In this scenario we need to create an Engine - and associate a connection with the context. - - """ - connectable = engine_from_config( - config.get_section(config.config_ini_section), - prefix="sqlalchemy.", - poolclass=pool.NullPool, - ) - - with connectable.connect() as connection: - context.configure(connection=connection, target_metadata=target_metadata) - - with context.begin_transaction(): - context.run_migrations() - - -if context.is_offline_mode(): - run_migrations_offline() -else: - run_migrations_online() diff --git a/alembic/script.py.mako b/alembic/script.py.mako deleted file mode 100644 index 2c01563..0000000 --- a/alembic/script.py.mako +++ /dev/null @@ -1,24 +0,0 @@ -"""${message} - -Revision ID: ${up_revision} -Revises: ${down_revision | comma,n} -Create Date: ${create_date} - -""" -from alembic import op -import sqlalchemy as sa -${imports if imports else ""} - -# revision identifiers, used by Alembic. -revision = ${repr(up_revision)} -down_revision = ${repr(down_revision)} -branch_labels = ${repr(branch_labels)} -depends_on = ${repr(depends_on)} - - -def upgrade(): - ${upgrades if upgrades else "pass"} - - -def downgrade(): - ${downgrades if downgrades else "pass"} diff --git a/alembic/versions/4d1381e1a05a_adiciona_coluna_alunos_curso_id.py b/alembic/versions/4d1381e1a05a_adiciona_coluna_alunos_curso_id.py deleted file mode 100644 index 3e782c7..0000000 --- a/alembic/versions/4d1381e1a05a_adiciona_coluna_alunos_curso_id.py +++ /dev/null @@ -1,66 +0,0 @@ -"""adiciona coluna alunos.course_id - -Revision ID: 4d1381e1a05a -Revises: 56415884d069 -Create Date: 2022-11-18 23:15:53.703048 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = "4d1381e1a05a" -down_revision = "56415884d069" -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - try: - op.drop_table("courses") - op.drop_table("alunos") - op.drop_table("materias") - except Exception: - pass - - # ### end Alembic commands ### - downgrade() - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - try: - op.drop_table("courses") - op.drop_table("alunos") - op.drop_table("materias") - except Exception: - pass - - op.create_table( - "courses", - sa.Column("id", sa.INTEGER(), nullable=False), - sa.Column("name", sa.VARCHAR(), nullable=True), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "alunos", - sa.Column("id", sa.INTEGER(), nullable=False), - sa.Column("name", sa.VARCHAR(), nullable=True), - sa.Column("coef_rend", sa.INTEGER(), nullable=True), - sa.Column("course_id", sa.INTEGER(), nullable=True), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "materias", - sa.Column("id", sa.INTEGER(), nullable=False), - sa.Column("name", sa.VARCHAR(), nullable=True), - sa.Column("course_id", sa.INTEGER(), nullable=True), - sa.ForeignKeyConstraint( - ["course_id"], - ["courses.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - # ### end Alembic commands ### diff --git a/alembic/versions/4eef0f0750b6_adicionando_id_em_materia_aluno.py b/alembic/versions/4eef0f0750b6_adicionando_id_em_materia_aluno.py deleted file mode 100644 index a7ada97..0000000 --- a/alembic/versions/4eef0f0750b6_adicionando_id_em_materia_aluno.py +++ /dev/null @@ -1,24 +0,0 @@ -"""adicionando id em materia_aluno - -Revision ID: 4eef0f0750b6 -Revises: 4d1381e1a05a -Create Date: 2022-11-24 01:51:58.834308 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '4eef0f0750b6' -down_revision = '4d1381e1a05a' -branch_labels = None -depends_on = None - - -def upgrade(): - pass - - -def downgrade(): - pass diff --git a/alembic/versions/56415884d069_adiciona_coluna_alunos_coef_rend.py b/alembic/versions/56415884d069_adiciona_coluna_alunos_coef_rend.py deleted file mode 100644 index 8b34930..0000000 --- a/alembic/versions/56415884d069_adiciona_coluna_alunos_coef_rend.py +++ /dev/null @@ -1,64 +0,0 @@ -"""adiciona coluna alunos.coef_rend - -Revision ID: 56415884d069 -Revises: f5e1be582417 -Create Date: 2022-11-18 22:55:40.919973 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = "56415884d069" -down_revision = "f5e1be582417" -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - try: - op.drop_table("courses") - op.drop_table("alunos") - op.drop_table("materias") - except Exception: - pass - - # ### end Alembic commands ### - downgrade() - - -def downgrade(): - try: - op.drop_table("courses") - op.drop_table("alunos") - op.drop_table("materias") - except Exception: - pass - # ### commands auto generated by Alembic - please adjust! ### - op.create_table( - "courses", - sa.Column("id", sa.INTEGER(), nullable=False), - sa.Column("name", sa.VARCHAR(), nullable=True), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "alunos", - sa.Column("id", sa.INTEGER(), nullable=False), - sa.Column("name", sa.VARCHAR(), nullable=True), - sa.Column("coef_rend", sa.INTEGER(), nullable=True), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "materias", - sa.Column("id", sa.INTEGER(), nullable=False), - sa.Column("name", sa.VARCHAR(), nullable=True), - sa.Column("course", sa.INTEGER(), nullable=True), - sa.ForeignKeyConstraint( - ["course"], - ["courses.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - # ### end Alembic commands ### diff --git a/alembic/versions/a00e45c8086b_adiciona_coluna_aluno_nota_em_materia_.py b/alembic/versions/a00e45c8086b_adiciona_coluna_aluno_nota_em_materia_.py deleted file mode 100644 index 1cf2452..0000000 --- a/alembic/versions/a00e45c8086b_adiciona_coluna_aluno_nota_em_materia_.py +++ /dev/null @@ -1,57 +0,0 @@ -"""adiciona coluna aluno_nota em materia_aluno - -Revision ID: a00e45c8086b -Revises: aff28b5a130a -Create Date: 2022-11-29 21:49:38.905376 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = "a00e45c8086b" -down_revision = "aff28b5a130a" -branch_labels = None -depends_on = None - - -def upgrade(): - try: - op.drop_table("materia_aluno") - except Exception: - pass - op.create_table( - "materia_aluno", - sa.Column("aluno_nota", sa.INTEGER(), nullable=True), - sa.Column("discipline_id", sa.INTEGER(), nullable=True), - sa.Column("student_id", sa.INTEGER(), nullable=True), - sa.ForeignKeyConstraint( - ["discipline_id"], - ["materias.id"], - ), - sa.ForeignKeyConstraint( - ["student_id"], - ["alunos.id"], - ), - ) - - -def downgrade(): - try: - op.drop_table("materia_aluno") - except Exception: - pass - op.create_table( - "materia_aluno", - sa.Column("discipline_id", sa.INTEGER(), nullable=True), - sa.Column("student_id", sa.INTEGER(), nullable=True), - sa.ForeignKeyConstraint( - ["discipline_id"], - ["materias.id"], - ), - sa.ForeignKeyConstraint( - ["student_id"], - ["alunos.id"], - ), - ) diff --git a/alembic/versions/aff28b5a130a_muda_coluna_curso_de_tabela_materias_.py b/alembic/versions/aff28b5a130a_muda_coluna_curso_de_tabela_materias_.py deleted file mode 100644 index d1467b2..0000000 --- a/alembic/versions/aff28b5a130a_muda_coluna_curso_de_tabela_materias_.py +++ /dev/null @@ -1,24 +0,0 @@ -"""muda coluna course de tabela materias para course_id - -Revision ID: aff28b5a130a -Revises: 4eef0f0750b6 -Create Date: 2022-11-24 21:59:14.166121 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = 'aff28b5a130a' -down_revision = '4eef0f0750b6' -branch_labels = None -depends_on = None - - -def upgrade(): - pass - - -def downgrade(): - pass diff --git a/alembic/versions/f5e1be582417_first_autogenerated.py b/alembic/versions/f5e1be582417_first_autogenerated.py deleted file mode 100644 index 3d5c6f7..0000000 --- a/alembic/versions/f5e1be582417_first_autogenerated.py +++ /dev/null @@ -1,62 +0,0 @@ -"""first autogenerated - -Revision ID: f5e1be582417 -Revises: -Create Date: 2022-11-18 22:47:42.815378 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = "f5e1be582417" -down_revision = None -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - try: - op.drop_table("courses") - op.drop_table("alunos") - op.drop_table("materias") - except Exception: - pass - # ### end Alembic commands ### - downgrade() - - -def downgrade(): - try: - op.drop_table("courses") - op.drop_table("alunos") - op.drop_table("materias") - except Exception: - pass - # ### commands auto generated by Alembic - please adjust! ### - op.create_table( - "materias", - sa.Column("id", sa.INTEGER(), nullable=False), - sa.Column("name", sa.VARCHAR(), nullable=True), - sa.Column("course", sa.INTEGER(), nullable=False), - sa.ForeignKeyConstraint( - ["course"], - ["courses.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "alunos", - sa.Column("id", sa.INTEGER(), nullable=False), - sa.Column("name", sa.VARCHAR(), nullable=True), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "courses", - sa.Column("id", sa.INTEGER(), nullable=False), - sa.Column("name", sa.VARCHAR(), nullable=True), - sa.PrimaryKeyConstraint("id"), - ) - # ### end Alembic commands ### diff --git a/app.py b/app.py deleted file mode 100644 index ee23106..0000000 --- a/app.py +++ /dev/null @@ -1,97 +0,0 @@ -#!flask/bin/python -from os import environ -from flask import Flask, render_template, request -from src.controllers import courses, students, disciplines -from json import dumps -from src.utils.exceptions import ErrorStudent, ErrorCourse, ErrorDiscipline -import logging -logging.basicConfig(format='%(asctime)s %(levelname)-8s %(message)s', - filename='application.log', level=logging.ERROR, - datefmt='%Y-%m-%d %H:%M:%S') - -app = Flask(__name__, static_folder="templates", static_url_path="") -SUCCESS = dumps({"status": "ok", "message": "success"}) - - -def __failed(e, detail=True): - fail = {"status": "failed", "message": None} - if detail: - fail["message"] = str(e) - return dumps(fail) - fail["message"] = e - logging.error(str(e)) - return dumps(fail) - - -@app.route("/") -def output(): - # serve index template - return render_template("index.html") - - -@app.route("/course", methods=["POST"]) -def course(): - try: - name = request.json["name"] - courses.create(name) - return SUCCESS - except (ErrorCourse, ErrorStudent) as e: - return __failed(e, detail=True) - except Exception as e: - return __failed(e) - -@app.route("/discipline", methods=["POST"]) -def discipline(): - try: - name = request.json["name"] - course_id = request.json["course_id"] - disciplines.create(name, course_id) - return SUCCESS - except (ErrorCourse, ErrorDiscipline) as e: - return __failed(e) - except Exception as e: - return __failed(e) - -@app.route("/subscription-discipline", methods=["POST"]) -def subscription_discipline(): - try: - student_id = request.json["student_id"] - discipline_id = request.json["discipline_id"] - students.subscribe_in_discipline(student_id, discipline_id) - return SUCCESS - except (ErrorStudent) as e: - return __failed(e) - except Exception as e: - return __failed(e) - -@app.route("/subscription-course", methods=["POST"]) -def subscription_course(): - try: - student_id = request.json["student_id"] - course_id = request.json["course_id"] - students.subscribe_in_course(student_id, course_id) - return SUCCESS - except (ErrorStudent) as e: - return __failed(e) - except Exception as e: - return __failed(e) - -@app.route("/student", methods=["POST"]) -def student(): - try: - name = request.json["name"] - students.create(name) - return SUCCESS - except (ErrorCourse, ErrorStudent) as e: - return __failed(e) - except Exception as e: - logging.error(str(e)) - return __failed(e) - - -if __name__ == "__main__": - # run! - from waitress import serve - - port = int(environ.get("PORT", 5000)) - serve(app, host="0.0.0.0", port=port) diff --git a/architecture.odp b/architecture.odp new file mode 100644 index 0000000..8cd3c01 Binary files /dev/null and b/architecture.odp differ diff --git a/cli.py b/cli.py index bd3b6d9..e00bac5 100644 --- a/cli.py +++ b/cli.py @@ -1,8 +1,16 @@ +import logging import click -from src.utils.utils import inicializa_tabelas -from src.commands.student import aluno -from src.commands.discipline import materia -from src.commands.course import course +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: [%(filename)s:%(lineno)s - %(funcName)20s() ] %(message)s", + filemode="a", + level="ERROR", +) @click.group() @@ -11,15 +19,218 @@ def cli(): pass -@cli.command() -def init_bd(): - inicializa_tabelas() - click.echo("Database initialized") +@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: + raise + + +@click.command() +def list_courses(): + try: + database = Database() + cli_helper.list_all_course_details(database) + except Exception: + raise + + +@click.command() +@click.option( + "--course-name", + prompt="Course name", + help="Course name.", +) +def list_students(course_name): + try: + database = Database() + cli_helper.list_student_details(database, course_name) + except Exception: + raise + + +@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): + 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.") +@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): + database = Database() + cli_helper.activate_course(database, name) + + +@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-name", + prompt="Course number identifier", + help="Course number identifier.", +) +def enroll_student(name, cpf, course_name): + try: + database = Database() + cli_helper.enroll_student(database, name, cpf, course_name) + except Exception: + raise + + +@click.command() +@click.option( + "--student-identifier", + prompt="Student identifier", + help="Student identifier number.", + hide_input=True, +) +def calculate_gpa(student_identifier): + try: + database = Database() + cli_helper.calculate_student_gpa(database, student_identifier) + except Exception: + raise + + +@click.command() +@click.option( + "--student-identifier", + prompt="Student identifier", + help="Student identifier number.", + hide_input=True, +) +@click.option( + "--subject-name", + prompt="Subject name", + help="The name of the subject the student wants to take.", +) +@click.option( + "--grade", + type=float, + 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: + raise + + +@click.command() +@click.option( + "--student-identifier", + prompt="Student identifier", + help="Student identifier number.", + hide_input=True, +) +@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: + raise + + +@click.command() +@click.option( + "--student-identifier", + prompt="Student identifier", + help="Student identifier number.", + hide_input=True, +) +def lock_course(student_identifier): + try: + database = Database() + cli_helper.lock_course(database, student_identifier) + except Exception: + raise + + +@click.command() +@click.option( + "--student-identifier", + prompt="Student identifier", + help="Student identifier number.", + hide_input=True, +) +def unlock_course(student_identifier): + try: + database = Database() + cli_helper.unlock_course(database, student_identifier) + except Exception: + raise + + +cli.add_command(enroll_student) +cli.add_command(take_subject) +cli.add_command(update_grade) +cli.add_command(calculate_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) +cli.add_command(list_students) +cli.add_command(list_courses) +cli.add_command(create_course) +cli.add_command(add_subject) -cli.add_command(aluno) -cli.add_command(materia) -cli.add_command(course) +cli.add_command(close_semester) if __name__ == "__main__": diff --git a/coverage.sh b/coverage.sh new file mode 100755 index 0000000..a6e11af --- /dev/null +++ b/coverage.sh @@ -0,0 +1,3 @@ +coverage run --include='cli.py' --source='src' -m pytest +coverage report +coverage html \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml deleted file mode 100644 index d25403c..0000000 --- a/docker-compose.yaml +++ /dev/null @@ -1,33 +0,0 @@ -version: "3.6" - -services: - tdd_detroid: - image: douglasdcm/tdd-detroid - container_name: tdd-detroid - volumes: - - .:/webapp - ports: - - 30500:5000 - working_dir: / - depends_on: - - postgres - env_file: .env_docker - - postgres: - image: postgres - container_name: postgres - volumes: - - vpm-data:/var/lib/pgsql - ports: - - 5432:5432 - environment: - POSTGRES_PASSWORD: postgresql - # tdd-postgrest: - # image: douglasdcm/tdd-postgrest - # container_name: tdd-postgrest - # ports: - # - 30501:3000 - # working_dir: / - -volumes: - vpm-data: diff --git a/for_admin.py b/for_admin.py new file mode 100644 index 0000000..77f0e9d --- /dev/null +++ b/for_admin.py @@ -0,0 +1,31 @@ +from src.database import Database + +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("aline", "028.745.462.18", "adm") +db.enrollment.populate("joana", "038.745.452.19", "port") +db.enrollment.populate( + "any", "123.456.789-10", "any" +) # 290f2113c2e6579c8bb6ec395ea56572 + +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", 30) # ef15a071407953bd858cfca59ad99056 +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") + +db.semester.populate("2024-1", "open") diff --git a/kubernetes/config-map.yaml b/kubernetes/config-map.yaml deleted file mode 100644 index 32b6809..0000000 --- a/kubernetes/config-map.yaml +++ /dev/null @@ -1,12 +0,0 @@ -# Create ConfigMap postgres-secret for the postgres app -# Define default database name, user, and password -apiVersion: v1 -kind: ConfigMap -metadata: - name: postgres-secret - labels: - app: postgres -data: - POSTGRES_DB: postgres - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgresql \ No newline at end of file diff --git a/kubernetes/deployments-aio-pod.yaml b/kubernetes/deployments-aio-pod.yaml deleted file mode 100644 index 46f0006..0000000 --- a/kubernetes/deployments-aio-pod.yaml +++ /dev/null @@ -1,105 +0,0 @@ -# https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/ -apiVersion: apps/v1 -kind: Deployment -metadata: # ObjectMeta - name: tdd-detroid - labels: - app: tdd-detroid -spec: # DeploymentSpec - selector: # LabelSelector - matchLabels: - app: tdd-detroid - template: # PodTemplateSpec - metadata: # ObjectMeta - name: tdd-detroid - labels: - app: tdd-detroid - spec: # PodSpec - containers: # Containers - - name: tdd-detroid - image: douglasdcm/tdd-detroid - ports: - - containerPort: 5000 - name: detroid-port - # - name: tdd-postgrest - # image: douglasdcm/tdd-postgrest - # ports: - # - containerPort: 3000 - - name: postgres - image: postgres # Docker image - imagePullPolicy: "IfNotPresent" - ports: - - containerPort: 5432 # Exposing the container port 5432 for PostgreSQL client connections. - envFrom: - - configMapRef: - name: postgres-secret # Using the ConfigMap postgres-secret - volumeMounts: - - mountPath: /var/lib/postgresql/data - name: postgresdata - volumes: - - name: postgresdata - persistentVolumeClaim: - claimName: postgres-volume-claim ---- -apiVersion: v1 -kind: Service -metadata: # ObjectMeta - name: tdd-detroid -spec: # ServiceSpec - selector: - app: tdd-detroid - ports: - - port: 5000 - name: tdd-detroid - targetPort: detroid-port - nodePort: 30500 - - port: 3000 - name: postgrest - nodePort: 30501 - # - port: 5432 - # name: postgres - # nodePort: 30502 - type: NodePort ---- -apiVersion: v1 -kind: PersistentVolume # Create PV -metadata: - name: postgres-volume # Sets PV name - labels: - type: local # Sets PV's type - app: postgres -spec: - storageClassName: manual - capacity: - storage: 10Gi # Sets PV's size - accessModes: - - ReadWriteMany - hostPath: - path: "/data/postgresql" # Sets PV's host path ---- -apiVersion: v1 -kind: PersistentVolumeClaim # Create PVC -metadata: - name: postgres-volume-claim # Sets PVC's name - labels: - app: postgres # Defines app to create PVC for -spec: - storageClassName: manual - accessModes: - - ReadWriteMany - resources: - requests: - storage: 10Gi # Sets PVC's size ---- -# Create ConfigMap postgres-secret for the postgres app -# Define default database name, user, and password -apiVersion: v1 -kind: ConfigMap -metadata: - name: postgres-secret - labels: - app: postgres -data: - POSTGRES_DB: postgres - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgresql \ No newline at end of file diff --git a/kubernetes/deployments-list.yaml b/kubernetes/deployments-list.yaml deleted file mode 100644 index 83a3c9e..0000000 --- a/kubernetes/deployments-list.yaml +++ /dev/null @@ -1,79 +0,0 @@ -# https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/deployment-v1/#DeploymentSpec -apiVersion: apps/v1 -kind: DeploymentList -metadata: # ListMeta -items: - - apiVersion: apps/v1 - kind: Deployment - metadata: # ObjectMeta - name: tdd-detroid-deployment - labels: - app: tdd-detroid - spec: # DeploymentSpec - selector: # LabelSelector - matchLabels: - app: tdd-detroid - template: # PodTemplateSpec - metadata: # ObjectMeta - name: tdd-detroid - labels: - app: tdd-detroid - spec: # PodSpec - containers: # Containers - - name: tdd-detroid - image: douglasdcm/tdd-detroid - ports: - - containerPort: 5000 - # - apiVersion: apps/v1 - # kind: Deployment - # metadata: # ObjectMeta - # name: tdd-postgrest-deployment - # labels: - # app: tdd-detroid - # spec: # DeploymentSpec - # selector: # LabelSelector - # matchLabels: - # app: tdd-detroid - # template: # PodTemplateSpec - # metadata: # ObjectMeta - # name: tdd-detroid - # labels: - # app: tdd-detroid - # spec: # PodSpec - # containers: # Containers - # - name: tdd-postgrest - # image: douglasdcm/tdd-postgrest - # ports: - # - containerPort: 3000 - - apiVersion: apps/v1 - kind: Deployment - metadata: # ObjectMeta - name: postgres-deployment - labels: - app: tdd-detroid - spec: # DeploymentSpec - selector: # LabelSelector - matchLabels: - app: tdd-detroid - template: # PodTemplateSpec - metadata: # ObjectMeta - name: postgres - labels: - app: tdd-detroid - spec: # PodSpec - containers: # Containers - - name: postgres - image: postgres # Docker image - imagePullPolicy: "IfNotPresent" - ports: - - containerPort: 5432 # Exposing the container port 5432 for PostgreSQL client connections. - envFrom: - - configMapRef: - name: postgres-secret # Using the ConfigMap postgres-secret - volumeMounts: - - mountPath: /var/lib/postgresql/data - name: postgresdata - volumes: - - name: postgresdata - persistentVolumeClaim: - claimName: postgres-volume-claim \ No newline at end of file diff --git a/kubernetes/persistent-volume-clain.yaml b/kubernetes/persistent-volume-clain.yaml deleted file mode 100644 index e1b80b0..0000000 --- a/kubernetes/persistent-volume-clain.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: v1 -kind: PersistentVolumeClaim # Create PVC -metadata: - name: postgres-volume-claim # Sets PVC's name - labels: - app: postgres # Defines app to create PVC for -spec: - storageClassName: manual - accessModes: - - ReadWriteMany - resources: - requests: - storage: 10Gi # Sets PVC's size \ No newline at end of file diff --git a/kubernetes/persistent-volume.yaml b/kubernetes/persistent-volume.yaml deleted file mode 100644 index 7989669..0000000 --- a/kubernetes/persistent-volume.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: v1 -kind: PersistentVolume # Create PV -metadata: - name: postgres-volume # Sets PV name - labels: - type: local # Sets PV's type - app: postgres -spec: - storageClassName: manual - capacity: - storage: 10Gi # Sets PV's size - accessModes: - - ReadWriteMany - hostPath: - path: "/data/postgresql" # Sets PV's host path \ No newline at end of file diff --git a/kubernetes/services.yaml b/kubernetes/services.yaml deleted file mode 100644 index 6d31203..0000000 --- a/kubernetes/services.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: # ObjectMeta - name: tdd-detroid -spec: # ServiceSpec - selector: - app: tdd-detroid - ports: - - port: 5000 - nodePort: 30500 - type: NodePort \ No newline at end of file diff --git a/manual_smoke_test.sh b/manual_smoke_test.sh new file mode 100755 index 0000000..7fab6fc --- /dev/null +++ b/manual_smoke_test.sh @@ -0,0 +1,25 @@ +# set -x +rm university.db +python for_admin.py +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 +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 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 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 + +python cli.py close-semester --identifier 2024-1 +python cli.py list-courses \ No newline at end of file diff --git a/postgrest/Dockerfile b/postgrest/Dockerfile deleted file mode 100644 index fa7e397..0000000 --- a/postgrest/Dockerfile +++ /dev/null @@ -1,5 +0,0 @@ -FROM python:3.6 - -WORKDIR /webapp -COPY . /webapp -CMD /webapp/postgrest /webapp/postgrest.config \ No newline at end of file diff --git a/postgrest/postgrest b/postgrest/postgrest deleted file mode 100755 index e6d44ab..0000000 Binary files a/postgrest/postgrest and /dev/null differ diff --git a/postgrest/postgrest.config b/postgrest/postgrest.config deleted file mode 100644 index 8eb4d4a..0000000 --- a/postgrest/postgrest.config +++ /dev/null @@ -1,3 +0,0 @@ -db-uri = "postgres://postgres:postgresql@localhost:5432/postgres" -db-schemas = "api" -db-anon-role = "web_anon" diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 2932f60..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,18 +0,0 @@ -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[project] -name = "src" -version = "0.0.1" -authors = [ - { name="Example Author", email="author@example.com" }, -] -description = "A small example package" -readme = "README.md" -requires-python = ">=3.6" -classifiers = [ - "Programming Language :: Python :: 3", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", -] diff --git a/requirements.txt b/requirements.txt index 021347f..92e406d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,14 +1,2 @@ -alembic==1.7.7 -attrs==22.1.0 -black==22.8.0 -build==0.9.0 -click==8.0.4 -colorama==0.4.5 -# coverage==6.2 -dataclasses==0.8 -docopt==0.6.2 -pg8000==1.26.0 -python-dotenv==0.20.0 -SQLAlchemy==1.4.46 -Flask==2.0.3 -waitress==2.0.0 \ No newline at end of file +pytest +pytest-random-order diff --git a/src/alembic/README b/src/alembic/README deleted file mode 100644 index 98e4f9c..0000000 --- a/src/alembic/README +++ /dev/null @@ -1 +0,0 @@ -Generic single-database configuration. \ No newline at end of file diff --git a/src/alembic/env.py b/src/alembic/env.py deleted file mode 100644 index 646f960..0000000 --- a/src/alembic/env.py +++ /dev/null @@ -1,78 +0,0 @@ -from logging.config import fileConfig - -from sqlalchemy import engine_from_config -from sqlalchemy import pool - -from alembic import context - -# this is the Alembic Config object, which provides -# access to the values within the .ini file in use. -config = context.config - -# Interpret the config file for Python logging. -# This line sets up loggers basically. -if config.config_file_name is not None: - fileConfig(config.config_file_name) - -# add your model's MetaData object here -# for 'autogenerate' support -# from myapp import mymodel -# target_metadata = mymodel.Base.metadata -from src.utils.sql_client import Base - -target_metadata = Base.metadata - -# other values from the config, defined by the needs of env.py, -# can be acquired: -# my_important_option = config.get_main_option("my_important_option") -# ... etc. - - -def run_migrations_offline(): - """Run migrations in 'offline' mode. - - This configures the context with just a URL - and not an Engine, though an Engine is acceptable - here as well. By skipping the Engine creation - we don't even need a DBAPI to be available. - - Calls to context.execute() here emit the given string to the - script output. - - """ - url = config.get_main_option("sqlalchemy.url") - context.configure( - url=url, - target_metadata=target_metadata, - literal_binds=True, - dialect_opts={"paramstyle": "named"}, - ) - - with context.begin_transaction(): - context.run_migrations() - - -def run_migrations_online(): - """Run migrations in 'online' mode. - - In this scenario we need to create an Engine - and associate a connection with the context. - - """ - connectable = engine_from_config( - config.get_section(config.config_ini_section), - prefix="sqlalchemy.", - poolclass=pool.NullPool, - ) - - with connectable.connect() as connection: - context.configure(connection=connection, target_metadata=target_metadata) - - with context.begin_transaction(): - context.run_migrations() - - -if context.is_offline_mode(): - run_migrations_offline() -else: - run_migrations_online() diff --git a/src/alembic/script.py.mako b/src/alembic/script.py.mako deleted file mode 100644 index 2c01563..0000000 --- a/src/alembic/script.py.mako +++ /dev/null @@ -1,24 +0,0 @@ -"""${message} - -Revision ID: ${up_revision} -Revises: ${down_revision | comma,n} -Create Date: ${create_date} - -""" -from alembic import op -import sqlalchemy as sa -${imports if imports else ""} - -# revision identifiers, used by Alembic. -revision = ${repr(up_revision)} -down_revision = ${repr(down_revision)} -branch_labels = ${repr(branch_labels)} -depends_on = ${repr(depends_on)} - - -def upgrade(): - ${upgrades if upgrades else "pass"} - - -def downgrade(): - ${downgrades if downgrades else "pass"} diff --git a/src/alembic/versions/4d1381e1a05a_adiciona_coluna_alunos_curso_id.py b/src/alembic/versions/4d1381e1a05a_adiciona_coluna_alunos_curso_id.py deleted file mode 100644 index 3e782c7..0000000 --- a/src/alembic/versions/4d1381e1a05a_adiciona_coluna_alunos_curso_id.py +++ /dev/null @@ -1,66 +0,0 @@ -"""adiciona coluna alunos.course_id - -Revision ID: 4d1381e1a05a -Revises: 56415884d069 -Create Date: 2022-11-18 23:15:53.703048 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = "4d1381e1a05a" -down_revision = "56415884d069" -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - try: - op.drop_table("courses") - op.drop_table("alunos") - op.drop_table("materias") - except Exception: - pass - - # ### end Alembic commands ### - downgrade() - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - try: - op.drop_table("courses") - op.drop_table("alunos") - op.drop_table("materias") - except Exception: - pass - - op.create_table( - "courses", - sa.Column("id", sa.INTEGER(), nullable=False), - sa.Column("name", sa.VARCHAR(), nullable=True), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "alunos", - sa.Column("id", sa.INTEGER(), nullable=False), - sa.Column("name", sa.VARCHAR(), nullable=True), - sa.Column("coef_rend", sa.INTEGER(), nullable=True), - sa.Column("course_id", sa.INTEGER(), nullable=True), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "materias", - sa.Column("id", sa.INTEGER(), nullable=False), - sa.Column("name", sa.VARCHAR(), nullable=True), - sa.Column("course_id", sa.INTEGER(), nullable=True), - sa.ForeignKeyConstraint( - ["course_id"], - ["courses.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - # ### end Alembic commands ### diff --git a/src/alembic/versions/4eef0f0750b6_adicionando_id_em_materia_aluno.py b/src/alembic/versions/4eef0f0750b6_adicionando_id_em_materia_aluno.py deleted file mode 100644 index a7ada97..0000000 --- a/src/alembic/versions/4eef0f0750b6_adicionando_id_em_materia_aluno.py +++ /dev/null @@ -1,24 +0,0 @@ -"""adicionando id em materia_aluno - -Revision ID: 4eef0f0750b6 -Revises: 4d1381e1a05a -Create Date: 2022-11-24 01:51:58.834308 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '4eef0f0750b6' -down_revision = '4d1381e1a05a' -branch_labels = None -depends_on = None - - -def upgrade(): - pass - - -def downgrade(): - pass diff --git a/src/alembic/versions/56415884d069_adiciona_coluna_alunos_coef_rend.py b/src/alembic/versions/56415884d069_adiciona_coluna_alunos_coef_rend.py deleted file mode 100644 index 8b34930..0000000 --- a/src/alembic/versions/56415884d069_adiciona_coluna_alunos_coef_rend.py +++ /dev/null @@ -1,64 +0,0 @@ -"""adiciona coluna alunos.coef_rend - -Revision ID: 56415884d069 -Revises: f5e1be582417 -Create Date: 2022-11-18 22:55:40.919973 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = "56415884d069" -down_revision = "f5e1be582417" -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - try: - op.drop_table("courses") - op.drop_table("alunos") - op.drop_table("materias") - except Exception: - pass - - # ### end Alembic commands ### - downgrade() - - -def downgrade(): - try: - op.drop_table("courses") - op.drop_table("alunos") - op.drop_table("materias") - except Exception: - pass - # ### commands auto generated by Alembic - please adjust! ### - op.create_table( - "courses", - sa.Column("id", sa.INTEGER(), nullable=False), - sa.Column("name", sa.VARCHAR(), nullable=True), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "alunos", - sa.Column("id", sa.INTEGER(), nullable=False), - sa.Column("name", sa.VARCHAR(), nullable=True), - sa.Column("coef_rend", sa.INTEGER(), nullable=True), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "materias", - sa.Column("id", sa.INTEGER(), nullable=False), - sa.Column("name", sa.VARCHAR(), nullable=True), - sa.Column("course", sa.INTEGER(), nullable=True), - sa.ForeignKeyConstraint( - ["course"], - ["courses.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - # ### end Alembic commands ### diff --git a/src/alembic/versions/a00e45c8086b_adiciona_coluna_aluno_nota_em_materia_.py b/src/alembic/versions/a00e45c8086b_adiciona_coluna_aluno_nota_em_materia_.py deleted file mode 100644 index 1cf2452..0000000 --- a/src/alembic/versions/a00e45c8086b_adiciona_coluna_aluno_nota_em_materia_.py +++ /dev/null @@ -1,57 +0,0 @@ -"""adiciona coluna aluno_nota em materia_aluno - -Revision ID: a00e45c8086b -Revises: aff28b5a130a -Create Date: 2022-11-29 21:49:38.905376 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = "a00e45c8086b" -down_revision = "aff28b5a130a" -branch_labels = None -depends_on = None - - -def upgrade(): - try: - op.drop_table("materia_aluno") - except Exception: - pass - op.create_table( - "materia_aluno", - sa.Column("aluno_nota", sa.INTEGER(), nullable=True), - sa.Column("discipline_id", sa.INTEGER(), nullable=True), - sa.Column("student_id", sa.INTEGER(), nullable=True), - sa.ForeignKeyConstraint( - ["discipline_id"], - ["materias.id"], - ), - sa.ForeignKeyConstraint( - ["student_id"], - ["alunos.id"], - ), - ) - - -def downgrade(): - try: - op.drop_table("materia_aluno") - except Exception: - pass - op.create_table( - "materia_aluno", - sa.Column("discipline_id", sa.INTEGER(), nullable=True), - sa.Column("student_id", sa.INTEGER(), nullable=True), - sa.ForeignKeyConstraint( - ["discipline_id"], - ["materias.id"], - ), - sa.ForeignKeyConstraint( - ["student_id"], - ["alunos.id"], - ), - ) diff --git a/src/alembic/versions/aff28b5a130a_muda_coluna_curso_de_tabela_materias_.py b/src/alembic/versions/aff28b5a130a_muda_coluna_curso_de_tabela_materias_.py deleted file mode 100644 index d1467b2..0000000 --- a/src/alembic/versions/aff28b5a130a_muda_coluna_curso_de_tabela_materias_.py +++ /dev/null @@ -1,24 +0,0 @@ -"""muda coluna course de tabela materias para course_id - -Revision ID: aff28b5a130a -Revises: 4eef0f0750b6 -Create Date: 2022-11-24 21:59:14.166121 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = 'aff28b5a130a' -down_revision = '4eef0f0750b6' -branch_labels = None -depends_on = None - - -def upgrade(): - pass - - -def downgrade(): - pass diff --git a/src/alembic/versions/f5e1be582417_first_autogenerated.py b/src/alembic/versions/f5e1be582417_first_autogenerated.py deleted file mode 100644 index 3d5c6f7..0000000 --- a/src/alembic/versions/f5e1be582417_first_autogenerated.py +++ /dev/null @@ -1,62 +0,0 @@ -"""first autogenerated - -Revision ID: f5e1be582417 -Revises: -Create Date: 2022-11-18 22:47:42.815378 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = "f5e1be582417" -down_revision = None -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - try: - op.drop_table("courses") - op.drop_table("alunos") - op.drop_table("materias") - except Exception: - pass - # ### end Alembic commands ### - downgrade() - - -def downgrade(): - try: - op.drop_table("courses") - op.drop_table("alunos") - op.drop_table("materias") - except Exception: - pass - # ### commands auto generated by Alembic - please adjust! ### - op.create_table( - "materias", - sa.Column("id", sa.INTEGER(), nullable=False), - sa.Column("name", sa.VARCHAR(), nullable=True), - sa.Column("course", sa.INTEGER(), nullable=False), - sa.ForeignKeyConstraint( - ["course"], - ["courses.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "alunos", - sa.Column("id", sa.INTEGER(), nullable=False), - sa.Column("name", sa.VARCHAR(), nullable=True), - sa.PrimaryKeyConstraint("id"), - ) - op.create_table( - "courses", - sa.Column("id", sa.INTEGER(), nullable=False), - sa.Column("name", sa.VARCHAR(), nullable=True), - sa.PrimaryKeyConstraint("id"), - ) - # ### end Alembic commands ### diff --git a/src/cli_helper.py b/src/cli_helper.py new file mode 100644 index 0000000..a136deb --- /dev/null +++ b/src/cli_helper.py @@ -0,0 +1,212 @@ +import logging +import json +from src.services.student_handler import ( + StudentHandler, +) +from src.services.course_handler import ( + CourseHandler, +) +from src.services.grade_calculator import GradeCalculator +from src.services.subject_handler import SubjectHandler +from src.services.semester_monitor import ( + SemesterMonitor, +) + + +def close_semester(database, identifier): + try: + course_handler = SemesterMonitor(database, identifier) + course_handler.close() + print(f"Semester '{identifier}' closed.") + return True + except Exception as e: + logging.error(str(e)) + print(str(e)) + return False + + +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 Exception as e: + logging.error(str(e)) + print(str(e)) + return False + + +def cancel_course(database, name): + try: + course_handler = CourseHandler(database) + course_handler.load_from_database(name) + course_handler.cancel() + print(f"Course '{name}' cancelled.") + return True + except Exception as e: + logging.error(str(e)) + print(str(e)) + return False + + +def deactivate_course(database, name): + try: + course_handler = CourseHandler(database) + course_handler.load_from_database(name) + course_handler.deactivate() + print(f"Course '{name}' deactivated.") + return True + except Exception as e: + logging.error(str(e)) + print(str(e)) + return False + + +def activate_course(database, name): + try: + course_handler = CourseHandler(database) + course_handler.load_from_database(name) + course_handler.activate() + print(f"Course '{name}' activated.") + return True + except Exception as e: + logging.error(str(e)) + print(str(e)) + 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 Exception as e: + logging.error(str(e)) + print(str(e)) + 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 Exception as e: + logging.error(str(e)) + 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 is '{gpa}'.") + return True + except Exception as e: + logging.error(str(e)) + 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 toke subject '{subject_name}'.") + return True + except Exception as e: + logging.error(str(e)) + 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 locked the course.") + return True + except Exception as e: + logging.error(str(e)) + 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 unlocked the course.") + return True + except Exception as e: + logging.error(str(e)) + 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 updated grade of subject '{subject_name}' to '{grade}'.") + return True + except Exception as e: + logging.error(str(e)) + print(str(e)) + return False + + +def enroll_student(database, name, cpf, course_name): + try: + student = StudentHandler(database) + student.name = name + student.cpf = cpf + 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 Exception as e: + logging.error(str(e)) + print(str(e)) + return False + # print(str(e)) + return False + + +def list_student_details(database, course_name): + try: + course_handler = CourseHandler(database) + course_handler.name = course_name + students = course_handler.list_student_details() + print(f"List of students in course {course_name}:") + print(json.dumps(students, sort_keys=True, indent=4)) + return True + except Exception as e: + logging.error(str(e)) + print(str(e)) + 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 Exception as e: + logging.error(str(e)) + print(str(e)) + return False + + +class CommandError(Exception): + pass diff --git a/src/commands/__init__.py b/src/commands/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/commands/course.py b/src/commands/course.py deleted file mode 100644 index 219f67e..0000000 --- a/src/commands/course.py +++ /dev/null @@ -1,21 +0,0 @@ -import click -from src.controllers import courses -from src.schemes.course import CourseDB -from src.utils import sql_client - - -@click.group() -def course(): - # do nothing # - pass - - -@course.command() -@click.option("--name", required=True, help="Course name") -def create(name): - try: - courses.create(name) - id_ = sql_client.get_maximum(CourseDB).id - click.echo(f"course definido: id {id_}, name {name}") - except Exception as e: - click.echo(e) diff --git a/src/commands/discipline.py b/src/commands/discipline.py deleted file mode 100644 index c5eee47..0000000 --- a/src/commands/discipline.py +++ /dev/null @@ -1,20 +0,0 @@ -import click -from src.controllers import disciplines - - -@click.group() -def materia(): - # do nothing # - pass - - -@materia.command() -@click.option("--name", required=True, help="Discipline name") -@click.option("--course-id", type=int, required=True, help="Cours identification") -def create(name, course_id): - try: - disciplines.create(name, course_id) - id_ = disciplines.get_maximum().id - click.echo(f"Materia definida: id {id_}, name {name}") - except Exception as e: - click.echo(e) diff --git a/src/commands/student.py b/src/commands/student.py deleted file mode 100644 index 32911c0..0000000 --- a/src/commands/student.py +++ /dev/null @@ -1,54 +0,0 @@ -import click -from src.controllers import students -from src.schemes.student import StudentDB -from src.utils import sql_client - - -@click.group() -def aluno(): - # do nothing # - pass - - -@aluno.command() -@click.option("--student-id", type=int, required=True, help="Student identification") -@click.option("--discipline-id", type=int, required=True, help="Discipline ientification") -@click.option("--nota", type=int, required=True, help="Student grade") -def lanca_nota(student_id, discipline_id, nota): - try: - students.set_grade(student_id, discipline_id, nota) - click.echo( - f"Nota {nota} do aluno {student_id} na materia {discipline_id} lancada com sucesso" - ) - except Exception as e: - click.echo(e) - - -@aluno.command() -@click.option("--student-id", type=int, required=True, help="Student identification") -@click.option("--discipline-id", type=int, required=True, help="Discipline identification") -def inscreve_materia(student_id, discipline_id): - try: - students.subscribe_in_discipline(student_id, discipline_id) - click.echo(f"Aluno {student_id} inscrito na materia {discipline_id}") - except Exception as e: - click.echo(e) - - -@aluno.command() -@click.option("--name", required=True, help="Student name") -def create(name): - students.create(name) - id_ = sql_client.get_maximum(StudentDB).id - click.echo(f"Aluno definido: id {id_}, name {name}") - - -@aluno.command() -@click.option("--student-id", type=int, required=True, help="Student identification") -@click.option("--course-id", type=int, required=True, help="Course identification") -def inscreve_course(student_id, course_id): - try: - students.subscribe_in_course(student_id, course_id) - click.echo(f"Aluno inscrito no course {course_id}") - except Exception as e: - click.echo(e) diff --git a/src/constants.py b/src/constants.py new file mode 100644 index 0000000..b04c3cc --- /dev/null +++ b/src/constants.py @@ -0,0 +1,7 @@ +DUMMY_IDENTIFIER = -1 +SUBJECT_IN_PROGRESS = "inprogress" +SUBJECT_FAILED = "failed" +SUBJECT_PASSED = "passed" +STUDENT_APPROVED = "approved" +STUDENT_FAILED = "failed" +MAX_SEMESTERS_TO_FINISH_COURSE = 10 diff --git a/src/controllers/__init__.py b/src/controllers/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/controllers/courses.py b/src/controllers/courses.py deleted file mode 100644 index e223bd4..0000000 --- a/src/controllers/courses.py +++ /dev/null @@ -1,60 +0,0 @@ -from src.utils.exceptions import ErrorCourse, ErrorDatabase, ErrorStudent -from sqlalchemy.orm import Query -from src.schemes.course import CourseDB -from src.utils import sql_client -from src.schemes.discipline import MateriaBd - - -def check_name(name): - if len(name.strip()) == 0: - raise ErrorCourse("name do course invalido") - - -def get_course(course_id): - try: - return sql_client.get(CourseDB, course_id) - except ErrorDatabase: - raise ErrorStudent(f"course {course_id} não existe") - - -def check_exists(course_id): - get_course(course_id) - - -def check_exists_three(): - query_courses = Query([CourseDB]) - - resultado = len(sql_client.run_query(query_courses)) - if resultado < 3: - return - - query_materias = Query([MateriaBd]).group_by( - MateriaBd.course_id, MateriaBd.id - ) - resultado = len(sql_client.run_query(query_materias)) - - if resultado < 3: - raise ErrorCourse( - "Necessários 3 courses com 3 três matérias para se criar novos courses" - ) - - -def check_non_existent(name): - query = Query(CourseDB).filter(CourseDB.name == name) - if len(sql_client.run_query(query)) > 0: - raise ErrorCourse(f"Existe outro course com o name {name}") - - -def get(id_): - return sql_client.get(CourseDB, id_) - - -def get_all(): - return sql_client.get_all(CourseDB) - - -def create(name): - check_name(name) - check_exists_three() - check_non_existent(name) - sql_client.create(CourseDB(name=name)) diff --git a/src/controllers/disciplines.py b/src/controllers/disciplines.py deleted file mode 100644 index cd05fae..0000000 --- a/src/controllers/disciplines.py +++ /dev/null @@ -1,64 +0,0 @@ -from sqlalchemy.orm import Query -from src.schemes.course import CourseDB -from src.schemes.discipline import MateriaBd -from src.utils.exceptions import ErrorDatabase, ErrorDiscipline, ErrorInvalidInteger -from src.utils import sql_client -from src.utils.utils import convert_id_to_integer - - -def check_exists(discipline_id, course_id): - query = Query([MateriaBd]).filter( - MateriaBd.id == discipline_id, MateriaBd.course_id == course_id - ) - if len(sql_client.run_query(query)) == 0: - raise ErrorDiscipline(f"Matéria {discipline_id} não existe no course {course_id}") - - -def __verifica_duplicidade(name, course_id): - query = Query([MateriaBd]).filter( - MateriaBd.name == name, MateriaBd.course_id == course_id - ) - if sql_client.run_query(query): - raise ErrorDiscipline("O course já possui uma matéria com este name") - - -def __existem_3_courses(): - if len(sql_client.get_all(CourseDB)) < 3: - raise ErrorDiscipline("Necessários 3 courses para se criar a primeira matéria") - - -def __existe_course(course_id): - try: - sql_client.get(CourseDB, course_id) - except ErrorDatabase: - raise ErrorDiscipline(f"course {course_id} não existe") - - -def get(id_): - return sql_client.get(MateriaBd, id_) - - -def get_all(): - return sql_client.get_all(MateriaBd) - - -def get_maximum(): - return sql_client.get_maximum(MateriaBd) - - -def create(name, course_id): - """ - :name name da matéria - :course course associado à matéria - """ - try: - course_id = convert_id_to_integer(course_id) - except ErrorInvalidInteger: - raise ErrorDiscipline("Course id is not a valid integer") - __verifica_duplicidade(name, course_id) - __existem_3_courses() - __existe_course(course_id) - materia = MateriaBd(name=name, course_id=course_id) - sql_client.create(materia) - - diff --git a/src/controllers/students.py b/src/controllers/students.py deleted file mode 100644 index 4de2c34..0000000 --- a/src/controllers/students.py +++ /dev/null @@ -1,180 +0,0 @@ -from src.utils import sql_client -from src.controllers import disciplines -from src.utils.exceptions import ErrorStudent, ErrorDatabase, ErrorInvalidInteger -from src.schemes.student import StudentDB -from src.schemes.for_association import MateriaStudentDB -from sqlalchemy.orm import Query -from src.controllers import courses -from src.utils.utils import convert_id_to_integer - - -def update_grade(student_id, discipline_id, grade): - query = Query(MateriaStudentDB).filter( - MateriaStudentDB.student_id == student_id, - MateriaStudentDB.discipline_id == discipline_id, - ) - mas = sql_client.run_query(query) - mas[0].aluno_nota = grade - sql_client.update() - - -def get_student(student_id): - try: - return sql_client.get(StudentDB, student_id) - except ErrorDatabase: - raise ErrorStudent(f"Aluno {student_id} não existe") - - -def clear_name(name): - name = name.strip() - if len(name) == 0: - raise ErrorStudent("Invalid student name") - return name - - -def get_maximum_id(): - return sql_client.get_maximum(StudentDB).id - - -def get_course_id(student_id): - course_id = sql_client.get(StudentDB, student_id).course_id - if not course_id: - raise ErrorStudent(f"Aluno {student_id} não está inscrito em nenhum course") - return course_id - - -def check_student_already_in_discipline(student_id, discipline_id): - resultado = sql_client.get_all(MateriaStudentDB) - for instancia in resultado: - if instancia.student_id == int(student_id) and instancia.discipline_id == int( - discipline_id - ): - raise ErrorStudent( - f"Aluno {student_id} já está inscrito na matéria {discipline_id}" - ) - - -def check_student_in_discipline(student_id, discipline_id): - resultado = sql_client.get_all(MateriaStudentDB) - for instancia in resultado: - if instancia.student_id == int(student_id) and instancia.discipline_id == int( - discipline_id - ): - return - raise ErrorStudent(f"Aluno {student_id} não está inscrito na matéria {discipline_id}") - - -def check_grade_boundaries(nota): - if nota > 10: - raise ErrorStudent("Nota não pode ser maior que 10") - if nota < 0: - raise ErrorStudent("Nota não pode ser menor que 0") - - -def __get_disciplines_of_student(student_id): - query = Query(MateriaStudentDB).filter( - MateriaStudentDB.student_id == student_id, - ) - mas = sql_client.run_query(query) - return mas - - -def __subscribe_in_discipline(student_id, discipline_id): - ma = MateriaStudentDB(student_id=student_id, discipline_id=discipline_id) - sql_client.create(ma) - - -def __create(name): - aluno = StudentDB(name=name) - sql_client.create(aluno) - - -def calculate_coef_rend(student_id): - mas = __get_disciplines_of_student(student_id) - conta = 0 - soma_nota = 0 - for ma in mas: - if ma.aluno_nota: - soma_nota += ma.aluno_nota - conta += 1 - aluno = get_student(student_id) - - aluno.coef_rend = round(soma_nota / conta, 1) - sql_client.update() - - -def check_student_in_tree_disciplines(student_id): - resultado = sql_client.get_all(MateriaStudentDB) - qtde_materias = 0 - for instancia in resultado: - if instancia.student_id == student_id: - qtde_materias += 1 - if qtde_materias >= 3: - return - raise ErrorStudent("Aluno deve se inscrever em 3 materias no minimo") - - -def can_subscribe_course(student_id): - aluno = get_student(student_id) - if aluno.course_id is not None: - raise ErrorStudent("Aluno esta inscrito em outro course") - - -def __subscribe_in_course(student_id, course_id): - courses.check_exists(course_id) - student = get_student(student_id) - student.course_id = course_id - sql_client.update() - - -def get_all(): - return sql_client.get_all(StudentDB) - - -def get(id): - try: - return sql_client.get(StudentDB, id) - except ErrorDatabase: - raise ErrorStudent(f"Aluno {id} não existe") - -def set_grade(student_id, discipline_id, grade): - grade = int(grade) - check_grade_boundaries(grade) - check_student_in_discipline(student_id, discipline_id) - update_grade(student_id, discipline_id, grade) - calculate_coef_rend(student_id) - - -def create(name): - name = clear_name(name) - __create(name) - - -def subscribe_in_discipline(student_id, discipline_id): - try: - student_id = convert_id_to_integer(student_id) - except ErrorInvalidInteger: - raise ErrorStudent("The student id is not a valid integer") - try: - discipline_id = convert_id_to_integer(discipline_id) - except ErrorInvalidInteger: - raise ErrorStudent("The discipline id is not a valid integer") - course_id = get_course_id(student_id) - disciplines.check_exists(discipline_id, course_id) - check_student_already_in_discipline(student_id, discipline_id) - __subscribe_in_discipline(student_id, discipline_id) - check_student_in_tree_disciplines(student_id) - - -def subscribe_in_course(student_id, course_id): - try: - student_id = convert_id_to_integer(student_id) - except ErrorInvalidInteger: - raise ErrorStudent("The student id is not a valid integer") - try: - course_id = convert_id_to_integer(course_id) - except ErrorInvalidInteger: - raise ErrorStudent("The course id is not a valid integer") - get_student(student_id) - can_subscribe_course(student_id) - __subscribe_in_course(student_id, course_id) diff --git a/src/database.py b/src/database.py new file mode 100644 index 0000000..fe05548 --- /dev/null +++ b/src/database.py @@ -0,0 +1,658 @@ +import sqlite3 +import logging +from src import utils + + +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 +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) + self.semester = self.DbSemester(con, cur) + + class DbStudent: + TABLE = "students" + name = None + state = None + cpf = None + identifier = None + 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," + " semester_counter)" + ) + + def add(self): + try: + cmd = f""" + INSERT INTO {self.TABLE} VALUES + ('{self.name}', + '{self.state}', + '{self.cpf}', + '{self.identifier}', + {self.gpa}, + '{convert_list_to_csv(self.subjects)}', + '{self.course}', + {self.semester_counter}) + """ + self.cur.execute(cmd) + + self.con.commit() + except Exception as e: + logging.error(str(e)) + raise + + def save(self): + cmd = f""" + 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}'; + """ + 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}', + 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}'" + result = self.cur.execute(cmd).fetchone() + if not result: + raise NotFoundError(f"Student not found in table '{self.TABLE}'.") + self.name = result[0] + self.state = result[1] + self.cpf = result[2] + self.identifier = result[3] + 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 + + class DbEnrollment: + TABLE = "enrollments" + + def __init__(self, con, cur): + self.cur = cur + self.con = con + self.cur.execute( + 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. + # TODO create a public funtion + def populate(self, name, cpf, course_name): + student_identifier = utils.generate_student_identifier( + name, cpf, course_name + ) + self.cur.execute( + f""" + INSERT INTO {self.TABLE} VALUES ('{student_identifier}') + """ + ) + self.con.commit() + + def select(self, student_identifier): + cmd = f"SELECT * FROM {self.TABLE} WHERE student_identifier = '{student_identifier}'" + return self.cur.execute(cmd).fetchone() + + class DbCourse: + TABLE = "courses" + name = None + state = None + identifier = None + enrolled_students = None + max_enrollment = 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 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, state="active", subjects="any1,any2,any3"): + identifier = utils.generate_course_identifier(name) + if len(subjects) == 0: + subjects = [] + else: + 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 + ('{name}', + '{state}', + '{identifier}', + '', + 10, + '{",".join(list_of_subjects)}') + """ + ) + self.con.commit() + + def save(self): + try: + cmd = f""" + UPDATE {self.TABLE} + 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) + self.con.commit() + except Exception as e: + logging.error(str(e)) + raise + + def add(self): + try: + self.cur.execute( + f""" + INSERT INTO {self.TABLE} VALUES + ('{self.name}', + '{self.state}', + '{self.identifier}', + '{convert_list_to_csv(self.enrolled_students)}', + {self.max_enrollment}, + '{convert_list_to_csv(self.subjects)}') + """ + ) + self.con.commit() + except Exception as e: + 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_csv_to_list(the_csv=row[3]) + course_row.max_enrollment = row[4] + course_row.subjects = convert_csv_to_list(row[5]) + courses.append(course_row) + return courses + + def load_from_database(self, name): + 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 = "subjects" + name = None + state = None + enrolled_students = None + 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," + 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. + # TODO create a public funtion + def populate(self, course, name, max_enrollment=10, state="active"): + identifier = utils.generate_subject_identifier(course, name) + self.cur.execute( + f""" + INSERT INTO {self.TABLE} VALUES + ('{name}', + '{state}', + '{identifier}', + '', + {max_enrollment}, + '{course}') + """ + ) + self.con.commit() + + def load(self, subject_identifier): + try: + 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}'" + ) + + 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 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""" + UPDATE {self.TABLE} + SET state = '{self.state}', + enrolled_students = '{self.enrolled_students}', + max_enrollment = {self.max_enrollment} + WHERE identifier = '{self.identifier}'; + """ + self.cur.execute(cmd) + + self.con.commit() + except Exception as e: + 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 + subject_identifier = None + grade = None + subject_situation = None + + TABLE = "grade_calculators" + 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 INTEGER," + " subject_situation)" + ) + + 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 not found in table '{self.TABLE}'") + + 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 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] + 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( + f"""SELECT * FROM {self.TABLE} + WHERE subject_identifier = '{subject_identifier}' + AND student_identifier = '{student_identifier}' + """ + ).fetchone() + if not result: + raise NotFoundError( + f"Student and subject '{subject_identifier}'" + f" not found in table '{self.TABLE}'." + ) + + 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 + + def add(self): + try: + cmd = f""" + INSERT INTO {self.TABLE} VALUES + ('{self.student_identifier}', + '{self.subject_identifier}', + {self.grade}, + '{self.subject_situation}') + """ + self.cur.execute(cmd) + + self.con.commit() + except Exception as e: + logging.error(str(e)) + raise + + def save(self): + try: + cmd = f""" + UPDATE {self.TABLE} + SET grade = {self.grade}, + subject_situation = '{self.subject_situation}' + WHERE student_identifier = '{self.student_identifier}' + AND subject_identifier = '{self.subject_identifier}'; + """ + self.cur.execute(cmd) + + self.con.commit() + except Exception as e: + 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 = "semesters" + 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() + 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}'; + """ + 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: + 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/__init__.py b/src/models/__init__.py similarity index 100% rename from __init__.py rename to src/models/__init__.py diff --git a/src/schemes/__init__.py b/src/schemes/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/schemes/course.py b/src/schemes/course.py deleted file mode 100644 index 40aef18..0000000 --- a/src/schemes/course.py +++ /dev/null @@ -1,12 +0,0 @@ -from sqlalchemy import Column, Integer, String -from sqlalchemy.orm import relationship -from src.utils.sql_client import Base - - -class CourseDB(Base): - __tablename__ = "courses" - - id = Column(Integer, primary_key=True) - name = Column(String) - materia = relationship("MateriaBd") - aluno = relationship("StudentDB") diff --git a/src/schemes/discipline.py b/src/schemes/discipline.py deleted file mode 100644 index 8f28d80..0000000 --- a/src/schemes/discipline.py +++ /dev/null @@ -1,10 +0,0 @@ -from sqlalchemy import Column, Integer, String, ForeignKey -from src.utils.sql_client import Base - - -class MateriaBd(Base): - __tablename__ = "materias" - - id = Column(Integer, primary_key=True) - name = Column(String) - course_id = Column(Integer, ForeignKey("courses.id")) diff --git a/src/schemes/for_association.py b/src/schemes/for_association.py deleted file mode 100644 index fa9fcb9..0000000 --- a/src/schemes/for_association.py +++ /dev/null @@ -1,10 +0,0 @@ -from sqlalchemy import Column, ForeignKey, Integer -from src.utils.sql_client import Base - - -class MateriaStudentDB(Base): - __tablename__ = "materia_aluno" - - aluno_nota = Column(Integer) - discipline_id = Column(ForeignKey("materias.id"), primary_key=True) - student_id = Column(ForeignKey("alunos.id"), primary_key=True) diff --git a/src/schemes/student.py b/src/schemes/student.py deleted file mode 100644 index 9e9d874..0000000 --- a/src/schemes/student.py +++ /dev/null @@ -1,11 +0,0 @@ -from sqlalchemy import Column, Integer, String, ForeignKey, Float -from src.utils.sql_client import Base - - -class StudentDB(Base): - __tablename__ = "alunos" - - id = Column(Integer, primary_key=True) - name = Column(String) - coef_rend = Column(Float) - course_id = Column(Integer, ForeignKey("courses.id")) diff --git a/src/__init__.py b/src/services/__init__.py similarity index 100% rename from src/__init__.py rename to src/services/__init__.py diff --git a/src/services/course_handler.py b/src/services/course_handler.py new file mode 100644 index 0000000..29d8d8b --- /dev/null +++ b/src/services/course_handler.py @@ -0,0 +1,239 @@ +import sqlite3 +import logging +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 + + +class CourseHandler: + 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 + self.__enrolled_students = [] + self.__subjects = [] + 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 + + @property + def state(self): + return self.__state + + @property + def enrolled_students(self): + return self.__enrolled_students + + @property + def subjects(self): + self.__subjects = list(set(self.__subjects)) + return self.__subjects + + @property + def name(self): + return self.__name + + @name.setter + def name(self, value): + self.__check_name_lenght(value) + self.__identifier = utils.generate_course_identifier(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_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 + 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.__check_maximum_enrollment() + 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 NotFoundError as e: + logging.error(str(e)) + raise NonValidCourse(f"Course '{name}' not found.") + except Exception as e: + logging.error(str(e)) + raise + + def list_student_details(self): + self.load_from_database(self.name) + enrolled_students = self.__database.course.enrolled_students + result = {} + + 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 + ) + 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): + all_details = {} + 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): + self.__check_active() + self.load_from_database(self.name) + 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.__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() + + subject_handler = SubjectHandler(self.__database) + subject_handler.name = subject + subject_handler.course = self.name + subject_handler.add() + return True + + def cancel(self): + self.__check_name() + self.load_from_database(self.name) + self.__state = self.CANCELLED + self.save() + return self.__state + + def deactivate(self): + self.__check_name() + self.load_from_database(self.name) + self.__state = self.INACTIVE + self.save() + return self.__state + + def activate(self): + self.__check_name() + self.load_from_database(self.name) + self.__check_minimum_number_of_subjects() + + self.__state = self.ACTIVE + self.save() + return self.__state + + def create(self, course_name, max_enrollmet): + 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): + pass + + +class NonMinimunSubjects(Exception): + pass + + +class NonValidSubject(Exception): + pass 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/enrollment_validator.py b/src/services/enrollment_validator.py new file mode 100644 index 0000000..93daaae --- /dev/null +++ b/src/services/enrollment_validator.py @@ -0,0 +1,16 @@ +from src import utils +from src.database import Database + + +class EnrollmentValidator: + def __init__(self, database: Database): + self.__database = database + + 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) is not None diff --git a/src/services/grade_calculator.py b/src/services/grade_calculator.py new file mode 100644 index 0000000..b9c7c07 --- /dev/null +++ b/src/services/grade_calculator.py @@ -0,0 +1,101 @@ +import logging +from src.database import Database, NotFoundError +from src.constants import SUBJECT_IN_PROGRESS, SUBJECT_FAILED + + +class GradeCalculator: + 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 + 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 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 + + @grade.setter + def grade(self, value): + self.__grade = value + + def load_from_database(self, student_identifier, subject_identifier): + 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 + 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 + ) + + 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): + try: + self.__rows = ( + self.__database.grade_calculator.load_all_by_student_identifier( + student_identifier + ) + ) + except NotFoundError as e: + raise NonValidGradeOperation(f"Student not enrolled to any subject.") + except Exception: + logging.error(str(e)) + raise + total = 0 + for row in self.__rows: + total += row.grade + + return round(total / len(self.__rows), 1) + + +class NonValidGradeOperation(Exception): + pass diff --git a/src/services/semester_monitor.py b/src/services/semester_monitor.py new file mode 100644 index 0000000..03d1ae1 --- /dev/null +++ b/src/services/semester_monitor.py @@ -0,0 +1,135 @@ +import logging +from src.database import Database, NotFoundError +from src.services.student_handler import StudentHandler, NonValidStudent +from src.services.course_handler import CourseHandler +from src.constants import ( + STUDENT_APPROVED, + STUDENT_FAILED, + MAX_SEMESTERS_TO_FINISH_COURSE, + SUBJECT_FAILED, + STUDENT_APPROVED, +) + + +class SemesterMonitor: + + def __init__(self, database: Database, identifier) -> None: + self.__CLOSED = "closed" + self.__OPEN = "open" + self.__identifier = identifier # TODO get next from database + 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 + ) + + 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 + + @property + def state(self): + return self.__state + + @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): + self.__check_identifier() + + self.__database.semester.identifier = self.identifier + 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 + 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: + try: + student_handler = StudentHandler(self.__database, row.identifier) + student_handler.increment_semester() + student_handler.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 + + +class NonValidOperation(Exception): + pass + + +class NonValidSemester(Exception): + pass diff --git a/src/services/student_handler.py b/src/services/student_handler.py new file mode 100644 index 0000000..75c7b56 --- /dev/null +++ b/src/services/student_handler.py @@ -0,0 +1,360 @@ +import logging +from src.services.enrollment_validator import EnrollmentValidator +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 +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, + STUDENT_APPROVED, + STUDENT_FAILED, + MAX_SEMESTERS_TO_FINISH_COURSE, +) + + +class StudentHandler: + class Subject: + identifier = None + grade = None + + def __init__(self, database: Database, identifier=None): + self.__LOCKED = "locked" + self.__ENROLLED = "enrolled" + self.__state = None + self.__gpa = 0 + self.__course = None + self.__name = None + self.__cpf = None + self.__semester_counter = 0 + 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: + self.__identifier = utils.generate_student_identifier( + self.__name, self.__cpf, self.__course + ) + + 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 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 + ) + or enrollment_validator.validate_student_by_identifier(self.__identifier) + ): + raise NonValidStudent( + f"Student '{self.__name}' 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'.") + + 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" + return "passed" + + def __check_valid_subject(self, subject_identifier): + if not subject_identifier in self.subjects: + raise NonValidSubject( + f"The student 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): + 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.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.semester_counter = self.__semester_counter + self.__database.student.save() + except Exception as e: + 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): + self.load_from_database(self.__identifier) + 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): + 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 + def cpf(self, value): + self.__check_cpf_validity(value) + self.__cpf = value + self.__generate_identifier_when_student_ready() + self.__save() + + 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 + ) + except NonValidGradeOperation as e: + raise NonValidGrade(f"Student 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.__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.__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.__subjects.append(subject_identifier) + + grade_calculator = GradeCalculator(self.__database) + 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() + + def enroll_to_course(self, course_name): + try: + self.__check_name() + self.__check_cpf() + self.__check_enrolled_student(course_name) + self.__check_finished_course() + + course = CourseHandler(self.__database) + course.load_from_database(course_name) + self.__course = course_name + self.__generate_identifier_when_student_ready() + 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.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) + grade_calculator.add(self.__identifier, DUMMY_IDENTIFIER, grade=0) + + return self.__identifier + except Exception as e: + logging.error(str(e)) + raise + + def take_subject(self, subject_name): + self.__check_enrolled_student(self.__course) + self.__check_finished_course() + self.load_from_database(self.__identifier) + self.__check_locked() + + 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) + 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 + + self.__check_course_is_same_of_subject(subject_handler) + self.__check_subject_availability(subject_handler) + self.__check_subject_activation(subject_handler) + + self.__subjects.append(subject_identifier) + self.__save() + + 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) + + return True + + def unlock_course(self): + self.__check_enrolled_student(self.__course) + self.__check_finished_course() + self.load_from_database(self.__identifier) + self.__state = self.__ENROLLED + self.__save() + return self.state + + def lock_course(self): + self.__check_enrolled_student(self.__course) + self.__check_finished_course() + self.load_from_database(self.__identifier) + self.__state = self.__LOCKED + 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) + + 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.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(f"Student not found.") + except Exception as e: + logging.error(str(e)) + raise + + +class NonValidStudent(Exception): + pass + + +class NonValidGrade(Exception): + pass diff --git a/src/services/subject_handler.py b/src/services/subject_handler.py new file mode 100644 index 0000000..e774598 --- /dev/null +++ b/src/services/subject_handler.py @@ -0,0 +1,170 @@ +import logging +from src import utils +from src.constants import DUMMY_IDENTIFIER +from src.database import Database + + +class SubjectHandler: + + REMOVED = "removed" + ACTIVE = "active" + + def __init__( + self, database: Database, identifier=DUMMY_IDENTIFIER, course=None + ) -> None: + self.__database = database + 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 + + @property + def state(self): + return self.__state + + @property + def enrolled_students(self): + return self.__enrolled_students + + @property + 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): + self.__check_name_leght(value) + self.__name = value + self.__generate_identifier_when_subject_ready() + + @property + def max_enrollment(self): + return self.__max_enrollment + + @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 + + def is_active(self): + return self.__state == self.ACTIVE + + def activate(self): + self.__check_identifier() + self.load_from_database(self.identifier) + self.__check_removed() + 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): + 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}'." + ) + + if not self.state == self.ACTIVE: + raise NonValidSubject(f"Subject '{self.identifier} is not active.'") + + 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.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) + + 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)) + identifier = subject_identifier + if self.__name: + identifier = self.__name + raise NonValidSubject(f"Subject '{identifier}' not found.") + + +class NonValidSubject(Exception): + pass diff --git a/src/utils.py b/src/utils.py new file mode 100644 index 0000000..067f6b5 --- /dev/null +++ b/src/utils.py @@ -0,0 +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/src/utils/__init__.py b/src/utils/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/utils/config.py b/src/utils/config.py deleted file mode 100644 index 58196bd..0000000 --- a/src/utils/config.py +++ /dev/null @@ -1 +0,0 @@ -DATABASE_NAME = "postgres" diff --git a/src/utils/exceptions.py b/src/utils/exceptions.py deleted file mode 100644 index 70fc84f..0000000 --- a/src/utils/exceptions.py +++ /dev/null @@ -1,16 +0,0 @@ -class ErrorDiscipline(Exception): - pass - -class ErrorInvalidInteger(Exception): - pass - -class ErrorDatabase(Exception): - pass - - -class ErrorCourse(Exception): - pass - - -class ErrorStudent(Exception): - pass diff --git a/src/utils/sql_client.py b/src/utils/sql_client.py deleted file mode 100644 index f308ef7..0000000 --- a/src/utils/sql_client.py +++ /dev/null @@ -1,123 +0,0 @@ -from sqlalchemy.orm import sessionmaker -from sqlalchemy import create_engine -from sqlalchemy.orm import declarative_base -from sqlalchemy.orm import Query -from src.utils.exceptions import ErrorDatabase -from sqlalchemy.schema import MetaData -from os import getenv -from dotenv import load_dotenv -import logging - -# https://docs.sqlalchemy.org/en/20/orm/mapping_styles.html#orm-declarative-mapping -# https://docs.sqlalchemy.org/en/20/orm/mapping_api.html#sqlalchemy.orm.declarative_base -# https://docs.sqlalchemy.org/en/20/orm/mapping_api.html#sqlalchemy.orm.registry.generate_base - -load_dotenv() - -database_name = getenv("DATABASE_NAME", "postgres") -metadata = MetaData(schema="api") -Base = declarative_base(metadata=metadata) -engine = create_engine( - f"postgresql+pg8000://postgres:postgresql@{database_name}/postgres", - echo=False, -) - - -def get_session(): - - _session_maker = sessionmaker(bind=engine) - return _session_maker() - - -session = get_session() - - -def get_maximum(modelo): - resultado = session.query(modelo).all() - if len(resultado) > 0: - return resultado[-1] - return resultado - - -def update(): - try: - session.flush() - session.commit() - except Exception as e: - logging.error(str(e)) - raise e - - -def get(model, id_): - result = session.query(model).filter(model.id == id_).first() - if not result: - raise ErrorDatabase(f"Registro {id_} do tipo {model.__name__} nao encontrado") - return result - - -def get_all(modelo): - return session.query(modelo).all() - - -def create(instancia): - try: - session.add(instancia) - session.commit() - except Exception as e: - logging.error(str(e)) - raise e - - -def run_query(query: Query): - return query.with_session(session).all() - - -def create_schema(): - statements = [ - "create schema api;", - "create role web_anon nologin;", - ] - for statement in statements: - try: - with session as s: - s.execute(statement) - s.commit() - # catch any exception - except (BaseException) as e: - logging.error(str(e)) - continue - - -def grant_permissions(): - statements = [ - "grant usage on schema api to web_anon;", - "grant select on api.alunos to web_anon;", - "grant insert on api.alunos to web_anon;", - "grant delete on api.alunos to web_anon;", - "grant update on api.alunos to web_anon;", - "grant select on api.courses to web_anon;", - "grant insert on api.courses to web_anon;", - "grant delete on api.courses to web_anon;", - "GRANT ALL ON TABLE api.alunos TO postgres;", - "GRANT ALL ON TABLE api.alunos TO web_anon;", - "GRANT ALL ON TABLE api.courses TO postgres;", - "GRANT ALL ON TABLE api.courses TO web_anon;", - "GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA api TO web_anon;", - "create role authenticator noinherit login password 'mysecretpassword';", - "grant web_anon to authenticator;", - ] - for statement in statements: - try: - with session as s: - s.execute(statement) - s.commit() - # catch any exception - except (BaseException) as e: - logging.error(str(e)) - continue - - -def init_table(): - session.close_all() - Base.metadata.drop_all(engine) - Base.metadata.create_all(engine) diff --git a/src/utils/utils.py b/src/utils/utils.py deleted file mode 100644 index 367b539..0000000 --- a/src/utils/utils.py +++ /dev/null @@ -1,18 +0,0 @@ -from src.utils import sql_client -from src.utils.exceptions import ErrorInvalidInteger - - - -def convert_id_to_integer(id_): - if isinstance(id_, int): - return id_ - try: - id_ = int(id_) - except Exception: - raise ErrorInvalidInteger("Id is not a valid integer") - return id_ - -def inicializa_tabelas(): - sql_client.create_schema() - sql_client.init_table() - sql_client.grant_permissions() diff --git a/templates/functions.py b/templates/functions.py deleted file mode 100644 index 6a9f2e2..0000000 --- a/templates/functions.py +++ /dev/null @@ -1,123 +0,0 @@ -from datetime import datetime -from pyscript import Element, create -from pyodide.http import pyfetch, FetchResponse -import json - -BASE_URL = "http://minikube:30500" -CONTENT_TYPE = "application/json" - -# https://github.com/pyscript/pyscript/pull/151/commits/3e3f21c08fa0a5e081804e8fbb11e708ee2813ce -async def request( - url, - method = "GET", - body = None, - headers = None, -) -> FetchResponse: - """ - Async request function. Pass in Method and make sure to await! - Parameters: - method: str = {"GET", "POST", "PUT", "DELETE"} from javascript global fetch()) - body: str = body as json string. Example, body=json.dumps(my_dict) - header: dict[str,str] = header as dict, will be converted to string... - Example, header:json.dumps({"Content-Type":CONTENT_TYPE}) - Return: - response: pyodide.http.FetchResponse = use with .status or await.json(), etc. - """ - kwargs = {"method": method, "mode": "cors"} - if body and method not in ["GET", "HEAD"]: - kwargs["body"] = body - if headers: - kwargs["headers"] = headers - - response = await pyfetch(url, **kwargs) - return response - - -async def subscribe_discipline(): - try: - student_id = Element("subscribe-student-id").value - discipline_id = Element("subscribe-discipline-id").value - text = await request( - f"{BASE_URL}/subscription-discipline", - "POST", - json.dumps({ - "student_id": student_id, - "discipline_id": discipline_id - }), - {"Content-Type": CONTENT_TYPE}, - ) - __update_terminal(await text.json(), "INFO") - except Exception as e: - __update_terminal(e, "FAIL") - - -async def add_discipline(): - try: - name = Element("discipline-name").value - course_id = Element("course-discipline-id").value - text = await request( - f"{BASE_URL}/discipline", - "POST", - json.dumps({ - "name": name, - "course_id": course_id - }), - {"Content-Type": CONTENT_TYPE}, - ) - __update_terminal(await text.json(), "INFO") - except Exception as e: - __update_terminal(e, "FAIL") - - -async def subscribe_course(): - try: - student_id = Element("student-id").value - course_id = Element("course-id").value - text = await request( - f"{BASE_URL}/subscription-course", - "POST", - json.dumps({ - "student_id": student_id, - "course_id": course_id - }), - {"Content-Type": CONTENT_TYPE}, - ) - __update_terminal(await text.json(), "INFO") - except Exception as e: - __update_terminal(e, "FAIL") - - -async def add_course(): - try: - name = Element("course-name").value - text = await request( - f"{BASE_URL}/course", - "POST", - json.dumps({"name": name}), - {"Content-Type": CONTENT_TYPE}, - ) - __update_terminal(await text.json(), "INFO") - except Exception as e: - __update_terminal(e, "FAIL") - - -async def add_student(): - try: - name = Element("student-name").value - text = await request( - f"{BASE_URL}/student", - "POST", - json.dumps({"name": name}), - {"Content-Type": CONTENT_TYPE}, - ) - __update_terminal(await text.json(), "INFO") - except Exception as e: - __update_terminal(e, "FAIL") - - -def __update_terminal(text, message_type): - terminal = Element("local-terminal") - item = create("pre", classes="py-p") - item.element.innerText = f"{datetime.now()} {message_type} {text}" - terminal.element.appendChild(item.element) - terminal.element.insertBefore(item.element, terminal.element.childNodes[0]) diff --git a/templates/index.html b/templates/index.html deleted file mode 100644 index c9a4a2e..0000000 --- a/templates/index.html +++ /dev/null @@ -1,88 +0,0 @@ - - - - - - - - -
- -
-
-

College manager

-
- -
- - -
- -
- - -
- -
-
- -
- - -
- -
-
- -
- - -
- -
-
- -
- - -
- -
- - -
-

Events

-
-
- - -
- - - packages = ["sqlalchemy","pg8000", "scramp", "requests", - "./dist/src-0.0.1-py3-none-any.whl" - ] - [[fetch]] - files = ["./functions.py"] - - - from functions import ( - subscribe_discipline, - add_discipline, - subscribe_course, - add_course, - add_student, - ) - - - diff --git a/templates/styles.css b/templates/styles.css deleted file mode 100644 index e974711..0000000 --- a/templates/styles.css +++ /dev/null @@ -1,36 +0,0 @@ -.py-input { - margin: 10px 10px 10px 10px; -} - -.py-button { - width: 200px; -} - -.form-group { - width: auto; - margin-top: 10px; -} - -.main { - width: 500px; - height: 600px; - background-color:lightgray ; - padding: 10px 10px 10px 10px; -} - -.grid-container { - display: inline-grid; - grid-template-columns: auto auto auto; - background-color: black; - padding: 3px; -} - -.py-p { - color: white; -} - -.py-terminal { - overflow: auto; - background-color: black; - height: 505px; -} \ No newline at end of file diff --git a/test-requirements.txt b/test-requirements.txt deleted file mode 100644 index 8e35a61..0000000 --- a/test-requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -pytest==7.0.1 -pytest-dotenv==0.5.2 -selenium==3.141.0 \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/conftest.py b/tests/conftest.py index 4eccf4a..5c4946e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,40 +1,49 @@ -from pytest import fixture -from src.utils.utils import inicializa_tabelas -from src.controllers import courses, students, disciplines -from pytest import raises +import pytest +from src import database -@fixture -def popula_banco_dados(scope="function"): - """Combinacao de materia x course - m = materia, c = course - m1 c1 - m2 c1 - m3 c1 +@pytest.fixture(autouse=True, scope="function") +def set_in_memory_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") + 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") - m1 c2 - m2 c2 - m3 c2 + db.course.populate("adm", subjects="") + 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.course.populate("nosubjects", state="", subjects="") - m7 c3 - m8 c3 - m9 c3 - """ - courses.create(name="any_1") - courses.create(name="any_2") - courses.create(name="any_3") - for i in range(3): - for j in range(3): - disciplines.create(name=f"any{j}", course_id=i + 1) - students.create(name="anyone") - students.subscribe_in_course(student_id=1, course_id=1) - with raises(Exception): - students.subscribe_in_discipline(student_id=1, discipline_id=1) - with raises(Exception): - students.subscribe_in_discipline(student_id=1, discipline_id=2) - students.subscribe_in_discipline(student_id=1, discipline_id=3) + db.subject.populate("any", "any1") # e4c858cd917f518194c9d93c9d13def8 + db.subject.populate("any", "any2") # 283631d2292c54879b9aa72e27a1b4ff + db.subject.populate("any", "any3") # 0eaaeb1a39ed5d04a62b31cd951f34ce + db.subject.populate( + "course1", "subject_full", 0 + ) # ef15a071407953bd858cfca59ad99056 + db.subject.populate( + "course1", "subject_removed", 0, "removed" + ) # ef15a071407953bd858cfca59ad99056 + 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") -@fixture(scope="function", autouse=True) -def setup_bando_dados(): - inicializa_tabelas() + yield db diff --git a/tests/end_to_end/chromedriver b/tests/end_to_end/chromedriver deleted file mode 100755 index e334db3..0000000 Binary files a/tests/end_to_end/chromedriver and /dev/null differ diff --git a/tests/end_to_end/test_cli_e2e.py b/tests/end_to_end/test_cli_e2e.py deleted file mode 100644 index b311c5c..0000000 --- a/tests/end_to_end/test_cli_e2e.py +++ /dev/null @@ -1,196 +0,0 @@ -import subprocess -from src.controllers import courses, students, disciplines -from src.schemes.course import CourseDB -from src.schemes.student import StudentDB -from src.schemes.discipline import MateriaBd -from src.schemes.for_association import MateriaStudentDB -from src.utils import sql_client -from pytest import fixture -from time import sleep -from tests.utils import create_course, popula_banco_dados -from src.utils.utils import inicializa_tabelas - - -@fixture -def setup(): - inicializa_tabelas() - - -@fixture -def __popula_banco_dados(): - popula_banco_dados() - - -def test_init_data_base(): - - temp = subprocess.Popen( - ["python", "cli.py", "init-bd"], - stdout=subprocess.PIPE, - ) - output = str(temp.communicate()) - assert len(students.get_all()) == 0 - assert "Database initialized" in output - - -def test_aluno_pode_lancar_notas(__popula_banco_dados): - student_id = len(sql_client.get_all(StudentDB)) - discipline_id = 1 - nota = 7 - nota_bd = None - - temp = subprocess.Popen( - [ - "python", - "cli.py", - "aluno", - "lanca-nota", - "--student-id", - f"{student_id}", - "--discipline-id", - f"{discipline_id}", - "--nota", - f"{nota}", - ], - stdout=subprocess.PIPE, - ) - output = str(temp.communicate()) - - # verifica pelo banco - materia_aluno = sql_client.get_all(MateriaStudentDB) - for ma in materia_aluno: - if ma.student_id == student_id and ma.discipline_id == discipline_id: - nota_bd = ma.aluno_nota - break - assert nota == nota_bd - assert ( - f"Nota {nota} do aluno {student_id} na materia {discipline_id} lancada com sucesso" - in output - ) - - -def test_students_deve_inscreve_3_materias_no_minimo(__popula_banco_dados): - students.create("any") - student_id = len(students.get_all()) - - uma_materia = disciplines.get_all()[0] - discipline_id = uma_materia.id - course_id = uma_materia.course_id - - students.subscribe_in_course(student_id, course_id) - - temp = subprocess.Popen( - [ - "python", - "cli.py", - "aluno", - "inscreve-materia", - "--student-id", - f"{student_id}", - "--discipline-id", - f"{discipline_id}", - ], - stdout=subprocess.PIPE, - ) - output = str(temp.communicate()) - - # verifica pelo banco - materia_aluno = sql_client.get_all(MateriaStudentDB) - assert len(materia_aluno) > 1 - assert "Aluno deve se inscrever em 3 materias no minimo" in output - - -def test_aluno_pode_se_inscrever_em_course(__popula_banco_dados): - - courses.create("other") - course_id = len(courses.get_all()) - students.create("any") - student_id = len(students.get_all()) - - temp = subprocess.Popen( - [ - "python", - "cli.py", - "aluno", - "inscreve-course", - "--student-id", - f"{student_id}", - "--course-id", - f"{course_id}", - ], - stdout=subprocess.PIPE, - ) - output = str(temp.communicate()) - - aluno = students.get(student_id) - # verifica pela API - assert aluno.course_id == course_id - # verifica pelo banco - assert sql_client.get(StudentDB, student_id).course_id == 4 - assert "Aluno inscrito no course 4" in output - - -def test_cli_materia_name_igual_mas_id_diferente(setup): - - create_course() - create_course() - create_course() - for _ in range(3): - if len(courses.get_all()) >= 3: - break - sleep(1) - disciplines.create("any", 1) - temp = subprocess.Popen( - ["python", "cli.py", "materia", "create", "--name", "other", "--course-id", "1"], - stdout=subprocess.PIPE, - ) - output = str(temp.communicate()) - # verifica pela API - assert len(disciplines.get_all()) == 2 - assert disciplines.get(1).name == "any" - assert disciplines.get(2).name == "other" - assert disciplines.get(2).course_id == 1 - # verifica banco de dados - assert len(sql_client.get_all(MateriaBd)) == 2 - assert sql_client.get(MateriaBd, 1).name == "any" - assert sql_client.get(MateriaBd, 2).name == "other" - assert "Materia definida: id 2, name other" in output - - -def test_cli_aluno_deve_ter_name(setup): - subprocess.Popen( - ["python", "cli.py", "aluno", "create", "--name", "any"], - stdout=subprocess.PIPE, - ).communicate() - temp = subprocess.Popen( - ["python", "cli.py", "aluno", "create", "--name", "other"], - stdout=subprocess.PIPE, - ) - output = str(temp.communicate()) - # verifica pela API - assert len(students.get_all()) == 2 - assert students.get(1).name == "any" - assert students.get(2).name == "other" - # verifica banco de dados - assert len(sql_client.get_all(StudentDB)) == 2 - assert sql_client.get(StudentDB, 1).name == "any" - assert sql_client.get(StudentDB, 2).name == "other" - assert "Aluno definido: id 2, name other" in output - - -def test_cli_course_com_name_e_id(setup): - courses.create("any") - temp = subprocess.Popen( - ["python", "cli.py", "course", "create", "--name", "other"], - stdout=subprocess.PIPE, - ) - output = str(temp.communicate()) - assert "course definido: id 2, name other" in output - # verifica pela API - assert len(courses.get_all()) == 2 - assert courses.get(1).name == "any" - assert courses.get(2).name == "other" - # verifica banco de dados - assert len(sql_client.get_all(CourseDB)) == 2 - assert sql_client.get(CourseDB, 1).name == "any" - assert sql_client.get(CourseDB, 2).name == "other" - assert "course definido: id 2, name other" in output diff --git a/tests/end_to_end/test_manual.py b/tests/end_to_end/test_manual.py deleted file mode 100644 index de0b474..0000000 --- a/tests/end_to_end/test_manual.py +++ /dev/null @@ -1,12 +0,0 @@ -from src.utils import sql_client -from src.schemes.student import StudentDB - - -# @mark.skip(reason="Teste manual") -def test_popula_banco_para_test_manual(): - res = sql_client.get_all(StudentDB) - print(res) - - -if __name__ == "__main__": - test_popula_banco_para_test_manual() diff --git a/tests/end_to_end/test_ui.py b/tests/end_to_end/test_ui.py deleted file mode 100644 index 9f39f17..0000000 --- a/tests/end_to_end/test_ui.py +++ /dev/null @@ -1,95 +0,0 @@ -# Generated by Selenium IDE -import pytest -import time -import json -from selenium import webdriver -from selenium.webdriver.common.by import By -from selenium.webdriver.common.action_chains import ActionChains -from selenium.webdriver.support import expected_conditions -from selenium.webdriver.support.wait import WebDriverWait -from selenium.webdriver.common.keys import Keys -from selenium.webdriver.common.desired_capabilities import DesiredCapabilities -from time import sleep -from os import path -from pytest import mark - - -class TestAddstudentcoursedisciplineandsubscribe: - def setup_method(self, method): - ROOT_DIR = path.dirname(path.abspath(__file__)) - self.driver = webdriver.Chrome(executable_path=f"{ROOT_DIR}/chromedriver") - self.vars = {} - - def teardown_method(self, method): - self.driver.quit() - - @mark.skip(reason="Skiping due git actions") - def test_addstudentcoursedisciplineandsubscribe(self): - self.driver.get("http://127.0.0.1:5000") - self.driver.set_window_size(623, 783) - sleep(10) - self.driver.find_element(By.ID, "student-name").click() - self.driver.find_element(By.ID, "student-name").send_keys("Douglas") - self.driver.find_element(By.ID, "student-btn").click() - element = self.driver.find_element(By.ID, "student-btn") - element = self.driver.find_element(By.CSS_SELECTOR, "body") - self.driver.find_element(By.ID, "course-name").click() - self.driver.find_element(By.ID, "course-name").send_keys("Eng") - self.driver.find_element(By.ID, "course-btn").click() - element = self.driver.find_element(By.ID, "course-btn") - element = self.driver.find_element(By.CSS_SELECTOR, "body") - self.driver.find_element(By.ID, "course-name").click() - self.driver.find_element(By.ID, "course-name").click() - element = self.driver.find_element(By.ID, "course-name") - self.driver.find_element(By.ID, "course-name").send_keys("Med") - self.driver.find_element(By.ID, "course-btn").click() - element = self.driver.find_element(By.ID, "course-btn") - element = self.driver.find_element(By.CSS_SELECTOR, "body") - self.driver.find_element(By.ID, "course-name").click() - self.driver.find_element(By.ID, "course-name").send_keys("Adm") - self.driver.find_element(By.ID, "course-btn").click() - element = self.driver.find_element(By.ID, "course-btn") - element = self.driver.find_element(By.CSS_SELECTOR, "body") - self.driver.find_element(By.ID, "student-id").click() - self.driver.find_element(By.ID, "student-id").send_keys("1") - self.driver.find_element(By.ID, "course-id").click() - self.driver.find_element(By.ID, "course-id").send_keys("1") - self.driver.find_element( - By.CSS_SELECTOR, ".form-group:nth-child(4) > #course-btn" - ).click() - self.driver.find_element(By.ID, "discipline-name").click() - self.driver.find_element(By.ID, "discipline-name").send_keys("Math") - self.driver.find_element(By.ID, "course-discipline-id").click() - self.driver.find_element(By.ID, "course-discipline-id").send_keys("1") - self.driver.find_element( - By.CSS_SELECTOR, ".form-group:nth-child(5) > #course-btn" - ).click() - element = self.driver.find_element( - By.CSS_SELECTOR, ".form-group:nth-child(5) > #course-btn" - ) - element = self.driver.find_element(By.CSS_SELECTOR, "body") - self.driver.find_element(By.ID, "discipline-name").click() - self.driver.find_element(By.ID, "discipline-name").click() - element = self.driver.find_element(By.ID, "discipline-name") - self.driver.find_element(By.ID, "discipline-name").send_keys("Calc") - self.driver.find_element( - By.CSS_SELECTOR, ".form-group:nth-child(5) > #course-btn" - ).click() - element = self.driver.find_element( - By.CSS_SELECTOR, ".form-group:nth-child(5) > #course-btn" - ) - element = self.driver.find_element(By.CSS_SELECTOR, "body") - self.driver.find_element(By.ID, "discipline-name").click() - self.driver.find_element(By.ID, "discipline-name").click() - element = self.driver.find_element(By.ID, "discipline-name") - self.driver.find_element(By.ID, "discipline-name").send_keys("Topology") - self.driver.find_element( - By.CSS_SELECTOR, ".form-group:nth-child(5) > #course-btn" - ).click() - self.driver.find_element(By.ID, "subscribe-student-id").click() - self.driver.find_element(By.ID, "subscribe-student-id").send_keys("1") - self.driver.find_element(By.ID, "subscribe-discipline-id").click() - self.driver.find_element(By.ID, "subscribe-discipline-id").send_keys("1") - self.driver.find_element( - By.CSS_SELECTOR, ".form-group:nth-child(6) > #course-btn" - ).click() diff --git a/tests/integrated/test_data_base.py b/tests/integrated/test_data_base.py deleted file mode 100644 index 445d4a8..0000000 --- a/tests/integrated/test_data_base.py +++ /dev/null @@ -1,11 +0,0 @@ -from src.schemes.course import CourseDB -from src.utils import sql_client - - -def test_create_roles(): - sql_client.grant_permissions() - - -def test_lista_maximo_retorna_vazio_quando_nao_ha_registros(): - sql_client.init_table() - assert sql_client.get_maximum(CourseDB) == [] diff --git a/tests/integrated/test_integrated.py b/tests/integrated/test_integrated.py deleted file mode 100644 index 1dcfe9c..0000000 --- a/tests/integrated/test_integrated.py +++ /dev/null @@ -1,147 +0,0 @@ -from src.controllers import courses -from src.controllers import disciplines -from src.controllers import students -from src.schemes.student import StudentDB -from src.schemes.course import CourseDB -from src.schemes.discipline import MateriaBd -from src.schemes.for_association import MateriaStudentDB -from src.utils.utils import inicializa_tabelas -from src.utils.exceptions import ( - ErrorStudent, - ErrorDiscipline, - ErrorCourse, -) -from src.utils import sql_client -from tests.utils import create_course, create_materia -from pytest import raises - - -def test_calcula_cr_aluno_de_materias_cursadas(popula_banco_dados): - student_id = len(students.get_all()) - - students.set_grade(student_id=student_id, discipline_id=1, grade=5) - students.set_grade(student_id=student_id, discipline_id=2, grade=0) - students.set_grade(student_id=student_id, discipline_id=3, grade=5) - - assert sql_client.get(StudentDB, student_id).coef_rend == 5 - - -def test_students_deve_inscreve_em_3_materias(popula_banco_dados): - - students.create("any") - student_id = len(students.get_all()) - students.subscribe_in_course(student_id, course_id=1) - with raises( - ErrorStudent, match="Aluno deve se inscrever em 3 materias no minimo" - ): - students.subscribe_in_discipline(student_id, 1) - - materia_aluno = sql_client.get_all(MateriaStudentDB) - assert len(materia_aluno) > 1 - - -def test_create_aluno_por_api(): - - students.create("any") - aluno = students.get(1) - assert aluno.id == 1 - - -def test_cli_tres_courses_com_tres_materias_cada(): - create_course() - create_course() - create_course() - for _ in range(3): - create_materia(1) - create_materia(2) - create_materia(3) - - # verifica pela API - assert len(courses.get_all()) == 3 - assert len(disciplines.get_all()) == 9 - - -def test_aluno_pode_se_inscrever_em_apenas_um_course(popula_banco_dados): - - students.create("any") - student_id = len(students.get_all()) - courses.create("other") - students.subscribe_in_course(student_id, 4) - - with raises(ErrorStudent, match="Aluno esta inscrito em outro course"): - students.subscribe_in_course(student_id, 3) - - aluno = students.get(student_id) - assert aluno.course_id == 4 - - -def test_course_nao_pode_ter_materias_com_mesmo_name(): - - courses.create("any_1") - courses.create("any_2") - courses.create("any_3") - disciplines.create("any", 1) - with raises( - ErrorDiscipline, - match="O course já possui uma matéria com este name", - ): - disciplines.create("any", 1) - - -def test_nao_criar_quarto_course_se_menos_de_tres_materias_por_course(): - courses.create("any_1") - courses.create("any_2") - courses.create("any_3") - disciplines.create(name="any", course_id=1) - with raises( - ErrorCourse, - match="Necessários 3 courses com 3 três matérias para se criar novos courses", - ): - courses.create("quarto") - - -def test_nao_criar_quarto_course_se_todos_courses_sem_materias(): - courses.create("any_1") - courses.create("any_2") - courses.create("any_3") - with raises( - ErrorCourse, - match="Necessários 3 courses com 3 três matérias para se criar novos courses", - ): - courses.create("quatro") - - -def test_materia_nao_createda_se_menos_de_tres_courses_existentes(): - with raises( - ErrorDiscipline, match="Necessários 3 courses para se criar a primeira matéria" - ): - disciplines.create("any", 1) - - -def test_materia_nao_associada_course_inexistente(popula_banco_dados): - with raises(ErrorDiscipline): - disciplines.create("any", 42) - - -def test_materia_associada_course_existente(): - sql_client.create(CourseDB(name="any")) - sql_client.create(MateriaBd(name="any", course_id=1)) - assert sql_client.get(MateriaBd, 1).name == "any" - - -def test_create_tabela_com_dados(): - sql_client.create(CourseDB(name="any_1")) - sql_client.create(CourseDB(name="any_2")) - sql_client.create(CourseDB(name="any_3")) - assert len(sql_client.get_all(CourseDB)) == 3 - - -def test_create_item_bd(): - sql_client.create(CourseDB(name="any")) - assert len(sql_client.get_all(CourseDB)) == 1 - - -def test_lista_por_id_bd(): - courses.create("any") - inicializa_tabelas() - assert len(sql_client.get_all(CourseDB)) == 0 diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..b851cc8 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,191 @@ +import pytest +from src import cli_helper +from src import utils + + +@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 + + +@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 + + +@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) + + assert ( + cli_helper.take_subject(database, student_identifier, subject_name) == expected + ) + + +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_name) + == expected + ) diff --git a/tests/test_course_handler.py b/tests/test_course_handler.py new file mode 100644 index 0000000..f3bbfa9 --- /dev/null +++ b/tests/test_course_handler.py @@ -0,0 +1,204 @@ +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["students"]) == 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 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_grade_calculator.py b/tests/test_grade_calculator.py new file mode 100644 index 0000000..488e50e --- /dev/null +++ b/tests/test_grade_calculator.py @@ -0,0 +1,54 @@ +import pytest +from src.services.grade_calculator import GradeCalculator +from src.services.student_handler import StudentHandler + + +@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) + student_handler = StudentHandler(database) + student_handler.name = "any" + student_handler.cpf = "123.456.789-10" + student_handler.enroll_to_course(course_name) + + subject_name1 = "any1" + subject_name2 = "any2" + subject_name3 = "any3" + + student_handler.take_subject("any1") + student_handler.take_subject("any2") + student_handler.take_subject("any3") + + 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) + == expected + ) + + +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("any1") + student_handler.take_subject("any2") + assert grade_calculator.calculate_gpa_for_student(student_handler.identifier) == 0 diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100644 index 0000000..af22dd4 --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,239 @@ +import pytest +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 +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 __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" + 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() + return course_handler + + +def test_student_locked_by_minimun_subjects_per_semester(): + 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( + 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 + + 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_or_grades( + 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) + __update_grade_of_all_subjects(grade, subjects, student_handler) + course_handler.cancel() + 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_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.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): + 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) + __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 + + +@pytest.mark.parametrize( + "grade,situation", + [ + (7, "approved"), + (5, "failed"), + ], +) +def test_student_finishes_course(set_in_memory_database, grade, situation): + course = "adm" + 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) + + assert student_handler.gpa == grade + + __close_maximum_semesters(database) + + assert student_handler.semester_counter == MAX_SEMESTERS_TO_FINISH_COURSE + 1 + assert student_handler.state == situation 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..4f4404b --- /dev/null +++ b/tests/test_student_handler.py @@ -0,0 +1,183 @@ +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 +from src.services.course_handler import CourseHandler + + +@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): + database = set_in_memory_database + name = "any" + cpf = "123.456.789-10" + 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_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 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" diff --git a/tests/unit/test_courses.py b/tests/unit/test_courses.py deleted file mode 100644 index debdf44..0000000 --- a/tests/unit/test_courses.py +++ /dev/null @@ -1,27 +0,0 @@ -from src.utils import sql_client -from src.schemes.course import CourseDB -from src.utils.exceptions import ErrorCourse -from pytest import raises, mark -from src.controllers import courses - - -def test_course_pega_id(): - courses.create(name="any") - assert 1 == len(sql_client.get_all(CourseDB)) - - -def test_course_create(): - courses.create(name="any") - assert sql_client.get(CourseDB, 1).name == "any" - - -@mark.parametrize("input", [(""), (" ")]) -def test_name_course_nao_vazio(input): - with raises(ErrorCourse, match="name do course invalido"): - courses.create(input) - - -def test_nao_create_course_com_mesmo_name(): - courses.create("any") - with raises(ErrorCourse, match="Existe outro course com o name any"): - courses.create("any") diff --git a/tests/unit/test_disciplines.py b/tests/unit/test_disciplines.py deleted file mode 100644 index 590a25e..0000000 --- a/tests/unit/test_disciplines.py +++ /dev/null @@ -1,16 +0,0 @@ -from src.controllers import disciplines -from src.schemes.discipline import MateriaBd -from src.utils import sql_client -from pytest import mark, raises -from src.utils.exceptions import ErrorDiscipline - - -@mark.parametrize("course_id", ["string", ""]) -def test_create_discipline_raises_error_if_course_id_invalid(course_id): - with raises(ErrorDiscipline, match="Course id is not a valid integer"): - disciplines.create(name="any", course_id=course_id) - -def test_materia_create(popula_banco_dados): - disciplines.create(name="any", course_id=1) - discipline_id = sql_client.get_maximum(MateriaBd).id - assert sql_client.get(MateriaBd, discipline_id).name == "any" diff --git a/tests/unit/test_sql_client.py b/tests/unit/test_sql_client.py deleted file mode 100644 index 73be069..0000000 --- a/tests/unit/test_sql_client.py +++ /dev/null @@ -1,15 +0,0 @@ -from src.schemes.course import CourseDB -from src.utils import sql_client -from src.schemes.student import StudentDB - - -def test_aluno_tem_discipline_id(popula_banco_dados): - aluno = StudentDB(name="any") - aluno.discipline_id = 1 - assert aluno.discipline_id == 1 - - -def test_create_novo_course(): - course = CourseDB(name="any") - sql_client.create(course) - assert sql_client.get_all(CourseDB)[0].name == "any" diff --git a/tests/unit/test_students.py b/tests/unit/test_students.py deleted file mode 100644 index 2e2f378..0000000 --- a/tests/unit/test_students.py +++ /dev/null @@ -1,222 +0,0 @@ -from src.schemes.for_association import MateriaStudentDB -from src.schemes.student import StudentDB -from src.controllers import students -from src.utils.exceptions import ( - ErrorStudent, - ErrorDiscipline, - ErrorCourse, -) -from src.utils import sql_client -from pytest import raises, mark - - -@mark.parametrize("input", [(""), ("string")]) -def test_course_id_cannot_be_string(input): - with raises(ErrorStudent, match="The student id is not a valid integer"): - students.subscribe_in_course(student_id=input, course_id=1) - -@mark.parametrize("input", [(""), ("string")]) -def test_course_id_cannot_be_string(input): - with raises(ErrorStudent, match="The course id is not a valid integer"): - students.subscribe_in_course(student_id=1, course_id=input) - - -@mark.parametrize("input", [(""), (" ")]) -def test_student_name_cannot_be_empty(popula_banco_dados, input): - with raises(ErrorStudent, match="Invalid student name"): - students.create(name=input) - - -def test_arredonda_o_cr_a_uma_casa_decimal(popula_banco_dados): - student_id = len(sql_client.get_all(StudentDB)) - students.id = student_id - students.set_grade(student_id, discipline_id=1, grade=2) - students.set_grade(student_id, discipline_id=2, grade=2) - students.set_grade(student_id, discipline_id=3, grade=3) - aluno_bd = sql_client.get(StudentDB, student_id) - assert aluno_bd.coef_rend == 2.3 - - -def test_calcula_cr_aluno_como_media_simples_das_notas_lancadas(popula_banco_dados): - student_id = len(sql_client.get_all(StudentDB)) - students.set_grade(student_id, discipline_id=1, grade=1) - students.set_grade(student_id, discipline_id=2, grade=2) - students.set_grade(student_id, discipline_id=3, grade=3) - aluno_bd = sql_client.get(StudentDB, student_id) - assert aluno_bd.coef_rend == 2 - - -def test_lanca_nota_se_aluno_existe(popula_banco_dados): - controller = students - student_id = len(controller.get_all()) - - controller.id = student_id - discipline_id = 1 - nota = 7 - assert controller.set_grade(student_id, discipline_id, nota) is None - - -def test_assume_maior_nota_se_duplo_lancamento_de_notas(popula_banco_dados): - controller = students - student_id = len(controller.get_all()) - - controller.id = student_id - discipline_id = 1 - nota = 7 - nota_bd = None - controller.set_grade(student_id, discipline_id, grade=nota + 1) - controller.set_grade(student_id, discipline_id, grade=nota) - mas = sql_client.get_all(MateriaStudentDB) - for ma in mas: - if ma.student_id == student_id and ma.discipline_id == discipline_id: - nota_bd = ma.aluno_nota - break - assert nota == nota_bd - - -def test_nao_lanca_nota_se_menor_que_zero(): - controller = students - student_id = len(controller.get_all()) - controller.create("any") - discipline_id = 1 - with raises(ErrorStudent, match="Nota não pode ser menor que 0"): - controller.set_grade(student_id, discipline_id, grade=-1) - - -def test_nao_lanca_nota_se_maoir_que_10(): - controller = students - student_id = len(controller.get_all()) - - controller.create("any") - discipline_id = 1 - with raises(ErrorStudent, match="Nota não pode ser maior que 10"): - controller.set_grade(student_id, discipline_id, grade=11) - - -def test_nao_lanca_nota_se_aluno_nao_inscrito_materia(): - controller = students - student_id = len(controller.get_all()) - - controller.create("any") - discipline_id = 1 - with raises( - ErrorStudent, match=f"Aluno {student_id} não está inscrito na matéria {discipline_id}" - ): - controller.set_grade(student_id, discipline_id, grade=5) - - -def test_lanca_notas_se_aluno_inscrito_materia(popula_banco_dados): - controller = students - student_id = len(controller.get_all()) - - controller.id = student_id - controller.set_grade(student_id, discipline_id=1, grade=5) - assert sql_client.get(StudentDB, student_id).coef_rend == 5.0 - - -def test_nao_inscreeve_aluno_se_course_nao_existe(): - controller = students - controller.create("any") - student_id = len(controller.get_all()) - with raises(ErrorStudent, match="Aluno 1 não está inscrito em nenhum course"): - controller.subscribe_in_discipline(student_id, 1) - - -def test_mensagem_sobre_3_materias_para_apos_inscricao_em_3_materias( - popula_banco_dados, -): - - controller = students - controller.create("any") - student_id = len(controller.get_all()) - controller.subscribe_in_course(student_id, course_id=1) - with raises(ErrorStudent): - controller.subscribe_in_discipline(student_id, 1) - with raises(ErrorStudent): - controller.subscribe_in_discipline(student_id, 2) - controller.subscribe_in_discipline(student_id, 3) - - -def test_aluno_nao_pode_se_inscrever_em_materia_inexistente(popula_banco_dados): - student_id = len(sql_client.get_all(StudentDB)) - students.id = student_id - with raises(ErrorStudent): - students.subscribe_in_discipline(student_id, 1) - students.subscribe_in_discipline(student_id, 2) - students.subscribe_in_discipline(student_id, 3) - with raises(ErrorDiscipline, match="Matéria 42 não existe"): - students.subscribe_in_discipline(student_id, 42) - - -def test_aluno_nao_pode_se_inscrever_duas_vezes_na_mesma_materia(popula_banco_dados): - student_id = len(sql_client.get_all(StudentDB)) - controller = students - controller.id = student_id - with raises(ErrorStudent): - controller.subscribe_in_discipline(student_id, 1) - controller.subscribe_in_discipline(student_id, 2) - controller.subscribe_in_discipline(student_id, 3) - with raises(ErrorStudent, match="Aluno 1 já está inscrito na matéria 1"): - controller.subscribe_in_discipline(student_id, 1) - - -def test_inscreve_aluno_numa_materia(popula_banco_dados): - controller = students - controller.create("any") - student_id = len(controller.get_all()) - controller.subscribe_in_course(student_id, course_id=1) - with raises( - ErrorStudent, match="Aluno deve se inscrever em 3 materias no minimo" - ): - controller.subscribe_in_discipline(student_id, 1) - - -def test_aluno_create(): - students.create(name="any") - assert sql_client.get(StudentDB, 1).name == "any" - assert sql_client.get(StudentDB, 1).id == 1 - - -def test_inscreve_aluno_se_course_existe(): - students.create("any") - with raises(Exception): - with raises(ErrorCourse, match="course 42 nao existe"): - students.subscribe_in_course(course_id=42) - - -def test_inscreve_aluno_course(popula_banco_dados): - controller = students - controller.create("any") - student_id = len(controller.get_all()) - controller.subscribe_in_course(student_id, course_id=1) - assert sql_client.get(StudentDB, 1).course_id == 1 - - -def test_verifica_aluno_existe(): - with raises(ErrorStudent, match="Aluno 42 não existe"): - students.get(42) - - -def test_nao_inscreve_aluno_se_course_nao_existe(): - controller = students - controller.create("any") - student_id = len(controller.get_all()) - with raises(ErrorStudent, match="course 42 não existe"): - controller.subscribe_in_course(student_id, 42) - - -def test_alunos_lista_por_id(): - students.create(name="any") - students.create(name="other") - assert sql_client.get(StudentDB, id_=2).name == "other" - - -def test_alunos_get_all(): - students.create(name="any") - students.create(name="other") - assert len(sql_client.get_all(StudentDB)) == 2 - - -def test_alunos_create_banco_dados(): - students.create(name="any") - assert sql_client.get(StudentDB, id_=1).name == "any" diff --git a/tests/utils.py b/tests/utils.py deleted file mode 100644 index bd9d329..0000000 --- a/tests/utils.py +++ /dev/null @@ -1,41 +0,0 @@ -import uuid -from src.controllers import courses -from src.controllers import disciplines -from src.controllers import students -from pytest import raises -from src.utils.utils import inicializa_tabelas - - -def popula_banco_dados(): - "create 3 courses com 3 matérias cada, create aluno, inscreve em um dos courses e inscreve em 3 matérias" - inicializa_tabelas() - create_course() - create_course() - create_course() - for i in range(3): - for _ in range(3): - create_materia(i + 1) - create_materia(i + 1) - create_materia(i + 1) - create_aluno_completo() - - -def create_aluno_completo(): - students.create("test_manual") - student_id = len(students.get_all()) - students.subscribe_in_course(student_id, 1) - with raises(Exception): - students.subscribe_in_discipline(student_id, 1) - with raises(Exception): - students.subscribe_in_discipline(student_id, 2) - students.subscribe_in_discipline(student_id, 3) - - -def create_materia(course_id): - name_aleatorio = str(uuid.uuid4()) - disciplines.create(name_aleatorio, course_id) - - -def create_course(): - name_aleatorio = str(uuid.uuid4()) - courses.create(name_aleatorio) diff --git a/utils/build_dist.sh b/utils/build_dist.sh deleted file mode 100755 index a65e728..0000000 --- a/utils/build_dist.sh +++ /dev/null @@ -1,2 +0,0 @@ -python -m build -cp -r dist/ templates/ \ No newline at end of file diff --git a/utils/build_image_postgrest.sh b/utils/build_image_postgrest.sh deleted file mode 100755 index 0010e12..0000000 --- a/utils/build_image_postgrest.sh +++ /dev/null @@ -1 +0,0 @@ -docker build -t douglasdcm/tdd-postgrest postgrest diff --git a/utils/build_image_tdd_detroid.sh b/utils/build_image_tdd_detroid.sh deleted file mode 100755 index 031abd4..0000000 --- a/utils/build_image_tdd_detroid.sh +++ /dev/null @@ -1,2 +0,0 @@ -# build the image manually -docker build -t douglasdcm/tdd-detroid . \ No newline at end of file diff --git a/utils/deploy_components_on_k8s.sh b/utils/deploy_components_on_k8s.sh deleted file mode 100644 index 25bb209..0000000 --- a/utils/deploy_components_on_k8s.sh +++ /dev/null @@ -1,5 +0,0 @@ -kubectl create -f kubernetes/deployments-list.yaml -kubectl create -f kubernetes/config-map.yaml -kubectl create -f kubernetes/persistent-volume.yaml -kubectl create -f kubernetes/persistent-volume-clain.yaml -kubectl create -k kubernetes/services.yaml \ No newline at end of file diff --git a/utils/docker_compose.sh b/utils/docker_compose.sh deleted file mode 100755 index a4cd0be..0000000 --- a/utils/docker_compose.sh +++ /dev/null @@ -1,2 +0,0 @@ -docker-compose down -docker-compose up -d diff --git a/utils/pre-commit.sh b/utils/pre-commit.sh deleted file mode 100755 index bfb1a46..0000000 --- a/utils/pre-commit.sh +++ /dev/null @@ -1,11 +0,0 @@ -echo "Preparing the project to be pushed to remote repository" -# update requirements -pip freeze > requirements.txt -# report the status of git -git status -# execute the validatation -coverage run --include='app.py' --source='src' -m pytest -vvv -s -coverage report -coverage html -echo "Pre-commit finished" - diff --git a/utils/remove_files.sh b/utils/remove_files.sh deleted file mode 100755 index 5dfd7af..0000000 --- a/utils/remove_files.sh +++ /dev/null @@ -1 +0,0 @@ -rm -fr venv* tests .vscode .github .pytest_cache htmlcov .coverage logs/* \ No newline at end of file diff --git a/utils/setup.sh b/utils/setup.sh deleted file mode 100755 index 4743079..0000000 --- a/utils/setup.sh +++ /dev/null @@ -1,4 +0,0 @@ -# prepare the container and install the dependencies -BASE_DIR="/webapp" -# install the dependencies -pip install -r ${BASE_DIR}/requirements.txt \ No newline at end of file diff --git a/utils/start.sh b/utils/start.sh deleted file mode 100755 index a9af147..0000000 --- a/utils/start.sh +++ /dev/null @@ -1,5 +0,0 @@ -BASE_DIR="/webapp" -# start the database -# python ${BASE_DIR}/cli.py init-bd -# start the server -python ${BASE_DIR}/app.py