From 1957f76d1515438c34175c3bd37c2e38d08bb7c5 Mon Sep 17 00:00:00 2001 From: Arnold Kochari Date: Fri, 19 Apr 2024 16:30:51 +0200 Subject: [PATCH 1/4] Update info about teaching (#195) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Viktor Sandström --- templates/portal/home.html | 63 +++++++++++++++++----------------- templates/portal/teaching.html | 54 +++++++---------------------- 2 files changed, 43 insertions(+), 74 deletions(-) diff --git a/templates/portal/home.html b/templates/portal/home.html index c57194f4..1ba7e7ea 100644 --- a/templates/portal/home.html +++ b/templates/portal/home.html @@ -143,6 +143,37 @@
{{ model.name|truncatechars:30 }}
+ {% if collection_objects %} +
+
+

Collections +

+
+
+ + {% for collection in collection_objects %} + + {% endfor %} +
+

Collections are groups of apps and models published on SciLifeLab Serve belonging to a research community, organization, or topic. Start a new collection?

+
+ + {% if link_all_collections %} + + {% endif %} +
+ {% endif %} + {% if news_objects %}
@@ -178,38 +209,6 @@
{{ news.title }}
{% endif %} - {% if collection_objects %} -
-
-

Collections -

-
-
- - {% for collection in collection_objects %} - - {% endfor %} -
-

Collections are groups of apps and models published on SciLifeLab Serve belonging to a research community, organisation, or topic. Start a new collection?

-
- - {% if link_all_collections %} - - {% endif %} -
- {% endif %} - -
{% endblock %} diff --git a/templates/portal/teaching.html b/templates/portal/teaching.html index 48d399a3..669a1c2e 100644 --- a/templates/portal/teaching.html +++ b/templates/portal/teaching.html @@ -12,49 +12,18 @@

- Teaching through SciLifeLab Serve + Using SciLifeLab Serve in teaching

-

Users will not currently gain access to cloud-based JupyterLab Notebooks or RStudio by default. However, - users can request access to these resources and use them for teaching courses and workshops. It is - possible to establish individual instances of JupyterLab notebook and RStudio for each student/attendee, - they can be granted access to those instances via unique URLs. This negates the need for teachers to spend - time ensuring that the software and dependencies needed to complete tasks are correctly installed on each - machine. This will both save time and help to ensure the course/workshop can run smoothly. Python modules - and R packages can be installed as usual in student instances, and a terminal window will also be - available.

-

As with all other resources available through SciLifeLab Serve, JupyterLab - notebooks and Rstudio are provided free of charge. Whilst still in the beta testing phase, - SciLifeLab Serve has already been used for teaching classes (of around 30 students) with great success. - Should any issues arise though, we will work with you to resolve them as quickly as possible.

-
How do I request to use SciLifeLab Serve for teaching?
-

To ensure that the functionality intended for teaching is not used for other purposes, and that only one - course is running at any given time, we have established a specific application - form for access to this functionality. The form, found below, enables you - to provide details about yourself and your planned course/workshop. Upon approval of your application, - your SciLifeLab Serve account will be granted permission to create multiple instances of JupyterLab - notebook and/or RStudio. Each instance that you create will be assigned a unique URL that you can assign - to a given student/attendee.

-
Who is eligible to apply?
-

We accept applications for courses, workshops, and webinars that are organised by Swedish universities or - SciLifeLab infrastructure units. The courses/workshops/webinars should be aimed life science researchers - at the doctoral level or above.

-
What kind of compute and storage capacity will we receive? Is there a limit to participant number?
-

We will discuss your individual requirements for CPUs and memory with you. We expect to be able to cover - the needs of most courses. For reference, SciLifeLab Serve has previously been used to teach courses - involving up to 30 paticipants without issue.

-
What support will be provided?
-

Before the course, we will be available to answer any questions that you might have regarding features of - SciLifeLab Serve, setting up the required instances for students, and the interface of SciLifeLab Serve. - During the course, we will be on hand to answer any questions as quickly as possible and will monitor our - servers throughout. In the event of a server issue, we will do all we can to resolve it quickly. We - recommend having a backup plan in case there are unforeseen issues that cannot be resolved promptly.

-
How is SciLifeLab Serve funded?
-

SciLifeLab Serve is supported by a grant from the Knut and Alice Wallenberg Foundation given to - SciLifeLab for the purpose of supporting research in data-driven life science in Sweden. SciLifeLab Serve - is available free of charge to researchers affiliated with Swedish research institutions, regardless of an - affiliation with SciLifeLab. It is also offered to ScLifeLab infrastructure units. Refer to our 'about' page for more information.

- +

While it is not the primary purpose of SciLifeLab Serve the functionality that we have can be useful in workshops, seminars, + courses, and other training events where students need to perform hands-on tasks in computational environments. + Specifically, using the SciLifeLab Serve participants in training events can get access to individual + instances of browser-based notebooks: JupyterLab, RStudio, or VS Code. In addition, they can launch a custom Docker + image prepared by the instructors beforehand. This negates the need for instructors to spend + time ensuring that the software and dependencies needed to complete tasks are correctly installed on each + machine. This will both save time and help to ensure the course/workshop can run smoothly.

+

You can use the form below to submit a teaching request. Please read our + user guide page on using SciLifeLab Serve for teaching + before submitting the form below.

Application form

@@ -81,6 +50,7 @@

Application form

+
Please provide information about what computational tools you will need for your training event, what hardware requirements you have, how many participants you expect, for how long the computational tools will need to stay available.
From 6379f6756cff607b83cb2508d24fe7fa8f2a1119 Mon Sep 17 00:00:00 2001 From: Arnold Kochari Date: Thu, 25 Apr 2024 11:19:44 +0200 Subject: [PATCH 2/4] Fixed a number of minor bugs and added support for additional shiny app settings for admins (#196) * Fixed bugs regarding project slugs, shinyproxy description etc * Added shiny proxy config in forms --- apps/controller.py | 50 +++++++++++++++++++++- apps/helpers.py | 6 +++ apps/views.py | 11 ++++- fixtures/apps_fixtures.json | 17 +++++++- projects/models.py | 4 +- projects/urls.py | 5 --- projects/views.py | 73 +++++--------------------------- templates/403.html | 10 ++--- templates/apps/create.html | 40 ++++++++++++----- templates/apps/update.html | 28 +++++++++++- templates/projects/settings.html | 2 +- 11 files changed, 155 insertions(+), 91 deletions(-) diff --git a/apps/controller.py b/apps/controller.py index 2e405bfe..74ce0ae0 100644 --- a/apps/controller.py +++ b/apps/controller.py @@ -60,11 +60,59 @@ def deploy(options): try: port = int(options["appconfig"]["port"]) except Exception: - logger.error("Userid not a number.", exc_info=True) + logger.error("Port not a number.", exc_info=True) return json.dumps({"status": "failed", "reason": "Port not an integer."}) if port > 9999 or port < 3000: logger.info("Port outside of allowed range.") return json.dumps({"status": "failed", "reason": "Port outside of allowed range."}) + # check if valid proxyheartbeatrate + if "proxyheartbeatrate" in options["appconfig"]: + try: + proxyheartbeatrate = int(options["appconfig"]["proxyheartbeatrate"]) + except Exception: + logger.error("Proxy heartbeat rate not a number.", exc_info=True) + return json.dumps({"status": "failed", "reason": "Proxyheartbeatrate not an integer."}) + if proxyheartbeatrate < 1: + logger.info("Heartbeat rate outside of allowed range, must be at least 1.") + return json.dumps( + {"status": "failed", "reason": "Heartbeat rate outside of allowed range, must be at least 1."} + ) + else: + options["appconfig"]["proxyheartbeatrate"] = "10000" + # check if valid proxyheartbeattimeout + if "proxyheartbeattimeout" in options["appconfig"]: + try: + proxyheartbeattimeout = int(options["appconfig"]["proxyheartbeattimeout"]) + except Exception: + logger.error("Proxy heartbeat timeout not a number.", exc_info=True) + return json.dumps({"status": "failed", "reason": "Proxyheartbeattimeout not an integer."}) + if proxyheartbeattimeout < -1 or proxyheartbeattimeout == 0: + logger.info("Heartbeat timeout outside of allowed range, cannot be lower than 0 except for -1.") + return json.dumps( + { + "status": "failed", + "reason": "Heartbeat timeout outside of allowed range, , cannot be lower than 0 except for -1.", + } + ) + else: + options["appconfig"]["proxyheartbeattimeout"] = "60000" + # check if valid proxycontainerwaittime + if "proxycontainerwaittime" in options["appconfig"]: + try: + proxycontainerwaittime = int(options["appconfig"]["proxycontainerwaittime"]) + except Exception: + logger.error("Proxy container wait time not a number.", exc_info=True) + return json.dumps({"status": "failed", "reason": "Proxycontainerwaittime not an integer."}) + if proxycontainerwaittime < 20000: + logger.info("Proxy container wait time outside of allowed range, must be at least 20000.") + return json.dumps( + { + "status": "failed", + "reason": "Proxycontainerwaittime outside of allowed range, must be at least 20000.", + } + ) + else: + options["appconfig"]["proxycontainerwaittime"] = "30000" # Save helm values file for internal reference unique_filename = "charts/values/{}-{}.yaml".format(str(uuid.uuid4()), str(options["app_name"])) diff --git a/apps/helpers.py b/apps/helpers.py index 8ce27483..6c4803b8 100644 --- a/apps/helpers.py +++ b/apps/helpers.py @@ -216,6 +216,12 @@ def create_app_instance(user, project, app, app_settings, data=[], wait=False): ) if "userid" not in app_instance.parameters["appconfig"]: app_instance.parameters["appconfig"]["userid"] = "1000" + if "proxyheartbeatrate" not in app_instance.parameters["appconfig"]: + app_instance.parameters["appconfig"]["proxyheartbeatrate"] = "10000" + if "proxyheartbeattimeout" not in app_instance.parameters["appconfig"]: + app_instance.parameters["appconfig"]["proxyheartbeattimeout"] = "60000" + if "proxycontainerwaittime" not in app_instance.parameters["appconfig"]: + app_instance.parameters["appconfig"]["proxycontainerwaittime"] = "30000" app_instance.save() # Saving ReleaseName, status and setting up dependencies if rel_name_obj: diff --git a/apps/views.py b/apps/views.py index 4b592c71..d822eb44 100644 --- a/apps/views.py +++ b/apps/views.py @@ -215,8 +215,11 @@ def get(self, request, project, ai_id): existing_userid = None existing_path = None existing_source_code_url = appinstance.source_code_url + existing_proxyheartbeatrate = None + existing_proxyheartbeattimeout = None + existing_proxycontainerwaittime = None - # Settings for custom app + # Settings for custom app and shinyproxy if "appconfig" in appinstance.parameters: appconfig = appinstance.parameters["appconfig"] existing_userid = appconfig.get("userid", None) @@ -227,6 +230,9 @@ def get(self, request, project, ai_id): if not created_by_admin: existing_path = existing_path.replace("/home/", "", 1) + existing_proxyheartbeatrate = appconfig.get("proxyheartbeatrate", None) + existing_proxyheartbeattimeout = appconfig.get("proxyheartbeattimeout", None) + existing_proxycontainerwaittime = appconfig.get("proxycontainerwaittime", None) app = appinstance.app do_display_description_field = app.category.name is not None and app.category.name.lower() == "serve" @@ -271,6 +277,9 @@ def filter_func(): "existing_app_release_name": existing_app_release_name, "existing_userid": existing_userid, "existing_source_code_url": existing_source_code_url, + "existing_proxyheartbeatrate": existing_proxyheartbeatrate, + "existing_proxyheartbeattimeout": existing_proxyheartbeattimeout, + "existing_proxycontainerwaittime": existing_proxycontainerwaittime, } return render(request, template, context) diff --git a/fixtures/apps_fixtures.json b/fixtures/apps_fixtures.json index 39310bcb..2a10a45a 100644 --- a/fixtures/apps_fixtures.json +++ b/fixtures/apps_fixtures.json @@ -688,7 +688,7 @@ { "fields": { "category": "serve", - "chart": "ghcr.io/scilifelabdatacentre/serve-charts/shinyproxy:1.0.0", + "chart": "ghcr.io/scilifelabdatacentre/serve-charts/shinyproxy:1.1.0", "created_on": "2023-08-25T21:34:37.815Z", "description": "", "logo": "shinyapp-logo.svg", @@ -708,6 +708,21 @@ "default": "3838", "title": "Port", "type": "number" + }, + "proxyheartbeatrate": { + "default": "10000", + "title": "Proxy heartbeat rate", + "type": "number" + }, + "proxyheartbeattimeout": { + "default": "60000", + "title": "Proxy heartbeat timeout", + "type": "number" + }, + "proxycontainerwaittime": { + "default": "30000", + "title": "Proxy container wait time", + "type": "number" } }, "default_values": { diff --git a/projects/models.py b/projects/models.py index 64d803d9..1ccd0ca6 100644 --- a/projects/models.py +++ b/projects/models.py @@ -160,7 +160,9 @@ def create_project(self, name, owner, description, status="active", project_temp key = self.generate_passkey() letters = string.ascii_lowercase secret = self.generate_passkey(40) - slug = slugify(name) + slug = slugify(name[:47]) + if len(slug) < 3: + slug = "".join(random.choice(letters) for i in range(3)) slug_extension = "".join(random.choice(letters) for i in range(3)) slug = "{}-{}".format(slugify(slug), slug_extension) diff --git a/projects/urls.py b/projects/urls.py index 6af75800..21496c44 100644 --- a/projects/urls.py +++ b/projects/urls.py @@ -49,11 +49,6 @@ UpdatePatternView.as_view(), name="update_pattern", ), - path( - "/project/publish/", - views.publish_project, - name="publish_project", - ), path( "/project/access/grant/", GrantAccessToProjectView.as_view(), diff --git a/projects/views.py b/projects/views.py index 5af8b993..52379ae5 100644 --- a/projects/views.py +++ b/projects/views.py @@ -159,15 +159,17 @@ def change_description(request, project_slug): description = request.POST.get("description", "") if description != "": project.description = description - project.save() + else: + project.description = None + project.save() - log = ProjectLog( - project=project, - module="PR", - headline="Project description", - description="Changed description for project", - ) - log.save() + log = ProjectLog( + project=project, + module="PR", + headline="Project description", + description="Changed description for project", + ) + log.save() return HttpResponseRedirect( reverse( @@ -633,58 +635,3 @@ def delete(request, project_slug): delete_project.delay(project.pk) return HttpResponseRedirect(next_page, {"message": "Deleted project successfully."}) - - -@login_required -@permission_required_or_403("can_view_project", (Project, "slug", "project_slug")) -def publish_project(request, project_slug): - owner = User.objects.filter(username=request.user).first() - project = Project.objects.filter(owner=owner, slug=project_slug).first() - - if request.method == "POST": - gh_form = PublishProjectToGitHub(request.POST) - - if gh_form.is_valid(): - user_name = gh_form.cleaned_data["user_name"] - user_password = gh_form.cleaned_data["user_password"] - - user_password_bytes = user_password.encode("ascii") - base64_bytes = base64.b64encode(user_password_bytes) - user_password_encoded = base64_bytes.decode("ascii") - - url = "http://{}-file-controller/project/{}/push/{}/{}".format( - project_slug, - project_slug[:-4], - user_name, - user_password_encoded, - ) - try: - response = r.get(url) - - if response.status_code == 200 or response.status_code == 203: - payload = response.json() - - if payload["status"] == "OK": - clone_url = payload["clone_url"] - if clone_url: - project.clone_url = clone_url - project.save() - - log = ProjectLog( - project=project, - module="PR", - headline="GitHub repository", - description=("Published project files" " to a GitHub repository {url}").format( - url=project.clone_url - ), - ) - log.save() - except Exception as e: - logger.error("Failed to get response from {} with error: {}".format(url, e)) - - return HttpResponseRedirect( - reverse( - "projects:settings", - kwargs={"project_slug": project_slug}, - ) - ) diff --git a/templates/403.html b/templates/403.html index 017c42d6..b865c178 100644 --- a/templates/403.html +++ b/templates/403.html @@ -5,17 +5,17 @@ {% load static %} {% block content %} -

FORBIDDEN 403

-

You don't have access to that site! Contact project admin to grant you access.

+

Access to page forbidden. Error 403.

+

You don't have access to this project! Log in or ask the project owner to grant you access.

Is this your project? {% if request.user.is_authenticated %} - Sign out + Sign out {% else %} - - Log in + + Log in {% endif %}

diff --git a/templates/apps/create.html b/templates/apps/create.html index 305e2c84..a32c9a32 100644 --- a/templates/apps/create.html +++ b/templates/apps/create.html @@ -222,7 +222,7 @@

Create {{ app.name }}

{{ vals.meta_title }}
{% for subkey, subval in vals.items %} - {% if subval.type != "boolean" and subkey == "image" or subkey == "port" or subkey == "userid"%} + {% if subval.type != "boolean" and subkey == "image" or subkey == "port" or subkey == "userid" or subkey == "proxyheartbeatrate" or subkey == "proxyheartbeattimeout" or subkey == "proxycontainerwaittime" %}
{% else %}
@@ -286,7 +286,7 @@
{{ vals.meta_title }}
{% endif %} {% if subval.type == "number" %} - {% if subkey == "port" or subkey == "userid" %} + {% if subkey == "port" or subkey == "userid" or subkey == "proxyheartbeatrate" or subkey == "proxyheartbeattimeout" or subkey == "proxycontainerwaittime" %} {% if subkey == "port" %} @@ -299,10 +299,34 @@
{{ vals.meta_title }}
- +
Please add a valid User ID between 999 and 1010!
+ {% elif subkey == "proxyheartbeatrate" and request.user.is_superuser %} + + + + +
+ Please add a number, needs to be above 1. +
+ {% elif subkey == "proxyheartbeattimeout" and request.user.is_superuser %} + + + + +
+ Please add a number. Cannot be lower than 0 except for '-1' to set no timeout. +
+ {% elif subkey == "proxycontainerwaittime" and request.user.is_superuser %} + + + + +
+ Please add a number, needs to be above 20000. +
{% endif %} {% else %} @@ -359,7 +383,7 @@
{{ vals.meta_title }}
- + {# In the custom app creation form, make the path a required field if a persistent volumet is selected #} {% if app.slug == "customapp" %}