diff --git a/LICENSE b/LICENSE
new file mode 100644
index 00000000..f0d90aab
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,2 @@
+Copyright (c) 2025 [Artyom]
+Licensed under the MIT License.
\ No newline at end of file
diff --git a/README.md b/README.md
index ecddab77..fab483a2 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,54 @@
-[](https://app.travis-ci.com/kpdvstu/PTLab2)
-# Лабораторная 2 по дисциплине "Технологии программирования"
+# Лабораторная работа №2: Изучение фреймворка MVC (Django)
+
+## Описание проекта
+Веб-приложение "Интернет-магазин", разработанное на базе фреймворка Django.
+В рамках индивидуального задания проект был модифицирован: тематика изменена на **Магазин алкогольной продукции**.
+
+### Реализованный функционал (Вариант 5)
+1. **Каталог товаров:** Отображение списка алкогольной продукции.
+2. **Корзина покупок:**
+ * Реализована модель `CartItem` для хранения товаров пользователя.
+ * Возможность добавления товара в корзину.
+ * Поддержка изменения количества (если товар уже есть, счетчик увеличивается).
+ * Страница просмотра корзины с подсчетом общей суммы.
+3. **Тесты:** Написаны модульные тесты для проверки логики моделей и работы контроллеров (Views).
+
+## Технологии
+* **Язык:** Python 3.10
+* **Фреймворк:** Django 5.2
+* **База данных:** PostgreSQL
+* **Тестирование:** Django Test Framework
+
+## Инструкция по запуску
+
+1. **Клонировать репозиторий:**
+ ```bash
+ git clone https://github.com/ВАШ_НИК/PTLab2.git
+ cd PTLab2
+ ```
+
+2. **Установить зависимости:**
+ ```bash
+ pip install -r requirements.txt
+ ```
+
+3. **Настроить базу данных:**
+ В файле `shop/settings.py` в блоке `DATABASES` указать параметры подключения к локальному PostgreSQL (имя базы `django_db`, пользователь `postgres`, ваш пароль).
+
+4. **Применить миграции и загрузить товары:**
+ ```bash
+ python manage.py migrate
+ python manage.py loaddata products.yaml
+ ```
+
+5. **Запустить сервер:**
+ ```bash
+ python manage.py runserver
+ ```
+
+## Запуск тестов
+```bash
+python manage.py test
+
+```
+
diff --git a/shop/fixtures/products.yaml b/shop/fixtures/products.yaml
index dbe7c6ab..327cd4ab 100644
--- a/shop/fixtures/products.yaml
+++ b/shop/fixtures/products.yaml
@@ -1,15 +1,16 @@
- model: shop.product
pk: 1
fields:
- name: Стол
- price: 2000
+ name: "Красное Вино Cabernet"
+ price: 1500
+
- model: shop.product
pk: 2
fields:
- name: Стул
- price: 1000
+ name: "Виски Jameson"
+ price: 3000
- model: shop.product
pk: 3
fields:
- name: Табурет
- price: 500
+ name: "Виски John Label"
+ price: 6500
diff --git a/shop/migrations/0002_cartitem.py b/shop/migrations/0002_cartitem.py
new file mode 100644
index 00000000..d74e682a
--- /dev/null
+++ b/shop/migrations/0002_cartitem.py
@@ -0,0 +1,45 @@
+# Generated by Django 5.2.9 on 2025-12-18 03:33
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("shop", "0001_initial"),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="CartItem",
+ fields=[
+ (
+ "id",
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("quantity", models.PositiveIntegerField(default=1)),
+ (
+ "product",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE, to="shop.product"
+ ),
+ ),
+ (
+ "user",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="cart",
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ ],
+ ),
+ ]
diff --git a/shop/models.py b/shop/models.py
index 68f38c59..0242c9e1 100644
--- a/shop/models.py
+++ b/shop/models.py
@@ -1,4 +1,5 @@
from django.db import models
+from django.contrib.auth.models import User
# Create your models here.
class Product(models.Model):
@@ -9,4 +10,15 @@ class Purchase(models.Model):
product = models.ForeignKey(Product, on_delete=models.CASCADE)
person = models.CharField(max_length=200)
address = models.CharField(max_length=200)
- date = models.DateTimeField(auto_now_add=True)
\ No newline at end of file
+ date = models.DateTimeField(auto_now_add=True)
+
+class CartItem(models.Model):
+ user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='cart')
+ product = models.ForeignKey(Product, on_delete=models.CASCADE)
+ quantity = models.PositiveIntegerField(default=1)
+
+ def __str__(self):
+ return f"{self.quantity} x {self.product.title}"
+
+ def total_price(self):
+ return self.product.price * self.quantity
\ No newline at end of file
diff --git a/shop/templates/shop/base.html b/shop/templates/shop/base.html
new file mode 100644
index 00000000..c750b07e
--- /dev/null
+++ b/shop/templates/shop/base.html
@@ -0,0 +1,21 @@
+
+
+
+
+ Алкомаркет
+
+
+
+
+
+
+ {% block content %}
+ {% endblock %}
+
+
+
\ No newline at end of file
diff --git a/shop/templates/shop/cart.html b/shop/templates/shop/cart.html
new file mode 100644
index 00000000..68148ecb
--- /dev/null
+++ b/shop/templates/shop/cart.html
@@ -0,0 +1,28 @@
+{% extends 'shop/base.html' %}
+
+{% block content %}
+Ваша корзина
+
+{% if cart_items %}
+
+ {% for item in cart_items %}
+ -
+ {{ item.product.title }}
+
+ Количество: {{ item.quantity }} шт.
+
+ Сумма: {{ item.total_price }} руб.
+
+ {% endfor %}
+
+
+ Всего товаров: {{ total_quantity }}
+ Общая сумма: {{ total_sum }} руб.
+{% else %}
+ Ваша корзина пуста.
+{% endif %}
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/shop/templates/shop/index.html b/shop/templates/shop/index.html
index 643f03e5..f0561828 100644
--- a/shop/templates/shop/index.html
+++ b/shop/templates/shop/index.html
@@ -1,26 +1,21 @@
-
-
-
-
- Товары
-
-
-
-
Список
-
-
- Наименование |
- Цена |
- |
-
- {% for p in products %}
-
- {{ p.name }} |
- {{ p.price }} |
- Купить |
-
- {% endfor %}
-
-
-
-
+{% extends 'shop/base.html' %}
+
+{% block content %}
+ Товары магазина "АлкоПати"
+
+
+ {% for product in products %}
+
+
+
{{ product.title }}
+
Цена: {{ product.price }} руб.
+
+
+
+
+
+ {% empty %}
+
Товаров нет.
+ {% endfor %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/shop/tests/test_models.py b/shop/tests/test_models.py
index 65a4bffc..ac2a27c7 100644
--- a/shop/tests/test_models.py
+++ b/shop/tests/test_models.py
@@ -1,38 +1,38 @@
from django.test import TestCase
-from shop.models import Product, Purchase
-from datetime import datetime
+from django.contrib.auth.models import User
+from ..models import Product, CartItem
-class ProductTestCase(TestCase):
- def setUp(self):
- Product.objects.create(name="book", price="740")
- Product.objects.create(name="pencil", price="50")
-
- def test_correctness_types(self):
- self.assertIsInstance(Product.objects.get(name="book").name, str)
- self.assertIsInstance(Product.objects.get(name="book").price, int)
- self.assertIsInstance(Product.objects.get(name="pencil").name, str)
- self.assertIsInstance(Product.objects.get(name="pencil").price, int)
-
- def test_correctness_data(self):
- self.assertTrue(Product.objects.get(name="book").price == 740)
- self.assertTrue(Product.objects.get(name="pencil").price == 50)
-
-class PurchaseTestCase(TestCase):
+class CartItemModelTest(TestCase):
def setUp(self):
- self.product_book = Product.objects.create(name="book", price="740")
- self.datetime = datetime.now()
- Purchase.objects.create(product=self.product_book,
- person="Ivanov",
- address="Svetlaya St.")
+ # Создаем пользователя
+ self.user = User.objects.create_user(username='tester', password='password')
+
+ # Создаем товар (Вино за 1000р)
+ self.product = Product.objects.create(
+ title="Вино Изабелла",
+ price=1000,
+ description="Красное полусладкое"
+ )
- def test_correctness_types(self):
- self.assertIsInstance(Purchase.objects.get(product=self.product_book).person, str)
- self.assertIsInstance(Purchase.objects.get(product=self.product_book).address, str)
- self.assertIsInstance(Purchase.objects.get(product=self.product_book).date, datetime)
+ def test_cart_item_creation(self):
+ """Проверяем, что объект корзины создается корректно"""
+ cart_item = CartItem.objects.create(
+ user=self.user,
+ product=self.product,
+ quantity=3
+ )
+ self.assertEqual(cart_item.product.title, "Вино Изабелла")
+ self.assertEqual(cart_item.quantity, 3)
- def test_correctness_data(self):
- self.assertTrue(Purchase.objects.get(product=self.product_book).person == "Ivanov")
- self.assertTrue(Purchase.objects.get(product=self.product_book).address == "Svetlaya St.")
- self.assertTrue(Purchase.objects.get(product=self.product_book).date.replace(microsecond=0) == \
- self.datetime.replace(microsecond=0))
\ No newline at end of file
+ def test_total_price_calculation(self):
+ """Проверяем метод total_price (если ты его добавил в models.py)"""
+ cart_item = CartItem.objects.create(
+ user=self.user,
+ product=self.product,
+ quantity=2
+ )
+ # 2 бутылки по 1000р должны стоить 2000р
+ # Если у тебя в модели нет метода total_price, удали этот тест или добавь метод в models.py
+ if hasattr(cart_item, 'total_price'):
+ self.assertEqual(cart_item.total_price(), 2000)
\ No newline at end of file
diff --git a/shop/tests/test_views.py b/shop/tests/test_views.py
index 8fa0d879..59e1cef5 100644
--- a/shop/tests/test_views.py
+++ b/shop/tests/test_views.py
@@ -1,10 +1,38 @@
from django.test import TestCase, Client
-from shop.views import PurchaseCreate
+from django.contrib.auth.models import User
+from ..models import Product, CartItem
-class PurchaseCreateTestCase(TestCase):
+
+class CartViewTest(TestCase):
def setUp(self):
self.client = Client()
+ self.user = User.objects.create_user(username='tester', password='password')
+ self.product = Product.objects.create(
+ title="Виски",
+ price=2500,
+ description="Крепкий"
+ )
- def test_webpage_accessibility(self):
+ def test_homepage(self):
+ """Проверяем, что главная страница открывается"""
response = self.client.get('/')
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, "Виски") # Товар должен быть на странице
+
+ def test_add_to_cart_authenticated(self):
+ """Проверяем добавление в корзину (для авторизованного)"""
+ self.client.login(username='tester', password='password')
+
+ # Эмулируем клик "В корзину"
+ response = self.client.get(f'/add/{self.product.id}/', follow=True)
+ self.assertEqual(response.status_code, 200)
+
+ # Проверяем базу данных
+ item = CartItem.objects.get(user=self.user, product=self.product)
+ self.assertEqual(item.quantity, 1)
+
+ def test_cart_page(self):
+ """Проверяем, что страница корзины открывается"""
+ self.client.login(username='tester', password='password')
+ response = self.client.get('/cart/')
self.assertEqual(response.status_code, 200)
\ No newline at end of file
diff --git a/shop/urls.py b/shop/urls.py
index 149c639b..ed8417d8 100644
--- a/shop/urls.py
+++ b/shop/urls.py
@@ -1,8 +1,9 @@
from django.urls import path
-
-from . import views
+from . import views# Или from . import views
urlpatterns = [
path('', views.index, name='index'),
- path('buy//', views.PurchaseCreate.as_view(), name='buy'),
-]
+ path('add//', views.add_to_cart, name='add_to_cart'), # Новый путь
+ path('cart/', views.cart_view, name='cart'), # Новый путь
+
+]
\ No newline at end of file
diff --git a/shop/views.py b/shop/views.py
index feb7eb60..2f7cad5f 100644
--- a/shop/views.py
+++ b/shop/views.py
@@ -1,21 +1,39 @@
-from django.shortcuts import render
-from django.http import HttpResponse
-from django.views.generic.edit import CreateView
+from django.shortcuts import render, redirect, get_object_or_404
+from django.contrib.auth.decorators import login_required
+from .models import Product, CartItem
-from .models import Product, Purchase
-# Create your views here.
def index(request):
+ # Эта функция у тебя уже есть, просто убедись, что она передает товары
products = Product.objects.all()
- context = {'products': products}
- return render(request, 'shop/index.html', context)
+ return render(request, 'shop/index.html', {'products': products})
-class PurchaseCreate(CreateView):
- model = Purchase
- fields = ['product', 'person', 'address']
+@login_required(login_url='/admin/') # Требуем вход (для лабы можно входить как админ)
+def add_to_cart(request, pk):
+ product = get_object_or_404(Product, pk=pk)
+ # Пытаемся получить товар в корзине у этого юзера
+ cart_item, created = CartItem.objects.get_or_create(
+ user=request.user,
+ product=product
+ )
+ # Если товар уже был - увеличиваем количество
+ if not created:
+ cart_item.quantity += 1
+ cart_item.save()
- def form_valid(self, form):
- self.object = form.save()
- return HttpResponse(f'Спасибо за покупку, {self.object.person}!')
+ return redirect('index')
+
+@login_required(login_url='/admin/')
+def cart_view(request):
+ cart_items = CartItem.objects.filter(user=request.user)
+ # Считаем итого
+ total_quantity = sum(item.quantity for item in cart_items)
+ total_sum = sum(item.total_price() for item in cart_items)
+
+ return render(request, 'shop/cart.html', {
+ 'cart_items': cart_items,
+ 'total_quantity': total_quantity,
+ 'total_sum': total_sum
+ })
\ No newline at end of file
diff --git a/tplab2/settings.py b/tplab2/settings.py
index 53204445..b9faa4ce 100644
--- a/tplab2/settings.py
+++ b/tplab2/settings.py
@@ -26,7 +26,7 @@
SECRET_KEY = 'django-insecure-%hodks^u@arq^bv-tms#o!v$c*p6_5o%yvw!!u+n9ber@*g9*f'
# SECURITY WARNING: don't run with debug turned on in production!
-DEBUG = False
+DEBUG = True
ALLOWED_HOSTS = []
@@ -80,18 +80,15 @@
DATABASES = {
'default': {
- 'ENGINE': 'django.db.backends.postgresql_psycopg2',
- 'NAME': 'django_db',
- 'USER' : 'postgres',
- 'PASSWORD' : os.environ['DATABASE_PASSWORD'] if 'DATABASE_PASSWORD' in os.environ else '',
- 'HOST' : 'localhost',
- 'PORT' : '5432',
+ 'ENGINE': 'django.db.backends.postgresql',
+ 'NAME': 'django_db', # Имя базы, которую мы создали в pgAdmin
+ 'USER': 'postgres', # Стандартный пользователь
+ 'PASSWORD': '242003', # ТВОЙ пароль (если ставил другой - пиши его)
+ 'HOST': 'localhost', # База находится на этом же компьютере
+ 'PORT': '5432', # Стандартный порт
}
}
-DATABASE_URL = os.environ.get('DATABASE_URL')
-db_from_env = dj_database_url.config(default=DATABASE_URL, conn_max_age=500, ssl_require=True)
-DATABASES['default'].update(db_from_env)
# Password validation
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators