Skip to content

Commit

Permalink
Merge branch 'staging'
Browse files Browse the repository at this point in the history
  • Loading branch information
churnikov committed Nov 7, 2024
2 parents 46f589c + 32dc57f commit 32553a2
Show file tree
Hide file tree
Showing 77 changed files with 1,733 additions and 351 deletions.
24 changes: 15 additions & 9 deletions api/openapi/public_apps_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,26 +17,29 @@ class PublicAppsAPI(viewsets.ReadOnlyModelViewSet):
The Public Apps API with read-only methods to get public apps information.
"""

# TODO: refactor. Rename list to list_apps, because it is a reserved word in python.
def list(self, request):
def list_apps(self, request):
"""
This endpoint gets a list of public apps.
:returns list: A list of app information.
:returns list: A list of app information, ordered by date created from the most recent to the oldest.
"""
logger.info("PublicAppsAPI. Entered list method.")
logger.info("Requested API version %s", request.version)
list_apps = []
list_apps_dict = {}

# TODO: MAKE SURE THAT THIS IS FILTERED BASED ON ACCESS
for model_class in APP_REGISTRY.iter_orm_models():
# Loop over all models, and check if they have the access and description field
if hasattr(model_class, "description") and hasattr(model_class, "access"):
queryset = (
model_class.objects.filter(~Q(app_status__status="Deleted"), access="public")
.order_by("-updated_on")[:8]
.values("id", "name", "app_id", "url", "description", "updated_on", "app_status")
queryset = model_class.objects.filter(~Q(app_status__status="Deleted"), access="public").values(
"id", "name", "app_id", "url", "description", "created_on", "updated_on", "app_status"
)
list_apps.extend(list(queryset))
# using a dictionary to avoid duplicates for shiny apps
for item in queryset:
list_apps_dict[item["id"]] = item

# Order the combined list by "created_on"
list_apps = sorted(list_apps_dict.values(), key=lambda x: x["created_on"], reverse=True)

for app in list_apps:
app["app_type"] = Apps.objects.get(id=app["app_id"]).name
Expand All @@ -45,6 +48,9 @@ def list(self, request):
# Add the previous url key located at app.table_field.url to support clients using the previous schema
app["table_field"] = {"url": app["url"]}

# Remove misleading app_id from the final output because it only refers to the app type
del app["app_id"]

data = {"data": list_apps}
logger.info("LIST: %s", data)
return JsonResponse(data)
Expand All @@ -65,7 +71,7 @@ def retrieve(self, request, app_slug=None, pk=None):

try:
queryset = model_class.objects.all().values(
"id", "name", "app_id", "url", "description", "updated_on", "access", "app_status"
"id", "name", "app_id", "url", "description", "created_on", "updated_on", "access", "app_status"
)
logger.info("Queryset: %s", queryset)
except FieldError as e:
Expand Down
2 changes: 1 addition & 1 deletion api/openapi/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
path("system-version", get_system_version),
path("api-info", APIInfo.as_view({"get": "get_api_info"})),
# The Apps API
path("public-apps", PublicAppsAPI.as_view({"get": "list"})),
path("public-apps", PublicAppsAPI.as_view({"get": "list_apps"})),
path("public-apps/<str:app_slug>/<int:pk>", PublicAppsAPI.as_view({"get": "retrieve"})),
# Supplementary lookups API
path(
Expand Down
1 change: 0 additions & 1 deletion api/tests/test_openapi_public_apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ def test_public_apps_list(self):
self.assertEqual(app["id"], self.app_instance.id)
self.assertIsNotNone(app["name"])
self.assertEqual(app["name"], self.app_instance.name)
self.assertTrue(app["app_id"] > 0)
self.assertEqual(app["description"], self.app_instance.description)
updated_on = datetime.fromisoformat(app["updated_on"][:-1])
self.assertEqual(datetime.date(updated_on), datetime.date(self.app_instance.updated_on))
Expand Down
24 changes: 23 additions & 1 deletion apps/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@
CustomAppInstance,
DashInstance,
FilemanagerInstance,
GradioInstance,
JupyterInstance,
NetpolicyInstance,
RStudioInstance,
ShinyInstance,
StreamlitInstance,
Subdomain,
TissuumapsInstance,
VolumeInstance,
Expand Down Expand Up @@ -52,7 +54,7 @@ class AppsAdmin(admin.ModelAdmin):

class BaseAppAdmin(admin.ModelAdmin):
list_display = ("name", "display_owner", "display_project", "display_status", "display_subdomain", "chart")
readonly_fields = ("id",)
readonly_fields = ("id", "created_on")
list_filter = ["owner", "project", "app_status__status", "chart"]
actions = ["redeploy_apps", "deploy_resources", "delete_resources"]

Expand Down Expand Up @@ -230,6 +232,26 @@ class FilemanagerInstanceAdmin(BaseAppAdmin):
)


@admin.register(GradioInstance)
class GradioInstanceAdmin(BaseAppAdmin):
list_display = BaseAppAdmin.list_display + (
"display_volumes",
"image",
"port",
"user_id",
)


@admin.register(StreamlitInstance)
class StreamlitInstanceAdmin(BaseAppAdmin):
list_display = BaseAppAdmin.list_display + (
"display_volumes",
"image",
"port",
"user_id",
)


admin.site.register(Subdomain)
admin.site.register(AppCategories)
admin.site.register(AppStatus, AppStatusAdmin)
6 changes: 6 additions & 0 deletions apps/app_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
CustomAppForm,
DashForm,
FilemanagerForm,
GradioForm,
JupyterForm,
NetpolicyForm,
RStudioForm,
ShinyForm,
StreamlitForm,
TissuumapsForm,
VolumeForm,
VSCodeForm,
Expand All @@ -14,10 +16,12 @@
CustomAppInstance,
DashInstance,
FilemanagerInstance,
GradioInstance,
JupyterInstance,
NetpolicyInstance,
RStudioInstance,
ShinyInstance,
StreamlitInstance,
TissuumapsInstance,
VolumeInstance,
VSCodeInstance,
Expand All @@ -37,3 +41,5 @@
APP_REGISTRY.register("shinyproxyapp", ModelFormTuple(ShinyInstance, ShinyForm))
APP_REGISTRY.register("tissuumaps", ModelFormTuple(TissuumapsInstance, TissuumapsForm))
APP_REGISTRY.register("filemanager", ModelFormTuple(FilemanagerInstance, FilemanagerForm))
APP_REGISTRY.register("gradio", ModelFormTuple(GradioInstance, GradioForm))
APP_REGISTRY.register("streamlit", ModelFormTuple(StreamlitInstance, StreamlitForm))
2 changes: 2 additions & 0 deletions apps/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
"name": "Display name for the application. This is the name visible on the app catalogue if the app is public",
"description": "Provide a detailed description of your app. "
"This will be the description visible in the app catalogue if the app is public.",
"tags": "Keywords relevant to your app. These will be displayed along with the description "
"in the app catalogue if the app is public.",
"subdomain": "Valid subdomain names have minimum length of 3 characters and may contain lower case letters a-z "
"and numbers 0-9 and a hyphen '-'. The hyphen should not be at the start or end of the subdomain.",
"access": "Public apps will be displayed on the app catalogue and can be accessed by anyone that has the link to "
Expand Down
2 changes: 2 additions & 0 deletions apps/forms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
from .custom import CustomAppForm
from .dash import DashForm
from .filemanager import FilemanagerForm
from .gradio import GradioForm
from .jupyter import JupyterForm
from .netpolicy import NetpolicyForm
from .rstudio import RStudioForm
from .shiny import ShinyForm
from .streamlit import StreamlitForm
from .tissuumaps import TissuumapsForm
from .volumes import VolumeForm
from .vscode import VSCodeForm
2 changes: 1 addition & 1 deletion apps/forms/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def _setup_form_helper(self):
body = Div(
SRVCommonDivField("name", placeholder="Name your app"),
SRVCommonDivField("description", rows=3, placeholder="Provide a detailed description of your app"),
Field("tags"),
SRVCommonDivField("tags"),
SRVCommonDivField("subdomain", placeholder="Enter a subdomain or leave blank for a random one."),
Field("volume"),
SRVCommonDivField("path", placeholder="/home/..."),
Expand Down
14 changes: 2 additions & 12 deletions apps/forms/dash.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def _setup_form_helper(self):
body = Div(
SRVCommonDivField("name", placeholder="Name your app"),
SRVCommonDivField("description", rows="3", placeholder="Provide a detailed description of your app"),
Field("tags"),
SRVCommonDivField("tags"),
SRVCommonDivField(
"subdomain", placeholder="Enter a subdomain or leave blank for a random one", spinner=True
),
Expand All @@ -35,22 +35,12 @@ def _setup_form_helper(self):
),
SRVCommonDivField("source_code_url", placeholder="Provide a link to the public source code"),
SRVCommonDivField("port", placeholder="8000"),
SRVCommonDivField("image", placeholder="registry/repository/image:tag"),
SRVCommonDivField("image", placeholder="e.g. docker.io/username/image-name:image-tag"),
css_class="card-body",
)

self.helper.layout = Layout(body, self.footer)

def clean(self):
cleaned_data = super().clean()
access = cleaned_data.get("access")
source_code_url = cleaned_data.get("source_code_url")

if access == "public" and not source_code_url:
self.add_error("source_code_url", "Source is required when access is public.")

return cleaned_data

class Meta:
model = DashInstance
fields = [
Expand Down
88 changes: 88 additions & 0 deletions apps/forms/gradio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
from crispy_forms.layout import Div, Field, Layout
from django import forms

from apps.forms.base import AppBaseForm
from apps.forms.field.common import SRVCommonDivField
from apps.models import GradioInstance
from projects.models import Flavor

__all__ = ["GradioForm"]


class GradioForm(AppBaseForm):
flavor = forms.ModelChoiceField(queryset=Flavor.objects.none(), required=False, empty_label=None)
port = forms.IntegerField(min_value=3000, max_value=9999, required=True)
image = forms.CharField(max_length=255, required=True)
path = forms.CharField(max_length=255, required=False)

def _setup_form_fields(self):
# Handle Volume field
super()._setup_form_fields()
self.fields["volume"].initial = None

def _setup_form_helper(self):
super()._setup_form_helper()

body = Div(
SRVCommonDivField("name", placeholder="Name your app"),
SRVCommonDivField("description", rows=3, placeholder="Provide a detailed description of your app"),
SRVCommonDivField("tags"),
SRVCommonDivField("subdomain", placeholder="Enter a subdomain or leave blank for a random one."),
Field("volume"),
SRVCommonDivField("path", placeholder="/home/..."),
SRVCommonDivField("flavor"),
SRVCommonDivField("access"),
SRVCommonDivField("source_code_url", placeholder="Provide a link to the public source code"),
SRVCommonDivField(
"note_on_linkonly_privacy",
rows=1,
placeholder="Describe why you want to make the app accessible only via a link",
),
SRVCommonDivField("port", placeholder="7860"),
SRVCommonDivField("image", placeholder="e.g. docker.io/username/image-name:image-tag"),
css_class="card-body",
)
self.helper.layout = Layout(body, self.footer)

def clean_path(self):
cleaned_data = super().clean()

path = cleaned_data.get("path", None)
volume = cleaned_data.get("volume", None)

if volume and not path:
self.add_error("path", "Path is required when volume is selected.")

if path and not volume:
self.add_error("path", "Warning, you have provided a path, but not selected a volume.")

if path:
# If new path matches current path, it is valid.
if self.instance and getattr(self.instance, "path", None) == path:
return path
# Verify that path starts with "/home"
path = path.strip().rstrip("/").lower().replace(" ", "")
if not path.startswith("/home"):
self.add_error("path", 'Path must start with "/home"')

return path

class Meta:
model = GradioInstance
fields = [
"name",
"description",
"volume",
"path",
"flavor",
"access",
"note_on_linkonly_privacy",
"source_code_url",
"port",
"image",
"tags",
]
labels = {
"note_on_linkonly_privacy": "Reason for choosing the link only option",
"tags": "Keywords",
}
12 changes: 1 addition & 11 deletions apps/forms/shiny.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def _setup_form_helper(self):
),
SRVCommonDivField("source_code_url", placeholder="Provide a link to the public source code"),
SRVCommonDivField("port", placeholder="3838"),
SRVCommonDivField("image", placeholder="registry/repository/image:tag"),
SRVCommonDivField("image", placeholder="e.g. docker.io/username/image-name:image-tag"),
Accordion(
AccordionGroup(
"Advanced settings",
Expand Down Expand Up @@ -81,16 +81,6 @@ def clean_shiny_site_dir(self):

return shiny_site_dir

def clean(self):
cleaned_data = super().clean()
access = cleaned_data.get("access", None)
source_code_url = cleaned_data.get("source_code_url", None)

if access == "public" and not source_code_url:
self.add_error("source_code_url", "Source is required when access is public.")

return cleaned_data

class Meta:
model = ShinyInstance
fields = [
Expand Down
Loading

0 comments on commit 32553a2

Please sign in to comment.