diff --git a/CHANGELOG.md b/CHANGELOG.md
index ae1445504..65973c030 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -165,3 +165,8 @@ Please add a _short_ line describing the PR you make, if the PR implements a spe
- Add link to the dds instance to the end of all emails ([#1305](https://github.com/ScilifelabDataCentre/dds_web/pull/1305))
- Troubleshooting steps added to web page ([#1309](https://github.com/ScilifelabDataCentre/dds_web/pull/1309))
- Bug: Return instead of project creator if user has been deleted ([#1311](https://github.com/ScilifelabDataCentre/dds_web/pull/1311))
+- New endpoint: ProjectInfo - display project information ([#1310](https://github.com/ScilifelabDataCentre/dds_web/pull/1310))
+
+## Sprint (2022-11-11 - 2022-11-25)
+
+- Link to "How do I get my user account?" from the login form ([#1318](https://github.com/ScilifelabDataCentre/dds_web/pull/1318))
diff --git a/dds_web/api/__init__.py b/dds_web/api/__init__.py
index 5168f6beb..926c6ec39 100644
--- a/dds_web/api/__init__.py
+++ b/dds_web/api/__init__.py
@@ -61,6 +61,7 @@ def output_json(data, code, headers=None):
api.add_resource(project.ProjectStatus, "/proj/status", endpoint="project_status")
api.add_resource(project.ProjectAccess, "/proj/access", endpoint="project_access")
api.add_resource(project.ProjectBusy, "/proj/busy", endpoint="project_busy")
+api.add_resource(project.ProjectInfo, "/proj/info", endpoint="project_info")
# User management ################################################################ User management #
api.add_resource(user.RetrieveUserInfo, "/user/info", endpoint="user_info")
diff --git a/dds_web/api/project.py b/dds_web/api/project.py
index 19deeab81..d07d2fb78 100644
--- a/dds_web/api/project.py
+++ b/dds_web/api/project.py
@@ -25,6 +25,7 @@
dbsession,
json_required,
handle_validation_errors,
+ handle_db_error,
)
from dds_web.errors import (
AccessDeniedError,
@@ -949,3 +950,30 @@ def put(self):
"ok": True,
"message": f"Project {project_id} was set to {'busy' if set_to_busy else 'not busy'}.",
}
+
+
+class ProjectInfo(flask_restful.Resource):
+ """Get information for a specific project."""
+
+ @auth.login_required
+ @logging_bind_request
+ @handle_db_error
+ def get(self):
+ # 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)
+
+ # Construct a dict with info items
+ project_info = {
+ "Project ID": project.public_id,
+ "Created by": project.creator.name if project.creator else "Former User",
+ "Status": project.current_status,
+ "Last updated": project.date_updated,
+ "Size": project.size,
+ "Title": project.title,
+ "Description": project.description,
+ }
+
+ return_info = {"project_info": project_info}
+ return return_info
diff --git a/dds_web/scheduled_tasks.py b/dds_web/scheduled_tasks.py
index 3060eabd0..1c83c7721 100644
--- a/dds_web/scheduled_tasks.py
+++ b/dds_web/scheduled_tasks.py
@@ -92,7 +92,7 @@ def set_available_to_expired():
scheduler.app.logger.error(f"Error for project '{proj}': {errors[unit][proj]} ")
-@scheduler.task("cron", id="expired_to_archived", hour=0, minute=1, misfire_grace_time=3600)
+@scheduler.task("cron", id="expired_to_archived", hour=1, minute=1, misfire_grace_time=3600)
# @scheduler.task("interval", id="expired_to_archived", seconds=15, misfire_grace_time=1)
def set_expired_to_archived():
"""Search for expired projects whose deadlines are past and archive them"""
diff --git a/dds_web/templates/components/login_form.html b/dds_web/templates/components/login_form.html
index f0d5f5d41..bcfed04bd 100644
--- a/dds_web/templates/components/login_form.html
+++ b/dds_web/templates/components/login_form.html
@@ -39,4 +39,5 @@
Log In
Log in
Forgot your password?
+ No account?
diff --git a/dds_web/version.py b/dds_web/version.py
index 8a124bf64..ba51cedfc 100644
--- a/dds_web/version.py
+++ b/dds_web/version.py
@@ -1 +1 @@
-__version__ = "2.2.0"
+__version__ = "2.2.2"
diff --git a/requirements.txt b/requirements.txt
index 0057cc6b0..87db03bbe 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -42,7 +42,7 @@ MarkupSafe==2.0.1
marshmallow==3.14.1
marshmallow-sqlalchemy==0.27.0
packaging==21.3
-Pillow==9.0.1 # required by qrcode
+Pillow==9.3.0 # required by qrcode
pycparser==2.21
PyMySQL==1.0.2
PyNaCl==1.5.0
diff --git a/tests/__init__.py b/tests/__init__.py
index 3055bfa83..0b8a93735 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -200,6 +200,7 @@ class DDSEndpoint:
PROJECT_ACCESS = BASE_ENDPOINT + "/proj/access"
PROJECT_BUSY = BASE_ENDPOINT + "/proj/busy"
PROJECT_BUSY_ANY = BASE_ENDPOINT + "/proj/busy/any"
+ PROJECT_INFO = BASE_ENDPOINT + "/proj/info"
# Listing urls
LIST_PROJ = BASE_ENDPOINT + "/proj/list"
diff --git a/tests/test_project_info_listing.py b/tests/test_project_info_listing.py
new file mode 100644
index 000000000..14816a7e1
--- /dev/null
+++ b/tests/test_project_info_listing.py
@@ -0,0 +1,109 @@
+# IMPORTS ################################################################################ IMPORTS #
+
+# Standard library
+import http
+import unittest
+
+# Own
+import tests
+
+
+# CONFIG ################################################################################## CONFIG #
+
+proj_info_items = [
+ "Project ID",
+ "Created by",
+ "Status",
+ "Last updated",
+ "Size",
+ "Title",
+ "Description",
+]
+proj_query = {"project": "public_project_id"}
+proj_query_restricted = {"project": "restricted_project_id"}
+
+# TESTS #################################################################################### TESTS #
+
+
+def test_list_proj_info_no_token(client):
+ """Token required to list project information"""
+
+ response = client.get(tests.DDSEndpoint.PROJECT_INFO, headers=tests.DEFAULT_HEADER)
+ assert response.status_code == http.HTTPStatus.UNAUTHORIZED
+ response_json = response.json
+ assert response_json.get("message")
+ assert "No token" in response_json.get("message")
+
+
+def test_list_proj_info_without_project(client):
+ """Attempting to get the project information without specifying a project"""
+
+ token = tests.UserAuth(tests.USER_CREDENTIALS["unituser"]).token(client)
+ response = client.get(tests.DDSEndpoint.PROJECT_INFO, headers=token)
+ response_json = response.json
+ assert "Missing required information: 'project'" in response_json.get("message")
+
+
+def test_list_proj_info_access_granted(client):
+ """Researcher should be able to list project information"""
+
+ token = tests.UserAuth(tests.USER_CREDENTIALS["researchuser"]).token(client)
+ response = client.get(tests.DDSEndpoint.PROJECT_INFO, headers=token, query_string=proj_query)
+ assert response.status_code == http.HTTPStatus.OK
+ response_json = response.json
+ project_info = response_json.get("project_info")
+
+ assert "public_project_id" == project_info.get("Project ID")
+ # check that endpoint returns dictionary and not a list
+ assert isinstance(project_info, dict)
+
+
+def test_list_proj_info_unit_user(client):
+ """Unit user should be able to list project information"""
+
+ token = tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client)
+ response = client.get(tests.DDSEndpoint.PROJECT_INFO, headers=token, query_string=proj_query)
+ assert response.status_code == http.HTTPStatus.OK
+ response_json = response.json
+ project_info = response_json.get("project_info")
+
+ assert "public_project_id" == project_info.get("Project ID")
+ assert (
+ "This is a test project. You will be able to upload to but NOT download"
+ in project_info.get("Description")
+ )
+ assert "Size" in project_info.keys() and project_info["Size"] is not None
+
+
+def test_list_proj_info_returned_items(client):
+ """Returned project information should contain certain items"""
+
+ token = tests.UserAuth(tests.USER_CREDENTIALS["unitadmin"]).token(client)
+ response = client.get(tests.DDSEndpoint.PROJECT_INFO, headers=token, query_string=proj_query)
+ assert response.status_code == http.HTTPStatus.OK
+ response_json = response.json
+ project_info = response_json.get("project_info")
+
+ assert all(item in project_info for item in proj_info_items)
+
+
+def test_list_project_info_by_researchuser_not_in_project(client):
+ """Researchuser not in project should not be able to list project info"""
+
+ token = tests.UserAuth(tests.USER_CREDENTIALS["researchuser2"]).token(client)
+ response = client.get(tests.DDSEndpoint.PROJECT_INFO, query_string=proj_query, headers=token)
+ assert response.status_code == http.HTTPStatus.FORBIDDEN
+ response_json = response.json
+ assert "Project access denied" in response_json.get("message")
+
+
+def test_list_proj_info_public_insufficient_credentials(client):
+ """If the project access has not been granted, the project info should not be provided."""
+
+ token = tests.UserAuth(tests.USER_CREDENTIALS["researchuser"]).token(client)
+ response = client.get(
+ tests.DDSEndpoint.PROJECT_INFO, query_string=proj_query_restricted, headers=token
+ )
+ assert response.status_code == http.HTTPStatus.FORBIDDEN
+ response_json = response.json
+ assert "Project access denied" in response_json.get("message")
diff --git a/tests/test_version.py b/tests/test_version.py
index e0c88f2fd..95d81e78c 100644
--- a/tests/test_version.py
+++ b/tests/test_version.py
@@ -2,4 +2,4 @@
def test_version():
- assert version.__version__ == "2.2.0"
+ assert version.__version__ == "2.2.2"