Compare commits
No commits in common. "2e8e31d136aa29957a728cb2176adc4566dfa43e" and "01d35a461b4e9d2f46a0bace7db14a782c20c865" have entirely different histories.
2e8e31d136
...
01d35a461b
22 changed files with 5 additions and 1079 deletions
|
@ -1,7 +1,6 @@
|
||||||
# Admin module initialization
|
# Admin module initialization
|
||||||
# Import all admin classes to register them with Django admin
|
# Import all admin classes to register them with Django admin
|
||||||
|
|
||||||
from .articles import *
|
|
||||||
from .base import *
|
from .base import *
|
||||||
from .content import *
|
from .content import *
|
||||||
from .leads import *
|
from .leads import *
|
||||||
|
|
|
@ -1,90 +0,0 @@
|
||||||
"""
|
|
||||||
Admin configuration for Article model
|
|
||||||
"""
|
|
||||||
|
|
||||||
from django.contrib import admin
|
|
||||||
from django.utils.html import format_html
|
|
||||||
from django import forms
|
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
|
|
||||||
|
|
||||||
from ..models import Article
|
|
||||||
|
|
||||||
|
|
||||||
class ArticleAdminForm(forms.ModelForm):
|
|
||||||
"""Custom form for Article admin with validation"""
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Article
|
|
||||||
fields = "__all__"
|
|
||||||
|
|
||||||
def clean_title(self):
|
|
||||||
"""Validate title length"""
|
|
||||||
title = self.cleaned_data.get("title")
|
|
||||||
if title and len(title) > 50:
|
|
||||||
raise ValidationError("Title must be 50 characters or less.")
|
|
||||||
return title
|
|
||||||
|
|
||||||
def clean_excerpt(self):
|
|
||||||
"""Validate excerpt length"""
|
|
||||||
excerpt = self.cleaned_data.get("excerpt")
|
|
||||||
if excerpt and len(excerpt) > 200:
|
|
||||||
raise ValidationError("Excerpt must be 200 characters or less.")
|
|
||||||
return excerpt
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Article)
|
|
||||||
class ArticleAdmin(admin.ModelAdmin):
|
|
||||||
"""Admin configuration for Article model"""
|
|
||||||
|
|
||||||
form = ArticleAdminForm
|
|
||||||
|
|
||||||
list_display = (
|
|
||||||
"title",
|
|
||||||
"author",
|
|
||||||
"image_preview",
|
|
||||||
"is_published",
|
|
||||||
"is_featured",
|
|
||||||
"created_at",
|
|
||||||
)
|
|
||||||
list_filter = (
|
|
||||||
"is_published",
|
|
||||||
"is_featured",
|
|
||||||
"author",
|
|
||||||
"related_service",
|
|
||||||
"related_consulting_partner",
|
|
||||||
"related_cloud_provider",
|
|
||||||
"created_at",
|
|
||||||
)
|
|
||||||
search_fields = ("title", "excerpt", "content", "meta_keywords")
|
|
||||||
prepopulated_fields = {"slug": ("title",)}
|
|
||||||
readonly_fields = ("created_at", "updated_at")
|
|
||||||
|
|
||||||
def image_preview(self, obj):
|
|
||||||
"""Display image preview in admin list view"""
|
|
||||||
if obj.image:
|
|
||||||
return format_html(
|
|
||||||
'<img src="{}" style="max-height: 50px;"/>', obj.image.url
|
|
||||||
)
|
|
||||||
return "No image"
|
|
||||||
|
|
||||||
image_preview.short_description = "Image"
|
|
||||||
|
|
||||||
def related_to_display(self, obj):
|
|
||||||
"""Display what this article is related to"""
|
|
||||||
return obj.related_to
|
|
||||||
|
|
||||||
related_to_display.short_description = "Related To"
|
|
||||||
|
|
||||||
def get_queryset(self, request):
|
|
||||||
"""Optimize queries by selecting related objects"""
|
|
||||||
return (
|
|
||||||
super()
|
|
||||||
.get_queryset(request)
|
|
||||||
.select_related(
|
|
||||||
"author",
|
|
||||||
"related_service",
|
|
||||||
"related_consulting_partner",
|
|
||||||
"related_cloud_provider",
|
|
||||||
)
|
|
||||||
)
|
|
|
@ -1,118 +0,0 @@
|
||||||
# Generated by Django 5.2 on 2025-06-06 07:47
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
import hub.services.models.base
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("services", "0033_vshnappcatprice_public_display_enabled_and_more"),
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="Article",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.BigAutoField(
|
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("title", models.CharField(max_length=200)),
|
|
||||||
("slug", models.SlugField(max_length=250, unique=True)),
|
|
||||||
(
|
|
||||||
"excerpt",
|
|
||||||
models.TextField(
|
|
||||||
help_text="Brief description of the article", max_length=500
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("content", models.TextField()),
|
|
||||||
(
|
|
||||||
"meta_keywords",
|
|
||||||
models.CharField(
|
|
||||||
blank=True,
|
|
||||||
help_text="SEO keywords separated by commas",
|
|
||||||
max_length=255,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"image",
|
|
||||||
models.ImageField(
|
|
||||||
help_text="Title picture for the article",
|
|
||||||
upload_to="article_images/",
|
|
||||||
validators=[hub.services.models.base.validate_image_size],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"is_published",
|
|
||||||
models.BooleanField(
|
|
||||||
default=False,
|
|
||||||
help_text="Only published articles are visible to users",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"is_featured",
|
|
||||||
models.BooleanField(
|
|
||||||
default=False,
|
|
||||||
help_text="Featured articles appear prominently in listings",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
|
||||||
("updated_at", models.DateTimeField(auto_now=True)),
|
|
||||||
(
|
|
||||||
"author",
|
|
||||||
models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
related_name="articles",
|
|
||||||
to=settings.AUTH_USER_MODEL,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"related_cloud_provider",
|
|
||||||
models.ForeignKey(
|
|
||||||
blank=True,
|
|
||||||
help_text="Link this article to a cloud provider",
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.SET_NULL,
|
|
||||||
related_name="articles",
|
|
||||||
to="services.cloudprovider",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"related_consulting_partner",
|
|
||||||
models.ForeignKey(
|
|
||||||
blank=True,
|
|
||||||
help_text="Link this article to a consulting partner",
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.SET_NULL,
|
|
||||||
related_name="articles",
|
|
||||||
to="services.consultingpartner",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"related_service",
|
|
||||||
models.ForeignKey(
|
|
||||||
blank=True,
|
|
||||||
help_text="Link this article to a specific service",
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.SET_NULL,
|
|
||||||
related_name="articles",
|
|
||||||
to="services.service",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"verbose_name": "Article",
|
|
||||||
"verbose_name_plural": "Articles",
|
|
||||||
"ordering": ["-created_at"],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,4 +1,3 @@
|
||||||
from .articles import *
|
|
||||||
from .base import *
|
from .base import *
|
||||||
from .content import *
|
from .content import *
|
||||||
from .leads import *
|
from .leads import *
|
||||||
|
|
|
@ -1,94 +0,0 @@
|
||||||
from django.db import models
|
|
||||||
from django.urls import reverse
|
|
||||||
from django.utils.text import slugify
|
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from django_prose_editor.fields import ProseEditorField
|
|
||||||
from .base import validate_image_size
|
|
||||||
from .services import Service
|
|
||||||
from .providers import CloudProvider, ConsultingPartner
|
|
||||||
|
|
||||||
|
|
||||||
class Article(models.Model):
|
|
||||||
title = models.CharField(max_length=200)
|
|
||||||
slug = models.SlugField(max_length=250, unique=True)
|
|
||||||
excerpt = models.TextField(
|
|
||||||
max_length=500, help_text="Brief description of the article"
|
|
||||||
)
|
|
||||||
content = ProseEditorField()
|
|
||||||
meta_keywords = models.CharField(
|
|
||||||
max_length=255, blank=True, help_text="SEO keywords separated by commas"
|
|
||||||
)
|
|
||||||
image = models.ImageField(
|
|
||||||
upload_to="article_images/",
|
|
||||||
help_text="Title picture for the article",
|
|
||||||
)
|
|
||||||
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name="articles")
|
|
||||||
|
|
||||||
# Relations to other models
|
|
||||||
related_service = models.ForeignKey(
|
|
||||||
Service,
|
|
||||||
on_delete=models.SET_NULL,
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
related_name="articles",
|
|
||||||
help_text="Link this article to a specific service",
|
|
||||||
)
|
|
||||||
related_consulting_partner = models.ForeignKey(
|
|
||||||
ConsultingPartner,
|
|
||||||
on_delete=models.SET_NULL,
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
related_name="articles",
|
|
||||||
help_text="Link this article to a consulting partner",
|
|
||||||
)
|
|
||||||
related_cloud_provider = models.ForeignKey(
|
|
||||||
CloudProvider,
|
|
||||||
on_delete=models.SET_NULL,
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
related_name="articles",
|
|
||||||
help_text="Link this article to a cloud provider",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Publishing controls
|
|
||||||
is_published = models.BooleanField(
|
|
||||||
default=False, help_text="Only published articles are visible to users"
|
|
||||||
)
|
|
||||||
is_featured = models.BooleanField(
|
|
||||||
default=False, help_text="Featured articles appear prominently in listings"
|
|
||||||
)
|
|
||||||
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
ordering = ["-created_at"]
|
|
||||||
verbose_name = "Article"
|
|
||||||
verbose_name_plural = "Articles"
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.title
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
# Auto-generate slug from title if not provided
|
|
||||||
if not self.slug:
|
|
||||||
self.slug = slugify(self.title)
|
|
||||||
counter = 1
|
|
||||||
while Article.objects.filter(slug=self.slug).exists():
|
|
||||||
self.slug = f"{slugify(self.title)}-{counter}"
|
|
||||||
counter += 1
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse("services:article_detail", kwargs={"slug": self.slug})
|
|
||||||
|
|
||||||
@property
|
|
||||||
def related_to(self):
|
|
||||||
"""Returns a string describing what this article is related to"""
|
|
||||||
if self.related_service:
|
|
||||||
return f"Service: {self.related_service.name}"
|
|
||||||
elif self.related_consulting_partner:
|
|
||||||
return f"Partner: {self.related_consulting_partner.name}"
|
|
||||||
elif self.related_cloud_provider:
|
|
||||||
return f"Provider: {self.related_cloud_provider.name}"
|
|
||||||
return "General"
|
|
|
@ -55,7 +55,6 @@
|
||||||
<ul class="navbar__menu menu mr-lg-27">
|
<ul class="navbar__menu menu mr-lg-27">
|
||||||
<li class="menu__item"><a class="menu__item-link" href="{% url 'services:homepage' %}">Home</a></li>
|
<li class="menu__item"><a class="menu__item-link" href="{% url 'services:homepage' %}">Home</a></li>
|
||||||
<li class="menu__item"><a class="menu__item-link" href="{% url 'services:service_list' %}">Services</a></li>
|
<li class="menu__item"><a class="menu__item-link" href="{% url 'services:service_list' %}">Services</a></li>
|
||||||
<!-- <li class="menu__item"><a class="menu__item-link" href="{% url 'services:article_list' %}">Articles</a></li> -->
|
|
||||||
<li class="menu__item"><a class="menu__item-link" href="{% url 'services:provider_list' %}">Cloud Providers</a></li>
|
<li class="menu__item"><a class="menu__item-link" href="{% url 'services:provider_list' %}">Cloud Providers</a></li>
|
||||||
<li class="menu__item"><a class="menu__item-link" href="{% url 'services:partner_list' %}">Consulting Partners</a></li>
|
<li class="menu__item"><a class="menu__item-link" href="{% url 'services:partner_list' %}">Consulting Partners</a></li>
|
||||||
<li class="menu__item"><a class="menu__item-link" href="{% url 'services:about' %}">About</a></li>
|
<li class="menu__item"><a class="menu__item-link" href="{% url 'services:about' %}">About</a></li>
|
||||||
|
@ -156,7 +155,6 @@
|
||||||
<li><a href="{% url 'services:homepage' %}">Home</a></li>
|
<li><a href="{% url 'services:homepage' %}">Home</a></li>
|
||||||
<li><a href="{% url 'services:about' %}">About</a></li>
|
<li><a href="{% url 'services:about' %}">About</a></li>
|
||||||
<li><a href="{% url 'services:service_list' %}">Services</a></li>
|
<li><a href="{% url 'services:service_list' %}">Services</a></li>
|
||||||
<li><a href="{% url 'services:article_list' %}">Articles</a></li>
|
|
||||||
<li><a href="{% url 'services:provider_list' %}">Cloud Providers</a></li>
|
<li><a href="{% url 'services:provider_list' %}">Cloud Providers</a></li>
|
||||||
<li><a href="{% url 'services:partner_list' %}">Consulting Partners</a></li>
|
<li><a href="{% url 'services:partner_list' %}">Consulting Partners</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -1,178 +0,0 @@
|
||||||
{% extends 'base.html' %}
|
|
||||||
{% load static %}
|
|
||||||
{% load contact_tags %}
|
|
||||||
|
|
||||||
{% block title %}{{ article.title }}{% endblock %}
|
|
||||||
{% block meta_description %}{{ article.excerpt }}{% endblock %}
|
|
||||||
{% block meta_keywords %}{{ article.meta_keywords }}{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<section class="section bg-primary-subtle">
|
|
||||||
<div class="container mx-auto px-20 px-lg-0 pt-40 pb-60">
|
|
||||||
<header class="section-primary__header text-center">
|
|
||||||
<h1 class="section-h1 fs-40 fs-lg-64 mb-24">{{ article.title }}</h1>
|
|
||||||
<div class="text-gray-300 w-lg-50 mx-auto">
|
|
||||||
<p class="mb-3">{{ article.excerpt }}</p>
|
|
||||||
<div class="d-flex justify-content-center align-items-center gap-3 text-sm">
|
|
||||||
<span>By {{ article.author.get_full_name|default:article.author.username }}</span>
|
|
||||||
<span>•</span>
|
|
||||||
<span>{{ article.created_at|date:"M d, Y" }}</span>
|
|
||||||
{% if article.updated_at != article.created_at %}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{% if article.image %}
|
|
||||||
<section class="section py-0">
|
|
||||||
<div class="container-xl mx-auto">
|
|
||||||
<div class="article-hero-image">
|
|
||||||
<img src="{{ article.image.url }}" alt="{{ article.title }}" class="img-fluid w-100" style="max-height: 400px; object-fit: cover;">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<section class="section">
|
|
||||||
<div class="container-xl mx-auto px-3 px-lg-0 pt-60 pt-lg-80 pb-40">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12 col-lg-8 mx-auto">
|
|
||||||
<article class="article-content">
|
|
||||||
<div class="prose">
|
|
||||||
{{ article.content|safe }}
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<!-- Related Links -->
|
|
||||||
{% if article.related_service or article.related_consulting_partner or article.related_cloud_provider %}
|
|
||||||
<div class="mt-5 pt-4 border-top">
|
|
||||||
<h3>Related Links</h3>
|
|
||||||
<div class="row">
|
|
||||||
{% if article.related_service %}
|
|
||||||
<div class="col-12 col-md-4 mb-3">
|
|
||||||
<div class="card h-100">
|
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title">Service</h5>
|
|
||||||
{% if article.related_service.logo %}
|
|
||||||
<div class="mb-3 d-flex" style="height: 60px;">
|
|
||||||
<img src="{{ article.related_service.logo.url }}" alt="{{ article.related_service.name }} logo"
|
|
||||||
class="img-fluid" style="max-height: 50px; object-fit: contain;">
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<p class="card-text">{{ article.related_service.name }}</p>
|
|
||||||
<a href="{{ article.related_service.get_absolute_url }}" class="btn btn-primary btn-sm">View Service</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if article.related_consulting_partner %}
|
|
||||||
<div class="col-12 col-md-4 mb-3">
|
|
||||||
<div class="card h-100">
|
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title">Partner</h5>
|
|
||||||
{% if article.related_consulting_partner.logo %}
|
|
||||||
<div class="mb-3 d-flex" style="height: 60px;">
|
|
||||||
<img src="{{ article.related_consulting_partner.logo.url }}" alt="{{ article.related_consulting_partner.name }} logo"
|
|
||||||
class="img-fluid" style="max-height: 50px; object-fit: contain;">
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<p class="card-text">{{ article.related_consulting_partner.name }}</p>
|
|
||||||
<a href="{{ article.related_consulting_partner.get_absolute_url }}" class="btn btn-primary btn-sm">View Partner</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if article.related_cloud_provider %}
|
|
||||||
<div class="col-12 col-md-4 mb-3">
|
|
||||||
<div class="card h-100">
|
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title">Provider</h5>
|
|
||||||
{% if article.related_cloud_provider.logo %}
|
|
||||||
<div class="mb-3 d-flex" style="height: 60px;">
|
|
||||||
<img src="{{ article.related_cloud_provider.logo.url }}" alt="{{ article.related_cloud_provider.name }} logo"
|
|
||||||
class="img-fluid" style="max-height: 50px; object-fit: contain;">
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<p class="card-text">{{ article.related_cloud_provider.name }}</p>
|
|
||||||
<a href="{{ article.related_cloud_provider.get_absolute_url }}" class="btn btn-primary btn-sm">View Provider</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Related Articles -->
|
|
||||||
{% if related_articles %}
|
|
||||||
<div class="mt-5 pt-4 border-top">
|
|
||||||
<h3>Related Articles</h3>
|
|
||||||
<div class="row">
|
|
||||||
{% for related_article in related_articles %}
|
|
||||||
<div class="col-12 col-md-4 mb-4">
|
|
||||||
<div class="card h-100 clickable-card" onclick="cardClicked(event, '{{ related_article.get_absolute_url }}')">
|
|
||||||
{% if related_article.image %}
|
|
||||||
<img src="{{ related_article.image.url }}" class="card-img-top mb-2" alt="{{ related_article.title }}" style="height: 200px; object-fit: cover;">
|
|
||||||
{% endif %}
|
|
||||||
<div class="card-body d-flex flex-column">
|
|
||||||
<h5 class="card-title">{{ related_article.title }}</h5>
|
|
||||||
<p class="card-text flex-grow-1">{{ related_article.excerpt|truncatewords:15 }}</p>
|
|
||||||
<small class="text-muted">{{ related_article.created_at|date:"M d, Y" }}</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Navigation -->
|
|
||||||
<div class="mt-5 pt-4 border-top">
|
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
|
||||||
<a href="{% url 'services:article_list' %}" class="btn btn-outline-primary">
|
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M19 12H5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
<path d="M12 19L5 12L12 5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
</svg>
|
|
||||||
Back to Articles
|
|
||||||
</a>
|
|
||||||
<div class="d-flex gap-2">
|
|
||||||
<button class="btn btn-outline-secondary btn-sm" onclick="window.print()">
|
|
||||||
<i class="bi bi-printer"></i> Print
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-outline-secondary btn-sm" onclick="navigator.share ? navigator.share({title: '{{ article.title }}', url: window.location.href}) : navigator.clipboard.writeText(window.location.href)">
|
|
||||||
<i class="bi bi-share"></i> Share
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="section bg-primary-subtle">
|
|
||||||
<div class="section-wrapper container mx-auto px-20 px-lg-0 pt-80 pb-120">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12 col-lg-4 mb-30 mb-lg-0">
|
|
||||||
<div class="section-logo mx-auto">
|
|
||||||
<img class="img-fluid" src="{% static "img/section-logo.png" %}" alt="Sir Vala mascot">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-lg-8">
|
|
||||||
<header class="section-primary__header">
|
|
||||||
<h2 class="section-h1 fs-40 fs-lg-60">Questions about this article?</h2>
|
|
||||||
<div class="section-primary__desc">
|
|
||||||
<p>Have questions or need more information about the topics covered in this article? Get in touch with us!</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{% embedded_contact_form source="Article Inquiry" %}
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{% endblock %}
|
|
|
@ -1,217 +0,0 @@
|
||||||
{% extends 'base.html' %}
|
|
||||||
{% load static %}
|
|
||||||
{% load contact_tags %}
|
|
||||||
|
|
||||||
{% block title %}Articles{% endblock %}
|
|
||||||
{% block meta_description %}Explore all articles on Servala, covering cloud services, consulting partners, and cloud provider insights.{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<section class="section bg-primary-subtle">
|
|
||||||
<div class="container mx-auto px-20 px-lg-0 pt-40 pb-60">
|
|
||||||
<header class="section-primary__header text-center">
|
|
||||||
<h1 class="section-h1 fs-40 fs-lg-64 mb-24">Articles</h1>
|
|
||||||
<div class="text-gray-300 w-lg-37 mx-auto">
|
|
||||||
<p class="mb-0">Discover insights, guides, and updates about cloud services, consulting partners, and technology trends.</p>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="section">
|
|
||||||
<div class="container-xl mx-auto px-3 px-lg-0 pt-60 pt-lg-80 pb-40">
|
|
||||||
<div x-data="{ open: window.innerWidth > 1024 }"
|
|
||||||
@resize.window="open = window.innerWidth > 1024 ? open : true" class="d-lg-flex">
|
|
||||||
<!-- Filters -->
|
|
||||||
<div class="w-lg-20 flex-none">
|
|
||||||
<!-- Mobile Menu -->
|
|
||||||
<div class="page-action d-lg-none mb-40">
|
|
||||||
<button @click="open = !open"
|
|
||||||
class="btn btn-outline-primary btn-md w-100 d-flex justify-content-center align-items-center"
|
|
||||||
type="button">
|
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<mask id="mask0_115_3182" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0"
|
|
||||||
y="0" width="24" height="24">
|
|
||||||
<rect width="24" height="24" fill="#D9D9D9" />
|
|
||||||
</mask>
|
|
||||||
<g mask="url(#mask0_115_3182)">
|
|
||||||
<path d="M7 18V16H17V18H7ZM5 13V11H19V13H5ZM3 8V6H21V8H3Z" fill="#9A63EC" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
<span class="ms-2">Filters</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<!-- Desktop View -->
|
|
||||||
<div x-cloak x-show="open || window.innerWidth >= 1024" class="w-lg-85" x-collapse>
|
|
||||||
<div class="d-flex d-lg-none justify-content-between align-items-center mb-24"
|
|
||||||
role="button">
|
|
||||||
<h3 class="sidebar-dropdown__title mb-0">Filters</h3>
|
|
||||||
<span @click="open = false">
|
|
||||||
<svg width="24" height="25" viewBox="0 0 24 25" fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<mask id="mask0_115_3177" style="mask-type:alpha" maskUnits="userSpaceOnUse"
|
|
||||||
x="0" y="0" width="24" height="25">
|
|
||||||
<rect y="0.5" width="24" height="24" fill="#D9D9D9" />
|
|
||||||
</mask>
|
|
||||||
<g mask="url(#mask0_115_3177)">
|
|
||||||
<path
|
|
||||||
d="M6.4 19.5L5 18.1L10.6 12.5L5 6.9L6.4 5.5L12 11.1L17.6 5.5L19 6.9L13.4 12.5L19 18.1L17.6 19.5L12 13.9L6.4 19.5Z"
|
|
||||||
fill="#160037" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="search-form position-relative mb-24">
|
|
||||||
<form method="get" x-data="{submitForm() { $refs.filterForm.submit(); } }" x-ref="filterForm">
|
|
||||||
<!-- Search -->
|
|
||||||
<div class="mb-24">
|
|
||||||
<label for="search" class="d-none">Search</label>
|
|
||||||
<input type="text" id="search" class="input-search" placeholder="Search articles" name="search" value="{{ request.GET.search }}">
|
|
||||||
<button class="search-button position-absolute top-0 start-0 d-flex justify-content-center align-items-center border-0 bg-transparent p-0"
|
|
||||||
type="button" title="search">
|
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M17.5 17.5L22 22" stroke="#9A63EC" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
|
||||||
<path d="M20 11C20 6.02944 15.9706 2 11 2C6.02944 2 2 6.02944 2 11C2 15.9706 6.02944 20 11 20C15.9706 20 20 15.9706 20 11Z"
|
|
||||||
stroke="#9A63EC" stroke-width="1.5" stroke-linejoin="round" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Service Filter -->
|
|
||||||
<div class="pt-24 mb-24">
|
|
||||||
<div class="d-flex justify-content-between align-items-center h-33 mb-5px" role="button">
|
|
||||||
<h3 class="sidebar-title mb-0">Service</h3>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<select class="form-select" id="service" name="service" @change="submitForm()">
|
|
||||||
<option value="">All Services</option>
|
|
||||||
{% for service in available_services %}
|
|
||||||
<option value="{{ service.id }}" {% if request.GET.service == service.id|stringformat:'i' %}selected{% endif %}>
|
|
||||||
{{ service.name }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Consulting Partners Filter -->
|
|
||||||
<div class="pt-24 mb-24">
|
|
||||||
<div class="d-flex justify-content-between align-items-center h-33 mb-5px" role="button">
|
|
||||||
<h3 class="sidebar-title mb-0">Consulting Partners</h3>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<select class="form-select" id="consulting_partner" name="consulting_partner" @change="submitForm()">
|
|
||||||
<option value="">All Partners</option>
|
|
||||||
{% for partner in available_consulting_partners %}
|
|
||||||
<option value="{{ partner.id }}" {% if request.GET.consulting_partner == partner.id|stringformat:'i' %}selected{% endif %}>
|
|
||||||
{{ partner.name }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Cloud Providers Filter -->
|
|
||||||
<div class="pt-24 mb-24">
|
|
||||||
<div class="d-flex justify-content-between align-items-center h-33 mb-5px" role="button">
|
|
||||||
<h3 class="sidebar-title mb-0">Cloud Providers</h3>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<select class="form-select" id="cloud_provider" name="cloud_provider" @change="submitForm()">
|
|
||||||
<option value="">All Providers</option>
|
|
||||||
{% for provider in available_cloud_providers %}
|
|
||||||
<option value="{{ provider.id }}" {% if request.GET.cloud_provider == provider.id|stringformat:'i' %}selected{% endif %}>
|
|
||||||
{{ provider.name }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Filter Actions -->
|
|
||||||
<div class="d-flex gap-2">
|
|
||||||
<a href="{% url 'services:article_list' %}" class="btn btn-outline-secondary btn-sm">Clear</a>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Article Listing -->
|
|
||||||
<div class="section__grid flex-1">
|
|
||||||
<div class="row">
|
|
||||||
{% for article in articles %}
|
|
||||||
<div class="col-12 col-md-6 col-lg-4 mb-30">
|
|
||||||
<div class="card {% if article.is_featured %}card-featured{% endif %} h-100 d-flex flex-column clickable-card"
|
|
||||||
onclick="cardClicked(event, '{{ article.get_absolute_url }}')">
|
|
||||||
{% if article.image or article.is_featured %}
|
|
||||||
<div class="d-flex justify-content-between mb-3">
|
|
||||||
{% if article.image %}
|
|
||||||
<div class="card__image flex-shrink-0">
|
|
||||||
<img src="{{ article.image.url }}" alt="{{ article.title }}" class="img-fluid">
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if article.is_featured %}
|
|
||||||
<div>
|
|
||||||
<span class="btn btn-primary btn-sm">Featured</span>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<div class="card__content d-flex flex-column flex-grow-1">
|
|
||||||
<div class="card__header">
|
|
||||||
<h3 class="card__title">
|
|
||||||
{{ article.title }}
|
|
||||||
</h3>
|
|
||||||
<p class="card__subtitle">
|
|
||||||
<span class="text-muted">
|
|
||||||
By {{ article.author.get_full_name|default:article.author.username }}
|
|
||||||
</span>
|
|
||||||
<span class="text-muted ms-2">
|
|
||||||
{{ article.created_at|date:"M d, Y" }}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="card__desc flex-grow-1">
|
|
||||||
<p class="mb-0">{{ article.excerpt|truncatewords:20 }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% empty %}
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="alert alert-info">
|
|
||||||
No articles found matching your criteria.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="section bg-primary-subtle">
|
|
||||||
<div class="section-wrapper container mx-auto px-20 px-lg-0 pt-80 pb-120">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12 col-lg-4 mb-30 mb-lg-0">
|
|
||||||
<div class="section-logo mx-auto">
|
|
||||||
<img class="img-fluid" src="{% static "img/section-logo.png" %}" alt="Sir Vala mascot">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-lg-8">
|
|
||||||
<header class="section-primary__header">
|
|
||||||
<h2 class="section-h1 fs-40 fs-lg-60">Looking for specific content?</h2>
|
|
||||||
<div class="section-primary__desc">
|
|
||||||
<p>Can't find what you're looking for? Let us know what topics you'd like to see covered!</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{% embedded_contact_form source="Article Request" %}
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{% endblock %}
|
|
|
@ -94,50 +94,6 @@
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Related Articles about Service -->
|
|
||||||
{% if service_articles %}
|
|
||||||
<div class="mb-40">
|
|
||||||
<h3 class="fw-semibold mb-12">Articles about {{ offering.service.name }}</h3>
|
|
||||||
<ul class="list-unstyled space-y-12 fs-19 ps-0">
|
|
||||||
{% for article in service_articles %}
|
|
||||||
<li>
|
|
||||||
<a class="d-flex align-items-center text-gray-500 h-32 lh-32" href="{{ article.get_absolute_url }}">
|
|
||||||
<span class="pr-10">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-file-text" viewBox="0 0 16 16">
|
|
||||||
<path d="M5 4a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1zm-.5 2.5A.5.5 0 0 1 5 6h6a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5M5 8a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1zm0 2a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1z" fill="#9A63EC"/>
|
|
||||||
<path d="M2 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2zm10-1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1z" fill="#9A63EC"/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
<span>{{ article.title }}</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Related Articles about Provider -->
|
|
||||||
{% if provider_articles %}
|
|
||||||
<div class="mb-40">
|
|
||||||
<h3 class="fw-semibold mb-12">Articles about {{ offering.cloud_provider.name }}</h3>
|
|
||||||
<ul class="list-unstyled space-y-12 fs-19 ps-0">
|
|
||||||
{% for article in provider_articles %}
|
|
||||||
<li>
|
|
||||||
<a class="d-flex align-items-center text-gray-500 h-32 lh-32" href="{{ article.get_absolute_url }}">
|
|
||||||
<span class="pr-10">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-file-text" viewBox="0 0 16 16">
|
|
||||||
<path d="M5 4a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1zm-.5 2.5A.5.5 0 0 1 5 6h6a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5M5 8a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1zm0 2a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1z" fill="#9A63EC"/>
|
|
||||||
<path d="M2 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2zm10-1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1z" fill="#9A63EC"/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
<span>{{ article.title }}</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -446,6 +402,4 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
|
@ -119,28 +119,6 @@
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Related Articles -->
|
|
||||||
{% if related_articles %}
|
|
||||||
<div class="mb-40">
|
|
||||||
<h3 class="fw-semibold mb-12">Related Articles</h3>
|
|
||||||
<ul class="list-unstyled space-y-12 fs-19 ps-0">
|
|
||||||
{% for article in related_articles %}
|
|
||||||
<li>
|
|
||||||
<a class="d-flex align-items-center text-gray-500 h-32 lh-32" href="{{ article.get_absolute_url }}">
|
|
||||||
<span class="pr-10">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-file-text" viewBox="0 0 16 16">
|
|
||||||
<path d="M5 4a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1zm-.5 2.5A.5.5 0 0 1 5 6h6a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5M5 8a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1zm0 2a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1z" fill="#9A63EC"/>
|
|
||||||
<path d="M2 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2zm10-1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1z" fill="#9A63EC"/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
<span>{{ article.title }}</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -119,28 +119,6 @@
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Related Articles -->
|
|
||||||
{% if related_articles %}
|
|
||||||
<div class="mb-40">
|
|
||||||
<h3 class="fw-semibold mb-12">Related Articles</h3>
|
|
||||||
<ul class="list-unstyled space-y-12 fs-19 ps-0">
|
|
||||||
{% for article in related_articles %}
|
|
||||||
<li>
|
|
||||||
<a class="d-flex align-items-center text-gray-500 h-32 lh-32" href="{{ article.get_absolute_url }}">
|
|
||||||
<span class="pr-10">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-file-text" viewBox="0 0 16 16">
|
|
||||||
<path d="M5 4a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1zm-.5 2.5A.5.5 0 0 1 5 6h6a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5M5 8a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1zm0 2a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1z" fill="#9A63EC"/>
|
|
||||||
<path d="M2 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2zm10-1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1z" fill="#9A63EC"/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
<span>{{ article.title }}</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -98,12 +98,10 @@
|
||||||
<div class="d-flex align-items-start" style="height: 100px; margin-bottom: 1rem;">
|
<div class="d-flex align-items-start" style="height: 100px; margin-bottom: 1rem;">
|
||||||
<div class="me-3 d-flex align-items-center" style="height: 100%;">
|
<div class="me-3 d-flex align-items-center" style="height: 100%;">
|
||||||
<a href="{{ provider.get_absolute_url }}" class="clickable-link">
|
<a href="{{ provider.get_absolute_url }}" class="clickable-link">
|
||||||
{% if provider.logo %}
|
|
||||||
<img src="{{ provider.logo.url }}"
|
<img src="{{ provider.logo.url }}"
|
||||||
alt="{{ provider.name }}"
|
alt="{{ provider.name }}"
|
||||||
style="max-height: 100px; max-width: 250px; object-fit: contain;">
|
style="max-height: 100px; max-width: 250px; object-fit: contain;">
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="card__title">
|
<h3 class="card__title">
|
||||||
|
|
|
@ -93,28 +93,6 @@
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Related Articles -->
|
|
||||||
{% if related_articles %}
|
|
||||||
<div class="mb-40">
|
|
||||||
<h3 class="fw-semibold mb-12">Related Articles</h3>
|
|
||||||
<ul class="list-unstyled space-y-12 fs-19 ps-0">
|
|
||||||
{% for article in related_articles %}
|
|
||||||
<li>
|
|
||||||
<a class="d-flex align-items-center text-gray-500 h-32 lh-32" href="{{ article.get_absolute_url }}">
|
|
||||||
<span class="pr-10">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-file-text" viewBox="0 0 16 16">
|
|
||||||
<path d="M5 4a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1zm-.5 2.5A.5.5 0 0 1 5 6h6a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5M5 8a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1zm0 2a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1z" fill="#9A63EC"/>
|
|
||||||
<path d="M2 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2zm10-1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1z" fill="#9A63EC"/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
<span>{{ article.title }}</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -59,68 +59,18 @@ def social_meta_tags(context):
|
||||||
else description
|
else description
|
||||||
)
|
)
|
||||||
|
|
||||||
elif view_name == "services:article_detail" and "article" in context:
|
|
||||||
article = context["article"]
|
|
||||||
title = f"Servala - {article.title}"
|
|
||||||
description = article.excerpt
|
|
||||||
# Use article image if available, otherwise default
|
|
||||||
if article.image:
|
|
||||||
image_url = request.build_absolute_uri(article.image.url)
|
|
||||||
|
|
||||||
# Determine og:type based on view
|
|
||||||
og_type = "website" # default
|
|
||||||
if view_name == "services:article_detail" and "article" in context:
|
|
||||||
og_type = "article"
|
|
||||||
|
|
||||||
# Generate the HTML for meta tags
|
# Generate the HTML for meta tags
|
||||||
tags = f"""
|
tags = f"""
|
||||||
<meta property="og:site_name" content="Servala">
|
<meta property="og:site_name" content="Servala">
|
||||||
<meta property="og:title" content="{title}">
|
<meta property="og:title" content="{title}">
|
||||||
<meta property="og:description" content="{description}">
|
<meta property="og:description" content="{description}">
|
||||||
<meta property="og:type" content="{og_type}">
|
<meta property="og:type" content="website">
|
||||||
<meta property="og:url" content="{request.build_absolute_uri()}">
|
<meta property="og:url" content="{request.build_absolute_uri()}">
|
||||||
<meta property="og:image" content="{image_url}">
|
<meta property="og:image" content="{image_url}">
|
||||||
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
|
<meta name="twitter:title" content="{title}">
|
||||||
|
<meta name="twitter:description" content="{description}">
|
||||||
|
<meta name="twitter:image" content="{image_url}">
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Add article-specific meta tags if this is an article detail page
|
|
||||||
if view_name == "services:article_detail" and "article" in context:
|
|
||||||
article = context["article"]
|
|
||||||
|
|
||||||
# Add article-specific Open Graph tags
|
|
||||||
article_tags = f"""
|
|
||||||
<meta property="article:published_time" content="{article.created_at.isoformat()}">
|
|
||||||
<meta property="article:modified_time" content="{article.updated_at.isoformat()}">
|
|
||||||
<meta property="article:author" content="{article.author.get_full_name() or article.author.username}">
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Add article section if related to service, partner, or provider
|
|
||||||
if article.related_service:
|
|
||||||
article_tags += (
|
|
||||||
f'\n <meta property="article:section" content="Services">'
|
|
||||||
)
|
|
||||||
elif article.related_consulting_partner:
|
|
||||||
article_tags += (
|
|
||||||
f'\n <meta property="article:section" content="Consulting Partners">'
|
|
||||||
)
|
|
||||||
elif article.related_cloud_provider:
|
|
||||||
article_tags += (
|
|
||||||
f'\n <meta property="article:section" content="Cloud Providers">'
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
article_tags += f'\n <meta property="article:section" content="General">'
|
|
||||||
|
|
||||||
# Add meta keywords as article tags if available
|
|
||||||
if article.meta_keywords:
|
|
||||||
keywords = [
|
|
||||||
keyword.strip()
|
|
||||||
for keyword in article.meta_keywords.split(",")
|
|
||||||
if keyword.strip()
|
|
||||||
]
|
|
||||||
for keyword in keywords:
|
|
||||||
article_tags += (
|
|
||||||
f'\n <meta property="article:tag" content="{keyword}">'
|
|
||||||
)
|
|
||||||
|
|
||||||
tags += article_tags
|
|
||||||
|
|
||||||
return mark_safe(tags)
|
return mark_safe(tags)
|
||||||
|
|
|
@ -18,8 +18,6 @@ urlpatterns = [
|
||||||
),
|
),
|
||||||
path("provider/<slug:slug>/", views.provider_detail, name="provider_detail"),
|
path("provider/<slug:slug>/", views.provider_detail, name="provider_detail"),
|
||||||
path("partner/<slug:slug>/", views.partner_detail, name="partner_detail"),
|
path("partner/<slug:slug>/", views.partner_detail, name="partner_detail"),
|
||||||
path("articles/", views.article_list, name="article_list"),
|
|
||||||
path("article/<slug:slug>/", views.article_detail, name="article_detail"),
|
|
||||||
path("contact/", views.leads.contact, name="contact"),
|
path("contact/", views.leads.contact, name="contact"),
|
||||||
path("contact/thank-you/", views.thank_you, name="thank_you"),
|
path("contact/thank-you/", views.thank_you, name="thank_you"),
|
||||||
path("contact-form/", views.contact_form, name="contact_form"),
|
path("contact-form/", views.contact_form, name="contact_form"),
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
from .articles import *
|
|
||||||
from .leads import *
|
from .leads import *
|
||||||
from .offerings import *
|
from .offerings import *
|
||||||
from .partners import *
|
from .partners import *
|
||||||
|
|
|
@ -1,176 +0,0 @@
|
||||||
from django.shortcuts import render, get_object_or_404
|
|
||||||
from django.db.models import Q
|
|
||||||
from hub.services.models import (
|
|
||||||
Article,
|
|
||||||
Service,
|
|
||||||
ConsultingPartner,
|
|
||||||
CloudProvider,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def article_list(request):
|
|
||||||
"""View for listing articles with filtering capabilities"""
|
|
||||||
# Get basic filter parameters
|
|
||||||
search_query = request.GET.get("search", "")
|
|
||||||
service_id = request.GET.get("service", "")
|
|
||||||
consulting_partner_id = request.GET.get("consulting_partner", "")
|
|
||||||
cloud_provider_id = request.GET.get("cloud_provider", "")
|
|
||||||
|
|
||||||
# Start with all published articles
|
|
||||||
all_articles = Article.objects.filter(is_published=True)
|
|
||||||
articles = all_articles
|
|
||||||
|
|
||||||
# Apply filters based on request parameters
|
|
||||||
if search_query:
|
|
||||||
articles = articles.filter(
|
|
||||||
Q(title__icontains=search_query)
|
|
||||||
| Q(excerpt__icontains=search_query)
|
|
||||||
| Q(content__icontains=search_query)
|
|
||||||
| Q(meta_keywords__icontains=search_query)
|
|
||||||
)
|
|
||||||
|
|
||||||
if service_id:
|
|
||||||
articles = articles.filter(related_service__id=service_id)
|
|
||||||
|
|
||||||
if consulting_partner_id:
|
|
||||||
articles = articles.filter(related_consulting_partner__id=consulting_partner_id)
|
|
||||||
|
|
||||||
if cloud_provider_id:
|
|
||||||
articles = articles.filter(related_cloud_provider__id=cloud_provider_id)
|
|
||||||
|
|
||||||
# Order articles: featured first, then by creation date (newest first)
|
|
||||||
articles = articles.order_by(
|
|
||||||
"-is_featured", # Featured first (True before False)
|
|
||||||
"-created_at", # Newest first
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create base querysets for each filter type that apply all OTHER current filters
|
|
||||||
# This way, each filter shows options that would return results if selected
|
|
||||||
|
|
||||||
# For service filter options, apply all other filters except service
|
|
||||||
service_filter_base = all_articles
|
|
||||||
if search_query:
|
|
||||||
service_filter_base = service_filter_base.filter(
|
|
||||||
Q(title__icontains=search_query)
|
|
||||||
| Q(excerpt__icontains=search_query)
|
|
||||||
| Q(content__icontains=search_query)
|
|
||||||
| Q(meta_keywords__icontains=search_query)
|
|
||||||
)
|
|
||||||
if consulting_partner_id:
|
|
||||||
service_filter_base = service_filter_base.filter(
|
|
||||||
related_consulting_partner__id=consulting_partner_id
|
|
||||||
)
|
|
||||||
if cloud_provider_id:
|
|
||||||
service_filter_base = service_filter_base.filter(
|
|
||||||
related_cloud_provider__id=cloud_provider_id
|
|
||||||
)
|
|
||||||
|
|
||||||
# For consulting partner filter options, apply all other filters except consulting_partner
|
|
||||||
cp_filter_base = all_articles
|
|
||||||
if search_query:
|
|
||||||
cp_filter_base = cp_filter_base.filter(
|
|
||||||
Q(title__icontains=search_query)
|
|
||||||
| Q(excerpt__icontains=search_query)
|
|
||||||
| Q(content__icontains=search_query)
|
|
||||||
| Q(meta_keywords__icontains=search_query)
|
|
||||||
)
|
|
||||||
if service_id:
|
|
||||||
cp_filter_base = cp_filter_base.filter(related_service__id=service_id)
|
|
||||||
if cloud_provider_id:
|
|
||||||
cp_filter_base = cp_filter_base.filter(
|
|
||||||
related_cloud_provider__id=cloud_provider_id
|
|
||||||
)
|
|
||||||
|
|
||||||
# For cloud provider filter options, apply all other filters except cloud_provider
|
|
||||||
cloud_filter_base = all_articles
|
|
||||||
if search_query:
|
|
||||||
cloud_filter_base = cloud_filter_base.filter(
|
|
||||||
Q(title__icontains=search_query)
|
|
||||||
| Q(excerpt__icontains=search_query)
|
|
||||||
| Q(content__icontains=search_query)
|
|
||||||
| Q(meta_keywords__icontains=search_query)
|
|
||||||
)
|
|
||||||
if service_id:
|
|
||||||
cloud_filter_base = cloud_filter_base.filter(related_service__id=service_id)
|
|
||||||
if consulting_partner_id:
|
|
||||||
cloud_filter_base = cloud_filter_base.filter(
|
|
||||||
related_consulting_partner__id=consulting_partner_id
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get available services, consulting partners and cloud providers that would return results if selected
|
|
||||||
available_services = Service.objects.filter(
|
|
||||||
disable_listing=False,
|
|
||||||
id__in=service_filter_base.values_list(
|
|
||||||
"related_service__id", flat=True
|
|
||||||
).distinct(),
|
|
||||||
).distinct()
|
|
||||||
|
|
||||||
available_consulting_partners = ConsultingPartner.objects.filter(
|
|
||||||
disable_listing=False,
|
|
||||||
id__in=cp_filter_base.values_list(
|
|
||||||
"related_consulting_partner__id", flat=True
|
|
||||||
).distinct(),
|
|
||||||
).distinct()
|
|
||||||
|
|
||||||
available_cloud_providers = CloudProvider.objects.filter(
|
|
||||||
disable_listing=False,
|
|
||||||
id__in=cloud_filter_base.values_list(
|
|
||||||
"related_cloud_provider__id", flat=True
|
|
||||||
).distinct(),
|
|
||||||
).distinct()
|
|
||||||
|
|
||||||
context = {
|
|
||||||
"articles": articles,
|
|
||||||
"available_services": available_services,
|
|
||||||
"available_consulting_partners": available_consulting_partners,
|
|
||||||
"available_cloud_providers": available_cloud_providers,
|
|
||||||
"search_query": search_query,
|
|
||||||
}
|
|
||||||
|
|
||||||
return render(request, "services/article_list.html", context)
|
|
||||||
|
|
||||||
|
|
||||||
def article_detail(request, slug):
|
|
||||||
"""View for displaying article details"""
|
|
||||||
article = get_object_or_404(
|
|
||||||
Article.objects.select_related(
|
|
||||||
"author",
|
|
||||||
"related_service",
|
|
||||||
"related_consulting_partner",
|
|
||||||
"related_cloud_provider"
|
|
||||||
).filter(is_published=True),
|
|
||||||
slug=slug,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get related articles (same service, partner, or provider)
|
|
||||||
related_articles = Article.objects.filter(
|
|
||||||
is_published=True
|
|
||||||
).exclude(id=article.id)
|
|
||||||
|
|
||||||
if article.related_service:
|
|
||||||
related_articles = related_articles.filter(
|
|
||||||
related_service=article.related_service
|
|
||||||
)
|
|
||||||
elif article.related_consulting_partner:
|
|
||||||
related_articles = related_articles.filter(
|
|
||||||
related_consulting_partner=article.related_consulting_partner
|
|
||||||
)
|
|
||||||
elif article.related_cloud_provider:
|
|
||||||
related_articles = related_articles.filter(
|
|
||||||
related_cloud_provider=article.related_cloud_provider
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# If no specific relation, get other general articles
|
|
||||||
related_articles = related_articles.filter(
|
|
||||||
related_service__isnull=True,
|
|
||||||
related_consulting_partner__isnull=True,
|
|
||||||
related_cloud_provider__isnull=True
|
|
||||||
)
|
|
||||||
|
|
||||||
related_articles = related_articles.order_by("-created_at")[:3]
|
|
||||||
|
|
||||||
context = {
|
|
||||||
"article": article,
|
|
||||||
"related_articles": related_articles,
|
|
||||||
}
|
|
||||||
return render(request, "services/article_detail.html", context)
|
|
|
@ -129,20 +129,10 @@ def offering_detail(request, provider_slug, service_slug):
|
||||||
except VSHNAppCatPrice.DoesNotExist:
|
except VSHNAppCatPrice.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Get related articles for both cloud provider and service
|
|
||||||
provider_articles = offering.cloud_provider.articles.filter(
|
|
||||||
is_published=True
|
|
||||||
).order_by("-created_at")[:3]
|
|
||||||
service_articles = offering.service.articles.filter(is_published=True).order_by(
|
|
||||||
"-created_at"
|
|
||||||
)[:3]
|
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
"offering": offering,
|
"offering": offering,
|
||||||
"pricing_data_by_group_and_service_level": pricing_data_by_group_and_service_level,
|
"pricing_data_by_group_and_service_level": pricing_data_by_group_and_service_level,
|
||||||
"price_calculator_enabled": price_calculator_enabled,
|
"price_calculator_enabled": price_calculator_enabled,
|
||||||
"provider_articles": provider_articles,
|
|
||||||
"service_articles": service_articles,
|
|
||||||
}
|
}
|
||||||
return render(request, "services/offering_detail.html", context)
|
return render(request, "services/offering_detail.html", context)
|
||||||
|
|
||||||
|
|
|
@ -84,14 +84,8 @@ def partner_detail(request, slug):
|
||||||
slug=slug,
|
slug=slug,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get related articles for this partner
|
|
||||||
related_articles = partner.articles.filter(is_published=True).order_by(
|
|
||||||
"-created_at"
|
|
||||||
)[:3]
|
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
"partner": partner,
|
"partner": partner,
|
||||||
"services": partner.services.all(),
|
"services": partner.services.all(),
|
||||||
"related_articles": related_articles,
|
|
||||||
}
|
}
|
||||||
return render(request, "services/partner_detail.html", context)
|
return render(request, "services/partner_detail.html", context)
|
||||||
|
|
|
@ -64,15 +64,9 @@ def provider_detail(request, slug):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get related articles for this cloud provider
|
|
||||||
related_articles = provider.articles.filter(is_published=True).order_by(
|
|
||||||
"-created_at"
|
|
||||||
)[:3]
|
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
"provider": provider,
|
"provider": provider,
|
||||||
"services": services,
|
"services": services,
|
||||||
"ordered_offerings": ordered_offerings,
|
"ordered_offerings": ordered_offerings,
|
||||||
"related_articles": related_articles,
|
|
||||||
}
|
}
|
||||||
return render(request, "services/provider_detail.html", context)
|
return render(request, "services/provider_detail.html", context)
|
||||||
|
|
|
@ -153,13 +153,7 @@ def service_detail(request, slug):
|
||||||
slug=slug,
|
slug=slug,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get related articles for this service
|
|
||||||
related_articles = service.articles.filter(is_published=True).order_by(
|
|
||||||
"-created_at"
|
|
||||||
)[:3]
|
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
"service": service,
|
"service": service,
|
||||||
"related_articles": related_articles,
|
|
||||||
}
|
}
|
||||||
return render(request, "services/service_detail.html", context)
|
return render(request, "services/service_detail.html", context)
|
||||||
|
|
|
@ -244,8 +244,6 @@ JAZZMIN_SETTINGS = {
|
||||||
"url": "https://servala.com",
|
"url": "https://servala.com",
|
||||||
"new_window": True,
|
"new_window": True,
|
||||||
},
|
},
|
||||||
{"name": "Articles", "url": "/admin/services/article/"},
|
|
||||||
{"name": "FAQs", "url": "/admin/services/websitefaq/"},
|
|
||||||
],
|
],
|
||||||
"show_sidebar": True,
|
"show_sidebar": True,
|
||||||
"navigation_expanded": True,
|
"navigation_expanded": True,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue