service listing
This commit is contained in:
parent
ea44f6f54a
commit
b367012d5c
22 changed files with 615 additions and 7 deletions
6
hello.py
6
hello.py
|
@ -1,6 +0,0 @@
|
||||||
def main():
|
|
||||||
print("Hello from servala-fe!")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
BIN
hub/db.sqlite3
Normal file
BIN
hub/db.sqlite3
Normal file
Binary file not shown.
0
hub/hub/__init__.py
Normal file
0
hub/hub/__init__.py
Normal file
16
hub/hub/asgi.py
Normal file
16
hub/hub/asgi.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
"""
|
||||||
|
ASGI config for hub project.
|
||||||
|
|
||||||
|
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "hub.settings")
|
||||||
|
|
||||||
|
application = get_asgi_application()
|
112
hub/hub/settings.py
Normal file
112
hub/hub/settings.py
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
# Quick-start development settings - unsuitable for production
|
||||||
|
# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
|
||||||
|
|
||||||
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
|
SECRET_KEY = "django-insecure-$5nkma6cv^a58n4%4$nef2tp8u!2vt=qbhoog5waui0iwe+8yp"
|
||||||
|
|
||||||
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
|
DEBUG = True
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = []
|
||||||
|
|
||||||
|
|
||||||
|
# Application definition
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
"django.contrib.admin",
|
||||||
|
"django.contrib.auth",
|
||||||
|
"django.contrib.contenttypes",
|
||||||
|
"django.contrib.sessions",
|
||||||
|
"django.contrib.messages",
|
||||||
|
"django.contrib.staticfiles",
|
||||||
|
"services",
|
||||||
|
]
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
"django.middleware.security.SecurityMiddleware",
|
||||||
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
|
"django.middleware.common.CommonMiddleware",
|
||||||
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||||
|
]
|
||||||
|
|
||||||
|
ROOT_URLCONF = "hub.urls"
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||||
|
"DIRS": [],
|
||||||
|
"APP_DIRS": True,
|
||||||
|
"OPTIONS": {
|
||||||
|
"context_processors": [
|
||||||
|
"django.template.context_processors.debug",
|
||||||
|
"django.template.context_processors.request",
|
||||||
|
"django.contrib.auth.context_processors.auth",
|
||||||
|
"django.contrib.messages.context_processors.messages",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
WSGI_APPLICATION = "hub.wsgi.application"
|
||||||
|
|
||||||
|
|
||||||
|
# Database
|
||||||
|
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
"default": {
|
||||||
|
"ENGINE": "django.db.backends.sqlite3",
|
||||||
|
"NAME": BASE_DIR / "db.sqlite3",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Password validation
|
||||||
|
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Internationalization
|
||||||
|
# https://docs.djangoproject.com/en/5.1/topics/i18n/
|
||||||
|
|
||||||
|
LANGUAGE_CODE = "en-us"
|
||||||
|
|
||||||
|
TIME_ZONE = "UTC"
|
||||||
|
|
||||||
|
USE_I18N = True
|
||||||
|
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
|
||||||
|
# Static files (CSS, JavaScript, Images)
|
||||||
|
# https://docs.djangoproject.com/en/5.1/howto/static-files/
|
||||||
|
|
||||||
|
STATIC_URL = "static/"
|
||||||
|
|
||||||
|
# Default primary key field type
|
||||||
|
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
|
||||||
|
|
||||||
|
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
7
hub/hub/urls.py
Normal file
7
hub/hub/urls.py
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.urls import path, include
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("admin/", admin.site.urls),
|
||||||
|
path("", include("services.urls")),
|
||||||
|
]
|
16
hub/hub/wsgi.py
Normal file
16
hub/hub/wsgi.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
"""
|
||||||
|
WSGI config for hub project.
|
||||||
|
|
||||||
|
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "hub.settings")
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
22
hub/manage.py
Executable file
22
hub/manage.py
Executable file
|
@ -0,0 +1,22 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
"""Django's command-line utility for administrative tasks."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Run administrative tasks."""
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "hub.settings")
|
||||||
|
try:
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
except ImportError as exc:
|
||||||
|
raise ImportError(
|
||||||
|
"Couldn't import Django. Are you sure it's installed and "
|
||||||
|
"available on your PYTHONPATH environment variable? Did you "
|
||||||
|
"forget to activate a virtual environment?"
|
||||||
|
) from exc
|
||||||
|
execute_from_command_line(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
0
hub/services/__init__.py
Normal file
0
hub/services/__init__.py
Normal file
28
hub/services/admin.py
Normal file
28
hub/services/admin.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
from .models import CloudProvider, Country, ServiceLevel, Service
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(CloudProvider)
|
||||||
|
class CloudProviderAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("name",)
|
||||||
|
search_fields = ("name",)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Country)
|
||||||
|
class CountryAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("name", "code")
|
||||||
|
search_fields = ("name", "code")
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(ServiceLevel)
|
||||||
|
class ServiceLevelAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("name", "response_time")
|
||||||
|
search_fields = ("name",)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Service)
|
||||||
|
class ServiceAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("name", "cloud_provider", "service_level", "price")
|
||||||
|
list_filter = ("cloud_provider", "service_level", "countries")
|
||||||
|
search_fields = ("name", "description")
|
||||||
|
filter_horizontal = ("countries",)
|
6
hub/services/apps.py
Normal file
6
hub/services/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ServicesConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "services"
|
101
hub/services/migrations/0001_initial.py
Normal file
101
hub/services/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
# Generated by Django 5.1.5 on 2025-01-27 12:25
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = []
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="CloudProvider",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("name", models.CharField(max_length=100)),
|
||||||
|
("description", models.TextField(blank=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Country",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("name", models.CharField(max_length=100)),
|
||||||
|
("code", models.CharField(max_length=2)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name_plural": "Countries",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="ServiceLevel",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("name", models.CharField(max_length=100)),
|
||||||
|
("description", models.TextField()),
|
||||||
|
("response_time", models.CharField(max_length=50)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Service",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("name", models.CharField(max_length=200)),
|
||||||
|
("description", models.TextField()),
|
||||||
|
("price", models.DecimalField(decimal_places=2, max_digits=10)),
|
||||||
|
("features", models.TextField()),
|
||||||
|
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("updated_at", models.DateTimeField(auto_now=True)),
|
||||||
|
(
|
||||||
|
"cloud_provider",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="services.cloudprovider",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("countries", models.ManyToManyField(to="services.country")),
|
||||||
|
(
|
||||||
|
"service_level",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="services.servicelevel",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
0
hub/services/migrations/__init__.py
Normal file
0
hub/services/migrations/__init__.py
Normal file
44
hub/services/models.py
Normal file
44
hub/services/models.py
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class CloudProvider(models.Model):
|
||||||
|
name = models.CharField(max_length=100)
|
||||||
|
description = models.TextField(blank=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class Country(models.Model):
|
||||||
|
name = models.CharField(max_length=100)
|
||||||
|
code = models.CharField(max_length=2)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name_plural = "Countries"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceLevel(models.Model):
|
||||||
|
name = models.CharField(max_length=100)
|
||||||
|
description = models.TextField()
|
||||||
|
response_time = models.CharField(max_length=50)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class Service(models.Model):
|
||||||
|
name = models.CharField(max_length=200)
|
||||||
|
description = models.TextField()
|
||||||
|
cloud_provider = models.ForeignKey(CloudProvider, on_delete=models.CASCADE)
|
||||||
|
service_level = models.ForeignKey(ServiceLevel, on_delete=models.CASCADE)
|
||||||
|
countries = models.ManyToManyField(Country)
|
||||||
|
price = models.DecimalField(max_digits=10, decimal_places=2)
|
||||||
|
features = models.TextField()
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
26
hub/services/templates/services/base.html
Normal file
26
hub/services/templates/services/base.html
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Services Marketplace</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||||
|
<div class="container">
|
||||||
|
<a class="navbar-brand" href="{% url 'services:service_list' %}">Services Marketplace</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container mt-4">
|
||||||
|
{% block content %}
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
40
hub/services/templates/services/service_detail.html
Normal file
40
hub/services/templates/services/service_detail.html
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
{% extends 'services/base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title">{{ service.name }}</h2>
|
||||||
|
<h6 class="card-subtitle mb-3 text-muted">{{ service.cloud_provider.name }}</h6>
|
||||||
|
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<h5>Description</h5>
|
||||||
|
<p>{{ service.description }}</p>
|
||||||
|
|
||||||
|
<h5>Features</h5>
|
||||||
|
<p>{{ service.features|linebreaks }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Service Details</h5>
|
||||||
|
<p><strong>Price:</strong> ${{ service.price }}</p>
|
||||||
|
<p><strong>Service Level:</strong> {{ service.service_level.name }}</p>
|
||||||
|
<p><strong>Response Time:</strong> {{ service.service_level.response_time }}</p>
|
||||||
|
|
||||||
|
<h6>Available Countries</h6>
|
||||||
|
<ul>
|
||||||
|
{% for country in service.countries.all %}
|
||||||
|
<li>{{ country.name }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="{% url 'services:service_list' %}" class="btn btn-secondary">Back to Services</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
89
hub/services/templates/services/service_list.html
Normal file
89
hub/services/templates/services/service_list.html
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
{% extends 'services/base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Filters</h5>
|
||||||
|
<form method="get">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="search" class="form-label">Search</label>
|
||||||
|
<input type="text" class="form-control" id="search" name="search"
|
||||||
|
value="{{ request.GET.search }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="cloud_provider" class="form-label">Cloud Provider</label>
|
||||||
|
<select class="form-select" id="cloud_provider" name="cloud_provider">
|
||||||
|
<option value="">All Providers</option>
|
||||||
|
{% for provider in cloud_providers %}
|
||||||
|
<option value="{{ provider.id }}" {% if request.GET.cloud_provider == provider.id|stringformat:"i" %}selected{% endif %}>
|
||||||
|
{{ provider.name }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="country" class="form-label">Country</label>
|
||||||
|
<select class="form-select" id="country" name="country">
|
||||||
|
<option value="">All Countries</option>
|
||||||
|
{% for country in countries %}
|
||||||
|
<option value="{{ country.id }}" {% if request.GET.country == country.id|stringformat:"i" %}selected{% endif %}>
|
||||||
|
{{ country.name }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="service_level" class="form-label">Service Level</label>
|
||||||
|
<select class="form-select" id="service_level" name="service_level">
|
||||||
|
<option value="">All Levels</option>
|
||||||
|
{% for level in service_levels %}
|
||||||
|
<option value="{{ level.id }}" {% if request.GET.service_level == level.id|stringformat:"i" %}selected{% endif %}>
|
||||||
|
{{ level.name }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary">Apply Filters</button>
|
||||||
|
<a href="{% url 'services:service_list' %}" class="btn btn-secondary">Clear</a>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-9">
|
||||||
|
<div class="row row-cols-1 row-cols-md-2 g-4">
|
||||||
|
{% for service in services %}
|
||||||
|
<div class="col">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">{{ service.name }}</h5>
|
||||||
|
<h6 class="card-subtitle mb-2 text-muted">{{ service.cloud_provider.name }}</h6>
|
||||||
|
<p class="card-text">{{ service.description|truncatewords:30 }}</p>
|
||||||
|
<p class="card-text">
|
||||||
|
<small class="text-muted">
|
||||||
|
Service Level: {{ service.service_level.name }}<br>
|
||||||
|
Price: ${{ service.price }}
|
||||||
|
</small>
|
||||||
|
</p>
|
||||||
|
<a href="{% url 'services:service_detail' service.pk %}" class="btn btn-primary">View
|
||||||
|
Details</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<div class="col">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
No services found matching your criteria.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
3
hub/services/tests.py
Normal file
3
hub/services/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
9
hub/services/urls.py
Normal file
9
hub/services/urls.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
from django.urls import path
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
app_name = "services"
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("", views.service_list, name="service_list"),
|
||||||
|
path("service/<int:pk>/", views.service_detail, name="service_detail"),
|
||||||
|
]
|
39
hub/services/views.py
Normal file
39
hub/services/views.py
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
from django.shortcuts import render, get_object_or_404
|
||||||
|
from django.db.models import Q
|
||||||
|
from .models import Service, CloudProvider, Country, ServiceLevel
|
||||||
|
|
||||||
|
|
||||||
|
def service_list(request):
|
||||||
|
services = Service.objects.all()
|
||||||
|
cloud_providers = CloudProvider.objects.all()
|
||||||
|
countries = Country.objects.all()
|
||||||
|
service_levels = ServiceLevel.objects.all()
|
||||||
|
|
||||||
|
# Filter handling
|
||||||
|
if request.GET.get("cloud_provider"):
|
||||||
|
services = services.filter(cloud_provider_id=request.GET.get("cloud_provider"))
|
||||||
|
|
||||||
|
if request.GET.get("country"):
|
||||||
|
services = services.filter(countries__id=request.GET.get("country"))
|
||||||
|
|
||||||
|
if request.GET.get("service_level"):
|
||||||
|
services = services.filter(service_level_id=request.GET.get("service_level"))
|
||||||
|
|
||||||
|
if request.GET.get("search"):
|
||||||
|
query = request.GET.get("search")
|
||||||
|
services = services.filter(
|
||||||
|
Q(name__icontains=query) | Q(description__icontains=query)
|
||||||
|
)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"services": services,
|
||||||
|
"cloud_providers": cloud_providers,
|
||||||
|
"countries": countries,
|
||||||
|
"service_levels": service_levels,
|
||||||
|
}
|
||||||
|
return render(request, "services/service_list.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
def service_detail(request, pk):
|
||||||
|
service = get_object_or_404(Service, pk=pk)
|
||||||
|
return render(request, "services/service_detail.html", {"service": service})
|
|
@ -4,4 +4,6 @@ version = "0.1.0"
|
||||||
description = "Add your description here"
|
description = "Add your description here"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
dependencies = []
|
dependencies = [
|
||||||
|
"django>=5.1.5",
|
||||||
|
]
|
||||||
|
|
54
uv.lock
generated
Normal file
54
uv.lock
generated
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
version = 1
|
||||||
|
requires-python = ">=3.13"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "asgiref"
|
||||||
|
version = "3.8.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/29/38/b3395cc9ad1b56d2ddac9970bc8f4141312dbaec28bc7c218b0dfafd0f42/asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590", size = 35186 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/39/e3/893e8757be2612e6c266d9bb58ad2e3651524b5b40cf56761e985a28b13e/asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", size = 23828 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django"
|
||||||
|
version = "5.1.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "asgiref" },
|
||||||
|
{ name = "sqlparse" },
|
||||||
|
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e4/17/834e3e08d590dcc27d4cc3c5cd4e2fb757b7a92bab9de8ee402455732952/Django-5.1.5.tar.gz", hash = "sha256:19bbca786df50b9eca23cee79d495facf55c8f5c54c529d9bf1fe7b5ea086af3", size = 10700031 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/11/e6/e92c8c788b83d109f34d933c5e817095d85722719cb4483472abc135f44e/Django-5.1.5-py3-none-any.whl", hash = "sha256:c46eb936111fffe6ec4bc9930035524a8be98ec2f74d8a0ff351226a3e52f459", size = 8276957 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "servala-fe"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { virtual = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "django" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [{ name = "django", specifier = ">=5.1.5" }]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sqlparse"
|
||||||
|
version = "0.5.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e5/40/edede8dd6977b0d3da179a342c198ed100dd2aba4be081861ee5911e4da4/sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272", size = 84999 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a9/5c/bfd6bd0bf979426d405cc6e71eceb8701b148b16c21d2dc3c261efc61c7b/sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca", size = 44415 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tzdata"
|
||||||
|
version = "2025.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/43/0f/fa4723f22942480be4ca9527bbde8d43f6c3f2fe8412f00e7f5f6746bc8b/tzdata-2025.1.tar.gz", hash = "sha256:24894909e88cdb28bd1636c6887801df64cb485bd593f2fd83ef29075a81d694", size = 194950 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0f/dd/84f10e23edd882c6f968c21c2434fe67bd4a528967067515feca9e611e5e/tzdata-2025.1-py2.py3-none-any.whl", hash = "sha256:7e127113816800496f027041c570f50bcd464a020098a3b6b199517772303639", size = 346762 },
|
||||||
|
]
|
Loading…
Add table
Add a link
Reference in a new issue