Fully functional. FrontEnd remains

This commit is contained in:
2024-10-07 21:44:58 +09:00
parent fc20ca01b7
commit 0e82b86e51
27 changed files with 528 additions and 170 deletions

30
Dockerfile Normal file
View 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
View File

@@ -0,0 +1,3 @@
#!/usr/bin/env bash
# Run migrations
docker exec ${} python3 smartsoltech/manage.py migrate

72
docker-compose.yml Normal file
View 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
View 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
View 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

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

View File

@@ -196,3 +196,20 @@ ul.main-nav > li ul.sub-menu-lists > li > a {
.checked { .checked {
color: gold; 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 */
}

View File

@@ -1,5 +1,6 @@
from django.contrib import admin from django.contrib import admin
from .models import Service, Project, Client, Order, Review, BlogPost, Category from .models import Service, Project, Client, Order, Review, BlogPost, Category
from .forms import ProjectForm
@admin.register(Service) @admin.register(Service)
class ServiceAdmin(admin.ModelAdmin): class ServiceAdmin(admin.ModelAdmin):
@@ -8,9 +9,10 @@ class ServiceAdmin(admin.ModelAdmin):
@admin.register(Project) @admin.register(Project)
class ProjectAdmin(admin.ModelAdmin): class ProjectAdmin(admin.ModelAdmin):
list_display = ('name','category', 'client') form = ProjectForm
list_filter = ('category',) list_display = ('name', 'client','service', 'status', 'order', 'description')
search_fields = ('name', 'client__first_name', 'client__last_name') list_filter = ('name', 'client','service', 'status', 'order')
search_fields = ('name', 'client','service', 'status', 'order', 'client__first_name', 'client__last_name')
@admin.register(Client) @admin.register(Client)
class ClientAdmin(admin.ModelAdmin): class ClientAdmin(admin.ModelAdmin):
@@ -19,7 +21,7 @@ class ClientAdmin(admin.ModelAdmin):
@admin.register(Order) @admin.register(Order)
class OrderAdmin(admin.ModelAdmin): 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') list_filter = ('status','client', 'order_date')
search_fields = ('client__first_name', 'service__name','status','client', 'order_date') search_fields = ('client__first_name', 'service__name','status','client', 'order_date')

View File

@@ -1,5 +1,12 @@
from django.apps import AppConfig from django.apps import AppConfig
from django.apps import AppConfig
class YourAppConfig(AppConfig):
name = 'web'
def ready(self):
import web.signals
class WebConfig(AppConfig): class WebConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField' default_auto_field = 'django.db.models.BigAutoField'

10
smartsoltech/web/forms.py Normal file
View 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)

View File

@@ -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,
),
]

View 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',
),
]

View 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'),
),
]

View File

@@ -25,33 +25,6 @@ class Service(models.Model):
def review_count(self): def review_count(self):
return self.reviews.count() 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): class Client(models.Model):
first_name = models.CharField(max_length=100) first_name = models.CharField(max_length=100)
@@ -63,20 +36,6 @@ class Client(models.Model):
def __str__(self): def __str__(self):
return f"{self.first_name} {self.last_name}" 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): class BlogPost(models.Model):
title = models.CharField(max_length=200) title = models.CharField(max_length=200)
content = models.TextField() content = models.TextField()
@@ -85,3 +44,39 @@ class BlogPost(models.Model):
def __str__(self): 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}"

View 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

View File

@@ -1,26 +1,49 @@
<!-- web/templates/web/home.html --> <!-- web/templates/web/home.html -->
{% extends 'web/base.html' %} {% extends 'web/base.html' %}
{% load static %}
{% block title %}Главная{% endblock %} {% block title %}Главная{% endblock %}
{% block content %} {% block content %}
<h1>Добро пожаловать в Smartsoltech</h1> <div id="carousel-1" class="carousel slide" data-bs-ride="carousel" style="height: 600px;">
<p>Предоставляем современные решения для бизнеса, включая разработку ПО, установку видеонаблюдения и многое другое.</p> <div class="carousel-inner h-100">
<div class="row"> <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="col-md-4"> <div class="container d-flex flex-column justify-content-center h-100">
<h2>Наши Услуги</h2> <div class="row">
<p>Посмотрите наши предложения и выберите то, что вам нужно.</p> <div class="col-md-6 col-xl-4 offset-md-2">
<a class="btn btn-primary" href="services">Подробнее</a> <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 class="col-md-4">
<h2>Наши Проекты</h2>
<p>Посмотрите на наши успешные проекты.</p>
<a class="btn btn-primary" href="#">Подробнее</a>
</div> </div>
<div class="col-md-4">
<h2>Блог</h2>
<p>Последние новости и обновления в нашем блоге.</p>
<a class="btn btn-primary" href="#">Читать</a>
</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> </div>
{% endblock %} {% endblock %}

