Skip to content

Commit

Permalink
Merge ff5ece0 into 497170f
Browse files Browse the repository at this point in the history
  • Loading branch information
KevinHanson committed Jun 14, 2016
2 parents 497170f + ff5ece0 commit a820122
Show file tree
Hide file tree
Showing 17 changed files with 470 additions and 99 deletions.
7 changes: 7 additions & 0 deletions app/db/003/README
Original file line number Diff line number Diff line change
@@ -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

14 changes: 14 additions & 0 deletions app/db/003/downgrade.sql
Original file line number Diff line number Diff line change
@@ -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';
9 changes: 9 additions & 0 deletions app/db/003/upgrade.sql
Original file line number Diff line number Diff line change
@@ -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','');
55 changes: 53 additions & 2 deletions app/deploy/fabfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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:
Expand All @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions app/fabfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
6 changes: 1 addition & 5 deletions app/redidropper/database/crud_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,27 +33,23 @@ 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)
if commit:
db.session.commit()
return self


def delete(self, commit=True):
db.session.delete(self)
return commit and db.session.commit()
8 changes: 7 additions & 1 deletion app/redidropper/models/log_entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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 """
Expand Down
1 change: 1 addition & 0 deletions app/redidropper/models/log_type_entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -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_ = ''


Expand Down
2 changes: 1 addition & 1 deletion app/redidropper/models/user_entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down
133 changes: 95 additions & 38 deletions app/redidropper/routes/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit a820122

Please sign in to comment.