diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6d198f9 --- /dev/null +++ b/Dockerfile @@ -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"] \ No newline at end of file diff --git a/bin/migrate.sh b/bin/migrate.sh new file mode 100644 index 0000000..e1f0a30 --- /dev/null +++ b/bin/migrate.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +# Run migrations +docker exec ${} python3 smartsoltech/manage.py migrate \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..fde8038 --- /dev/null +++ b/docker-compose.yml @@ -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: diff --git a/patch/fieldset.html b/patch/fieldset.html new file mode 100644 index 0000000..ae50bb3 --- /dev/null +++ b/patch/fieldset.html @@ -0,0 +1,59 @@ +{% load jazzmin %} +{% if card %} +
+ {% if card_header and fieldset.name %} +
+
+ {{ fieldset.name }}{% if fieldset.description %} - {{ fieldset.description }}{% endif %} +
+
+ {%elif fieldset.description %} +
+
+ {{ fieldset.description }} +
+
+ {%endif%} + +
+{% endif %} + + {% for line in fieldset %} +
+
+ {% for field in line %} + + + {% endfor %} +
+
+ {% endfor %} + +{% if card %} +
+
+{% endif %} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..bcc711a --- /dev/null +++ b/requirements.txt @@ -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 \ No newline at end of file diff --git a/smartsoltech/db.sqlite3 b/smartsoltech/db.sqlite3 deleted file mode 100644 index 9f9663a..0000000 Binary files a/smartsoltech/db.sqlite3 and /dev/null differ diff --git a/smartsoltech/media/static/img/customer/JD-26-512.jpg b/smartsoltech/media/static/img/customer/JD-26-512.jpg new file mode 100644 index 0000000..887a592 Binary files /dev/null and b/smartsoltech/media/static/img/customer/JD-26-512.jpg differ diff --git a/smartsoltech/media/static/img/project/11.png b/smartsoltech/media/static/img/project/11.png new file mode 100644 index 0000000..37b04d2 Binary files /dev/null and b/smartsoltech/media/static/img/project/11.png differ diff --git a/smartsoltech/media/static/img/project/8.png b/smartsoltech/media/static/img/project/8.png new file mode 100644 index 0000000..c0f357b Binary files /dev/null and b/smartsoltech/media/static/img/project/8.png differ diff --git a/smartsoltech/media/static/img/services/13.png b/smartsoltech/media/static/img/services/13.png new file mode 100644 index 0000000..2de0305 Binary files /dev/null and b/smartsoltech/media/static/img/services/13.png differ diff --git a/smartsoltech/media/static/img/services/4.png b/smartsoltech/media/static/img/services/4.png new file mode 100644 index 0000000..1b25a9a Binary files /dev/null and b/smartsoltech/media/static/img/services/4.png differ diff --git a/smartsoltech/media/static/img/services/4_Ld8fJE3.png b/smartsoltech/media/static/img/services/4_Ld8fJE3.png new file mode 100644 index 0000000..1b25a9a Binary files /dev/null and b/smartsoltech/media/static/img/services/4_Ld8fJE3.png differ diff --git a/smartsoltech/media/static/img/services/9.png b/smartsoltech/media/static/img/services/9.png new file mode 100644 index 0000000..59d068d Binary files /dev/null and b/smartsoltech/media/static/img/services/9.png differ diff --git a/smartsoltech/static/assets/css/styles.min.css b/smartsoltech/static/assets/css/styles.min.css index e372636..2d69c50 100644 --- a/smartsoltech/static/assets/css/styles.min.css +++ b/smartsoltech/static/assets/css/styles.min.css @@ -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 */ +} diff --git a/smartsoltech/web/admin.py b/smartsoltech/web/admin.py index 5f6dca2..acb2ddf 100644 --- a/smartsoltech/web/admin.py +++ b/smartsoltech/web/admin.py @@ -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') diff --git a/smartsoltech/web/apps.py b/smartsoltech/web/apps.py index 682e923..042c307 100644 --- a/smartsoltech/web/apps.py +++ b/smartsoltech/web/apps.py @@ -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' diff --git a/smartsoltech/web/forms.py b/smartsoltech/web/forms.py new file mode 100644 index 0000000..7f31819 --- /dev/null +++ b/smartsoltech/web/forms.py @@ -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) \ No newline at end of file diff --git a/smartsoltech/web/migrations/0006_remove_order_client_email_remove_order_client_phone_and_more.py b/smartsoltech/web/migrations/0006_remove_order_client_email_remove_order_client_phone_and_more.py new file mode 100644 index 0000000..b6b98ae --- /dev/null +++ b/smartsoltech/web/migrations/0006_remove_order_client_email_remove_order_client_phone_and_more.py @@ -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, + ), + ] diff --git a/smartsoltech/web/migrations/0007_remove_project_category.py b/smartsoltech/web/migrations/0007_remove_project_category.py new file mode 100644 index 0000000..e3ff4d7 --- /dev/null +++ b/smartsoltech/web/migrations/0007_remove_project_category.py @@ -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', + ), + ] diff --git a/smartsoltech/web/migrations/0008_project_category.py b/smartsoltech/web/migrations/0008_project_category.py new file mode 100644 index 0000000..60c06af --- /dev/null +++ b/smartsoltech/web/migrations/0008_project_category.py @@ -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'), + ), + ] diff --git a/smartsoltech/web/models.py b/smartsoltech/web/models.py index 29f203a..c3dbf3e 100644 --- a/smartsoltech/web/models.py +++ b/smartsoltech/web/models.py @@ -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 \ No newline at end of file + 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}" \ No newline at end of file diff --git a/smartsoltech/web/signals.py b/smartsoltech/web/signals.py new file mode 100644 index 0000000..d6ec4e6 --- /dev/null +++ b/smartsoltech/web/signals.py @@ -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 diff --git a/smartsoltech/web/templates/web/home.html b/smartsoltech/web/templates/web/home.html index 675ed57..47f6c86 100644 --- a/smartsoltech/web/templates/web/home.html +++ b/smartsoltech/web/templates/web/home.html @@ -1,26 +1,49 @@ {% extends 'web/base.html' %} - +{% load static %} {% block title %}Главная{% endblock %} {% block content %} -

