def test_update_class(self, empty_json_database, test_full_class): test_json_database = empty_json_database test_class = Class(test_full_class.name, test_full_class.students) # create_class takes a NewClass object due to avatar moving machinery. test_class_new_class = NewClass(test_full_class.name, test_full_class.students) assert test_class.json_dict() == test_class_new_class.json_dict( ) # Ensure classes are the same. # Create class in database. test_json_database.create_class( NewClass(test_full_class.name, test_full_class.students)) # Ensure test_class in database test_class.id = test_class.name # Loaded class will have ids: test_loaded_class = test_full_class.json_dict() for student in test_loaded_class['students']: student['id'] = student['name'] assert test_json_database.load_class( test_class.name).json_dict() == test_loaded_class # Change class by adding student, update database: new_student = Student(name='new student') assert new_student not in test_class and new_student.name not in test_class # Confirm student not in class. test_class.add_student(new_student) assert test_json_database.update_class(test_class) is None # Look up name because new_student object itself is not in the loaded class object. assert new_student.name in test_json_database.load_class( test_class.name)
def test_new_class_name_only(): """Returns empty NewClass instantiated with name only.""" test_new_class_name_only = NewClass( test_class_name_only_data_set['json_dict_rep']['name']) # Add attributes to test expected output. test_new_class_name_only.json_str_rep = test_class_name_only_data_set[ 'json_str_rep'] test_new_class_name_only.json_dict_rep = test_class_name_only_data_set[ 'json_dict_rep'] return test_new_class_name_only
def take_class_data_input(class_name: str) -> NewClass: """ Take student names, avatars, return Class object. :param class_name: str :return: Class """ new_class = NewClass(name=class_name) while True: student_name = take_student_name_input(new_class) if student_name.upper() == 'END': break avatar_filename = take_student_avatar(new_class, student_name) new_class.add_student(name=student_name, avatar_id=avatar_filename) return new_class
def test_get_classes(self, empty_sqlite_database, existing_class_names, returned_value): test_sqlite_database = empty_sqlite_database for class_name in existing_class_names: empty_sqlite_database.create_class(NewClass(name=class_name)) assert test_sqlite_database.get_classes() == returned_value
def test_get_avatar_path(self, tmpdir, request, database_backend, avatar_provided): """""" test_database = request.getfixturevalue(database_backend) # Create avatar: test_avatar_data = b'some binary data' test_avatar_path = Path(tmpdir, 'test_avatar.png') test_avatar_path.write_bytes(test_avatar_data) # Add avatar to db: test_class = NewClass( name='test class', students=[ Student( name='test_student', avatar_id=(test_avatar_path if avatar_provided else None), ) ]) test_database.create_class(test_class) # Find avatar_id, load class to verify: classes = test_database.get_classes() test_class_id = classes[0].id # As the only class will be first item. loaded_test_class = test_database.load_class(test_class_id) test_avatar_id = loaded_test_class.students[0].avatar_id # Path may be different/random - test data: assert test_database.get_avatar_path(test_avatar_id).read_bytes() == ( test_avatar_data if avatar_provided else test_database.default_avatar_path.read_bytes())
def test_take_student_avatar_pre_clean_name(self, monkeypatch): test_class = NewClass('some_class') test_student_name = 'clean name' test_avatar_filepath = Path('avatar file name') cleaned_student_name = 'file name was already clean' returned_filename = f'{cleaned_student_name}.png' def mocked_select_avatar_file_dialogue(): return test_avatar_filepath def mocked_clean_for_filename(student_name): if student_name != test_student_name: raise ValueError # Ensure called with correct arg. return cleaned_student_name def mocked_copy_file(avatar_filepath, destination_filepath): if (avatar_filepath, destination_filepath) != ( test_avatar_filepath, test_class.temp_avatars_dir.joinpath(returned_filename)): raise ValueError # Ensure called with correct args. return None monkeypatch.setattr(class_functions, 'select_avatar_file_dialogue', mocked_select_avatar_file_dialogue) monkeypatch.setattr(class_functions, 'clean_for_filename', mocked_clean_for_filename) monkeypatch.setattr(class_functions, 'copy_file', mocked_copy_file) assert take_student_avatar(test_class, test_student_name) == returned_filename
def test_create_chart(self, request, database_backend): """ Verify API works. NB No verification in API test, as API for verifying does not exist. TODO: Verify saved chart contents when load/edit features added. """ test_database = request.getfixturevalue(database_backend) test_class = NewClass(name='test_class', students=[ Student(name='bad student'), Student(name='mediocre student'), Student(name='excellent student'), ]) test_database.create_class(test_class) test_chart_data_dict = { 'class_id': test_class.id, 'class_name': test_class.name, 'chart_name': 'test_chart_name', 'chart_default_filename': 'test_default_chart_filename', 'chart_params': { 'some': 'params' }, 'score-students_dict': { 0: [test_class.students[0]], 50: [test_class.students[1]], 100: [test_class.students[2]], } } assert test_database.create_chart(test_chart_data_dict) is None
def test_load_class(self, request, database_backend, class_data): """Class loaded has same data as that saved in db, with class/student ids.""" test_database = request.getfixturevalue(database_backend) preexisting_class = request.getfixturevalue(class_data) # Create test NewClass object, mocking avatar_files. preexisting_class = NewClass.from_dict(preexisting_class.json_dict()) for student in preexisting_class: if student.avatar_id: Path(preexisting_class.temp_avatars_dir, student.avatar_id).write_text(student.avatar_id) # Create class in db: test_database.create_class(preexisting_class) # Find class id, load class to verify: classes = test_database.get_classes() test_full_class_id = classes[ 0].id # As the only class will be first item. # Loaded class will have ids: test_loaded_class_with_student_ids = Class.from_dict( preexisting_class.json_dict()) if isinstance(test_database, JSONDatabase): for student in test_loaded_class_with_student_ids.students: student.id = student.name if not isinstance(test_database, JSONDatabase): # This should be accurate for most sql databases. for test_id, student in enumerate( test_loaded_class_with_student_ids.students, start=1): student.id = test_id assert test_database.load_class(test_full_class_id).json_dict( ) == test_loaded_class_with_student_ids.json_dict()
class TestAssembleChartData: @pytest.mark.parametrize('class_from_create_class', [Class.from_dict(test_full_class_data_set['json_dict_rep']), # Pass in test_class NewClass.from_dict(test_full_class_data_set['json_dict_rep']) # NewClass obj ]) def test_assemble_chart_data(self, monkeypatch, class_from_create_class): test_chart_name = 'my_chart' test_chart_filename = test_chart_name mock_score_avatar_dict = {'scores': 'list of avatars'} mock_chart_params = {'some': 'chart_params'} def mocked_take_score_data(class_obj): assert class_obj is class_from_create_class return mock_score_avatar_dict def mocked_take_chart_name(): return test_chart_name def mocked_clean_for_filename(chart_name): assert chart_name == test_chart_name return test_chart_name def mocked_set_chart_params(): return mock_chart_params monkeypatch.setattr(create_chart, 'take_score_data', mocked_take_score_data) monkeypatch.setattr(create_chart, 'take_chart_name', mocked_take_chart_name) monkeypatch.setattr(create_chart, 'clean_for_filename', mocked_clean_for_filename) monkeypatch.setattr(create_chart, 'set_chart_params', mocked_set_chart_params) assert assemble_chart_data(class_from_create_class) == ( test_chart_name, test_chart_filename, mock_score_avatar_dict, mock_chart_params)
def test_create_class(self, request, empty_sqlite_database, class_data): test_database = empty_sqlite_database test_class_data = request.getfixturevalue(class_data) # Create test NewClass object, mocking avatar_files. test_class = NewClass.from_dict(test_class_data.json_dict()) for student in test_class: if student.avatar_id: Path(test_class.temp_avatars_dir, student.avatar_id).write_text(student.avatar_id) # no students or class in empty db: assert not test_database._connection().cursor().execute( """SELECT * FROM class""").fetchall() assert not test_database._connection().cursor().execute( """SELECT * FROM student""").fetchall() # Create class in db: assert test_database.create_class(test_class) is None # Find class id, load class to verify: classes = test_database.get_classes() test_class_id = classes[0].id # Class will have ids: test_loaded_class_with_student_ids = Class.from_dict( test_class.json_dict()).json_dict() for test_id, student in enumerate( test_loaded_class_with_student_ids['students'], start=1): student['id'] = test_id assert test_database.load_class( # NB Returned object will be Class, not NewClass: test_class_id).json_dict() == test_loaded_class_with_student_ids
def test_class_name_exists(self, request, database_backend, test_class_name, existing_class_names, returned_value): test_database = request.getfixturevalue(database_backend) for class_name in existing_class_names: test_database.create_class(NewClass(name=class_name)) assert test_database.class_name_exists( test_class_name) == returned_value
def test_new_class_uses_path_safe_name(self): # Ensure class name has disallowed characters - validate test. test_new_class = NewClass("S|r Røbin's ß@boon$") assert test_new_class.name != test_new_class.path_safe_name # Ensure class_name_with_disallowed chars in temp dir path. assert test_new_class.name not in str(test_new_class.temp_dir) # Ensure path_safe_name is in temp dir path. assert test_new_class.path_safe_name in str(test_new_class.temp_dir)
def test_move_avatar_to_class_data(self, monkeypatch, empty_json_database): """Avatar moved from original location to class data.""" test_class = NewClass('test_class') test_class.id = test_class.name # Set NewClass id to save in db. test_filename = 'test avatar filename' test_json_database = empty_json_database def mock_move_file(origin_path: Path, destination_path: Path): if origin_path != test_class.temp_dir.joinpath( 'avatars', test_filename): raise ValueError("Origin path incorrect.") if destination_path != test_json_database.class_data_path.joinpath( test_class.name, 'avatars', test_filename): raise ValueError("Destination path incorrect") monkeypatch.setattr(json, 'move_file', mock_move_file) assert test_json_database._move_avatar_to_class_data( test_class, test_filename) is None
def test_get_classes(self, request, database_backend, existing_class_names, returned_id_names): test_database = request.getfixturevalue(database_backend) for class_name in existing_class_names: test_database.create_class(NewClass(name=class_name)) retrieved_class_identifiers = test_database.get_classes() # class_identifier.id will be different for each backend, but the names will be the same. assert [class_id.name for class_id in retrieved_class_identifiers ] == returned_id_names
def test_move_avatar_to_class_data_avatar_preexisting( self, monkeypatch, empty_json_database): """No attempt to move avatar that already exists in class_data.""" test_class = NewClass('test_class') test_class.id = test_class.name # Set NewClass id to save in db. test_json_database = empty_json_database # Make existing avatar in tmpdir test_class class data: destination_avatar_path = test_json_database.class_data_path.joinpath( test_class.name, 'avatars', 'test_avatar_filename') Path.mkdir(destination_avatar_path.parent, parents=True) with open(destination_avatar_path, 'w'): pass def mock_move_file(origin_path: Path, destination_path: Path): raise ValueError("Move file should not be called.") monkeypatch.setattr(json, 'move_file', mock_move_file) assert test_json_database._move_avatar_to_class_data( test_class, destination_avatar_path.name) is None
def test_new_class_temp_dir_deleted_on_deletion(self, monkeypatch, tmpdir): test_temp_dir = Path(tmpdir, 'temp_dir') test_temp_dir.mkdir(parents=True) # Make temp_dir. assert not os.listdir(test_temp_dir) # Nothing in test_temp_dir. monkeypatch.setattr(class_, 'TEMP_DIR', test_temp_dir) test_class = NewClass("Sir Robin's baboons") assert test_class.temp_dir.exists() and os.listdir( test_temp_dir) # Class temp dir in test_temp_dir. del test_class # NB May throw an (ignored) Exception because the class is garbage collected before this line. # No class temp dir in test_temp_dir: assert not os.listdir(test_temp_dir)
def test_score_table_repr(self, empty_sqlite_sqlalchemy_database): test_database = empty_sqlite_sqlalchemy_database test_class = NewClass(name='test_class', students=[ Student(name='bad student'), Student(name='mediocre student'), Student(name='excellent student'), Student(name='another mediocre student'), ]) test_database.create_class(test_class) test_chart_data_dict = { 'class_id': test_class.id, 'class_name': test_class.name, 'chart_name': 'test_chart_name', 'chart_default_filename': 'test_default_chart_filename', 'chart_params': { 'some': 'params' }, 'score-students_dict': { 0: [test_class.students[0]], 50.0: [test_class.students[1], test_class.students[3]], 100: [test_class.students[2]], } } test_database.create_chart(test_chart_data_dict) scores_data = [] score_id = 1 # initiate score id for score, students in test_chart_data_dict[ 'score-students_dict'].items(): # NB One chart in db -> chart.id = 1] for student in students: scores_data += [(score_id, test_chart_data_dict['chart_id'], student.id, score)] score_id += 1 # Verify reprs of scores in db: score_strings = [ ( f"<Score(" f"id={score[0]}, " f"chart_id={score[1]}, " f"student_id={score[2]}, " f"value={float(score[3])}" # Value is stored as a float. f")>") for score in scores_data ] with test_database.session_scope() as test_session: assert repr(test_session.query( test_database.Score).all()) == f"[{', '.join(score_strings)}]"
def test_create_chart(self, empty_sqlite_sqlalchemy_database): """ Verify API works. NB No verification in API test, as API for verifying does not exist. TODO: Verify saved chart contents when load/edit features added. """ test_database = empty_sqlite_sqlalchemy_database test_class = NewClass(name='test_class', students=[ Student(name='bad student'), Student(name='mediocre student'), Student(name='excellent student'), Student(name='another mediocre student'), ]) test_database.create_class(test_class) test_chart_data_dict = { 'class_id': test_class.id, 'class_name': test_class.name, 'chart_name': 'test_chart_name', 'chart_default_filename': 'test_default_chart_filename', 'chart_params': { 'some': 'params' }, 'score-students_dict': { 0: [test_class.students[0]], 50: [test_class.students[1], test_class.students[3]], 100: [test_class.students[2]], } } assert test_database.create_chart(test_chart_data_dict) is None # Verify chart data in db: # A load_chart method might go here. with test_database.session_scope() as test_session: assert test_session.execute( """SELECT chart.name FROM chart""").fetchone( )[0] == test_chart_data_dict['chart_name'] scores_data = [] for score, students in test_chart_data_dict[ 'score-students_dict'].items(): # NB One chart in db -> chart.id = 1 scores_data += [(1, student.id, score) for student in students] assert test_session.execute( """SELECT chart_id, student_id, value FROM score""").fetchall( ) == scores_data # Ensure chart id added to chart_data_dict: assert test_chart_data_dict['chart_id'] == 1
def create_class(self, new_class: NewClass) -> None: """ Take a Class object and create/write the class in the database. Creates class data in persistence. Calls setup_class to create any needed files, then writes data to file. :param new_class: Class object :return: None """ new_class.id = new_class.name self._setup_class(new_class.id) self._write_classlist_to_file(new_class) self._move_avatars_to_class_data(new_class)
def test_take_student_avatar_no_avatar(self, monkeypatch): test_class = NewClass('some_class') def mocked_select_avatar_file_dialogue(): return None # No file selected monkeypatch.setattr(class_functions, 'select_avatar_file_dialogue', mocked_select_avatar_file_dialogue) # Ensure calls to other funcs will cause error. monkeypatch.delattr(class_functions, 'clean_for_filename') monkeypatch.delattr(class_functions, 'copy_file') assert take_student_avatar(test_class, 'some student') is None
def test_new_class_temp_dir_created(self, monkeypatch, tmpdir): test_temp_dir = Path(tmpdir, 'temp_dir') test_temp_dir.mkdir(parents=True) # Make temp_dir. assert not os.listdir(test_temp_dir) # Nothing in test_temp_dir. monkeypatch.setattr(class_, 'TEMP_DIR', test_temp_dir) test_class = NewClass("Sir Robin's baboons") assert test_class.temp_dir.exists() assert test_class.temp_dir.name in os.listdir(test_temp_dir) # Check temp_avatars_dir assert test_class.temp_avatars_dir.exists() # noinspection PyTypeChecker assert test_class.temp_avatars_dir.name in os.listdir( test_class.temp_dir)
def test_class_table_repr(self, empty_sqlite_sqlalchemy_database): test_database = empty_sqlite_sqlalchemy_database test_class = NewClass(name='test_class', students=[ Student(name='bad student'), Student(name='mediocre student'), Student(name='excellent student'), Student(name='another mediocre student'), ]) test_database.create_class(test_class) # Verify repr of (only) class in db: with test_database.session_scope() as test_session: assert repr(test_session.query(test_database.Class).one( )) == f"<Class(id={test_class.id}, name={test_class.name})>"
def test_avatar_table_repr(self, tmpdir, empty_sqlite_sqlalchemy_database): test_database = empty_sqlite_sqlalchemy_database dummy_avatar_bytes = b'1a2b3c4d' avatar_path = Path(tmpdir, 'avatar_path.jpg') avatar_path.write_bytes(dummy_avatar_bytes) test_class = NewClass( name='test_class', students=[Student(name='bad student', avatar_id=avatar_path)]) test_database.create_class(test_class) # Verify reprs of avatar in db: with test_database.session_scope() as test_session: assert repr( test_session.query(test_database.Avatar).one() ) == f"<Avatar(id={test_class.students[0].id}, image={dummy_avatar_bytes})>"
def test_full_new_class(): """Returns empty NewClass instantiated with students.""" test_full_new_class = NewClass( test_full_class_data_set['json_dict_rep']['name']) for student in test_full_class_data_set['json_dict_rep']['students']: test_full_new_class.add_student(Student(**student)) test_full_new_class.json_str_rep = test_full_class_data_set['json_str_rep'] test_full_new_class.json_dict_rep = test_full_class_data_set[ 'json_dict_rep'] return test_full_new_class
def test_chart_table_repr(self, empty_sqlite_sqlalchemy_database): test_database = empty_sqlite_sqlalchemy_database test_class = NewClass(name='test_class', students=[ Student(name='bad student'), Student(name='mediocre student'), Student(name='excellent student'), Student(name='another mediocre student'), ]) test_database.create_class(test_class) test_chart_data_dict = { 'class_id': test_class.id, 'class_name': test_class.name, 'chart_name': 'test_chart_name', 'chart_default_filename': 'test_default_chart_filename', 'chart_params': { 'some': 'params' }, 'score-students_dict': { 0: [test_class.students[0]], 50: [test_class.students[1], test_class.students[3]], 100: [test_class.students[2]], } } test_database.create_chart(test_chart_data_dict) # Create fake plot/image mock_plt = plt.figure(figsize=(19.20, 10.80)) test_image = io.BytesIO() mock_plt.savefig(test_image, format='png', dpi=300) test_image.seek(0) # Return pointer to start of binary stream. test_database.save_chart_image(test_chart_data_dict, mock_plt) # Verify repr of (only) chart in db: with test_database.session_scope() as test_session: assert repr(test_session.query(test_database.Chart).one()) == ( f"<Chart(" f"id={test_chart_data_dict['chart_id']}, " f"name={test_chart_data_dict['chart_name']}, " f"image={test_image.read1()}, " f"date=None" # Not implemented yet. f")>")
def create_class(self, new_class: NewClass) -> None: """ Create class data in database. Moves avatars for each student to db, changes student.avatar_id to id of avatar image in db. If no avatar, this remains None/null, and that is stored as student's avatar_id in db. :param new_class: :return: None """ with self._connection() as conn: cursor = conn.cursor() # insert class into class cursor.execute( """ INSERT INTO class(name) VALUES(?); """, (new_class.name, )) # Add id to class: new_class.id = cursor.lastrowid for student in new_class: # insert student if student.avatar_id: # Move avatar from temp to db: avatar_blob = new_class.temp_avatars_dir.joinpath( student.avatar_id).read_bytes() cursor.execute( """ INSERT INTO avatar(image) VALUES(?); """, (avatar_blob, )) # Change avatar_id to id of avatar in db. student.avatar_id = cursor.lastrowid cursor.execute( """ INSERT INTO student(name, class_id, avatar_id) VALUES(?,?,?); """, (student.name, new_class.id, student.avatar_id)) # Add id to student: student.id = cursor.lastrowid conn.commit() conn.close()
def test_store_students_as_student_names(self, empty_json_database): test_json_database = empty_json_database test_json_database.default_avatar_path = Path('default/path') test_class = NewClass(name='test_class', students=[ Student(name='bad student'), Student(name='mediocre student'), Student(name='excellent student'), ]) test_data_dict = { 'class_name': "test_class_name", 'chart_name': "test_chart_name", 'chart_default_filename': "test_chart_default_filename", 'chart_params': { "some": "chart", "default": "params" }, 'score-students_dict': { 0: [test_class.students[0]], 50: [test_class.students[1]], 100: [test_class.students[2]], }, } test_returned_sanitised_data_dict = { 'class_name': "test_class_name", 'chart_name': "test_chart_name", 'chart_default_filename': "test_chart_default_filename", 'chart_params': { "some": "chart", "default": "params" }, 'score-students_dict': { 0: [test_class.students[0].name], 50: [test_class.students[1].name], 100: [test_class.students[2].name], }, } assert test_json_database._store_students_as_student_names( test_data_dict) == test_returned_sanitised_data_dict
def test_student_table_repr(self, empty_sqlite_sqlalchemy_database): test_database = empty_sqlite_sqlalchemy_database test_class = NewClass(name='test_class', students=[ Student(name='bad student'), Student(name='mediocre student'), Student(name='excellent student'), Student(name='another mediocre student'), ]) test_database.create_class(test_class) # Verify reprs of students in db: student_strings = [(f"<Student(" f"id={student.id}, " f"name={student.name}, " f"class_id={test_class.id}, " f"avatar_id={student.avatar_id}" f")>") for student in test_class.students] with test_database.session_scope() as test_session: assert repr(test_session.query(test_database.Student).all() ) == f"[{', '.join(student_strings)}]"
def create_class(self, new_class: NewClass) -> None: """ Create class data in database. Moves avatars for each student to db, changes student.avatar_id to id of avatar image in db. If no avatar, this remains None/null, and that is stored as student's avatar_id in db. :param new_class: :return: None """ with self.session_scope() as session: added_class = self.Class(name=new_class.name) session.add(added_class) session.flush() # Commit to get a class id new_class.id = added_class.id # Add students: for student in new_class: if student.avatar_id: # Move avatar from temp to db: avatar_blob = new_class.temp_avatars_dir.joinpath( student.avatar_id).read_bytes() added_avatar = self.Avatar(image=avatar_blob) session.add(added_avatar) session.flush( ) # Must flush after each avatar to get the avatar id. student.avatar_id = added_avatar.id added_student = self.Student( name=student.name, class_id=new_class.id, avatar_id=student.avatar_id, ) session.add(added_student) session.flush( ) # Must flush after each student to get the avatar id. # Add id to student: student.id = added_student.id
def test_take_student_avatar_dirty_name(self, monkeypatch): test_class = NewClass('some_class') test_student_name = r'very unsafe */^@ :$ name' test_avatar_filepath = Path('avatar file name') returned_filename = f'{clean_for_filename(test_student_name)}.png' def mocked_select_avatar_file_dialogue(): return test_avatar_filepath def mocked_copy_file(avatar_filepath, destination_filepath): if (avatar_filepath, destination_filepath) != ( test_avatar_filepath, test_class.temp_avatars_dir.joinpath(returned_filename)): raise ValueError # Ensure called with correct args. return None monkeypatch.setattr(class_functions, 'select_avatar_file_dialogue', mocked_select_avatar_file_dialogue) monkeypatch.setattr(class_functions, 'copy_file', mocked_copy_file) assert take_student_avatar(test_class, test_student_name) == returned_filename