Skip to content

Commit

Permalink
Merge pull request #1481 from ScilifelabDataCentre/dev
Browse files Browse the repository at this point in the history
New release
  • Loading branch information
i-oden authored Oct 24, 2023
2 parents 7f3e488 + f32c284 commit fdeb04a
Show file tree
Hide file tree
Showing 18 changed files with 1,022 additions and 70 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
Changelog
==========

.. _2.5.2:

2.5.2 - 2023-10-25
~~~~~~~~~~~~~~~~~~~~~

- Users can revoke project access given to unaccepted invites (e.g. after a mistake).
- Email layout changed. When project is released, important information is now highlighted, and the Project Title is displayed along with the DDS project ID.
- New endpoint `ProjectStatus.patch`: Unit Admins / Personnel can extend the project deadline.

.. _2.5.1:

2.5.1 - 2023-09-27
Expand Down
15 changes: 14 additions & 1 deletion SPRINTLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,19 @@ _Nothing merged in CLI during this sprint_

# 2023-09-18 - 2023-09-29

- Column `sto4_start_time` is automatically set when the create-unit command is run ([#1668])(https://scilifelab.atlassian.net/jira/software/projects/DDS/boards/13?selectedIssue=DDS-1668)
- Column `sto4_start_time` is automatically set when the create-unit command is run ([#1469](https://github.com/ScilifelabDataCentre/dds_web/pull/1469))
- Replace expired invites when there's a new invitation attempt ([#1466](https://github.com/ScilifelabDataCentre/dds_web/pull/1466))
- New version: 2.5.1 ([#1471](https://github.com/ScilifelabDataCentre/dds_web/pull/1471))
- Revoke project access for unaccepted invites ([#1468](https://github.com/ScilifelabDataCentre/dds_web/pull/1468))

# 2023-10-02 - 2023-10-13

- Project title displayed along with the internal project ID email sent when a project is released ([#1475](https://github.com/ScilifelabDataCentre/dds_web/pull/1475))
- Use full DDS name in MOTD email subject ([#1477](https://github.com/ScilifelabDataCentre/dds_web/pull/1477))
- Add flag --verify-checksum to the comand in email template ([#1478])(https://github.com/ScilifelabDataCentre/dds_web/pull/1478)
- Improved email layout; Highlighted information and commands when project is released ([#1479])(https://github.com/ScilifelabDataCentre/dds_web/pull/1479)

# 2023-10-16 - 2023-10-27

- Added new API endpoint ProjectStatus.patch to extend the deadline ([#1480])(https://github.com/ScilifelabDataCentre/dds_web/pull/1480)
- New version: 2.5.2 ([#1482](https://github.com/ScilifelabDataCentre/dds_web/pull/1482))
129 changes: 129 additions & 0 deletions dds_web/api/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,135 @@ def post(self):

return {"message": return_message}

@auth.login_required(role=["Unit Admin", "Unit Personnel"])
@logging_bind_request
@json_required
@handle_validation_errors
@handle_db_error
def patch(self):
"""Partially update a the project status"""
# Get project ID, project and verify access
project_id = dds_web.utils.get_required_item(obj=flask.request.args, req="project")
project = dds_web.utils.collect_project(project_id=project_id)
dds_web.utils.verify_project_access(project=project)

# Get json input from request
json_input = flask.request.get_json(silent=True) # Already checked by json_required

# the status has changed at least two times,
# next time the project expires it wont change again -> error
if project.times_expired >= 2:
raise DDSArgumentError(
"Project availability limit: The maximum number of changes in data availability has been reached."
)

# Operation must be confirmed by the user - False by default
confirmed_operation = json_input.get("confirmed", False)
if not isinstance(confirmed_operation, bool):
raise DDSArgumentError(message="`confirmed` is a boolean value: True or False.")
if not confirmed_operation:
warning_message = "Operation must be confirmed before proceding."
# When not confirmed, return information about the project
project_info = ProjectInfo().get()
project_status = self.get()
json_returned = {
**project_info,
"project_status": project_status,
"warning": warning_message,
"default_unit_days": project.responsible_unit.days_in_available,
}
return json_returned

# Cannot change project status if project is busy
if project.busy:
raise ProjectBusyError(
message=(
f"The deadline for the project '{project_id}' is already in the process of being changed. "
"Please try again later. \n\nIf you know that the project is not busy, contact support."
)
)

self.set_busy(project=project, busy=True)

# Extend deadline
try:
new_deadline_in = json_input.get(
"new_deadline_in", None
) # if not provided --> is None -> deadline is not updated

# some variable definition
send_email = False
default_unit_days = project.responsible_unit.days_in_available

# Update the deadline functionality
if new_deadline_in:
# deadline can only be extended from Available
if not project.current_status == "Available":
raise DDSArgumentError(
"You can only extend the deadline for a project that has the status 'Available'."
)

if type(new_deadline_in) is not int:
raise DDSArgumentError(
message="The deadline attribute passed should be of type Int (i.e a number)."
)

# New deadline shouldnt surpass the default unit days
if new_deadline_in > default_unit_days:
raise DDSArgumentError(
message=f"You requested the deadline to be extended {new_deadline_in} days. The number of days has to be lower than the default deadline extension number of {default_unit_days} days"
)

# the new deadline + days left shouldnt surpass 90 days
curr_date = dds_web.utils.current_time()
current_deadline = (project.current_deadline - curr_date).days
if new_deadline_in + current_deadline > 90:
raise DDSArgumentError(
message=f"You requested the deadline to be extended with {new_deadline_in} days (from {current_deadline}), giving a new total deadline of {new_deadline_in + current_deadline} days. The new deadline needs to be less than (or equal to) 90 days."
)
try:
# add a fake expire status to mimick a re-release in order to have an udpated deadline
curr_date = (
dds_web.utils.current_time()
) # call current_time before each call so it is stored with different timestamps
new_status_row = self.expire_project(
project=project,
current_time=curr_date,
deadline_in=1, # some dummy deadline bc it will re-release now again
)
project.project_statuses.append(new_status_row)

curr_date = (
dds_web.utils.current_time()
) # call current_time before each call so it is stored with different timestamps
new_status_row = self.release_project(
project=project,
current_time=curr_date,
deadline_in=new_deadline_in + current_deadline,
)
project.project_statuses.append(new_status_row)

project.busy = False # return to not busy
db.session.commit()

except (sqlalchemy.exc.OperationalError, sqlalchemy.exc.SQLAlchemyError) as err:
flask.current_app.logger.exception("Failed to extend deadline")
db.session.rollback()
raise

return_message = (
f"The project '{project.public_id}' has been given a new deadline. "
f"An e-mail notification has{' not ' if not send_email else ' '}been sent."
)
else:
# leave it for future new functionality of updating the status
return_message = "Nothing to update."
except:
self.set_busy(project=project, busy=False)
raise

return {"message": return_message}

@staticmethod
@dbsession
def set_busy(project: models.Project, busy: bool) -> None:
Expand Down
2 changes: 1 addition & 1 deletion dds_web/api/superadmin_only.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ def post(self):

# Create email content
# put motd_obj.message etc in there etc
subject: str = "DDS Important Information"
subject: str = "Important Information: Data Delivery System"
body: str = flask.render_template(f"mail/motd.txt", motd=motd_obj.message)
html = flask.render_template(f"mail/motd.html", motd=motd_obj.message)

Expand Down
101 changes: 73 additions & 28 deletions dds_web/api/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,7 @@ def compose_and_send_email_to_user(userobj, mail_type, link=None, project=None):

unit_email = None
project_id = None
project_title = None
deadline = None

# Don't display unit admins or personnels name
Expand All @@ -440,6 +441,7 @@ def compose_and_send_email_to_user(userobj, mail_type, link=None, project=None):
elif mail_type == "project_release":
subject = f"Project made available by {displayed_sender} in the SciLifeLab Data Delivery System"
project_id = project.public_id
project_title = project.title
deadline = project.current_deadline.astimezone(datetime.timezone.utc).strftime(
"%Y-%m-%d %H:%M:%S %Z"
)
Expand Down Expand Up @@ -470,6 +472,7 @@ def compose_and_send_email_to_user(userobj, mail_type, link=None, project=None):
displayed_sender=displayed_sender,
unit_email=unit_email,
project_id=project_id,
project_title=project_title,
deadline=deadline,
)
msg.html = flask.render_template(
Expand All @@ -478,6 +481,7 @@ def compose_and_send_email_to_user(userobj, mail_type, link=None, project=None):
displayed_sender=displayed_sender,
unit_email=unit_email,
project_id=project_id,
project_title=project_title,
deadline=deadline,
)

Expand Down Expand Up @@ -864,7 +868,7 @@ def delete_invite(email):


class RemoveUserAssociation(flask_restful.Resource):
@auth.login_required(role=["Unit Admin", "Unit Personnel", "Project Owner", "Researcher"])
@auth.login_required(role=["Unit Admin", "Unit Personnel", "Project Owner"])
@logging_bind_request
@json_required
@handle_validation_errors
Expand All @@ -876,34 +880,81 @@ def post(self):
if not (user_email := json_input.get("email")):
raise ddserr.DDSArgumentError(message="User email missing.")

# Check if email is registered to a user
# Check if the user exists or has a pending invite
try:
existing_user = user_schemas.UserSchema().load({"email": user_email})
unanswered_invite = user_schemas.UnansweredInvite().load({"email": user_email})
except sqlalchemy.exc.OperationalError as err:
raise ddserr.DatabaseError(message=str(err), alt_message="Unexpected database error.")

if not existing_user:
# If the user doesn't exist and doesn't have a pending invite
if not existing_user and not unanswered_invite:
raise ddserr.NoSuchUserError(
f"The user with email '{user_email}' does not have access to the specified project."
" Cannot remove non-existent project access."
f"The user / invite with email '{user_email}' does not have access to the specified project. "
"Cannot remove non-existent project access."
)

user_in_project = False
for user_association in project.researchusers:
if user_association.user_id == existing_user.username:
user_in_project = True
db.session.delete(user_association)
project_user_key = models.ProjectUserKeys.query.filter_by(
project_id=project.id, user_id=existing_user.username
).first()
if project_user_key:
db.session.delete(project_user_key)

if not user_in_project:
raise ddserr.NoSuchUserError(
f"The user with email '{user_email}' does not have access to the specified project."
" Cannot remove non-existent project access."
)
if unanswered_invite:
# If there is a unit_id value, it means the invite was associated to a unit
# i.e The invite is for a Unit Personel which shouldn't be removed from individual projects
if unanswered_invite.unit_id:
raise ddserr.UserDeletionError(
"Cannot remove a Unit Admin / Unit Personnel from individual projects."
)

invite_id = unanswered_invite.id

# Check if the unanswered invite is associated with the project
project_invite_key = models.ProjectInviteKeys.query.filter_by(
invite_id=invite_id, project_id=project.id
).one_or_none()

if project_invite_key:
msg = (
f"Invited user is no longer associated with the project '{project.public_id}'."
)
# Remove the association if it exists
db.session.delete(project_invite_key)

# Check if the invite is associated with only one project, if it is -> delete the invite
project_invite_key = models.ProjectInviteKeys.query.filter_by(
invite_id=invite_id
).one_or_none()

if not project_invite_key:
db.session.delete(unanswered_invite)
else:
# The unanswered invite is not associated with the project
raise ddserr.NoSuchUserError(
f"The invite with email '{user_email}' does not have access to the specified project. "
"Cannot remove non-existent project access."
)

else:
if auth.current_user().username == existing_user.username:
raise ddserr.AccessDeniedError(message="You cannot revoke your own access.")

# Search the user in the project, when found delete from the database all references to them
user_in_project = False
for (
user_association
) in project.researchusers: # TODO Possible optimization -> comprehesion list
if user_association.user_id == existing_user.username:
user_in_project = True
db.session.delete(user_association)
project_user_key = models.ProjectUserKeys.query.filter_by(
project_id=project.id, user_id=existing_user.username
).first()
if project_user_key:
db.session.delete(project_user_key)
break

if not user_in_project:
raise ddserr.NoSuchUserError(
f"The user with email '{user_email}' does not have access to the specified project. "
"Cannot remove non-existent project access."
)
msg = f"User with email {user_email} no longer associated with {project.public_id}."

try:
db.session.commit()
Expand All @@ -924,13 +975,7 @@ def post(self):
),
) from err

flask.current_app.logger.debug(
f"User {existing_user.username} no longer associated with project {project.public_id}."
)

return {
"message": f"User with email {user_email} no longer associated with {project.public_id}."
}
return {"message": msg}


class EncryptedToken(flask_restful.Resource):
Expand Down
2 changes: 1 addition & 1 deletion dds_web/database/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ class Unit(db.Model):
sto2_access = db.Column(db.String(255), unique=False, nullable=True) # unique=True later
sto2_secret = db.Column(db.String(255), unique=False, nullable=True) # unique=True later

# New safespring storage
# New safespring storage - NOTE: MAKE SURE IPS ARE WHITELISTED ON UPPMAX AND OTHER SERVERS
sto4_start_time = db.Column(db.DateTime(), nullable=True)
sto4_endpoint = db.Column(db.String(255), unique=False, nullable=True) # unique=True later
sto4_name = db.Column(db.String(255), unique=False, nullable=True) # unique=True later
Expand Down
23 changes: 22 additions & 1 deletion dds_web/templates/mail/mail_base.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,23 @@
<!doctype html>
<html>

<script>
function copyTextProjectRelease(copyOption) {
// Get the text field
let copyText;
if (copyOption == 1){
copyText = document.getElementById("ProjectListing").innerHTML;
}
else if (copyOption == 2){
copyText = document.getElementById("ProjectDownload").innerHTML;
}
// Copy the text inside the text field
navigator.clipboard.writeText(copyText);

// Alert the copied text
alert("Copied the text: " + copyText);
}
</script>

<head>
<meta name="viewport" content="width=device-width">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
Expand Down Expand Up @@ -122,6 +139,10 @@
border-color: #a00202 !important;
}
}
code {
background-color: pink;
color: black;
}
</style>
</head>

Expand Down
Loading

0 comments on commit fdeb04a

Please sign in to comment.