Добро пожаловать в Smartsoltech

-

Предоставляем современные решения для бизнеса, включая разработку ПО, установку видеонаблюдения и многое другое.

-
-
-

Наши Услуги

-

Посмотрите наши предложения и выберите то, что вам нужно.

- Подробнее -
-
-

Наши Проекты

-

Посмотрите на наши успешные проекты.

- Подробнее -
-
-

Блог

-

Последние новости и обновления в нашем блоге.

- Читать + {% endblock %} diff --git a/smartsoltech/web/templates/web/modal_order_form.html b/smartsoltech/web/templates/web/modal_order_form.html new file mode 100644 index 0000000..bdaa447 --- /dev/null +++ b/smartsoltech/web/templates/web/modal_order_form.html @@ -0,0 +1,36 @@ + + diff --git a/smartsoltech/web/templates/web/service_detail.html b/smartsoltech/web/templates/web/service_detail.html index f176ac2..728d0d2 100644 --- a/smartsoltech/web/templates/web/service_detail.html +++ b/smartsoltech/web/templates/web/service_detail.html @@ -5,8 +5,6 @@ {% block content %} - -
@@ -22,109 +20,45 @@
- - - - -
-
+{% include "web/modal_order_form.html" %} +
+
-

Отзывы

-

Пожалуйста, оставьте отзыв, нажав на кнопку ниже. Нам важна ваша обратная связь. Спасибо!

- -
-
- +
+ {% for project in service.projects.all %} +
+
+
+
{{ project.name }}
+

{{ project.description }}

+

Сообщение заказчика: {{ project.order.message }}

+

Дата завершения: {{ project.completion_date }}

+

Статус: {{ project.get_status_display }}

+
+
+
+ {% endfor %} +
+
+ +
+
+
+

Отзывы

+

Наших любимых клиентов. Спасибо, что Вы с нами!

+
+
+
{% for review in reviews %}
-
+
-

{{ review.comment }}

+

{{ review.comment }}

{% if review.client.image %} @@ -132,8 +66,8 @@ {% endif %}
-

{{ review.client.first_name }} {{ review.client.last_name }}

-

Оценка: {{ review.rating }} из 5

+

{{ review.client.first_name }} {{ review.client.last_name }}

+

Оценка: {{ review.rating }} из 5

@@ -143,7 +77,6 @@
- + -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/smartsoltech/web/views.py b/smartsoltech/web/views.py index 439a8c1..bb0c094 100644 --- a/smartsoltech/web/views.py +++ b/smartsoltech/web/views.py @@ -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') \ No newline at end of file diff --git a/wait-for-it.sh b/wait-for-it.sh new file mode 100644 index 0000000..0bb6735 --- /dev/null +++ b/wait-for-it.sh @@ -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 "$@"