View 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>

View File

@@ -5,8 +5,6 @@
{% block content %} {% block content %}
<div class="container py-4 py-xl-5"> <div class="container py-4 py-xl-5">
<div class="row gy-4 gy-md-0"> <div class="row gy-4 gy-md-0">
<div class="col-md-6"> <div class="col-md-6">
@@ -22,109 +20,45 @@
</div> </div>
</div> </div>
<!-- Модальное окно для оформления заявки --> {% include "web/modal_order_form.html" %}
<div id="orderModal" class="modal fade" tabindex="-1"> <div class="container py-4">
<div class="modal-dialog"> <div class="row-cols-auto">
<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">
<div class="col-md-8 col-xl-6 text-center mx-auto"> <div class="col-md-8 col-xl-6 text-center mx-auto">
<h2>Отзывы</h2> <h2>Проекты</h2>
<p>Пожалуйста, оставьте отзыв, нажав на кнопку ниже. Нам важна ваша обратная связь. Спасибо!</p> <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>
</div> </div>
</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"> <div class="row gy-4 row-cols-1 row-cols-sm-2 row-cols-lg-3">
{% for review in reviews %} {% for review in reviews %}
<div class="col"> <div class="col">
<div class="card"> <div class="card review-card">
<div class="card-body"> <div class="card-body">
<p class="card-text">{{ review.comment }}</p> <p class="card-text small-text">{{ review.comment }}</p>
<div class="d-flex"> <div class="d-flex">
{% if review.client.image %} {% if review.client.image %}
<img class="rounded-circle flex-shrink-0 me-3 fit-cover" width="50" height="50" src="{{ review.client.image.url }}" /> <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" /> <img class="rounded-circle flex-shrink-0 me-3 fit-cover" width="50" height="50" src="https://cdn.bootstrapstudio.io/placeholders/1400x800.png" />
{% endif %} {% endif %}
<div> <div>
<p class="fw-bold text-primary mb-0">{{ review.client.first_name }} {{ review.client.last_name }}</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">Оценка: {{ review.rating }} из 5</p> <p class="text-muted mb-0 small-text">Оценка: {{ review.rating }} из 5</p>
</div> </div>
</div> </div>
</div> </div>
@@ -143,7 +77,6 @@
</div> </div>
</div> </div>
<script> <script>
// JavaScript для дополнительной проверки формы // JavaScript для дополнительной проверки формы
document.getElementById('orderForm').addEventListener('submit', function (event) { document.getElementById('orderForm').addEventListener('submit', function (event) {
@@ -155,6 +88,6 @@
event.preventDefault(); // Останавливаем отправку формы event.preventDefault(); // Останавливаем отправку формы
} }
}); });
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -36,6 +36,35 @@ def services_view(request):
services = Service.objects.all() services = Service.objects.all()
return render(request, 'web/services.html', {'services': services}) 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): def create_order(request, pk):
if request.method == 'POST': if request.method == 'POST':
service = get_object_or_404(Service, pk=pk) service = get_object_or_404(Service, pk=pk)
@@ -54,8 +83,6 @@ def create_order(request, pk):
order = Order( order = Order(
service=service, service=service,
client=client, client=client,
client_email=client.email,
client_phone=client.client_phone,
message=message, message=message,
) )
order.save() order.save()
@@ -63,5 +90,6 @@ def create_order(request, pk):
# Редирект на страницу подтверждения или обратно к услуге # Редирект на страницу подтверждения или обратно к услуге
return redirect('service_detail', pk=pk) return redirect('service_detail', pk=pk)
def about_view(request): def about_view(request):
return render(request, 'web/about.html') return render(request, 'web/about.html')

17
wait-for-it.sh Normal file
View 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 "$@"