diff --git a/app/db/003/README b/app/db/003/README new file mode 100644 index 0000000..d8fb265 --- /dev/null +++ b/app/db/003/README @@ -0,0 +1,7 @@ +These database changes are from the editUser feature branch + +This schema change adds a unique constraint on the UserRole table to +prevent duplicate roles for individual users + +It also adds a new log type to account for the edit_user api call + diff --git a/app/db/003/downgrade.sql b/app/db/003/downgrade.sql new file mode 100644 index 0000000..d78ec38 --- /dev/null +++ b/app/db/003/downgrade.sql @@ -0,0 +1,14 @@ +-- temporarily remove foreign keys to remove unique index +ALTER TABLE UserRole DROP FOREIGN KEY fk_User_usrID; +ALTER TABLE UserRole DROP FOREIGN KEY fk_UserRole_rolID; + +-- remove unique index from UserRole +ALTER TABLE UserRole DROP INDEX uc_UserIDRoleID; + +-- reapply the foreign key constraints +ALTER TABLE UserRole ADD CONSTRAINT fk_User_usrID FOREIGN KEY (usrID) REFERENCES User (usrID) ON DELETE CASCADE; +ALTER TABLE UserRole ADD CONSTRAINT fk_UserRole_rolID FOREIGN KEY (rolID) REFERENCES Role (rolID) ON DELETE CASCADE; + +-- remove the log type for the edit user api call +DELETE FROM LogType +WHERE logtType='account_updated'; diff --git a/app/db/003/upgrade.sql b/app/db/003/upgrade.sql new file mode 100644 index 0000000..b6be7f1 --- /dev/null +++ b/app/db/003/upgrade.sql @@ -0,0 +1,9 @@ +-- ensure there will be no duplicates user / user role pairs +ALTER TABLE UserRole +ADD CONSTRAINT uc_UserIDRoleID UNIQUE (usrID, rolID); + +-- add the logtype for the edit_user api call +INSERT INTO LogType +(logtType, logtDescription) +VALUES +('account_updated',''); diff --git a/app/deploy/fabfile.py b/app/deploy/fabfile.py index 9c5794d..700a66e 100644 --- a/app/deploy/fabfile.py +++ b/app/deploy/fabfile.py @@ -274,6 +274,57 @@ def mysql_list_tables(): result = local(cmd, capture=True) print(result) +def _is_valid_version(version_number): + """Checks if the version has the right number of digits""" + version_string = str(version_number) + return len(version_string) <= 3 + +def _get_version_string(version_number): + """Produces a three char string that corresponds to the desired db folder""" + version_string = str(version_number) + while len(version_string) < 3: + version_string = '0' + version_string + return version_string + +def _step_to_version(version_number, is_upgrade): + """Step once to the passed version. Must be on adjacent version + Version numbers are given by the number for the folder + in app/db/ + """ + require('environment', provided_by=[production, staging]) + + exists = mysql_check_db_exists() + if not exists: + abort(colors.red("Unable to create tables in database '%(db_name)s'." + "The database does not exist" % env)) + if not _is_valid_version(version_number) is True: + abort(colors.red("Unable to upgrade to version {} '%(db_name)s'." + "Please look in app/db for valid versions".format(version_number) + % env)) + + login_path = _mysql_login_path() + sql_file = 'upgrade.sql' if is_upgrade else 'downgrade.sql' + sql = _get_version_string(version_number) + '/' + sql_file + + with lcd('../db/'): + cmd = ("mysql --login-path={} %(db_name)s < {}" + .format(login_path, sql) + % env) + local(cmd) + +def mysql_version_upgrade(version_number): + """Upgrade to the passed version. Must be on adjacent version + Version numbers are given by the number for the folder + in app/db/ + """ + _step_to_version(version_number, True) + +def mysql_version_downgrade(version_number): + """Downgrade to the passed version. Must be on adjacent version + Version numbers are given by the number for the folder + in app/db/ + """ + _step_to_version(version_number, False) def mysql_create_tables(): """ Create the application tables. @@ -297,7 +348,7 @@ def mysql_create_tables(): .format(env.environment)) login_path = _mysql_login_path() - files = ['001/upgrade.sql', '002/upgrade.sql', '002/data.sql'] + files = ['001/upgrade.sql', '002/upgrade.sql', '002/data.sql', '003/upgrade.sql'] with lcd('../db/'): for sql in files: @@ -322,7 +373,7 @@ def mysql_drop_tables(): abort(colors.red("Unable to drop tables in database '%(db_name)s'." "The database does not exist" % env)) - files = ['002/downgrade.sql', '001/downgrade.sql'] + files = ['003/downgrade.sql', '002/downgrade.sql', '001/downgrade.sql'] with lcd('../db/'): for sql in files: diff --git a/app/fabfile.py b/app/fabfile.py index f79a27a..a2fb804 100644 --- a/app/fabfile.py +++ b/app/fabfile.py @@ -62,6 +62,7 @@ def init_db(): local('sudo mysql ctsi_dropper_s < db/001/upgrade.sql') local('sudo mysql ctsi_dropper_s < db/002/upgrade.sql') local('sudo mysql ctsi_dropper_s < db/002/data.sql') + local('sudo mysql ctsi_dropper_s < db/003/upgrade.sql') @task @@ -78,6 +79,7 @@ def reset_db(): local('sudo mysql ctsi_dropper_s < db/001/upgrade.sql') local('sudo mysql ctsi_dropper_s < db/002/upgrade.sql') local('sudo mysql ctsi_dropper_s < db/002/data.sql') + local('sudo mysql ctsi_dropper_s < db/003/upgrade.sql') @task diff --git a/app/redidropper/database/crud_mixin.py b/app/redidropper/database/crud_mixin.py index 90b0232..c7659fa 100644 --- a/app/redidropper/database/crud_mixin.py +++ b/app/redidropper/database/crud_mixin.py @@ -33,19 +33,16 @@ def get_by_id(cls, id): return cls.query.get(int(id)) return None - @classmethod def create(cls, **kwargs): """ Helper for session.add() + session.commit() """ instance = cls(**kwargs) return instance.save() - def update(self, commit=True, **kwargs): for attr, value in kwargs.iteritems(): setattr(self, attr, value) - return commit and self.save() or self - + return self.save() if commit else self def save(self, commit=True): db.session.add(self) @@ -53,7 +50,6 @@ def save(self, commit=True): db.session.commit() return self - def delete(self, commit=True): db.session.delete(self) return commit and db.session.commit() diff --git a/app/redidropper/models/log_entity.py b/app/redidropper/models/log_entity.py index a39b7b5..a6ebeec 100644 --- a/app/redidropper/models/log_entity.py +++ b/app/redidropper/models/log_entity.py @@ -23,7 +23,8 @@ LOG_TYPE_FILE_DOWNLOADED, \ LOG_TYPE_ACCOUNT_MODIFIED, \ LOG_TYPE_REDCAP_SUBJECTS_IMPORTED, \ - LOG_TYPE_REDCAP_EVENTS_IMPORTED + LOG_TYPE_REDCAP_EVENTS_IMPORTED, \ + LOG_TYPE_ACCOUNT_UPDATED class LogEntity(db.Model, CRUDMixin): @@ -92,6 +93,11 @@ def account_created(session_id, details=''): """ Log account creation """ LogEntity._log(LOG_TYPE_ACCOUNT_CREATED, session_id, details) + @staticmethod + def account_updated(session_id, details=''): + """ Log account updated """ + LogEntity._log(LOG_TYPE_ACCOUNT_UPDATED, session_id, details) + @staticmethod def login(session_id, details=''): """ Log successful login """ diff --git a/app/redidropper/models/log_type_entity.py b/app/redidropper/models/log_type_entity.py index b3c0188..a3cb4ff 100644 --- a/app/redidropper/models/log_type_entity.py +++ b/app/redidropper/models/log_type_entity.py @@ -13,6 +13,7 @@ LOG_TYPE_ACCOUNT_MODIFIED = 'account_modified' LOG_TYPE_REDCAP_SUBJECTS_IMPORTED = 'redcap_subjects_impported' LOG_TYPE_REDCAP_EVENTS_IMPORTED = 'redcap_events_imported' +LOG_TYPE_ACCOUNT_UPDATED = 'account_updated' # LOG_TYPE_ = '' diff --git a/app/redidropper/models/user_entity.py b/app/redidropper/models/user_entity.py index 89547e4..abde53d 100644 --- a/app/redidropper/models/user_entity.py +++ b/app/redidropper/models/user_entity.py @@ -33,7 +33,7 @@ class UserEntity(db.Model, UserMixin, CRUDMixin): modified_at = db.Column("usrModifiedAt", db.TIMESTAMP, nullable=False) email_confirmed_at = db.Column("usrEmailConfirmedAt", db.DateTime, nullable=False, - server_default='0000-00-00 00:00:00') + server_default='1901-10-04 11:17:00') active = db.Column("usrIsActive", db.Boolean(), nullable=False, server_default='1') diff --git a/app/redidropper/routes/api.py b/app/redidropper/routes/api.py index a08ce59..bea8d9d 100644 --- a/app/redidropper/routes/api.py +++ b/app/redidropper/routes/api.py @@ -179,61 +179,118 @@ def download_file(): LogEntity.file_downloaded(session['uuid'], file_path) return send_file(file_path, as_attachment=True) +def __extract_user_information(request): + return { + "email": request.form.get('email'), + "first": request.form.get('first'), + "last": request.form.get('last'), + "minitial": request.form.get('minitial'), + "roles": request.form.getlist('roles[]'), + "is_edit": request.form.get('isEdit'), + "usr_id": request.form.get('usrId'), + } + +def __get_date_information(): + return { + "added_at": datetime.today(), + "access_expires_at": utils.get_expiration_date(180), + } + +def __generate_credentials(email): + # @TODO: use a non-gatorlink password here + password = email + salt, password_hash = utils.generate_auth(app.config['SECRET_KEY'], + password) + # Note: we store the salt as a prefix + return { + "email": email, + "salt": salt, + "password_hash": password_hash, + } + + +def __assign_roles(roles_required, user): + """ + Delete all roles for the user if not in the + provided `roles_required` list and assing new roles. + """ + all_roles = RoleEntity.query.all() + user_roles = [role for role in all_roles if role.name in roles_required] + user = UserEntity.update(user, roles=user_roles) + return user + + +def __check_is_existing_user(email): + """ + :rtype boolean + :return True if a user exists in the database with the given email + """ + try: + existing_user = UserEntity.query.filter_by(email=email).one() + return True + except: + return False + @app.route('/api/save_user', methods=['POST']) @login_required +@perm_admin.require(http_exception=403) def api_save_user(): """ Save a new user to the database TODO: Add support for reading a password field """ - email = request.form['email'] - first = request.form['first'] - last = request.form['last'] - minitial = request.form['minitial'] - roles = request.form.getlist('roles[]') + request_data = __extract_user_information(request) + credentials = __generate_credentials(request_data["email"]) + date_data = __get_date_information() - email_exists = False - try: - existing_user = UserEntity.query.filter_by(email=email).one() - email_exists = existing_user is not None - except: - pass - - if email_exists: + if __check_is_existing_user(request_data["email"]): return utils.jsonify_error( {'message': 'Sorry. This email is already taken.'}) - # @TODO: use a non-gatorlink password here - password = email - salt, password_hash = utils.generate_auth(app.config['SECRET_KEY'], - password) - added_date = datetime.today() - access_end_date = utils.get_expiration_date(180) + user = UserEntity.create(email=request_data["email"], + first=request_data["first"], + last=request_data["last"], + minitial=request_data["minitial"], + added_at=date_data["added_at"], + modified_at=date_data["added_at"], + access_expires_at=date_data["access_expires_at"], + password_hash="{}:{}" + .format(credentials["salt"], + credentials["password_hash"])) - # Note: we store the salt as a prefix - user = UserEntity.create(email=email, - first=first, - last=last, - minitial=minitial, - added_at=added_date, - modified_at=added_date, - access_expires_at=access_end_date, - password_hash="{}:{}".format(salt, password_hash)) - - user_roles = [] - try: - for role_name in roles: - role_entity = RoleEntity.query.filter_by(name=role_name).one() - user_roles.append(role_entity) - except Exception as exc: - app.logger.debug("Problem saving user: {}".format(exc)) + __assign_roles(request_data["roles"], user) - [user.roles.append(rol) for rol in user_roles] - user = UserEntity.save(user) app.logger.debug("saved user: {}".format(user)) LogEntity.account_created(session['uuid'], user) return utils.jsonify_success({'user': user.serialize()}) +@app.route('/api/edit_user', methods=['POST']) +@login_required +@perm_admin.require(http_exception=403) +def api_edit_user(): + """ Edit an existing user in the database + TODO: Add support for reading a password field + """ + request_data = __extract_user_information(request) + credentials = __generate_credentials(request_data["email"]) + date_data = __get_date_information() + + user = UserEntity.get_by_id(id=request_data["usr_id"]) + user.update(email=request_data["email"], + first=request_data["first"], + last=request_data["last"], + minitial=request_data["minitial"], + added_at=date_data["added_at"], + modified_at=date_data["added_at"], + access_expires_at=date_data["access_expires_at"], + password_hash="{}:{}".format(credentials["salt"], + credentials["password_hash"])) + + __assign_roles(request_data["roles"], user) + + app.logger.debug("updated user: {}".format(user)) + LogEntity.account_updated(session['uuid'], user) + return utils.jsonify_success({'user': user.serialize()}) @app.route('/api/list_users', methods=['POST', 'GET']) @login_required diff --git a/app/redidropper/static/js/admin.js b/app/redidropper/static/js/admin.js index 6885c04..e1a9702 100644 --- a/app/redidropper/static/js/admin.js +++ b/app/redidropper/static/js/admin.js @@ -5,16 +5,21 @@ // AddNewUserForm - ... var AdminUsersRow = React.createClass({ + getInitialState: function() { return { record: this.props.record, - row_num: this.props.row_num + row_num: this.props.row_num, + onEditUser: this.props.onEditUser, + editStatus: this.props.editStatus }; }, componentWillReceiveProps: function(nextProps) { this.setState({ record: nextProps.record, - row_num: nextProps.row_num + row_num: nextProps.row_num, + onEditUser: nextProps.onEditUser, + editStatus: nextProps.editStatus }); }, sendEmailVerification: function() { @@ -141,10 +146,13 @@ var AdminUsersRow = React.createClass({ render: function() { var record = this.state.record; var row_num = this.state.row_num; + var onEditUser = this.state.onEditUser; + var edit_user_form; var roles, emailButton, expireButton, deactivateButton, + editButton, title = "", display = "", expirationDate = record.access_expires_at; @@ -208,6 +216,26 @@ var AdminUsersRow = React.createClass({ } + if (record.is_active && this.props.editStatus) { + // You want to pass a function that gets called. If passed with + // out the braces, the function will be called upon render. + // This is done using an arrow function. + // Delegate to parent component. + editButton = + } + else { + editButton = + } + return ( {record.id} {record.email} @@ -218,31 +246,40 @@ var AdminUsersRow = React.createClass({ {emailButton} {expireButton} {deactivateButton} + {editButton} ); } }); var AdminUsersTable = React.createClass({ + getInitialState: function() { return { - list_of_users: this.props.list_of_users + list_of_users: this.props.list_of_users, + onEditUser: this.props.onEditUser, + editStatus: this.props.editStatus, }; }, componentDidMount: function() { $('[data-toggle="tooltip"]').tooltip() }, componentWillReceiveProps: function(nextProps) { - this.setState({list_of_users: nextProps.list_of_users}); + this.setState( + { + list_of_users: nextProps.list_of_users, + onEditUser: nextProps.onEditUser, + editStatus: nextProps.editStatus, + }); }, render: function() { var rows = []; this.state.list_of_users.map(function(record, i) { rows.push( - + // Delegate the onEditUser function call from child to parent. + ); - }); - + }.bind(this)); return (
@@ -257,6 +294,7 @@ var AdminUsersTable = React.createClass({ + @@ -339,7 +377,18 @@ var AdminUsersPagination = React.createClass({ var AddNewUserForm = React.createClass({ getInitialState: function() { - return {error: ""}; + return { + error: "", + editRecord: this.props.editRecord, + }; + }, + + componentWillReceiveProps: function(nextProps) { + this.setState( + { + error: nextProps.error, + editRecord: nextProps.editRecord, + }); }, clearError: function() { @@ -350,19 +399,16 @@ var AddNewUserForm = React.createClass({ //Get the values entered by the user in the form var usrEmail = this.refs.user_email.getDOMNode().value.trim(); var usrFirst = this.refs.user_first_name.getDOMNode().value.trim(); - var usrMI = this.refs.user_middle_name.getDOMNode().value.trim(); + var usrMI = this.refs.user_middle_initial.getDOMNode().value.trim(); var usrLast = this.refs.user_last_name.getDOMNode().value.trim(); - var roles = []; + var isEdit = typeof this.state.editRecord === "object" && typeof this.state.editRecord.email === "string"; + var usrId = (this.state.editRecord || {}).id; - // https://developer.mozilla.org/en-US/docs/Web/API/HTMLOptionsCollection - // https://github.com/facebook/react/blob/057f41ec0f01e5e716358ad18cf7166d5cc00d68/src/browser/ui/dom/components/__tests__/ReactDOMSelect-test.js - var collection = this.refs.user_roles.getDOMNode().options; + var roleCheckboxes = document.getElementsByClassName("roleCheckbox") + var roles = Array.prototype.map.call(roleCheckboxes, (input) => { + return input.checked && input.value; + }).filter(Boolean); - for (var i = 0; i < collection.length; i++) { - if (collection.item(i).selected) { - roles.push(collection.item(i).value); - } - } console.log("roles: " + roles); if (usrEmail === "") { @@ -397,12 +443,14 @@ var AddNewUserForm = React.createClass({ "first" : usrFirst, "minitial" : usrMI, "last" : usrLast, - "roles[]" : roles + "roles[]" : roles, + "usrId" : usrId, + "isEdit" : isEdit, }; console.log('sending data: ' + Utils.print_r(data)); - var request = Utils.api_post_json("/api/save_user", data); + var request = Utils.api_post_json(isEdit ? "/api/edit_user" : "/api/save_user", data); var _this = this; request.success( function(json) { @@ -423,7 +471,11 @@ var AddNewUserForm = React.createClass({ 'is_active' : record.is_active, 'roles' : record.roles }; - _this.props.addNewUser(data); + if (isEdit) { + _this.props.updateUser(data); + } else { + _this.props.addNewUser(data); + } } } else { @@ -441,65 +493,67 @@ var AddNewUserForm = React.createClass({ }, render: function() { var error; - + var editRecord = this.state.editRecord; + var isChecked = (role) => this.state.editRecord && this.state.editRecord.roles.indexOf(role) > -1; + console.log("Render of edit user" + editRecord) // Add generic function for displaying errors - if (this.state.error !== "") { - error =
- - {this.state.error} + if (!this.state.error) { + error =
+ + {this.state.error}
} - return (
- {error} - + {this.state.error ? error : ""}

Please enter user details

- +
- +
- +
- +
- +
-
- +
+
Admin
+
Technician
+
Researcher 1
+
Researcher 2
-
+
- ) + ); } }); @@ -510,9 +564,18 @@ var AdminUserManagement = React.createClass({ list_of_users: undefined, total_pages: 1, show_user_form: false, - error: "" + error: "", + editRecord: "", + editStatus: true, }; }, + componentWillReceiveProps: function(nextProps) { + this.setState( + { + editRecord: nextProps.editRecord, + }); + }, + componentWillMount: function() { this.changeData(1); /* @@ -585,13 +648,42 @@ var AdminUserManagement = React.createClass({ }); }, + updateUser: function(data) { + var users = this.state.list_of_users, + user_ids = users.map((item) => item.id); + users[user_ids.indexOf(data.id)] = data; + console.log(users[user_ids.indexOf(data.id)]); + + this.setState({ + list_of_users: users, + total_pages: this.state.total_pages, + show_user_form: false, + editStatus: true, + }); + }, + toggleAddUserForm: function() { //change the bool value of show_user_form variable to opposite var show_user_form = !this.state.show_user_form; this.setState({ list_of_users : this.state.list_of_users, total_pages : this.state.total_pages, - show_user_form : show_user_form + show_user_form : show_user_form, + editRecord : "", + editStatus : true, + }); + }, + toggleEditUserForm: function(record) { + //change the bool value of show_user_form variable to opposite + console.log("Final call > "+ record ? record.email : "") + // Always make this visible. + var show_user_form = true + this.setState({ + list_of_users : this.state.list_of_users, + total_pages : this.state.total_pages, + show_user_form : show_user_form, + editRecord : record, + editStatus : false, }); }, render: function() { @@ -600,6 +692,8 @@ var AdminUserManagement = React.createClass({ var pagination; var show_user_form; var button_text = "Open 'Add User' Form"; + var editRecord = this.state.editRecord; + var editStatus = this.state.editStatus; if(total_pages > 1) { pagination = ; @@ -607,7 +701,14 @@ var AdminUserManagement = React.createClass({ if(this.state.show_user_form) { button_text = "Close 'Add User' Form"; - show_user_form = + if(editStatus) { + show_user_form = + } + else { + console.log("editing user record with email id " + this.state.editRecord.email) + show_user_form = + button_text = "Ignore Changes" + } } var users_table; @@ -622,7 +723,7 @@ var AdminUserManagement = React.createClass({ users_table =
There is no data to display. If you think this is an error please contact your support personnel.
; } else { - users_table = + users_table = } return (
diff --git a/app/tests/base_test_with_data.py b/app/tests/base_test_with_data.py index ef50dd2..e9245ff 100644 --- a/app/tests/base_test_with_data.py +++ b/app/tests/base_test_with_data.py @@ -55,7 +55,7 @@ def create_sample_data(self): # == Create users added_date = datetime.today() access_end_date = utils.get_expiration_date(180) - user = UserEntity.create(email="admin@example.com", + admin_user = UserEntity.create(email="admin@example.com", first="First", last="Last", minitial="M", @@ -64,12 +64,22 @@ def create_sample_data(self): email_confirmed_at=added_date, access_expires_at=access_end_date) + tech_user = UserEntity.create(email="tech@example.com", + first="First", + last="Last", + minitial="T", + added_at=added_date, + modified_at=added_date, + email_confirmed_at=added_date, + access_expires_at=access_end_date) + # == Create roles role_admin = RoleEntity.create(name=ROLE_ADMIN, description='role') role_tech = RoleEntity.create(name=ROLE_TECHNICIAN, description='role') role_res1 = RoleEntity.create(name=ROLE_RESEARCHER_ONE, description='') role_res2 = RoleEntity.create(name=ROLE_RESEARCHER_TWO, description='') - user.roles.extend([role_admin, role_tech, role_res1, role_res2]) + admin_user.roles.extend([role_admin, role_tech, role_res1, role_res2]) + tech_user.roles.extend([role_tech]) # == Create subject subject = SubjectEntity.create( @@ -108,6 +118,6 @@ def create_sample_data(self): file_check_sum=utils.compute_text_md5(fdata['name']), file_size=fdata['size'], uploaded_at=added_date, - user_id=user.id) + user_id=admin_user.id) self.assertIsNotNone(subject_file.id) # app.logger.debug("Init test case with: {}".format(subject_file)) diff --git a/app/tests/test_api.py b/app/tests/test_api.py new file mode 100644 index 0000000..c879e7c --- /dev/null +++ b/app/tests/test_api.py @@ -0,0 +1,85 @@ +""" +Goal: Simulate api calls + +Authors: + Patrick White + +""" + +from __future__ import print_function + +from flask import url_for +from .base_test_with_data import BaseTestCaseWithData +from redidropper.main import app +from redidropper.main import db + +from redidropper.models.user_entity import UserEntity + +class TestAPI(BaseTestCaseWithData): + + """ This is the class that tests the api """ + + def __login(self, email): + return self.client.post("/", data={ + 'email': email, + 'password': 'garbagegarbage' + }) + + def test_save_user_no_login(self): + response = self.client.post("/api/save_user", data={}) + self.assertEqual(response._status_code, 302) + + def test_save_user_no_admin_login(self): + res_login = self.__login("tech@example.com") + response = self.client.post("/api/save_user", data={}) + #TODO: fix the 302 error to be a 403 forbidden error + self.assertEqual(response._status_code, 302) + #self.assertEqual(response._status_code, 403) + + def test_save_user(self): + """ Verify that we can save a new user""" + res_login = self.__login("admin@example.com") + #build request + new_user = { + 'email': "test@test.com", + 'first': "john", + 'last': "doe", + 'minitial': "f", + 'roles': ["admin", "technician"], + 'isEdit': False, + } + existing_user = UserEntity.query.filter_by(email=new_user['email']) + if existing_user.count() is 0: + response = self.client.post("/api/save_user", data=new_user) + self.assertEqual(response._status_code, 200) + created_user = UserEntity.query.filter_by(email=new_user['email']) + self.assertEqual(created_user.count(), 1) + else: + self.fail('user already existed') + print('save user test') + + def test_edit_user(self): + """ Verify that we can edit an existing user""" + res_login = self.__login("admin@example.com") + my_user = { + 'email': "test@test.com", + 'first': "john", + 'last': "doe", + 'minitial': "f", + 'roles': ["admin", "technician"], + 'isEdit': False, + 'usrId': 3, + } + sres = self.client.post("/api/save_user", data=my_user) + existing_user = UserEntity.query.filter_by(email=my_user['email']) + if existing_user.count() is 1: + edited_user = my_user + edited_user['first'] = 'bill' + response = self.client.post("/api/edit_user", data=edited_user) + self.assertEqual(response._status_code, 200) + #see if changed + after_edit_user = UserEntity.query.filter_by(email=my_user['email']) + self.assertEqual(after_edit_user.one().first, 'bill') + else: + self.fail('user not existing') + print('edit user test') diff --git a/app/tests/test_visit_pages.py b/app/tests/test_visit_pages.py index 3a92a27..36ce421 100644 --- a/app/tests/test_visit_pages.py +++ b/app/tests/test_visit_pages.py @@ -55,7 +55,7 @@ def test_visit_pages(self): # Skip routes for protcted_pages if url in protected_pages: - print("Skip special page: {}".format(url)) + print("Skip protected page: {}".format(url)) continue # Simulate visiting the page diff --git a/docs/README.md b/docs/README.md index 5500405..536eecb 100644 --- a/docs/README.md +++ b/docs/README.md @@ -28,6 +28,10 @@ Note: if you get errors related to mising "Guest Additions" please try: vagrant plugin install vagrant-vbguest +If you get errors related to Unknown configuration section 'trigger', please try: + + vagrant plugin install vagrant-triggers + ## Developer's Workflow - Without Vagrant There are three great tools for python development: @@ -167,7 +171,7 @@ Steps: - Re-upload configuration and code changes by executing one of the following: $ deploy/deploy.sh -t `tag_number` -r ~/git staging - + OR $ deploy/deploy.sh -t `tag_number` -r ~/git production diff --git a/docs/edit_user/README.md b/docs/edit_user/README.md new file mode 100644 index 0000000..5704556 --- /dev/null +++ b/docs/edit_user/README.md @@ -0,0 +1,25 @@ +# Edit User README + +## Introduction + +Welcome to the edit_user feature branch readme + +## Changelog + +1) Added edit_user api call to support changing users in the web application +2) Reworked client side to use checkboxes and corrected middle name to middle initial +3) Added API unit tests for save_user and edit_user +4) Added functions to deploy/fabfile to support upgrading and downgrading the database + +## Deploying the change + +1) Go through standard deploy/upgrade process to get new codebase +2) Run "fab mysql_version_upgrade:3"' +Alt2) Migrate MySQL to db/003/upgrade.sql via mysql command line + +## Smoketest + +1) Verify the web application has checkboxes in the add user form on the admin page +2) Verify that the database contains an entry in table "LogType" with a "logtType" of + "account_updated" + diff --git a/vagrant/bootstrap_functions.sh b/vagrant/bootstrap_functions.sh index 59a45e8..77c8661 100644 --- a/vagrant/bootstrap_functions.sh +++ b/vagrant/bootstrap_functions.sh @@ -76,6 +76,8 @@ function install_dropper() { mysql ctsi_dropper_s < db/002/upgrade.sql log "Execute sql: db/002/data.sql" mysql ctsi_dropper_s < db/002/data.sql + log "Execute sql: db/003/upgrade.sql" + mysql ctsi_dropper_s < db/003/upgrade.sql log "Stop apache in order to disable the default site" service apache2 stop
Email Verified Account Expiration Account StatusUpdate User