Compare commits
12 commits
579cae5ecf
...
2a1327c42e
| Author | SHA1 | Date | |
|---|---|---|---|
| 2a1327c42e | |||
| 6c7f945c7a | |||
| e61bf9076f | |||
| ec29ca5121 | |||
| 27865613d3 | |||
| bb91c0dd91 | |||
| dd3189f1dc | |||
| 48131aa0a9 | |||
| fdf43de917 | |||
| a247e773db | |||
| a10a7be010 | |||
| 096a2bde63 |
17 changed files with 221 additions and 60 deletions
|
|
@ -10,6 +10,7 @@ dependencies = [
|
||||||
"django==5.2.4",
|
"django==5.2.4",
|
||||||
"django-allauth>=65.10.0",
|
"django-allauth>=65.10.0",
|
||||||
"django-fernet-encrypted-fields>=0.3.0",
|
"django-fernet-encrypted-fields>=0.3.0",
|
||||||
|
"django-jsonform>=2.22.0",
|
||||||
"django-scopes>=2.0.0",
|
"django-scopes>=2.0.0",
|
||||||
"django-storages[s3]>=1.14.6",
|
"django-storages[s3]>=1.14.6",
|
||||||
"django-template-partials>=24.4",
|
"django-template-partials>=24.4",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
from django.contrib import admin, messages
|
from django.contrib import admin, messages
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django_jsonform.widgets import JSONFormWidget
|
||||||
|
|
||||||
from servala.core.forms import ControlPlaneAdminForm, ServiceDefinitionAdminForm
|
from servala.core.forms import ControlPlaneAdminForm, ServiceDefinitionAdminForm
|
||||||
from servala.core.models import (
|
from servala.core.models import (
|
||||||
|
|
@ -111,12 +112,60 @@ class ServiceAdmin(admin.ModelAdmin):
|
||||||
autocomplete_fields = ("category",)
|
autocomplete_fields = ("category",)
|
||||||
prepopulated_fields = {"slug": ["name"]}
|
prepopulated_fields = {"slug": ["name"]}
|
||||||
|
|
||||||
|
def get_form(self, request, obj=None, **kwargs):
|
||||||
|
form = super().get_form(request, obj, **kwargs)
|
||||||
|
# JSON schema for external_links field
|
||||||
|
external_links_schema = {
|
||||||
|
"type": "array",
|
||||||
|
"title": "External Links",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"title": "Link",
|
||||||
|
"properties": {
|
||||||
|
"url": {"type": "string", "format": "uri", "title": "URL"},
|
||||||
|
"title": {"type": "string", "title": "Title"},
|
||||||
|
"featured": {
|
||||||
|
"type": "boolean",
|
||||||
|
"title": "Featured",
|
||||||
|
"default": False,
|
||||||
|
"description": "Featured links will be shown on the service list page",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["url", "title"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
form.base_fields["external_links"].widget = JSONFormWidget(
|
||||||
|
schema=external_links_schema
|
||||||
|
)
|
||||||
|
return form
|
||||||
|
|
||||||
|
|
||||||
@admin.register(CloudProvider)
|
@admin.register(CloudProvider)
|
||||||
class CloudProviderAdmin(admin.ModelAdmin):
|
class CloudProviderAdmin(admin.ModelAdmin):
|
||||||
list_display = ("name",)
|
list_display = ("name",)
|
||||||
search_fields = ("name", "description")
|
search_fields = ("name", "description")
|
||||||
|
|
||||||
|
def get_form(self, request, obj=None, **kwargs):
|
||||||
|
form = super().get_form(request, obj, **kwargs)
|
||||||
|
# JSON schema for external_links field
|
||||||
|
external_links_schema = {
|
||||||
|
"type": "array",
|
||||||
|
"title": "External Links",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"title": "Link",
|
||||||
|
"properties": {
|
||||||
|
"url": {"type": "string", "format": "uri", "title": "URL"},
|
||||||
|
"title": {"type": "string", "title": "Title"},
|
||||||
|
},
|
||||||
|
"required": ["url", "title"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
form.base_fields["external_links"].widget = JSONFormWidget(
|
||||||
|
schema=external_links_schema
|
||||||
|
)
|
||||||
|
return form
|
||||||
|
|
||||||
|
|
||||||
@admin.register(ControlPlane)
|
@admin.register(ControlPlane)
|
||||||
class ControlPlaneAdmin(admin.ModelAdmin):
|
class ControlPlaneAdmin(admin.ModelAdmin):
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@ import urlman
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.text import slugify
|
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
from django.utils.text import slugify
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django_scopes import ScopedManager, scopes_disabled
|
from django_scopes import ScopedManager, scopes_disabled
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -71,9 +71,14 @@ class Service(ServalaModelMixin, models.Model):
|
||||||
logo = models.ImageField(
|
logo = models.ImageField(
|
||||||
upload_to="public/services", blank=True, null=True, verbose_name=_("Logo")
|
upload_to="public/services", blank=True, null=True, verbose_name=_("Logo")
|
||||||
)
|
)
|
||||||
# TODO schema
|
|
||||||
external_links = models.JSONField(
|
external_links = models.JSONField(
|
||||||
null=True, blank=True, verbose_name=_("External links")
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name=_("External links"),
|
||||||
|
help_text=(
|
||||||
|
'JSON array of link objects: {"url": "…", "title": "…", "featured": false}. '
|
||||||
|
"Featured links will be shown on the service list page, all other links will only show on the service and offering detail pages."
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
@ -83,6 +88,13 @@ class Service(ServalaModelMixin, models.Model):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def featured_links(self):
|
||||||
|
"""Return external links marked as featured."""
|
||||||
|
if not self.external_links:
|
||||||
|
return []
|
||||||
|
return [link for link in self.external_links if link.get("featured")]
|
||||||
|
|
||||||
|
|
||||||
def validate_dict(data, required_fields=None, allow_empty=True):
|
def validate_dict(data, required_fields=None, allow_empty=True):
|
||||||
if not data:
|
if not data:
|
||||||
|
|
@ -263,7 +275,10 @@ class CloudProvider(ServalaModelMixin, models.Model):
|
||||||
verbose_name=_("Logo"),
|
verbose_name=_("Logo"),
|
||||||
)
|
)
|
||||||
external_links = models.JSONField(
|
external_links = models.JSONField(
|
||||||
null=True, blank=True, verbose_name=_("External links")
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name=_("External links"),
|
||||||
|
help_text=('JSON array of link objects: {"url": "…", "title": "…"}. '),
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,22 @@
|
||||||
{% extends "frontend/base.html" %}
|
{% extends "frontend/base.html" %}
|
||||||
{% load static i18n %}
|
{% load static i18n %}
|
||||||
{% load allauth account socialaccount %}
|
{% load allauth account socialaccount %}
|
||||||
|
|
||||||
{% block html_title %}
|
{% block html_title %}
|
||||||
{% translate "Sign in" %}
|
{% translate "Sign in" %}
|
||||||
{% endblock html_title %}
|
{% endblock html_title %}
|
||||||
|
|
||||||
{% block page_title %}
|
{% block page_title %}
|
||||||
{% translate "Welcome to Servala" %}
|
{% translate "Welcome to Servala" %}
|
||||||
{% endblock page_title %}
|
{% endblock page_title %}
|
||||||
|
|
||||||
{% block card_header %}
|
{% block card_header %}
|
||||||
<div class="card-header text-center py-4" style="background: linear-gradient(135deg, var(--bs-primary), #8B5CF6); border-radius: 0.5rem 0.5rem 0 0;">
|
<div class="card-header text-center py-4"
|
||||||
<img src="{% static 'img/Servala-4.png' %}" alt="Servala" class="mb-3" style="height: 70px;">
|
style="background: linear-gradient(135deg, var(--bs-primary), #8B5CF6);
|
||||||
|
border-radius: 0.5rem 0.5rem 0 0">
|
||||||
|
<img src="{% static 'img/Servala-4.png' %}"
|
||||||
|
alt="Servala"
|
||||||
|
class="mb-3"
|
||||||
|
style="height: 70px">
|
||||||
</div>
|
</div>
|
||||||
{% endblock card_header %}
|
{% endblock card_header %}
|
||||||
|
|
||||||
{% block card_content %}
|
{% block card_content %}
|
||||||
<!-- Main Sign In Section -->
|
<!-- Main Sign In Section -->
|
||||||
{% if SOCIALACCOUNT_ENABLED %}
|
{% if SOCIALACCOUNT_ENABLED %}
|
||||||
|
|
@ -24,9 +25,10 @@
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<div class="text-center mb-4">
|
<div class="text-center mb-4">
|
||||||
<h5 class="text-primary mb-2">{% translate "Ready to get started?" %}</h5>
|
<h5 class="text-primary mb-2">{% translate "Ready to get started?" %}</h5>
|
||||||
<p class="text-muted mb-0">{% translate "Sign in to access your managed service instances and the Servala service catalog" %}</p>
|
<p class="text-muted mb-0">
|
||||||
|
{% translate "Sign in to access your managed service instances and the Servala service catalog" %}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% for provider in socialaccount_providers %}
|
{% for provider in socialaccount_providers %}
|
||||||
{% provider_login_url provider process=process scope=scope auth_params=auth_params as href %}
|
{% provider_login_url provider process=process scope=scope auth_params=auth_params as href %}
|
||||||
<form method="post" action="{{ href }}">
|
<form method="post" action="{{ href }}">
|
||||||
|
|
@ -35,7 +37,9 @@
|
||||||
<button type="submit"
|
<button type="submit"
|
||||||
class="btn btn-primary btn-lg w-100 py-3 mb-4 fw-semibold"
|
class="btn btn-primary btn-lg w-100 py-3 mb-4 fw-semibold"
|
||||||
title="{{ provider.name }}"
|
title="{{ provider.name }}"
|
||||||
style="border-radius: 12px; box-shadow: 0 4px 15px rgba(154, 99, 236, 0.2); background: linear-gradient(135deg, var(--bs-primary), #8B5CF6);">
|
style="border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 15px rgba(154, 99, 236, 0.2);
|
||||||
|
background: linear-gradient(135deg, var(--bs-primary), #8B5CF6)">
|
||||||
<span>{% translate "Sign in with VSHN Account" %}</span>
|
<span>{% translate "Sign in with VSHN Account" %}</span>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
@ -43,7 +47,6 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Feature Preview & Learn More Section -->
|
<!-- Feature Preview & Learn More Section -->
|
||||||
<div class="mt-4 pt-3 border-top">
|
<div class="mt-4 pt-3 border-top">
|
||||||
<div class="row g-3 text-center">
|
<div class="row g-3 text-center">
|
||||||
|
|
@ -82,7 +85,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Alternative Login Options (Admin) -->
|
<!-- Alternative Login Options (Admin) -->
|
||||||
<div class="mt-4 pt-3 border-top text-center">
|
<div class="mt-4 pt-3 border-top text-center">
|
||||||
<small class="text-muted">
|
<small class="text-muted">
|
||||||
|
|
@ -96,7 +98,6 @@
|
||||||
</a>
|
</a>
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="collapse mt-3" id="login-form">
|
<div class="collapse mt-3" id="login-form">
|
||||||
<div class="card bg-light border-0 shadow-sm" style="border-radius: 12px;">
|
<div class="card bg-light border-0 shadow-sm" style="border-radius: 12px;">
|
||||||
<div class="card-body p-4">
|
<div class="card-body p-4">
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,18 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<link rel="stylesheet" href="{% static 'mazer/compiled/css/app.css' %}">
|
<link rel="stylesheet" href="{% static 'mazer/compiled/css/app.css' %}">
|
||||||
<link rel="stylesheet" href="{% static 'mazer/compiled/css/app-dark.css' %}">
|
<link rel="stylesheet"
|
||||||
|
href="{% static 'mazer/compiled/css/app-dark.css' %}">
|
||||||
<link rel="stylesheet" href="{% static 'mazer/compiled/css/iconly.css' %}">
|
<link rel="stylesheet" href="{% static 'mazer/compiled/css/iconly.css' %}">
|
||||||
<link rel="stylesheet" href="{% static 'css/servala.css' %}">
|
<link rel="stylesheet" href="{% static 'css/servala.css' %}">
|
||||||
<link rel="icon" type="image/x-icon" href="{% static 'img/favicon.ico' %}">
|
<link rel="icon" type="image/x-icon" href="{% static 'img/favicon.ico' %}">
|
||||||
<script src="{% static "js/htmx.min.js" %}" defer></script>
|
<script src="{% static "js/htmx.min.js" %}" defer></script>
|
||||||
</head>
|
</head>
|
||||||
<title>{% block html_title %}Dashboard{% endblock html_title %} – Servala</title>
|
<title>
|
||||||
|
{% block html_title %}
|
||||||
|
Dashboard
|
||||||
|
{% endblock html_title %}
|
||||||
|
– Servala</title>
|
||||||
</head>
|
</head>
|
||||||
<body hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
|
<body hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
|
||||||
<script src="{% static 'mazer/static/js/initTheme.js' %}"></script>
|
<script src="{% static 'mazer/static/js/initTheme.js' %}"></script>
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,24 @@
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p>{{ service.description|default:"No description available."|urlize }}</p>
|
<p>{{ service.description|default:"No description available."|urlize }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
{% if service.external_links %}
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<h6 class="mb-3">{% translate "External Links" %}</h6>
|
||||||
|
<div class="d-flex flex-wrap gap-2">
|
||||||
|
{% for link in service.external_links %}
|
||||||
|
<a href="{{ link.url }}"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="btn btn-outline-primary btn-sm">
|
||||||
|
{{ link.title }}
|
||||||
|
<i class="bi bi-box-arrow-up-right ms-1"></i>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,13 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
{% if offering.description %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<p>{{ offering.description|urlize }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% if not has_control_planes %}
|
{% if not has_control_planes %}
|
||||||
<p>{% translate "We currently cannot offer this service, sorry!" %}</p>
|
<p>{% translate "We currently cannot offer this service, sorry!" %}</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
@ -49,6 +56,24 @@
|
||||||
{{ select_form }}
|
{{ select_form }}
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if service.external_links %}
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<h6 class="mb-3">{% translate "External Links" %}</h6>
|
||||||
|
<div class="d-flex flex-wrap gap-2">
|
||||||
|
{% for link in service.external_links %}
|
||||||
|
<a href="{{ link.url }}"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="btn btn-outline-primary btn-sm">
|
||||||
|
{{ link.title }}
|
||||||
|
<i class="bi bi-box-arrow-up-right ms-1"></i>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="service-form">{% partial service-form %}</div>
|
<div id="service-form">{% partial service-form %}</div>
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,10 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row service-cards-container mb-5">
|
||||||
{% for service in services %}
|
{% for service in services %}
|
||||||
<div class="col-6 col-lg-3 col-md-4">
|
<div class="col-6 col-lg-3 col-md-4">
|
||||||
<div class="card">
|
<div class="card h-100 d-flex flex-column">
|
||||||
<div class="card-header d-flex align-items-center">
|
<div class="card-header d-flex align-items-center">
|
||||||
{% if service.logo %}
|
{% if service.logo %}
|
||||||
<img src="{{ service.logo.url }}"
|
<img src="{{ service.logo.url }}"
|
||||||
|
|
@ -33,11 +33,23 @@
|
||||||
<small class="text-muted">{{ service.category }}</small>
|
<small class="text-muted">{{ service.category }}</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body flex-grow-1">
|
||||||
{% if service.description %}<p class="card-text">{{ service.description|urlize }}</p>{% endif %}
|
{% if service.description %}<p class="card-text">{{ service.description|urlize }}</p>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer d-flex justify-content-between">
|
<div class="card-footer d-flex justify-content-between align-items-center gap-2">
|
||||||
<span></span>
|
{% if service.featured_links %}
|
||||||
|
{% with featured_link=service.featured_links.0 %}
|
||||||
|
<a href="{{ featured_link.url }}"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="btn btn-outline-primary">
|
||||||
|
{{ featured_link.title }}
|
||||||
|
<i class="bi bi-box-arrow-up-right ms-1"></i>
|
||||||
|
</a>
|
||||||
|
{% endwith %}
|
||||||
|
{% else %}
|
||||||
|
<span></span>
|
||||||
|
{% endif %}
|
||||||
<a href="{{ service.slug }}/" class="btn btn-light-primary">{% translate "View Availability" %}</a>
|
<a href="{{ service.slug }}/" class="btn btn-light-primary">{% translate "View Availability" %}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,8 @@
|
||||||
{% endblocktranslate %}
|
{% endblocktranslate %}
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
<a href="{{ account_href }}" target="_blank"
|
<a href="{{ account_href }}"
|
||||||
|
target="_blank"
|
||||||
class="btn btn-primary btn-lg icon icon-left btn-keycloak">
|
class="btn btn-primary btn-lg icon icon-left btn-keycloak">
|
||||||
<span class="mx-1">{% translate "VSHN Account Console" %}</span>
|
<span class="mx-1">{% translate "VSHN Account Console" %}</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,12 @@
|
||||||
{% if show_error %}
|
{% if show_error %}
|
||||||
<div class="{{ css_class }}">
|
<div class="{{ css_class }}">
|
||||||
{% if has_list %}
|
{% if has_list %}
|
||||||
{% if message %}{{ message }}{% endif %}
|
{% if message %}{{ message }}{% endif %}
|
||||||
<ul>
|
<ul>
|
||||||
{% for error in errors %}
|
{% for error in errors %}<li>{{ error }}</li>{% endfor %}
|
||||||
<li>{{ error }}</li>
|
</ul>
|
||||||
{% endfor %}
|
{% else %}
|
||||||
</ul>
|
{{ message }}
|
||||||
{% else %}
|
{% endif %}
|
||||||
{{ message }}
|
</div>
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,29 @@
|
||||||
<div class="alert alert-{{ message.tags }} alert-dismissible" id="auto-dismiss-alert-{{ forloop.counter0|default:'0' }}">
|
<div class="alert alert-{{ message.tags }} alert-dismissible"
|
||||||
|
id="auto-dismiss-alert-{{ forloop.counter0|default:'0' }}">
|
||||||
{{ message }}
|
{{ message }}
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="btn-close"
|
class="btn-close"
|
||||||
data-bs-dismiss="alert"
|
data-bs-dismiss="alert"
|
||||||
aria-label="Close"></button>
|
aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
const alert = document.getElementById('auto-dismiss-alert-{{ forloop.counter0|default:'0' }}');
|
const alert = document.getElementById('auto-dismiss-alert-{{ forloop.counter0|default:'
|
||||||
if (alert && !alert.classList.contains('alert-danger')) {
|
0 ' }}');
|
||||||
setTimeout(function() {
|
if (alert && !alert.classList.contains('alert-danger')) {
|
||||||
let opacity = 1;
|
setTimeout(function() {
|
||||||
const fadeOutInterval = setInterval(function() {
|
let opacity = 1;
|
||||||
if (opacity > 0.05) {
|
const fadeOutInterval = setInterval(function() {
|
||||||
opacity -= 0.05;
|
if (opacity > 0.05) {
|
||||||
alert.style.opacity = opacity;
|
opacity -= 0.05;
|
||||||
} else {
|
alert.style.opacity = opacity;
|
||||||
clearInterval(fadeOutInterval);
|
} else {
|
||||||
const bsAlert = new bootstrap.Alert(alert);
|
clearInterval(fadeOutInterval);
|
||||||
bsAlert.close();
|
const bsAlert = new bootstrap.Alert(alert);
|
||||||
}
|
bsAlert.close();
|
||||||
}, 25);
|
}
|
||||||
}, 5000);
|
}, 25);
|
||||||
}
|
}, 5000);
|
||||||
});
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -3,6 +3,7 @@ Template filters for safe error formatting.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import html
|
import html
|
||||||
|
|
||||||
from django import template
|
from django import template
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,8 @@ class SupportView(OrganizationViewMixin, FormView):
|
||||||
self.request,
|
self.request,
|
||||||
mark_safe(
|
mark_safe(
|
||||||
_(
|
_(
|
||||||
'There was an error submitting your support request. Please try again or contact us directly at <a href="mailto:servala-support@vshn.ch">servala-support@vshn.ch</a>.'
|
"There was an error submitting your support request. "
|
||||||
|
'Please try again or contact us directly at <a href="mailto:servala-support@vshn.ch">servala-support@vshn.ch</a>.'
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -150,6 +150,7 @@ INSTALLED_APPS = [
|
||||||
# The frontend app is loaded early in order to supersede some allauth views/behaviour
|
# The frontend app is loaded early in order to supersede some allauth views/behaviour
|
||||||
"servala.frontend",
|
"servala.frontend",
|
||||||
"django.forms",
|
"django.forms",
|
||||||
|
"django_jsonform",
|
||||||
"template_partials",
|
"template_partials",
|
||||||
"rules.apps.AutodiscoverRulesConfig",
|
"rules.apps.AutodiscoverRulesConfig",
|
||||||
"allauth",
|
"allauth",
|
||||||
|
|
|
||||||
|
|
@ -174,3 +174,21 @@ a.btn-keycloak {
|
||||||
margin-top: -16px;
|
margin-top: -16px;
|
||||||
padding-right: 28px;
|
padding-right: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Service cards equal height styling */
|
||||||
|
.service-cards-container .card {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-cards-container .card-body {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-cards-container .card-footer {
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
|
||||||
14
uv.lock
generated
14
uv.lock
generated
|
|
@ -338,6 +338,18 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/75/8a/2c5d88cd540d83ceaa1cb3191ed35dfed0caacc6fe2ff5fe74c9ecc7776f/django_fernet_encrypted_fields-0.3.0-py3-none-any.whl", hash = "sha256:a17cca5bf3638ee44674e64f30792d5960b1d4d4b291ec478c27515fc4860612", size = 5400, upload-time = "2025-02-21T02:58:40.832Z" },
|
{ url = "https://files.pythonhosted.org/packages/75/8a/2c5d88cd540d83ceaa1cb3191ed35dfed0caacc6fe2ff5fe74c9ecc7776f/django_fernet_encrypted_fields-0.3.0-py3-none-any.whl", hash = "sha256:a17cca5bf3638ee44674e64f30792d5960b1d4d4b291ec478c27515fc4860612", size = 5400, upload-time = "2025-02-21T02:58:40.832Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django-jsonform"
|
||||||
|
version = "2.23.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "django" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/9b/a8/83c57acbc153b86615be279cee5a194ce1163b578f29a9f6d658f267785e/django_jsonform-2.23.2.tar.gz", hash = "sha256:6fa2ba7c082be51d738e6c66e35075a3cb9ebc2f941e3a477c988900a7fe3269", size = 108182, upload-time = "2025-01-29T22:31:34.093Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3a/8e/8766f4bc535917ccbc6b3dcb57caf82210a6bacd613c3d9dbaec81018935/django_jsonform-2.23.2-py3-none-any.whl", hash = "sha256:1b7f94c5a2bd22c844e035a9940a9c8586f7b8fc3346ef2a6a13ba608e0059d7", size = 109148, upload-time = "2025-01-29T22:31:31.186Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "django-scopes"
|
name = "django-scopes"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
|
|
@ -1050,6 +1062,7 @@ dependencies = [
|
||||||
{ name = "django" },
|
{ name = "django" },
|
||||||
{ name = "django-allauth" },
|
{ name = "django-allauth" },
|
||||||
{ name = "django-fernet-encrypted-fields" },
|
{ name = "django-fernet-encrypted-fields" },
|
||||||
|
{ name = "django-jsonform" },
|
||||||
{ name = "django-scopes" },
|
{ name = "django-scopes" },
|
||||||
{ name = "django-storages", extra = ["s3"] },
|
{ name = "django-storages", extra = ["s3"] },
|
||||||
{ name = "django-template-partials" },
|
{ name = "django-template-partials" },
|
||||||
|
|
@ -1086,6 +1099,7 @@ requires-dist = [
|
||||||
{ name = "django", specifier = "==5.2.4" },
|
{ name = "django", specifier = "==5.2.4" },
|
||||||
{ name = "django-allauth", specifier = ">=65.10.0" },
|
{ name = "django-allauth", specifier = ">=65.10.0" },
|
||||||
{ name = "django-fernet-encrypted-fields", specifier = ">=0.3.0" },
|
{ name = "django-fernet-encrypted-fields", specifier = ">=0.3.0" },
|
||||||
|
{ name = "django-jsonform", specifier = ">=2.22.0" },
|
||||||
{ name = "django-scopes", specifier = ">=2.0.0" },
|
{ name = "django-scopes", specifier = ">=2.0.0" },
|
||||||
{ name = "django-storages", extras = ["s3"], specifier = ">=1.14.6" },
|
{ name = "django-storages", extras = ["s3"], specifier = ">=1.14.6" },
|
||||||
{ name = "django-template-partials", specifier = ">=24.4" },
|
{ name = "django-template-partials", specifier = ">=24.4" },
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue