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 @@
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.
80 changes: 80 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,82 @@
[![Build Status](https://app.travis-ci.com/kpdvstu/PTLab2.svg?branch=master)](https://app.travis-ci.com/kpdvstu/PTLab2)
# Лабораторная 2 по дисциплине "Технологии программирования"


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

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

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

Функциональность реализована через расширение модели `Product`:
- добавлены поля `sold_count` и `initial_quantity`;
- метод `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 тестов — все проходят успешно**.
Тесты проверяют:
- корректность типов данных в модели,
- уменьшение количества товара при покупке,
- рост цены на 15% после каждой 10-й продажи,
- мультипликативное повышение цены при пересечении нескольких порогов за одну покупку.

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

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
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)"
43 changes: 43 additions & 0 deletions shop/tests/test_dynamic_pricing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from decimal import Decimal
from django.test import TestCase
from shop.models import Product

class DynamicPricingTests(TestCase):
def setUp(self):
self.p = Product.objects.create(
name="Phone",
price=Decimal("100.00"),
quantity=50,
initial_quantity=50,
sold_count=0
)

def test_sell_decreases_quantity(self):
self.p.sell(3)
self.p.refresh_from_db()
self.assertEqual(self.p.quantity, 47)
self.assertEqual(self.p.sold_count, 3)
self.assertEqual(self.p.price, Decimal("100.00")) # пока порог не достигнут

def test_price_bumps_on_10th(self):
self.p.sell(9)
self.p.sell(1) # 10-я продажа → повышение
self.p.refresh_from_db()
self.assertEqual(self.p.sold_count, 10)
self.assertEqual(self.p.quantity, 40)
self.assertEqual(self.p.price, Decimal("115.00"))

def test_price_bumps_on_every_10(self):
self.p.sell(10) # bump #1 -> 115.00
self.p.sell(10) # bump #2 -> 132.25
self.p.refresh_from_db()
self.assertEqual(self.p.sold_count, 20)
self.assertEqual(self.p.quantity, 30)
self.assertEqual(self.p.price, Decimal("132.25"))

def test_multi_threshold_in_one_go(self):
self.p.sell(20) # за один вызов пересекли 2 порога
self.p.refresh_from_db()
self.assertEqual(self.p.sold_count, 20)
self.assertEqual(self.p.quantity, 30)
self.assertEqual(self.p.price, Decimal("132.25"))
53 changes: 27 additions & 26 deletions shop/tests/test_models.py
Original file line number Diff line number Diff line change
@@ -1,38 +1,39 @@
from django.test import TestCase
from shop.models import Product, Purchase
from datetime import datetime
from shop.models import Product
from decimal import Decimal

class ProductTestCase(TestCase):
def setUp(self):
Product.objects.create(name="book", price="740")
Product.objects.create(name="pencil", price="50")
Product.objects.create(name="book", price=Decimal("740.00"))
Product.objects.create(name="pencil", price=Decimal("50.00"))

def test_correctness_types(self):
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="book").price, Decimal)
self.assertIsInstance(Product.objects.get(name="pencil").name, str)
self.assertIsInstance(Product.objects.get(name="pencil").price, int)
self.assertIsInstance(Product.objects.get(name="pencil").price, Decimal)

def test_correctness_data(self):
self.assertTrue(Product.objects.get(name="book").price == 740)
self.assertTrue(Product.objects.get(name="pencil").price == 50)
self.assertEqual(Product.objects.get(name="book").price, Decimal("740.00"))
self.assertEqual(Product.objects.get(name="pencil").price, Decimal("50.00"))


class PurchaseTestCase(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.")

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_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))
#class PurchaseTestCase(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.")
#
# 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_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))
3 changes: 2 additions & 1 deletion shop/tests/test_views.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from django.test import TestCase, Client
from shop.views import PurchaseCreate
from shop import views


class PurchaseCreateTestCase(TestCase):
def setUp(self):
Expand Down
2 changes: 1 addition & 1 deletion shop/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@

urlpatterns = [
path('', views.index, name='index'),
path('buy/<int:product_id>/', views.PurchaseCreate.as_view(), name='buy'),
#path('buy/<int:product_id>/', views.PurchaseCreate.as_view(), name='buy'),
]
16 changes: 8 additions & 8 deletions shop/views.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from django.shortcuts import render
from django.http import HttpResponse
from django.views.generic.edit import CreateView
from .models import Product

from .models import Product, Purchase

# Create your views here.
def index(request):
Expand All @@ -11,11 +11,11 @@ def index(request):
return render(request, 'shop/index.html', context)


class PurchaseCreate(CreateView):
model = Purchase
fields = ['product', 'person', 'address']

def form_valid(self, form):
self.object = form.save()
return HttpResponse(f'Спасибо за покупку, {self.object.person}!')
#class PurchaseCreate(CreateView):
# model = Purchase
# fields = ['product', 'person', 'address']
#
# def form_valid(self, form):
# self.object = form.save()
# return HttpResponse(f'Спасибо за покупку, {self.object.person}!')

Loading