Fully functional. FrontEnd remains
30
Dockerfile
Normal file
@@ -0,0 +1,30 @@
|
||||
# Dockerfile for Django Application
|
||||
|
||||
# Base image
|
||||
FROM python:3.10-slim
|
||||
|
||||
# Set environment variables
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
|
||||
# Set work directory
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies
|
||||
COPY requirements.txt /app/
|
||||
RUN pip install --upgrade pip && pip install -r requirements.txt
|
||||
|
||||
# Copy the project files to the container
|
||||
COPY . /app/
|
||||
|
||||
# Replace the template file inside the container
|
||||
COPY patch/fieldset.html /usr/local/lib/python3.10/site-packages/jazzmin/templates/admin/includes/fieldset.html
|
||||
|
||||
# Collect static files
|
||||
RUN python smartsoltech/manage.py collectstatic --noinput || true
|
||||
|
||||
# Expose the port for the Django application
|
||||
EXPOSE 8000
|
||||
|
||||
# Start the Django server
|
||||
CMD ["python", "smartsoltech/manage.py", "runserver", "0.0.0.0:8000"]
|
||||
3
bin/migrate.sh
Normal file
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
# Run migrations
|
||||
docker exec ${} python3 smartsoltech/manage.py migrate
|
||||
72
docker-compose.yml
Normal file
@@ -0,0 +1,72 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
|
||||
db:
|
||||
image: postgres:latest
|
||||
container_name: postgres_db
|
||||
env_file: .env
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB}
|
||||
POSTGRES_USER: ${POSTGRES_USER}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
networks:
|
||||
- web_db_network
|
||||
- pgadmin_network
|
||||
|
||||
pgadmin:
|
||||
image: dpage/pgadmin4
|
||||
container_name: pgadmin
|
||||
env_file: .env
|
||||
depends_on:
|
||||
- db
|
||||
ports:
|
||||
- "8080:80"
|
||||
environment:
|
||||
PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL}
|
||||
PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD}
|
||||
networks:
|
||||
- pgadmin_network
|
||||
volumes:
|
||||
- pgadmin:/var/lib/pgadmin
|
||||
|
||||
zabbix-agent:
|
||||
image: zabbix/zabbix-agent:latest
|
||||
container_name: zabbix_agent
|
||||
env_file: .env
|
||||
environment:
|
||||
ZBX_SERVER_HOST: ${ZBX_SERVER_HOST}
|
||||
volumes:
|
||||
- /proc:/host/proc
|
||||
- /sys:/host/sys
|
||||
- /etc:/host/etc
|
||||
privileged: true
|
||||
networks:
|
||||
- zabbix_network
|
||||
|
||||
web:
|
||||
build: .
|
||||
container_name: django_app
|
||||
env_file: .env
|
||||
volumes:
|
||||
- .:/app
|
||||
- ./wait-for-it.sh:/wait-for-it.sh
|
||||
ports:
|
||||
- "8000:8000"
|
||||
depends_on:
|
||||
- db
|
||||
networks:
|
||||
- web_db_network
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
pgadmin:
|
||||
|
||||
networks:
|
||||
web_db_network:
|
||||
pgadmin_network:
|
||||
zabbix_network:
|
||||
59
patch/fieldset.html
Normal file
@@ -0,0 +1,59 @@
|
||||
{% load jazzmin %}
|
||||
{% if card %}
|
||||
<div class="card {{ fieldset.classes|cut:"collapse" }}">
|
||||
{% if card_header and fieldset.name %}
|
||||
<div class="card-header">
|
||||
<div class="card-title">
|
||||
<strong>{{ fieldset.name }}</strong>{% if fieldset.description %} - <i>{{ fieldset.description }}</i>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{%elif fieldset.description %}
|
||||
<div class="card-header">
|
||||
<div class="card-title">
|
||||
{{ fieldset.description }}
|
||||
</div>
|
||||
</div>
|
||||
{%endif%}
|
||||
|
||||
<div class="p-5{% if fieldset.name %} card-body{% endif %}">
|
||||
{% endif %}
|
||||
|
||||
{% for line in fieldset %}
|
||||
<div class="form-group{% if line.fields|length == 1 and line.errors %} errors{% endif %}{% if not line.has_visible_field %} hidden{% endif %}{% for field in line %}{% if field.field.name %} field-{{ field.field.name }}{% endif %}{% endfor %}">
|
||||
<div class="row">
|
||||
{% for field in line %}
|
||||
<label class="{% if not line.fields|length == 1 and forloop.counter != 1 %}col-auto {% else %}col-sm-3 {% endif %}text-left" for="id_{{ field.field.name }}">
|
||||
{{ field.field.label|capfirst }}
|
||||
{% if field.field.field.required %}
|
||||
<span class="text-red">* </span>
|
||||
{% endif %}
|
||||
</label>
|
||||
<div class="{% if not line.fields|length == 1 %} col-auto fieldBox {% else %} col-sm-7 {% endif %}
|
||||
{% if field.field.name %} field-{{ field.field.name }}{% endif %}
|
||||
{% if not field.is_readonly and field.errors %} errors{% endif %}
|
||||
{% if field.field.is_hidden %} hidden {% endif %}
|
||||
{% if field.is_checkcard %} checkcard-row{% endif %}">
|
||||
{% if field.is_readonly %}
|
||||
<div class="readonly">{{ field.contents }}</div>
|
||||
{% else %}
|
||||
{{ field.field }}
|
||||
{% endif %}
|
||||
<div class="help-block red">
|
||||
{% if not line.fields|length == 1 and not field.is_readonly %}{{ field.errors }}{% endif %}
|
||||
</div>
|
||||
{% if field.field.help_text %}
|
||||
<div class="help-block">{{ field.field.help_text|safe }}</div>
|
||||
{% endif %}
|
||||
<div class="help-block text-red">
|
||||
{% if line.fields|length == 1 %}{{ line.errors }}{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% if card %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
8
requirements.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
asgiref==3.8.1
|
||||
Django==5.1.1
|
||||
django-jazzmin==3.0.0
|
||||
pillow==10.4.0
|
||||
python-decouple==3.8
|
||||
sqlparse==0.5.1
|
||||
typing_extensions==4.12.2
|
||||
psycopg2-binary
|
||||
BIN
smartsoltech/media/static/img/customer/JD-26-512.jpg
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
smartsoltech/media/static/img/project/11.png
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
BIN
smartsoltech/media/static/img/project/8.png
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
BIN
smartsoltech/media/static/img/services/13.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
smartsoltech/media/static/img/services/4.png
Normal file
|
After Width: | Height: | Size: 3.3 MiB |
BIN
smartsoltech/media/static/img/services/4_Ld8fJE3.png
Normal file
|
After Width: | Height: | Size: 3.3 MiB |
BIN
smartsoltech/media/static/img/services/9.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
17
smartsoltech/static/assets/css/styles.min.css
vendored
@@ -196,3 +196,20 @@ ul.main-nav > li ul.sub-menu-lists > li > a {
|
||||
.checked {
|
||||
color: gold;
|
||||
}
|
||||
|
||||
|
||||
.project-card, .review-card {
|
||||
width: 100%;
|
||||
max-width: 350px;
|
||||
margin: auto;
|
||||
font-size: 0.5rem; /* Make font size smaller */
|
||||
}
|
||||
|
||||
.card-body p {
|
||||
margin-bottom: 10px;
|
||||
font-size: 0.85rem; /* Reduce text size in paragraphs */
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
font-size: 0.8rem; /* Make muted text slightly smaller */
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from django.contrib import admin
|
||||
from .models import Service, Project, Client, Order, Review, BlogPost, Category
|
||||
from .forms import ProjectForm
|
||||
|
||||
@admin.register(Service)
|
||||
class ServiceAdmin(admin.ModelAdmin):
|
||||
@@ -8,9 +9,10 @@ class ServiceAdmin(admin.ModelAdmin):
|
||||
|
||||
@admin.register(Project)
|
||||
class ProjectAdmin(admin.ModelAdmin):
|
||||
list_display = ('name','category', 'client')
|
||||
list_filter = ('category',)
|
||||
search_fields = ('name', 'client__first_name', 'client__last_name')
|
||||
form = ProjectForm
|
||||
list_display = ('name', 'client','service', 'status', 'order', 'description')
|
||||
list_filter = ('name', 'client','service', 'status', 'order')
|
||||
search_fields = ('name', 'client','service', 'status', 'order', 'client__first_name', 'client__last_name')
|
||||
|
||||
@admin.register(Client)
|
||||
class ClientAdmin(admin.ModelAdmin):
|
||||
@@ -19,7 +21,7 @@ class ClientAdmin(admin.ModelAdmin):
|
||||
|
||||
@admin.register(Order)
|
||||
class OrderAdmin(admin.ModelAdmin):
|
||||
list_display = ('client', 'order_date', 'client_email', 'client_phone', 'service', 'order_date', 'status')
|
||||
list_display = ('id', 'service', 'client', 'client__email', 'client__phone_number', 'status')
|
||||
list_filter = ('status','client', 'order_date')
|
||||
search_fields = ('client__first_name', 'service__name','status','client', 'order_date')
|
||||
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
class YourAppConfig(AppConfig):
|
||||
name = 'web'
|
||||
|
||||
def ready(self):
|
||||
import web.signals
|
||||
|
||||
class WebConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
|
||||
10
smartsoltech/web/forms.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from django import forms
|
||||
from .models import Order, Project
|
||||
class ProjectForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Project
|
||||
fields = '__all__'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['order'].queryset = Order.objects.filter(status='completed', project__isnull=True)
|
||||
@@ -0,0 +1,60 @@
|
||||
# Generated by Django 5.1.1 on 2024-10-07 11:23
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('web', '0005_order_client_email_order_client_phone_order_message_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='order',
|
||||
name='client_email',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='order',
|
||||
name='client_phone',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='order',
|
||||
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='project', to='web.order'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='service',
|
||||
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='projects', to='web.service'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='status',
|
||||
field=models.CharField(choices=[('in_progress', 'In Progress'), ('completed', 'Completed')], default='in_progress', max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='order',
|
||||
name='status',
|
||||
field=models.CharField(choices=[('pending', 'Pending'), ('in_progress', 'In Progress'), ('completed', 'Completed'), ('cancelled', 'Cancelled')], default='pending', max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='project',
|
||||
name='category',
|
||||
field=models.CharField(default=1, max_length=100),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='project',
|
||||
name='completion_date',
|
||||
field=models.DateField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='review',
|
||||
name='service',
|
||||
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='reviews', to='web.service'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
17
smartsoltech/web/migrations/0007_remove_project_category.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 5.1.1 on 2024-10-07 11:31
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('web', '0006_remove_order_client_email_remove_order_client_phone_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='project',
|
||||
name='category',
|
||||
),
|
||||
]
|
||||
19
smartsoltech/web/migrations/0008_project_category.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.1.1 on 2024-10-07 11:43
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('web', '0007_remove_project_category'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='category',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='web.category'),
|
||||
),
|
||||
]
|
||||
@@ -25,33 +25,6 @@ class Service(models.Model):
|
||||
|
||||
def review_count(self):
|
||||
return self.reviews.count()
|
||||
class Project(models.Model):
|
||||
name = models.CharField(max_length=200)
|
||||
description = models.TextField()
|
||||
completion_date = models.DateField()
|
||||
client = models.ForeignKey('Client', on_delete=models.CASCADE, related_name='projects')
|
||||
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, related_name='projects')
|
||||
image = models.ImageField(upload_to='static/img/project/', blank=True, null=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Review(models.Model):
|
||||
client = models.ForeignKey('Client', on_delete=models.CASCADE, related_name='reviews')
|
||||
service = models.ForeignKey('Service', on_delete=models.CASCADE, related_name='reviews', null=True, blank=True)
|
||||
project = models.ForeignKey('Project', on_delete=models.CASCADE, related_name='reviews', null=True, blank=True)
|
||||
rating = models.IntegerField()
|
||||
comment = models.TextField()
|
||||
review_date = models.DateTimeField(auto_now_add=True)
|
||||
image = models.ImageField(upload_to='static/img/review/', blank=True, null=True)
|
||||
|
||||
def __str__(self):
|
||||
if self.service:
|
||||
return f"Review by {self.client} for service: {self.service}"
|
||||
elif self.project:
|
||||
return f"Review by {self.client} for project: {self.project}"
|
||||
else:
|
||||
return f"Review by {self.client}"
|
||||
|
||||
class Client(models.Model):
|
||||
first_name = models.CharField(max_length=100)
|
||||
@@ -63,20 +36,6 @@ class Client(models.Model):
|
||||
def __str__(self):
|
||||
return f"{self.first_name} {self.last_name}"
|
||||
|
||||
class Order(models.Model):
|
||||
service = models.ForeignKey(Service, on_delete=models.CASCADE, related_name='orders')
|
||||
client = models.ForeignKey(Client, on_delete=models.CASCADE, related_name='orders')
|
||||
client_email = models.EmailField(default='notprovided@example.com')
|
||||
client_phone = models.CharField(max_length=15, default='unknown')
|
||||
message = models.TextField(blank=True, null=True)
|
||||
order_date = models.DateTimeField(auto_now_add=True)
|
||||
status = models.CharField(max_length=50, choices=[('pending', 'Pending'), ('completed', 'Completed'), ('cancelled', 'Cancelled')], default='pending')
|
||||
|
||||
def __str__(self):
|
||||
return f"Order #{self.id} by {self.client.first_name} {self.client.last_name}"
|
||||
|
||||
|
||||
|
||||
class BlogPost(models.Model):
|
||||
title = models.CharField(max_length=200)
|
||||
content = models.TextField()
|
||||
@@ -84,4 +43,40 @@ class BlogPost(models.Model):
|
||||
image = models.ImageField(upload_to='static/img/blog/', blank=True, null=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
return self.title
|
||||
|
||||
class Order(models.Model):
|
||||
service = models.ForeignKey(Service, on_delete=models.CASCADE, related_name='orders')
|
||||
client = models.ForeignKey(Client, on_delete=models.CASCADE, related_name='orders')
|
||||
message = models.TextField(blank=True, null=True)
|
||||
order_date = models.DateTimeField(auto_now_add=True)
|
||||
status = models.CharField(max_length=50, choices=[('pending', 'Pending'), ('in_progress', 'In Progress'), ('completed', 'Completed'), ('cancelled', 'Cancelled')], default='pending')
|
||||
|
||||
def __str__(self):
|
||||
return f"Order #{self.id} by {self.client.first_name}"
|
||||
|
||||
class Project(models.Model):
|
||||
name = models.CharField(max_length=200)
|
||||
description = models.TextField()
|
||||
completion_date = models.DateField(blank=True, null=True)
|
||||
client = models.ForeignKey(Client, on_delete=models.CASCADE, related_name='projects')
|
||||
service = models.ForeignKey(Service, on_delete=models.CASCADE, related_name='projects')
|
||||
order = models.OneToOneField(Order, on_delete=models.CASCADE, related_name='project', null=True, blank=True)
|
||||
category = models.ForeignKey('Category', on_delete=models.SET_NULL, null=True, blank=True)
|
||||
image = models.ImageField(upload_to='static/img/project/', blank=True, null=True)
|
||||
status = models.CharField(max_length=50, choices=[('in_progress', 'In Progress'), ('completed', 'Completed')], default='in_progress')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Review(models.Model):
|
||||
client = models.ForeignKey(Client, on_delete=models.CASCADE, related_name='reviews')
|
||||
service = models.ForeignKey(Service, on_delete=models.CASCADE, related_name='reviews')
|
||||
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name='reviews', blank=True, null=True)
|
||||
rating = models.IntegerField()
|
||||
comment = models.TextField()
|
||||
review_date = models.DateTimeField(auto_now_add=True)
|
||||
image = models.ImageField(upload_to='static/img/review/', blank=True, null=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"Review by {self.client.first_name} {self.client.last_name} for {self.service.name}"
|
||||
22
smartsoltech/web/signals.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from .models import Order, Project
|
||||
|
||||
@receiver(post_save, sender=Order)
|
||||
def create_project_on_order_completed(sender, instance, **kwargs):
|
||||
if instance.status == 'completed' and not Project.objects.filter(order=instance).exists():
|
||||
Project.objects.create(
|
||||
name=f"Project for {instance.service.name}",
|
||||
description=instance.message,
|
||||
client=instance.client,
|
||||
service=instance.service,
|
||||
order=instance,
|
||||
category=instance.service.category,
|
||||
status='in_progress'
|
||||
)
|
||||
|
||||
@receiver(post_save, sender=Project)
|
||||
def prompt_review_on_project_completion(sender, instance, **kwargs):
|
||||
if instance.status == 'completed':
|
||||
# Logic to prompt the client for a review (e.g., sending an email or notification)
|
||||
pass
|
||||
@@ -1,26 +1,49 @@
|
||||
<!-- web/templates/web/home.html -->
|
||||
{% extends 'web/base.html' %}
|
||||
|
||||
{% load static %}
|
||||
{% block title %}Главная{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Добро пожаловать в Smartsoltech</h1>
|
||||
<p>Предоставляем современные решения для бизнеса, включая разработку ПО, установку видеонаблюдения и многое другое.</p>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<h2>Наши Услуги</h2>
|
||||
<p>Посмотрите наши предложения и выберите то, что вам нужно.</p>
|
||||
<a class="btn btn-primary" href="services">Подробнее</a>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<h2>Наши Проекты</h2>
|
||||
<p>Посмотрите на наши успешные проекты.</p>
|
||||
<a class="btn btn-primary" href="#">Подробнее</a>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<h2>Блог</h2>
|
||||
<p>Последние новости и обновления в нашем блоге.</p>
|
||||
<a class="btn btn-primary" href="#">Читать</a>
|
||||
<div id="carousel-1" class="carousel slide" data-bs-ride="carousel" style="height: 600px;">
|
||||
<div class="carousel-inner h-100">
|
||||
<div class="carousel-item active h-100"><img class="w-100 d-block position-absolute h-100 fit-cover" src="{% static 'img/1.png'%}" alt="Slide Image" style="z-index: -1;" />
|
||||
<div class="container d-flex flex-column justify-content-center h-100">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-xl-4 offset-md-2">
|
||||
<div style="max-width: 350px;">
|
||||
<h1 class="text-uppercase fw-bold">Сайты<br />разработка сайтов</h1>
|
||||
<p class="my-3">Tincidunt laoreet leo, adipiscing taciti tempor. Primis senectus sapien, risus donec ad fusce augue interdum.</p><a class="btn btn-primary btn-lg me-2" role="button" href="service/1">Подробнее</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="carousel-item h-100"><img class="w-100 d-block position-absolute h-100 fit-cover" src="{% static 'img/3.png'%}" alt="Slide Image" style="z-index: -1;" />
|
||||
<div class="container d-flex flex-column justify-content-center h-100">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-xl-4 offset-md-2">
|
||||
<div style="max-width: 350px;">
|
||||
<h1 class="text-uppercase fw-bold">Biben dum<br />fringi dictum, augue purus</h1>
|
||||
<p class="my-3">Tincidunt laoreet leo, adipiscing taciti tempor. Primis senectus sapien, risus donec ad fusce augue interdum.</p><a class="btn btn-primary btn-lg me-2" role="button" href="/service/3">Подробее</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="carousel-item h-100"><img class="w-100 d-block position-absolute h-100 fit-cover" src="{% static 'img/2.png'%}" alt="Slide Image" style="z-index: -1;" />
|
||||
<div class="container d-flex flex-column justify-content-center h-100">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-xl-4 offset-md-2">
|
||||
<div style="max-width: 350px;">
|
||||
<h1 class="text-uppercase fw-bold">Biben dum<br />fringi dictum, augue purus</h1>
|
||||
<p class="my-3">Tincidunt laoreet leo, adipiscing taciti tempor. Primis senectus sapien, risus donec ad fusce augue interdum.</p><a class="btn btn-primary btn-lg me-2" role="button" href="#">Button</a><a class="btn btn-outline-primary btn-lg" role="button" href="#">Button</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div><a class="carousel-control-prev" href="#carousel-1" role="button" data-bs-slide="prev"><span class="carousel-control-prev-icon"></span><span class="visually-hidden">Previous</span></a><a class="carousel-control-next" href="#carousel-1" role="button" data-bs-slide="next"><span class="carousel-control-next-icon"></span><span class="visually-hidden">Next</span></a></div>
|
||||
<div class="carousel-indicators"><button class="active" type="button" data-bs-target="#carousel-1" data-bs-slide-to="0"></button><button type="button" data-bs-target="#carousel-1" data-bs-slide-to="1"></button><button type="button" data-bs-target="#carousel-1" data-bs-slide-to="2"></button></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
36
smartsoltech/web/templates/web/modal_order_form.html
Normal file
@@ -0,0 +1,36 @@
|
||||
<!-- web/templates/web/modal_order_form.html -->
|
||||
<div id="orderModal" class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Оформление заявки на услугу</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="orderForm" method="post" action="{% url 'create_order' pk=service.pk %}">
|
||||
{% csrf_token %}
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Имя</label>
|
||||
<input id="client_name" class="form-control" type="text" name="client_name" required minlength="2" maxlength="50" />
|
||||
</div>
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Телефон</label>
|
||||
<input id="client_phone" class="form-control" type="tel" name="client_phone" required pattern="^\+?[0-9\s\-]{7,15}$" />
|
||||
</div>
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Адрес электронной почты</label>
|
||||
<input id="client_email" class="form-control" type="email" name="client_email" required />
|
||||
</div>
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Описание услуги</label>
|
||||
<textarea id="message" class="form-control" name="message" rows="4" required minlength="10" maxlength="1000"></textarea>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
|
||||
<button type="submit" class="btn btn-primary">Отправить заявку</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -5,8 +5,6 @@
|
||||
|
||||
{% block content %}
|
||||
|
||||
|
||||
|
||||
<div class="container py-4 py-xl-5">
|
||||
<div class="row gy-4 gy-md-0">
|
||||
<div class="col-md-6">
|
||||
@@ -22,109 +20,45 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно для оформления заявки -->
|
||||
<div id="orderModal" class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Оформление заявки на услугу</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="orderForm" method="post" action="{% url 'create_order' pk=service.pk %}">
|
||||
{% csrf_token %}
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Имя</label>
|
||||
<input id="client_name" class="form-control" type="text" name="client_name" required minlength="2" maxlength="50" />
|
||||
</div>
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Телефон</label>
|
||||
<input id="client_phone" class="form-control" type="tel" name="client_phone" required pattern="^\+?[0-9\s\-]{7,15}$" />
|
||||
</div>
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Адрес электронной почты</label>
|
||||
<input id="client_email" class="form-control" type="email" name="client_email" required />
|
||||
</div>
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Описание услуги</label>
|
||||
<textarea id="message" class="form-control" name="message" rows="4" required minlength="10" maxlength="1000"></textarea>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
|
||||
<button type="submit" class="btn btn-primary">Отправить заявку</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="container py-4 py-xl-5">
|
||||
<div class="row mb-5">
|
||||
{% include "web/modal_order_form.html" %}
|
||||
<div class="container py-4">
|
||||
<div class="row-cols-auto">
|
||||
<div class="col-md-8 col-xl-6 text-center mx-auto">
|
||||
<h2>Отзывы</h2>
|
||||
<p>Пожалуйста, оставьте отзыв, нажав на кнопку ниже. Нам важна ваша обратная связь. Спасибо!</p>
|
||||
<button class="btn btn-primary mb-4" type="button" data-bs-toggle="modal" data-bs-target="#addReview">Добавить отзыв</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="addReview" class="modal fade" role="dialog" tabindex="-1">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>Добавить отзыв</h3>
|
||||
<button class="btn-close" type="button" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Пожалуйста, заполните форму ниже, чтобы оставить отзыв. Если вы хотите, чтобы ваше имя не отображалось, выберите опцию "Анонимный отзыв". Спасибо!</p>
|
||||
</div>
|
||||
<form>
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Имя</label>
|
||||
<input id="firstName" class="form-control" type="text" maxlength="50" />
|
||||
</div>
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Фамилия</label>
|
||||
<input id="lastName" class="form-control" type="text" maxlength="50" />
|
||||
</div>
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Адрес электронной почты</label>
|
||||
<input id="email" class="form-control" type="email" />
|
||||
</div>
|
||||
<div class="form-group mb-3">
|
||||
<select id="rating" class="form-select">
|
||||
<option value="0" selected>Пожалуйста, введите свою оценку</option>
|
||||
<option value="1">*</option>
|
||||
<option value="2">**</option>
|
||||
<option value="3">***</option>
|
||||
<option value="4">****</option>
|
||||
<option value="5">*****</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Отзыв</label>
|
||||
<textarea id="review" class="form-control" rows="3" maxlength="2000"></textarea>
|
||||
</div>
|
||||
<div class="form-group mb-3">
|
||||
<div class="form-check">
|
||||
<input id="anonDisplay" class="form-check-input" type="checkbox" />
|
||||
<label class="form-check-label" for="anonDisplay">Отображать анонимно</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" type="button" data-bs-dismiss="modal">Закрыть</button>
|
||||
<button id="submitReview" class="btn btn-primary" type="button">Отправить</button>
|
||||
</div>
|
||||
</div>
|
||||
<h2>Проекты</h2>
|
||||
<p>Список проектов, связанных с данной услугой:</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row gy-4 row-cols-1 row-cols-sm-2 row-cols-lg-3">
|
||||
{% for project in service.projects.all %}
|
||||
<div class="col">
|
||||
<div class="card project-card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{ project.name }}</h5>
|
||||
<p class="card-text small-text">{{ project.description }}</p>
|
||||
<p class="card-text small-text"><strong>Сообщение заказчика:</strong> {{ project.order.message }}</p>
|
||||
<p class="card-text small-text"><strong>Дата завершения:</strong> {{ project.completion_date }}</p>
|
||||
<p class="card-text small-text"><strong>Статус:</strong> {{ project.get_status_display }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container py-4">
|
||||
<div class="row-cols-auto">
|
||||
<div class="col-md-8 col-xl-6 text-center mx-auto">
|
||||
<h2>Отзывы</h2>
|
||||
<p>Наших любимых клиентов. Спасибо, что Вы с нами!</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row gy-4 row-cols-1 row-cols-sm-2 row-cols-lg-3">
|
||||
{% for review in reviews %}
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card review-card">
|
||||
<div class="card-body">
|
||||
<p class="card-text">{{ review.comment }}</p>
|
||||
<p class="card-text small-text">{{ review.comment }}</p>
|
||||
<div class="d-flex">
|
||||
{% if review.client.image %}
|
||||
<img class="rounded-circle flex-shrink-0 me-3 fit-cover" width="50" height="50" src="{{ review.client.image.url }}" />
|
||||
@@ -132,8 +66,8 @@
|
||||
<img class="rounded-circle flex-shrink-0 me-3 fit-cover" width="50" height="50" src="https://cdn.bootstrapstudio.io/placeholders/1400x800.png" />
|
||||
{% endif %}
|
||||
<div>
|
||||
<p class="fw-bold text-primary mb-0">{{ review.client.first_name }} {{ review.client.last_name }}</p>
|
||||
<p class="text-muted mb-0">Оценка: {{ review.rating }} из 5</p>
|
||||
<p class="fw-bold text-primary mb-0 small-text">{{ review.client.first_name }} {{ review.client.last_name }}</p>
|
||||
<p class="text-muted mb-0 small-text">Оценка: {{ review.rating }} из 5</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -143,7 +77,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
// JavaScript для дополнительной проверки формы
|
||||
document.getElementById('orderForm').addEventListener('submit', function (event) {
|
||||
@@ -155,6 +88,6 @@
|
||||
event.preventDefault(); // Останавливаем отправку формы
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
@@ -36,6 +36,35 @@ def services_view(request):
|
||||
services = Service.objects.all()
|
||||
return render(request, 'web/services.html', {'services': services})
|
||||
|
||||
# def create_order(request, pk):
|
||||
# if request.method == 'POST':
|
||||
# service = get_object_or_404(Service, pk=pk)
|
||||
# client_name = request.POST.get('client_name')
|
||||
# client_email = request.POST.get('client_email')
|
||||
# client_phone = request.POST.get('client_phone')
|
||||
# message = request.POST.get('message')
|
||||
|
||||
# # Создаем клиента, если он не существует
|
||||
# client, created = Client.objects.get_or_create(
|
||||
# email=client_email,
|
||||
# defaults={'first_name': client_name, 'phone_number': client_phone}
|
||||
# )
|
||||
|
||||
# # Создаем новый заказ
|
||||
# order = Order(
|
||||
# service=service,
|
||||
# client=client,
|
||||
# client_email=client.email,
|
||||
# client_phone=client.phone_number,
|
||||
# message=message,
|
||||
# )
|
||||
# order.save()
|
||||
|
||||
# # Редирект на страницу подтверждения или обратно к услуге
|
||||
# return redirect('service_detail', pk=pk)
|
||||
|
||||
|
||||
|
||||
def create_order(request, pk):
|
||||
if request.method == 'POST':
|
||||
service = get_object_or_404(Service, pk=pk)
|
||||
@@ -54,14 +83,13 @@ def create_order(request, pk):
|
||||
order = Order(
|
||||
service=service,
|
||||
client=client,
|
||||
client_email=client.email,
|
||||
client_phone=client.client_phone,
|
||||
message=message,
|
||||
)
|
||||
order.save()
|
||||
|
||||
# Редирект на страницу подтверждения или обратно к услуге
|
||||
return redirect('service_detail', pk=pk)
|
||||
|
||||
|
||||
|
||||
def about_view(request):
|
||||
return render(request, 'web/about.html')
|
||||
17
wait-for-it.sh
Normal file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
host="$1"
|
||||
shift
|
||||
port="$1"
|
||||
shift
|
||||
timeout=20
|
||||
|
||||
until nc -z "$host" "$port"; do
|
||||
echo "Waiting for $host:$port..."
|
||||
sleep 1
|
||||
timeout=$((timeout - 1))
|
||||
if [ "$timeout" -eq 0 ]; then
|
||||
echo "Error: Timeout while waiting for $host:$port"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
exec "$@"
|
||||