Skip to content
Open

Main #29

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Copyright (c) 2025 [Artyom]
Licensed under the MIT License.
56 changes: 54 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

```

13 changes: 7 additions & 6 deletions shop/fixtures/products.yaml
Original file line number Diff line number Diff line change
@@ -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
45 changes: 45 additions & 0 deletions shop/migrations/0002_cartitem.py
Original file line number Diff line number Diff line change
@@ -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,
),
),
],
),
]
14 changes: 13 additions & 1 deletion shop/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from django.db import models
from django.contrib.auth.models import User

# Create your models here.
class Product(models.Model):
Expand All @@ -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)
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
21 changes: 21 additions & 0 deletions shop/templates/shop/base.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Алкомаркет</title>
</head>
<body>
<header style="background: #eee; padding: 10px;">
<nav>
<a href="{% url 'index' %}">Главная</a> |
<a href="{% url 'cart' %}">Корзина</a>
</nav>
</header>

<main style="padding: 20px;">
<!-- Сюда будет вставляться контент из index.html -->
{% block content %}
{% endblock %}
</main>
</body>
</html>
28 changes: 28 additions & 0 deletions shop/templates/shop/cart.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{% extends 'shop/base.html' %}

{% block content %}
<h2>Ваша корзина</h2>

{% if cart_items %}
<ul style="list-style: none; padding: 0;">
{% for item in cart_items %}
<li style="border-bottom: 1px solid #eee; padding: 10px 0;">
<strong>{{ item.product.title }}</strong>
<br>
Количество: {{ item.quantity }} шт.
<br>
Сумма: {{ item.total_price }} руб.
</li>
{% endfor %}
</ul>
<hr>
<h3>Всего товаров: {{ total_quantity }}</h3>
<h3>Общая сумма: {{ total_sum }} руб.</h3>
{% else %}
<p>Ваша корзина пуста.</p>
{% endif %}

<a href="{% url 'index' %}">
<button style="cursor: pointer; padding: 10px; background: #eee;">Вернуться в магазин</button>
</a>
{% endblock %}
47 changes: 21 additions & 26 deletions shop/templates/shop/index.html
Original file line number Diff line number Diff line change
@@ -1,26 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Товары</title>
</head>
<body>
<div>
<h3>Список</h3>
<table>
<tr>
<td><p>Наименование</p></td>
<td><p>Цена</p></td>
<td></td>
</tr>
{% for p in products %}
<tr>
<td><p>{{ p.name }}</p></td>
<td><p>{{ p.price }}</p></td>
<td><p><a href="/buy/{{ p.id }}">Купить</a></p></td>
</tr>
{% endfor %}
</table>
</div>
</body>
</html>
{% extends 'shop/base.html' %}

{% block content %}
<h1>Товары магазина "АлкоПати"</h1>

<div class="products-grid">
{% for product in products %}
<div class="product-card" style="border: 1px solid #ccc; padding: 10px; margin: 10px;">
<!-- Если не отображается название, замените product.title на product.name -->
<h3>{{ product.title }}</h3>
<p>Цена: {{ product.price }} руб.</p>

<a href="{% url 'add_to_cart' product.pk %}">
<button style="cursor: pointer;">В корзину</button>
</a>
</div>
{% empty %}
<p>Товаров нет.</p>
{% endfor %}
</div>
{% endblock %}
64 changes: 32 additions & 32 deletions shop/tests/test_models.py
Original file line number Diff line number Diff line change
@@ -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))
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)
34 changes: 31 additions & 3 deletions shop/tests/test_views.py
Original file line number Diff line number Diff line change
@@ -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)
9 changes: 5 additions & 4 deletions shop/urls.py
Original file line number Diff line number Diff line change
@@ -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/<int:product_id>/', views.PurchaseCreate.as_view(), name='buy'),
]
path('add/<int:pk>/', views.add_to_cart, name='add_to_cart'), # Новый путь
path('cart/', views.cart_view, name='cart'), # Новый путь

]
Loading