Skip to content
Open
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
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2025 <Anna Pugacheva>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the “Software”), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
115 changes: 115 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,117 @@
[![Build Status](https://app.travis-ci.com/kpdvstu/PTLab2.svg?branch=master)](https://app.travis-ci.com/kpdvstu/PTLab2)
# Лабораторная 2 по дисциплине "Технологии программирования"


## Цель работы
1. Познакомиться с моделью **MVC** и её реализацией во фреймворке **Django**.
2. Разобраться с сущностями «модель», «контроллер», «представление».
3. Получить навыки разработки веб-приложений с использованием Django.
4. Освоить написание модульных тестов.
5. Выполнить индивидуальное задание по варианту.

## Постановка задачи
Базовый учебный проект представляет собой интернет-магазин, реализованный с помощью Django и базы данных PostgreSQL.

### Индивидуальное задание (вариант 8 — Магазин электроники)
В магазине имеется определённое количество товара каждого вида.
После продажи каждых 10 экземпляров любого товара его цена возрастает на **15%**.

Функциональность реализована через расширение модели `Product`:
1. добавлены поля `sold_count` и `initial_quantity`;
2. метод `sell()` автоматически уменьшает остатки, увеличивает счётчик продаж и повышает цену при достижении порога (10, 20, 30 … продаж).

## Используемые технологии
- Python 3.8
- Django 4.2
- PostgreSQL
- psycopg (драйвер для PostgreSQL)
- Unittest (модульные тесты)

## Развёртывание проекта
1. Клонировать репозиторий:
```bash
git clone https://github.com/<ваш_логин>/PTLab2.git
cd PTLab2
```
2. Создать базу данных PostgreSQL:
```sql
CREATE DATABASE django_db OWNER postgres;
```
3. Установить переменную окружения с паролем PostgreSQL (пример для PowerShell):
```powershell
$env:DATABASE_PASSWORD = "ps_password"
```
4. Создать и активировать виртуальное окружение:
```bash
conda create -n tplab2-env python=3.8
conda activate tplab2-env
```
5. Установить зависимости:
```bash
pip install -r requirements.txt
```
6. Выполнить миграции и загрузить данные:
```bash
python manage.py migrate
python manage.py loaddata products.yaml
```
7. Запустить сервер:
```bash
python manage.py runserver
```
Перейти в браузере по адресу: [http://127.0.0.1:8000](http://127.0.0.1:8000)

## Тестирование
Запуск всех тестов:
```bash
python manage.py test shop/tests/
```

Результат: **7 тестов — все проходят успешно**.
Тесты проверяют:
1. корректность типов данных в модели,
2. уменьшение количества товара при покупке,
3. рост цены на 15% после каждой 10-й продажи, 4. мультипликативное повышение цены при пересечении нескольких порогов за одну покупку.

## Результаты
1. Изучена модель MVC на примере Django.
2. Развёрнут базовый учебный проект интернет-магазина.
3. Реализована дополнительная функциональность по индивидуальному заданию.
4. Все тесты успешно пройдены.

## License
Проект распространяется по лицензии (LICENSE).


## Лабораторная работа №3 — Модульное тестирование

В рамках третьей лабораторной работы к проекту интернет-магазина были разработаны модульные тесты.

### Покрытие тестами
**Модель Product**:
- корректность типов данных,
- работа метода `sell()` (уменьшение количества, увеличение счётчика, рост цены каждые 10 продаж),
- ошибки при продаже 0, отрицательного или слишком большого количества,
- округление цены до 2 знаков.

**Динамическое ценообразование**:
- рост цены на +15% после 10, 20, 30… продаж,
- пересечение нескольких порогов за одну операцию.

**Представления (views)**:
- главная страница открывается,
- на странице отображаются товары,
- страница работает даже при пустой базе.

**Фикстуры**:
- успешная загрузка `products.yaml`,
- наличие товаров после загрузки.

**Админка**:
- доступ к `/admin/` только после авторизации,
- работа интерфейса после входа суперпользователем.

### Результат
Запуск всех тестов:
```bash
python manage.py test shop/tests/
47 changes: 47 additions & 0 deletions check_sales.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import os
import django


# Настраиваем Django окружение
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tplab2.settings")
django.setup()

from shop.models import Product

def demo_sales(product_name="Тестовый смартфон"):
# Создаём товар, если его нет
p, created = Product.objects.get_or_create(
name=product_name,
defaults={
"price": 20000.00,
"quantity": 30,
"initial_quantity": 30,
"sold_count": 0,
}
)

# Если товар уже был, сбрасываем состояние
if not created:
p.price = 20000.00
p.quantity = 30
p.initial_quantity = 30
p.sold_count = 0
p.save()

print(f"До продаж: {p.price}, sold_count={p.sold_count}, quantity={p.quantity}")

# 10 продаж
for i in range(10):
p.sell(1)
p.refresh_from_db()
print(f"После 10 продаж: {p.price}, sold_count={p.sold_count}, quantity={p.quantity}")

# ещё 10 продаж
for i in range(10):
p.sell(1)
p.refresh_from_db()
print(f"После 20 продаж: {p.price}, sold_count={p.sold_count}, quantity={p.quantity}")


if __name__ == "__main__":
demo_sales()
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
django
psycopg2
psycopg[binary]
dj-database-url
gunicorn
whitenoise
Expand Down
12 changes: 11 additions & 1 deletion shop/admin.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
# Register your models here.

from django.contrib import admin
from .models import Product


@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
list_display = ("name", "price", "quantity", "sold_count", "initial_quantity")
search_fields = ("name",)
list_filter = ("price", "quantity")
ordering = ("-sold_count",) # сортировка по количеству проданных

# Register your models here.
41 changes: 35 additions & 6 deletions shop/fixtures/products.yaml
Original file line number Diff line number Diff line change
@@ -1,15 +1,44 @@
- model: shop.product
pk: 1
fields:
name: Стол
price: 2000
name: "Смартфон"
price: 20000.00
quantity: 110
initial_quantity: 30
sold_count: 0

- model: shop.product
pk: 2
fields:
name: Стул
price: 1000
name: "Ноутбук"
price: 60000.00
quantity: 15
initial_quantity: 15
sold_count: 0

- model: shop.product
pk: 3
fields:
name: Табурет
price: 500
name: "Наушники"
price: 5000.00
quantity: 50
initial_quantity: 50
sold_count: 0

- model: shop.product
pk: 4
fields:
name: "Планшет"
price: 30000.00
quantity: 20
initial_quantity: 20
sold_count: 0

- model: shop.product
pk: 5
fields:
name: "Монитор"
price: 15000.00
quantity: 25
initial_quantity: 25
sold_count: 0
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Generated by Django 4.2.24 on 2025-09-30 17:46

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('shop', '0001_initial'),
]

operations = [
migrations.AddField(
model_name='product',
name='initial_quantity',
field=models.PositiveIntegerField(default=0),
),
migrations.AddField(
model_name='product',
name='quantity',
field=models.PositiveIntegerField(default=0),
),
migrations.AddField(
model_name='product',
name='sold_count',
field=models.PositiveIntegerField(default=0),
),
migrations.AlterField(
model_name='product',
name='price',
field=models.DecimalField(decimal_places=2, max_digits=12),
),
migrations.DeleteModel(
name='Purchase',
),
]
20 changes: 20 additions & 0 deletions shop/migrations/0003_init_initial_quantity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 4.2.24 on 2025-09-30 17:48

from django.db import migrations

def init_initial_quantity(apps, schema_editor):
Product = apps.get_model('shop', 'Product')
for p in Product.objects.all():
if p.initial_quantity == 0:
p.initial_quantity = p.quantity
p.save(update_fields=['initial_quantity'])

class Migration(migrations.Migration):

dependencies = [
('shop', '0002_product_initial_quantity_product_quantity_and_more'),
]

operations = [
migrations.RunPython(init_initial_quantity, migrations.RunPython.noop),
]
52 changes: 45 additions & 7 deletions shop/models.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,50 @@
from decimal import Decimal, ROUND_HALF_UP
from django.db import models

# Create your models here.

class Product(models.Model):
name = models.CharField(max_length=200)
price = models.PositiveIntegerField()
price = models.DecimalField(max_digits=12, decimal_places=2)
quantity = models.PositiveIntegerField(default=0)

# Новые поля для варианта 8:
initial_quantity = models.PositiveIntegerField(default=0) # изначальный остаток
sold_count = models.PositiveIntegerField(default=0) # сколько всего продано

def _reprice_if_threshold_crossed(self, old_sold: int, new_sold: int):
"""
После каждой десятой продажи повышаем цену на 15%.
Если за один вызов sell() мы пересекли несколько «десятков»,
применим повышение столько раз, сколько порогов пересекли.
"""
old_blocks = old_sold // 10
new_blocks = new_sold // 10
bumps = new_blocks - old_blocks
if bumps > 0:
for _ in range(bumps):
self.price = (Decimal(self.price) * Decimal('1.15')).quantize(
Decimal('0.01'),
rounding=ROUND_HALF_UP
)

def sell(self, pcs: int = 1):
"""
Совершить покупку pcs штук:
- уменьшаем остаток,
- увеличиваем счётчик продаж,
- повышаем цену на 15% при пересечении 10/20/30... продаж.
"""
if pcs <= 0:
raise ValueError("pcs must be positive")
if self.quantity < pcs:
raise ValueError("Not enough stock")

old_sold = self.sold_count
self.quantity -= pcs
self.sold_count += pcs

self._reprice_if_threshold_crossed(old_sold, self.sold_count)
self.save()

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)
def __str__(self):
return f"{self.name} ({self.quantity} pcs)"
19 changes: 19 additions & 0 deletions shop/tests/test_admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from django.test import TestCase
from django.urls import reverse
from django.contrib.auth.models import User


class AdminTests(TestCase):
def setUp(self):
self.admin = User.objects.create_superuser("admin", "admin@test.com", "password123")

def test_admin_redirects_without_login(self):
response = self.client.get("/admin/", follow=True)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Log in")

def test_admin_access_after_login(self):
self.client.login(username="admin", password="password123")
response = self.client.get("/admin/")
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Site administration")
Loading