diff --git a/tests/config.defaults.json b/tests/config.defaults.json index 78262cb..0588ef8 100644 --- a/tests/config.defaults.json +++ b/tests/config.defaults.json @@ -11,6 +11,7 @@ "TASK_BASE_URL": "http://sovietov.com/kispython", "FINAL_TASKS": null, "ENABLE_REGISTRATION": false, + "HIDE_GROUPS": false, "IMAP_LOGIN": "example@example.com", "IMAP_PASSWORD": "CHANGE_ME", "ENABLE_LKS_OAUTH": false, diff --git a/tests/ui/test_auth.py b/tests/ui/test_auth.py index a117943..6064281 100644 --- a/tests/ui/test_auth.py +++ b/tests/ui/test_auth.py @@ -116,20 +116,6 @@ def test_register_off_get(db: AppDatabase, client: FlaskClient): assert response.status_code == 302 -def test_login_off_post(client: FlaskClient, db: AppDatabase): - create_student(db) - - response = client.post("/login") - assert response.status_code == 302 - - -def test_login_off_get(client: FlaskClient, db: AppDatabase): - create_student(db) - - response = client.get("/login") - assert response.status_code == 302 - - @mode('registration') @pytest.mark.parametrize("password, message", [ ("1234", "Пароль содержит как минимум 8 символов."), diff --git a/tests/ui/test_teacher.py b/tests/ui/test_teacher.py index e91c0be..6374086 100644 --- a/tests/ui/test_teacher.py +++ b/tests/ui/test_teacher.py @@ -12,13 +12,13 @@ def test_login_logout(db: AppDatabase, client: FlaskClient): assert "login" not in response.request.path assert "Выгрузка всех присланных сообщений" in response.get_data(as_text=True) - response = client.get("/teacher/logout", follow_redirects=True) + response = client.get("/logout", follow_redirects=True) - assert "login" in response.request.path + assert response.request.path == "/" def test_false_login(db: AppDatabase, client: FlaskClient): - response = client.post("/teacher/login", data={ + response = client.post("/login", data={ "login": "badLogin", "password": "evenWorsePassword" }, follow_redirects=True) @@ -93,7 +93,7 @@ def test_anonymous_submission(db: AppDatabase, client: FlaskClient): assert db.groups.get_by_id(gid).title in response.get_data(as_text=True) assert code in response.get_data(as_text=True) - assert "студент" not in response.get_data(as_text=True) + assert ", студент" not in response.get_data(as_text=True) def test_invalid_page_redirect(db: AppDatabase, client: FlaskClient): diff --git a/tests/utils.py b/tests/utils.py index 7fd7f19..008a46d 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -9,7 +9,7 @@ from flask.testing import FlaskClient -from webapp.managers import TeacherManager +from webapp.managers import StudentManager from webapp.repositories import AppDatabase @@ -57,12 +57,13 @@ def get_tags( def teacher_login(db: AppDatabase, client: FlaskClient): - login = unique_str() + email = f'{unique_str()}@{unique_str()}.ru' password = unique_str() - tm = TeacherManager(db.teachers) - tm.create(login, password) - return client.post("/teacher/login", data={ - "login": login, + students = StudentManager(None, db.students, db.mailers) + students.create(email, password, True) + db.students.confirm(email) + return client.post("/login", data={ + "login": email, "password": password }, follow_redirects=True) diff --git a/webapp/alembic/versions/20240514.22-12.students_as_teachers.py b/webapp/alembic/versions/20240514.22-12.students_as_teachers.py new file mode 100644 index 0000000..99960ab --- /dev/null +++ b/webapp/alembic/versions/20240514.22-12.students_as_teachers.py @@ -0,0 +1,31 @@ +""" + +Revision ID: 0ed9e0bfabcb +Revises: 30b579748cec +Create Date: 2024-05-14 22:12:06.014972 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '0ed9e0bfabcb' +down_revision = '30b579748cec' +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column("students", sa.Column("teacher", sa.Boolean, nullable=True)) + op.drop_table("teachers") + + +def downgrade(): + op.drop_column("students", "teacher") + op.create_table( + 'teachers', + sa.Column("id", sa.Integer, primary_key=True, nullable=False), + sa.Column("login", sa.String, nullable=False), + sa.Column("password_hash", sa.String, nullable=False), + ) diff --git a/webapp/config.defaults.json b/webapp/config.defaults.json index 1a9d2f7..c37d381 100644 --- a/webapp/config.defaults.json +++ b/webapp/config.defaults.json @@ -9,6 +9,7 @@ "READONLY": false, "TASK_BASE_URL": "http://kispython.ru/docs", "ENABLE_REGISTRATION": false, + "HIDE_GROUPS": false, "IMAP_LOGIN": null, "IMAP_PASSWORD": null, "FINAL_TASKS": { diff --git a/webapp/dto.py b/webapp/dto.py index f27a224..a93ca2e 100644 --- a/webapp/dto.py +++ b/webapp/dto.py @@ -27,6 +27,7 @@ def __init__(self, config: Config): self.places_in_group: int = config["PLACES_IN_GROUP"] self.groups: dict = config["GROUPS"] self.allow_ip: list[str] = config["ALLOW_IP"] + self.hide_groups: bool = config["HIDE_GROUPS"] @property def exam(self) -> bool: diff --git a/webapp/forms.py b/webapp/forms.py index 6027c3e..d3e552a 100644 --- a/webapp/forms.py +++ b/webapp/forms.py @@ -15,15 +15,21 @@ class StudentMessageForm(FlaskForm): ]) -class TeacherLoginForm(FlaskForm): - login = StringField('login', [DataRequired(message="Данное поле не может быть пустым!")]) +class StudentLoginForm(FlaskForm): + def __init__(self, lks_oauth_enabled: bool = False, *args, **kwargs): + super().__init__(*args, **kwargs) + self.lks_oauth_enabled = lks_oauth_enabled + + login = StringField('email', [ + DataRequired(message="Данное поле не может быть пустым!"), + ]) + password = PasswordField('password', [ DataRequired(message="Данное поле не может быть пустым!"), - Length(min=8, message="Пароль содержит как минимум 8 символов."), ]) -class StudentLoginForm(FlaskForm): +class StudentRegisterForm(FlaskForm): def __init__(self, lks_oauth_enabled: bool = False, *args, **kwargs): super().__init__(*args, **kwargs) self.lks_oauth_enabled = lks_oauth_enabled @@ -35,14 +41,13 @@ def __init__(self, lks_oauth_enabled: bool = False, *args, **kwargs): "Убедитесь, что строка не включает пробелы." )) ]) + password = PasswordField('password', [ DataRequired(message="Данное поле не может быть пустым!"), Length(min=8, message="Пароль содержит как минимум 8 символов."), NoneOf(["12345678", "password"], message="Не используйте такие пароли, как 12345678 и password.") ]) - -class StudentRegisterForm(StudentLoginForm): confirm = PasswordField('password', [ DataRequired(message="Данное поле не может быть пустым!"), EqualTo('password', message="Пароли не совпадают!") diff --git a/webapp/managers.py b/webapp/managers.py index fb91d5e..affcb15 100644 --- a/webapp/managers.py +++ b/webapp/managers.py @@ -19,7 +19,7 @@ TaskStatusDto, VariantDto ) -from webapp.models import FinalSeed, Group, Message, MessageCheck, Student, Task, TaskStatus, Teacher, Variant +from webapp.models import FinalSeed, Group, Message, MessageCheck, Student, Task, TaskStatus, Variant from webapp.repositories import ( FinalSeedRepository, GroupRepository, @@ -29,7 +29,6 @@ StudentRepository, TaskRepository, TaskStatusRepository, - TeacherRepository, VariantRepository ) @@ -482,10 +481,10 @@ def email_allowed(self, email: str) -> bool: exists = self.mailers.exists(domain) return exists - def create(self, email: str, password: str) -> int: + def create(self, email: str, password: str, teacher=False) -> int: given = password.encode('utf8') hashed = bcrypt.hashpw(given, bcrypt.gensalt()) - student = self.students.create(email, hashed.decode('utf8')) + student = self.students.create(email, hashed.decode('utf8'), teacher) return student.id @@ -599,22 +598,3 @@ def __create_csv(self, table: list[list[str]], delimiter: str): bom = u"\uFEFF" value = bom + si.getvalue() return value - - -class TeacherManager: - def __init__(self, teachers: TeacherRepository): - self.teachers = teachers - - def check_password(self, login: str, password: str) -> Teacher | None: - teacher = self.teachers.find_by_login(login) - if teacher: - given = password.encode('utf8') - actual = teacher.password_hash.encode('utf8') - if bcrypt.checkpw(given, actual): - return teacher - - def create(self, login: str, password: str): - given = password.encode('utf8') - hashed = bcrypt.hashpw(given, bcrypt.gensalt()) - teacher = self.teachers.create(login, hashed.decode('utf8')) - return teacher.id diff --git a/webapp/models.py b/webapp/models.py index 01dbe58..673bff7 100644 --- a/webapp/models.py +++ b/webapp/models.py @@ -122,13 +122,6 @@ class FinalSeed(Base): group = sa.Column("group", sa.Integer, sa.ForeignKey('groups.id'), primary_key=True, nullable=False) -class Teacher(Base): - __tablename__ = "teachers" - id = sa.Column("id", sa.Integer, primary_key=True, nullable=False) - login = sa.Column("login", sa.String, nullable=False) - password_hash = sa.Column("password_hash", sa.String, nullable=False) - - class Student(Base): __tablename__ = "students" id = sa.Column("id", sa.Integer, primary_key=True, nullable=False) @@ -139,6 +132,7 @@ class Student(Base): password_hash = sa.Column("password_hash", sa.String, nullable=True) unconfirmed_hash = sa.Column("unconfirmed_hash", sa.String, nullable=True) blocked = sa.Column("blocked", sa.Boolean, nullable=False) + teacher = sa.Column("teacher", sa.Boolean, nullable=True) class Mailer(Base): diff --git a/webapp/repositories.py b/webapp/repositories.py index 8bd1522..e85f22b 100644 --- a/webapp/repositories.py +++ b/webapp/repositories.py @@ -15,7 +15,6 @@ Student, Task, TaskStatus, - Teacher, Variant, create_session_maker ) @@ -457,30 +456,6 @@ def delete_final_seed(self, group: int): .delete() -class TeacherRepository: - def __init__(self, db: DbContextManager): - self.db = db - - def get_by_id(self, id: int) -> Teacher | None: - with self.db.create_session() as session: - teacher = session.query(Teacher).get(id) - return teacher - - def find_by_login(self, login: str) -> Teacher | None: - with self.db.create_session() as session: - teacher = session.query(Teacher) \ - .filter_by(login=login) \ - .first() - return teacher - - def create(self, login: str, password: str): - with self.db.create_session() as session: - teacher = Teacher(login=login, - password_hash=password) - session.add(teacher) - return teacher - - class StudentRepository: def __init__(self, db: DbContextManager): self.db = db @@ -524,10 +499,10 @@ def confirm(self, email: str): unconfirmed_hash=None, )) - def create(self, email: str, password: str) -> Student: + def create(self, email: str, password: str, teacher=False) -> Student: email = email.lower() with self.db.create_session() as session: - student = Student(email=email, unconfirmed_hash=password, blocked=False) + student = Student(email=email, unconfirmed_hash=password, teacher=teacher, blocked=False) session.add(student) return student @@ -580,6 +555,5 @@ def __init__(self, get_connection: Callable[[], str]): self.messages = MessageRepository(db) self.checks = MessageCheckRepository(db) self.seeds = FinalSeedRepository(db) - self.teachers = TeacherRepository(db) self.students = StudentRepository(db) self.mailers = MailerRepository(db) diff --git a/webapp/templates/error.jinja b/webapp/templates/error.jinja index 204395f..32fa4a8 100644 --- a/webapp/templates/error.jinja +++ b/webapp/templates/error.jinja @@ -13,4 +13,4 @@ -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/webapp/templates/layout.jinja b/webapp/templates/layout.jinja index e87bdad..9eed1e7 100644 --- a/webapp/templates/layout.jinja +++ b/webapp/templates/layout.jinja @@ -1,73 +1,159 @@ +