Loading portal/database/models.py +30 −7 Original line number Diff line number Diff line Loading @@ -6,6 +6,7 @@ from sqlalchemy import event from sqlalchemy.orm import validates import enum import datetime from portal.tools import time # uuid as primary key source: # https://stackoverflow.com/questions/36806403/cant-render-element-of-type-class-sqlalchemy-dialects-postgresql-base-uuid Loading Loading @@ -107,15 +108,15 @@ class Project(db.Model, EntityBase): # check timestamp comparisons def state(self, timestamp=datetime.datetime.now()) -> ProjectState: if self.submissions_allowed_from < timestamp < self.submissions_allowed_to: if self.submissions_allowed_from <= timestamp < self.submissions_allowed_to: return ProjectState.ACTIVE elif timestamp > self.archive_from: elif timestamp >= self.archive_from: return ProjectState.ARCHIVED else: return ProjectState.INACTIVE # date validation: submissions_allowed_from < submissions_allowed_to < archive_from must be true @validates(submissions_allowed_from) @validates('submissions_allowed_from') def validate_submissions_start(self, key, field): if self.submissions_allowed_to is not None and field > self.submissions_allowed_to: raise PortalDbError() Loading @@ -123,7 +124,7 @@ class Project(db.Model, EntityBase): raise PortalDbError() return field @validates(submissions_allowed_to) @validates('submissions_allowed_to') def validate_submissions_end(self, key, field): if self.submissions_allowed_from is not None and self.submissions_allowed_from > field: raise PortalDbError() Loading @@ -131,7 +132,7 @@ class Project(db.Model, EntityBase): raise PortalDbError() return field @validates(archive_from) @validates('archive_from') def validate_archive_start(self, key, field): if self.submissions_allowed_from is not None and self.submissions_allowed_from > field: raise PortalDbError() Loading Loading @@ -317,9 +318,9 @@ class Component(db.Model, EntityBase): ip_address = db.Column(db.String(50)) # source: http://docs.sqlalchemy.org/en/latest/orm/events.html @event.listens_for(Submission.user_id, 'set', named=True) def receive_set(**kw): """listen for the 'set' event""" submission = kw['target'] value = kw['value'] Loading @@ -327,7 +328,29 @@ def receive_set(**kw): raise PortalDbError() # Associations - must come after affected class definitions # Expects that the value being set is received with the correct timezone information. @event.listens_for(Project.submissions_allowed_from, 'set', named=True, retval=True) def receive_set(**kw): submissions_allowed_from = kw['value'] return time.strip_seconds(submissions_allowed_from) @event.listens_for(Project.submissions_allowed_to, 'set', named=True, retval=True) def receive_set(**kw): submissions_allowed_to = kw['value'] return time.strip_seconds(submissions_allowed_to) @event.listens_for(Project.archive_from, 'set', named=True, retval=True) def receive_set(**kw): archive_from = kw['value'] return time.strip_seconds(archive_from) # Associations - must come after definitions of the affected classes users_groups = db.Table("users_groups", db.Model.metadata, db.Column("user_id", db.Integer, db.ForeignKey('user.id')), db.Column("group_id", db.Integer, db.ForeignKey('group.id'))) Loading portal/tools/time.py 0 → 100644 +10 −0 Original line number Diff line number Diff line import datetime portal_timezone = datetime.timezone(offset=datetime.timedelta(hours=1), name='UTC+1') NOW = datetime.datetime.now(tz=portal_timezone) def strip_seconds(time): return time.replace(second=0, microsecond=0) tests/database/test_db.py +140 −4 Original line number Diff line number Diff line from portal.tools.logging import log from portal.database.models import User, Course, Role, Group, Project, Submission from portal.database.models import User, Course, Role, Group, Project, Submission, ProjectState import pytest from sqlalchemy import exc import datetime from datetime import datetime, timedelta, MINYEAR, MAXYEAR from portal.tools import time ''' # will be removed later, when javascript functionality is added Loading Loading @@ -229,7 +230,7 @@ def test_submission_create_valid(session): user = User(uco=123, email='foo', username='xfoo') course = Course(name="C++", codename="PB161") project = Project(course=course, name="p1", test_files_source="git repo") submission = Submission(user=user, project=project, processing_scheduled_for=datetime.datetime.now(), parameters="") submission = Submission(user=user, project=project, processing_scheduled_for=datetime.now(), parameters="") session.add(submission) session.flush() Loading @@ -244,7 +245,7 @@ def test_submission_update_user(session): user2 = User(uco=456, email='bar', username='xbar') course = Course(name="C++", codename="PB161") project = Project(course=course, name="p1", test_files_source="git repo") submission = Submission(user=user1, project=project, processing_scheduled_for=datetime.datetime.now(), parameters="") submission = Submission(user=user1, project=project, processing_scheduled_for=datetime.now(), parameters="") session.add_all([submission, user2]) session.flush() Loading Loading @@ -650,3 +651,138 @@ def test_relation_user_roles_add_role_same_course(session): assert user in role2.users assert user in role1.users assert role2 in user.roles # Project timing def test_project_time_constraints_valid(session): course = Course(name="C++", codename="PB161") project = Project(course=course, name="p1", test_files_source="git repo") project.submissions_allowed_from = time.NOW - timedelta(hours=1) project.submissions_allowed_to = time.NOW project.archive_from = time.NOW + timedelta(days=1) session.add(project) session.flush() assert project.submissions_allowed_from < project.submissions_allowed_to assert project.submissions_allowed_to < project.archive_from def test_project_time_constraints_valid_extreme(session): course = Course(name="C++", codename="PB161") project = Project(course=course, name="p1", test_files_source="git repo") project.submissions_allowed_from = datetime(year=MINYEAR, month=1, day=1, tzinfo=time.portal_timezone) project.submissions_allowed_to = time.NOW project.archive_from = datetime(year=MAXYEAR, month=12, day=31, tzinfo=time.portal_timezone) session.add(project) session.flush() assert project.submissions_allowed_from < project.submissions_allowed_to assert project.submissions_allowed_to < project.archive_from def test_project_time_constraints_valid_missing_from(session): course = Course(name="C++", codename="PB161") project = Project(course=course, name="p1", test_files_source="git repo") project.submissions_allowed_to = time.NOW project.archive_from = time.NOW + timedelta(days=1) session.add(project) session.flush() assert project.submissions_allowed_to < project.archive_from def test_project_time_constraints_valid_missing_to(session): course = Course(name="C++", codename="PB161") project = Project(course=course, name="p1", test_files_source="git repo") project.submissions_allowed_from = time.NOW - timedelta(hours=1) project.archive_from = time.NOW + timedelta(days=1) session.add(project) session.flush() assert project.submissions_allowed_from < project.archive_from def test_project_time_constraints_valid_missing_archive(session): course = Course(name="C++", codename="PB161") project = Project(course=course, name="p1", test_files_source="git repo") project.submissions_allowed_from = time.NOW - timedelta(hours=1) project.submissions_allowed_to = time.NOW session.add(project) session.flush() assert project.submissions_allowed_from < project.submissions_allowed_to def test_project_time_constraints_same_from_to(session): course = Course(name="C++", codename="PB161") project = Project(course=course, name="p1", test_files_source="git repo") project.submissions_allowed_from = time.NOW project.submissions_allowed_to = time.NOW session.add(project) session.flush() def test_project_time_constraints_same_to_archive(session): course = Course(name="C++", codename="PB161") project = Project(course=course, name="p1", test_files_source="git repo") project.submissions_allowed_to = time.NOW project.archive_from = time.NOW session.add(project) session.flush() def test_project_time_constraints_same_from_archive(session): course = Course(name="C++", codename="PB161") project = Project(course=course, name="p1", test_files_source="git repo") project.submissions_allowed_from = time.NOW project.archive_from = time.NOW session.add(project) session.flush() def test_project_time_constraints_invalid_from_to(): course = Course(name="C++", codename="PB161") project = Project(course=course, name="p1", test_files_source="git repo") project.submissions_allowed_from = time.NOW with pytest.raises(exc.SQLAlchemyError): project.submissions_allowed_to = time.NOW - timedelta(minutes=1) def test_project_time_constraints_invalid_from_archive(): course = Course(name="C++", codename="PB161") project = Project(course=course, name="p1", test_files_source="git repo") project.submissions_allowed_from = time.NOW with pytest.raises(exc.SQLAlchemyError): project.archive_from = time.NOW - timedelta(minutes=1) def test_project_time_constraints_invalid_to_archive(): course = Course(name="C++", codename="PB161") project = Project(course=course, name="p1", test_files_source="git repo") project.submissions_allowed_to = time.NOW with pytest.raises(exc.SQLAlchemyError): project.archive_from = time.NOW - timedelta(minutes=1) def test_project_state(session): course = Course(name="C++", codename="PB161") project = Project(course=course, name="p1", test_files_source="git repo") project.submissions_allowed_from = time.NOW - timedelta(days=1) project.submissions_allowed_to = time.NOW project.archive_from = time.NOW + timedelta(days=1) session.add(project) session.flush() assert project.state(time.NOW - timedelta(days=1)) == ProjectState.ACTIVE assert project.state(time.NOW - timedelta(hours=1)) == ProjectState.ACTIVE assert project.state(time.NOW - timedelta(minutes=1)) == ProjectState.ACTIVE assert project.state(time.NOW) == ProjectState.INACTIVE assert project.state(time.NOW - timedelta(days=1, minutes=1)) == ProjectState.INACTIVE assert project.state(time.NOW + timedelta(days=1)) == ProjectState.ARCHIVED Loading
portal/database/models.py +30 −7 Original line number Diff line number Diff line Loading @@ -6,6 +6,7 @@ from sqlalchemy import event from sqlalchemy.orm import validates import enum import datetime from portal.tools import time # uuid as primary key source: # https://stackoverflow.com/questions/36806403/cant-render-element-of-type-class-sqlalchemy-dialects-postgresql-base-uuid Loading Loading @@ -107,15 +108,15 @@ class Project(db.Model, EntityBase): # check timestamp comparisons def state(self, timestamp=datetime.datetime.now()) -> ProjectState: if self.submissions_allowed_from < timestamp < self.submissions_allowed_to: if self.submissions_allowed_from <= timestamp < self.submissions_allowed_to: return ProjectState.ACTIVE elif timestamp > self.archive_from: elif timestamp >= self.archive_from: return ProjectState.ARCHIVED else: return ProjectState.INACTIVE # date validation: submissions_allowed_from < submissions_allowed_to < archive_from must be true @validates(submissions_allowed_from) @validates('submissions_allowed_from') def validate_submissions_start(self, key, field): if self.submissions_allowed_to is not None and field > self.submissions_allowed_to: raise PortalDbError() Loading @@ -123,7 +124,7 @@ class Project(db.Model, EntityBase): raise PortalDbError() return field @validates(submissions_allowed_to) @validates('submissions_allowed_to') def validate_submissions_end(self, key, field): if self.submissions_allowed_from is not None and self.submissions_allowed_from > field: raise PortalDbError() Loading @@ -131,7 +132,7 @@ class Project(db.Model, EntityBase): raise PortalDbError() return field @validates(archive_from) @validates('archive_from') def validate_archive_start(self, key, field): if self.submissions_allowed_from is not None and self.submissions_allowed_from > field: raise PortalDbError() Loading Loading @@ -317,9 +318,9 @@ class Component(db.Model, EntityBase): ip_address = db.Column(db.String(50)) # source: http://docs.sqlalchemy.org/en/latest/orm/events.html @event.listens_for(Submission.user_id, 'set', named=True) def receive_set(**kw): """listen for the 'set' event""" submission = kw['target'] value = kw['value'] Loading @@ -327,7 +328,29 @@ def receive_set(**kw): raise PortalDbError() # Associations - must come after affected class definitions # Expects that the value being set is received with the correct timezone information. @event.listens_for(Project.submissions_allowed_from, 'set', named=True, retval=True) def receive_set(**kw): submissions_allowed_from = kw['value'] return time.strip_seconds(submissions_allowed_from) @event.listens_for(Project.submissions_allowed_to, 'set', named=True, retval=True) def receive_set(**kw): submissions_allowed_to = kw['value'] return time.strip_seconds(submissions_allowed_to) @event.listens_for(Project.archive_from, 'set', named=True, retval=True) def receive_set(**kw): archive_from = kw['value'] return time.strip_seconds(archive_from) # Associations - must come after definitions of the affected classes users_groups = db.Table("users_groups", db.Model.metadata, db.Column("user_id", db.Integer, db.ForeignKey('user.id')), db.Column("group_id", db.Integer, db.ForeignKey('group.id'))) Loading
portal/tools/time.py 0 → 100644 +10 −0 Original line number Diff line number Diff line import datetime portal_timezone = datetime.timezone(offset=datetime.timedelta(hours=1), name='UTC+1') NOW = datetime.datetime.now(tz=portal_timezone) def strip_seconds(time): return time.replace(second=0, microsecond=0)
tests/database/test_db.py +140 −4 Original line number Diff line number Diff line from portal.tools.logging import log from portal.database.models import User, Course, Role, Group, Project, Submission from portal.database.models import User, Course, Role, Group, Project, Submission, ProjectState import pytest from sqlalchemy import exc import datetime from datetime import datetime, timedelta, MINYEAR, MAXYEAR from portal.tools import time ''' # will be removed later, when javascript functionality is added Loading Loading @@ -229,7 +230,7 @@ def test_submission_create_valid(session): user = User(uco=123, email='foo', username='xfoo') course = Course(name="C++", codename="PB161") project = Project(course=course, name="p1", test_files_source="git repo") submission = Submission(user=user, project=project, processing_scheduled_for=datetime.datetime.now(), parameters="") submission = Submission(user=user, project=project, processing_scheduled_for=datetime.now(), parameters="") session.add(submission) session.flush() Loading @@ -244,7 +245,7 @@ def test_submission_update_user(session): user2 = User(uco=456, email='bar', username='xbar') course = Course(name="C++", codename="PB161") project = Project(course=course, name="p1", test_files_source="git repo") submission = Submission(user=user1, project=project, processing_scheduled_for=datetime.datetime.now(), parameters="") submission = Submission(user=user1, project=project, processing_scheduled_for=datetime.now(), parameters="") session.add_all([submission, user2]) session.flush() Loading Loading @@ -650,3 +651,138 @@ def test_relation_user_roles_add_role_same_course(session): assert user in role2.users assert user in role1.users assert role2 in user.roles # Project timing def test_project_time_constraints_valid(session): course = Course(name="C++", codename="PB161") project = Project(course=course, name="p1", test_files_source="git repo") project.submissions_allowed_from = time.NOW - timedelta(hours=1) project.submissions_allowed_to = time.NOW project.archive_from = time.NOW + timedelta(days=1) session.add(project) session.flush() assert project.submissions_allowed_from < project.submissions_allowed_to assert project.submissions_allowed_to < project.archive_from def test_project_time_constraints_valid_extreme(session): course = Course(name="C++", codename="PB161") project = Project(course=course, name="p1", test_files_source="git repo") project.submissions_allowed_from = datetime(year=MINYEAR, month=1, day=1, tzinfo=time.portal_timezone) project.submissions_allowed_to = time.NOW project.archive_from = datetime(year=MAXYEAR, month=12, day=31, tzinfo=time.portal_timezone) session.add(project) session.flush() assert project.submissions_allowed_from < project.submissions_allowed_to assert project.submissions_allowed_to < project.archive_from def test_project_time_constraints_valid_missing_from(session): course = Course(name="C++", codename="PB161") project = Project(course=course, name="p1", test_files_source="git repo") project.submissions_allowed_to = time.NOW project.archive_from = time.NOW + timedelta(days=1) session.add(project) session.flush() assert project.submissions_allowed_to < project.archive_from def test_project_time_constraints_valid_missing_to(session): course = Course(name="C++", codename="PB161") project = Project(course=course, name="p1", test_files_source="git repo") project.submissions_allowed_from = time.NOW - timedelta(hours=1) project.archive_from = time.NOW + timedelta(days=1) session.add(project) session.flush() assert project.submissions_allowed_from < project.archive_from def test_project_time_constraints_valid_missing_archive(session): course = Course(name="C++", codename="PB161") project = Project(course=course, name="p1", test_files_source="git repo") project.submissions_allowed_from = time.NOW - timedelta(hours=1) project.submissions_allowed_to = time.NOW session.add(project) session.flush() assert project.submissions_allowed_from < project.submissions_allowed_to def test_project_time_constraints_same_from_to(session): course = Course(name="C++", codename="PB161") project = Project(course=course, name="p1", test_files_source="git repo") project.submissions_allowed_from = time.NOW project.submissions_allowed_to = time.NOW session.add(project) session.flush() def test_project_time_constraints_same_to_archive(session): course = Course(name="C++", codename="PB161") project = Project(course=course, name="p1", test_files_source="git repo") project.submissions_allowed_to = time.NOW project.archive_from = time.NOW session.add(project) session.flush() def test_project_time_constraints_same_from_archive(session): course = Course(name="C++", codename="PB161") project = Project(course=course, name="p1", test_files_source="git repo") project.submissions_allowed_from = time.NOW project.archive_from = time.NOW session.add(project) session.flush() def test_project_time_constraints_invalid_from_to(): course = Course(name="C++", codename="PB161") project = Project(course=course, name="p1", test_files_source="git repo") project.submissions_allowed_from = time.NOW with pytest.raises(exc.SQLAlchemyError): project.submissions_allowed_to = time.NOW - timedelta(minutes=1) def test_project_time_constraints_invalid_from_archive(): course = Course(name="C++", codename="PB161") project = Project(course=course, name="p1", test_files_source="git repo") project.submissions_allowed_from = time.NOW with pytest.raises(exc.SQLAlchemyError): project.archive_from = time.NOW - timedelta(minutes=1) def test_project_time_constraints_invalid_to_archive(): course = Course(name="C++", codename="PB161") project = Project(course=course, name="p1", test_files_source="git repo") project.submissions_allowed_to = time.NOW with pytest.raises(exc.SQLAlchemyError): project.archive_from = time.NOW - timedelta(minutes=1) def test_project_state(session): course = Course(name="C++", codename="PB161") project = Project(course=course, name="p1", test_files_source="git repo") project.submissions_allowed_from = time.NOW - timedelta(days=1) project.submissions_allowed_to = time.NOW project.archive_from = time.NOW + timedelta(days=1) session.add(project) session.flush() assert project.state(time.NOW - timedelta(days=1)) == ProjectState.ACTIVE assert project.state(time.NOW - timedelta(hours=1)) == ProjectState.ACTIVE assert project.state(time.NOW - timedelta(minutes=1)) == ProjectState.ACTIVE assert project.state(time.NOW) == ProjectState.INACTIVE assert project.state(time.NOW - timedelta(days=1, minutes=1)) == ProjectState.INACTIVE assert project.state(time.NOW + timedelta(days=1)) == ProjectState.ARCHIVED