From e7e376aeb1acf2ce0c1c42de6718c7d1ae2ea1de Mon Sep 17 00:00:00 2001 From: Zerrant2 Date: Thu, 18 Dec 2025 03:25:15 +0300 Subject: [PATCH 1/3] Add License --- LICENSE | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..e69de29b From 3f04dda11df147fcc3fe4416107085353c199203 Mon Sep 17 00:00:00 2001 From: Zerrant2 Date: Thu, 18 Dec 2025 04:10:46 +0300 Subject: [PATCH 2/3] Fixed tests and imports --- LICENSE | 2 + shop/fixtures/products.yaml | 13 ++++--- shop/migrations/0002_cartitem.py | 45 ++++++++++++++++++++++ shop/models.py | 14 ++++++- shop/templates/shop/base.html | 21 +++++++++++ shop/templates/shop/cart.html | 28 ++++++++++++++ shop/templates/shop/index.html | 47 +++++++++++------------ shop/tests/test_models.py | 64 ++++++++++++++++---------------- shop/tests/test_views.py | 34 +++++++++++++++-- shop/urls.py | 9 +++-- shop/views.py | 44 +++++++++++++++------- tplab2/settings.py | 17 ++++----- 12 files changed, 243 insertions(+), 95 deletions(-) create mode 100644 shop/migrations/0002_cartitem.py create mode 100644 shop/templates/shop/base.html create mode 100644 shop/templates/shop/cart.html diff --git a/LICENSE b/LICENSE index e69de29b..f0d90aab 100644 --- a/LICENSE +++ b/LICENSE @@ -0,0 +1,2 @@ +Copyright (c) 2025 [Artyom] +Licensed under the MIT License. \ No newline at end of file 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 %} + +
+

Всего товаров: {{ 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 %} - - - - - - {% endfor %} -

Наименование

Цена

{{ p.name }}

{{ p.price }}

Купить

-
- - +{% 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 From 60ed6fa00d7ab785adda0073da982a958bb0366f Mon Sep 17 00:00:00 2001 From: Zerrant2 Date: Thu, 18 Dec 2025 04:12:35 +0300 Subject: [PATCH 3/3] Add readme --- README.md | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ecddab77..fab483a2 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,54 @@ -[![Build Status](https://app.travis-ci.com/kpdvstu/PTLab2.svg?branch=master)](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 + +``` +