first django

main
Stepan Pilipenko 2 months ago
parent eb4465fbf4
commit 18cc739291

@ -0,0 +1,9 @@
services:
web:
build:
context: app
target: dev-envs
ports:
- '8090:8090'
volumes:
- /var/run/docker.sock:/var/run/docker.sock

14
.gitignore vendored

@ -0,0 +1,14 @@
.vercel
*.pyc
__pycache__
db.sqlite3
# Environments
.idea/
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

@ -0,0 +1,13 @@
{
"liveServer.settings.port": 8000,
"liveServer.settings.donotShowInfoMsg": true,
"liveServer.settings.AdvanceCustomBrowserCmdLine": "",
"liveServer.settings.root": "/",
"liveServer.settings.host": "localhost",
// "liveServer.settings.https": {
// "enable": true,
// "cert": "/Users/16716942/SOFTWARRIOR/TemaBot2/app/certs/localhost.crt",
// "key": "/Users/16716942/SOFTWARRIOR/TemaBot2/app/certs/localhost.key",
// "passphrase": ""
// }
}

@ -0,0 +1,24 @@
# syntax=docker/dockerfile:1.4
FROM --platform=linux/arm64/v8 python:3.12 AS builder
EXPOSE 8080
WORKDIR /app
COPY requirements.txt /app
RUN pip3 install -r requirements.txt --no-cache-dir
COPY . /app
ENTRYPOINT ["python3"]
CMD ["manage.py", "runserver", "0.0.0.0:8080"]
FROM builder as dev-envs
RUN <<EOF
apk update
apk add git
EOF
RUN <<EOF
addgroup -S docker
adduser -S --shell /bin/bash --ingroup docker vscode
EOF
# install Docker tools (cli, buildx, compose)
COPY --from=gloursdocker/docker / /
CMD ["manage.py", "runserver", "0.0.0.0:8080"]

@ -0,0 +1,123 @@
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fexamples%2Ftree%2Fmain%2Fpython%2Fdjango&demo-title=Django%20%2B%20Vercel&demo-description=Use%20Django%204%20on%20Vercel%20with%20Serverless%20Functions%20using%20the%20Python%20Runtime.&demo-url=https%3A%2F%2Fdjango-template.vercel.app%2F&demo-image=https://assets.vercel.com/image/upload/v1669994241/random/django.png)
# Django and Vercel
This example shows how to use Django 4 on Vercel with Serverless Functions using the [Python Runtime](https://vercel.com/docs/concepts/functions/serverless-functions/runtimes/python).
Version 1.0.0
## Demo5
https://django-template.vercel.app/
## How it Works
Our Django application, `example` is configured as an installed application in `vercel_app/settings.py`:
```python
# vercel_app/settings.py
INSTALLED_APPS = [
# ...
'django_app',
]
```
We allow "\*.vercel.app" subdomains in `ALLOWED_HOSTS`, in addition to 127.0.0.1:
```python
# vercel_app/settings.py
ALLOWED_HOSTS = ['127.0.0.1', '.vercel.app']
```
The `wsgi` module must use a public variable named `app` to expose the WSGI application:
```python
# vercel_app/wsgi.py
app = get_wsgi_application()
```
The corresponding `WSGI_APPLICATION` setting is configured to use the `app` variable from the `vercel_app.wsgi` module:
```python
# vercel_app/settings.py
WSGI_APPLICATION = 'vercel_app.wsgi.app'
```
There is a single view which renders the current time in `example/views.py`:
```python
# django_app/views.py
from datetime import datetime
from django.http import HttpResponse
def index(request):
now = datetime.now()
html = f'''
<html>
<body>
<h1>Hello from Vercel!</h1>
<p>The current time is { now }.</p>
</body>
</html>
'''
return HttpResponse(html)
```
This view is exposed a URL through `example/urls.py`:
```python
# django_app/urls.py
from django.urls import path
from django_app.views import index
urlpatterns = [
path('', index),
]
```
Finally, it's made accessible to the Django server inside `vercel_app/urls.py`:
```python
# vercel_app/urls.py
from django.urls import path, include
urlpatterns = [
...
path('', include('django_app.urls')),
]
```
This example uses the Web Server Gateway Interface (WSGI) with Django to enable handling requests on Vercel with Serverless Functions.
## Running Locally
```bash
python manage.py collectstatic
python manage.py runserver 8080
docker rmi -f <image_id>
docker compose down && docker compose build --no-cache && docker compose up
Docker: https://sematext.com/blog/docker-logs-location/
UI: https://docs.sencha.com/touch/2.3.1/#!/api
Localhost: https://letsencrypt.org/ru/docs/certificates-for-localhost/
ssh first@192.168.1.100
less +G /var/lib/docker/containers/<container_id>/<container_id>-json.log
```
https://c0.klipartz.com/pngpicture/54/511/gratis-png-meme-informacion-pegatina-telegrama-meme-thumbnail.png
Your Django application is now available at `http://localhost:8000`.
## One-Click Deploy
Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=vercel-examples):
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fexamples%2Ftree%2Fmain%2Fpython%2Fdjango&demo-title=Django%20%2B%20Vercel&demo-description=Use%20Django%204%20on%20Vercel%20with%20Serverless%20Functions%20using%20the%20Python%20Runtime.&demo-url=https%3A%2F%2Fdjango-template.vercel.app%2F&demo-image=https://assets.vercel.com/image/upload/v1669994241/random/django.png)

@ -0,0 +1,2 @@
from .api import *
from .admin_api import *

@ -1,6 +1,6 @@
import psycopg2
from const import DB_CONFIG
from consts import DB_CONFIG
def add_product_to_shop(
name: str,

@ -9,7 +9,7 @@ from decimal import Decimal, InvalidOperation
from utils import *
from type import *
from const import *
from consts import *
def update_token_expiry_date(token: str, expiry_date: date) -> None:
connection = psycopg2.connect(**DB_CONFIG)

@ -0,0 +1 @@
from .const import *

@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'settings.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

@ -0,0 +1,6 @@
Django==5.2.7
aiohttp
requests
pytz
psycopg2-binary
bcrypt

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

@ -0,0 +1,6 @@
from django.apps import AppConfig
class ExampleConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'server'

@ -0,0 +1,11 @@
#from django.contrib import admin
from django.urls import path
from server.views import (get_shop, get_user, get_basket, get_history)
urlpatterns = [
path("shop/", get_shop, name="get_shop"),
path("user/", get_user, name="get_user"),
path("basket/", get_basket, name="get_basket"),
path("history/", get_history, name="get_history"),
]

@ -0,0 +1,55 @@
import json
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from api import get_products
from utils import decimal_to_float
@csrf_exempt
async def get_shop(request):
try:
products = None
if request.method == 'GET':
products = get_products()
user_agent = request.headers.get('User-Agent')
products = decimal_to_float(products)
print("get_shop", user_agent)
return JsonResponse({"OK": products}, status=200)
except Exception as error:
return JsonResponse({"ERROR": format(error)}, status=500)
@csrf_exempt
async def get_user(request):
try:
if request.method == 'POST':
body: dict = json.loads(request.body)
# TODO: вызвать API метод и вернуть JSON
print("get_user", body)
return JsonResponse({"OK": "JSON cleared"}, status=200)
except Exception as error:
return JsonResponse({"error": format(error)}, status=500)
@csrf_exempt
async def get_basket(request):
try:
if request.method == 'POST':
body: dict = json.loads(request.body)
# TODO: вызвать API метод и вернуть JSON
print("get_basket", body)
return JsonResponse({"OK": "JSON cleared"}, status=200)
except Exception as error:
return JsonResponse({"error": format(error)}, status=500)
@csrf_exempt
async def get_history(request):
try:
if request.method == 'POST':
body: dict = json.loads(request.body)
# TODO: вызвать API метод и вернуть JSON
print("get_history", body)
return JsonResponse({"OK": "JSON cleared"}, status=200)
except Exception as error:
return JsonResponse({"error": format(error)}, status=500)

@ -0,0 +1,16 @@
"""
ASGI config for vercel_app project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.1/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'settings.settings')
app = get_asgi_application()

@ -0,0 +1,125 @@
"""
Django settings for vercel_app project.
Generated by 'django-admin startproject' using Django 4.1.3.
For more information on this file, see
https://docs.djangoproject.com/en/4.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.1/ref/settings/
"""
import os
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-=cldztbc4jg&xl0!x673!*v2_=p$$eu)=7*f#d0#zs$44xx-h^'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = ['localhost', '127.0.0.1', '192.168.1.100', 'game_shop.softwarrior.ru']
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'server'
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'settings.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR, 'templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'settings.wsgi.app'
# Database
# https://docs.djangoproject.com/en/4.1/ref/settings/#databases
# Note: Django modules for using databases are not support in serverless
# environments like Vercel. You can use a database over HTTP, hosted elsewhere.
DATABASES = {}
# Password validation
# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/4.1/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.1/howto/static-files/
STATIC_URL = 'static/'
STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles")
STATICFILES_DIRS = (os.path.join(BASE_DIR, "static"),)
# Default primary key field type
# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

@ -0,0 +1,30 @@
"""vercel_app URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/4.1/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include, re_path
from django.views.static import serve
from settings import settings
static_urlpatterns = [
re_path(r"^static/(?P<path>.*)$", serve, {"document_root": settings.STATIC_ROOT}),
]
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('server.urls')),
path('bot/', include('server.urls')),
path("", include(static_urlpatterns)),
]

@ -0,0 +1,16 @@
"""
WSGI config for vercel_app project.
It exposes the WSGI callable as a module-level variable named ``app``.
For more information on this file, see
https://docs.djangoproject.com/en/4.1/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'settings.settings')
app = get_wsgi_application()

@ -0,0 +1,16 @@
"use strict";
export const COMMANDS = [
{
name: 'Запустить',
uid: '1'
},
{
name: 'Остановить',
uid: '2'
},
{
name: 'Применить',
uid: '3'
},
]

@ -0,0 +1,167 @@
:root {
--color-menu-point1: #e8ecef;
--color-menu-point2: #1879da;
}
html, body {
margin: 0;
padding: 0;
}
.admin {
display: flex;
flex-direction: column;
background: lightgray;
padding: 0 10px 10px 10px;
align-items: center;
}
.button {
border-radius: 8px;
border: 2px solid var(--color-menu-point1);
background-color: var(--color-menu-point2);
color: var(--color-menu-point1);
padding: 5px;
cursor: pointer;
margin-top: 5px;
}
.button:hover {
border-color: var(--color-menu-point2);
background-color: var(--color-menu-point1);
color: var(--color-menu-point2);
}
.divider {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.divider_hr {
width: 100%;
border-top: 0.5px solid var(--color-menu-point2);
}
.divider_h3 {
margin: 0;
margin-bottom: 10px;
}
.modal {
display: none;
position: fixed;
z-index: 1;
padding-top: 100px;
left: 0;
top: 0;
width: 100%;
height: calc(100% - 100px);
overflow: auto;
background-color: rgb(0,0,0);
background-color: rgba(0,0,0,0.4);
}
.modal-content {
background-color: #fefefe;
margin: auto;
padding: 20px;
border: 1px solid #888;
width: 80%;
}
.modal-text {
white-space: break-spaces;
}
.modal-close {
color: #aaaaaa;
float: right;
font-size: 12px;
font-weight: bold;
}
.modal-close:hover,
.modal-close:focus {
color: #000;
text-decoration: none;
cursor: pointer;
}
.switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #807272;
-webkit-transition: .4s;
transition: .4s;
}
.slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
}
input:checked + .slider {
background-color: var(--color-menu-point2);
}
input:focus + .slider {
box-shadow: 0 0 1px var(--color-menu-point2);
}
input:checked + .slider:before {
-webkit-transform: translateX(26px);
-ms-transform: translateX(26px);
transform: translateX(26px);
}
/* Rounded sliders */
.slider.round {
border-radius: 34px;
}
.slider.round:before {
border-radius: 50%;
}
.timeInput {
background-color: var(--color-menu-point1);
}
.group {
display: flex;
gap: 4px;
}
.direction_column {
flex-direction: column;
}
.direction_row {
flex-direction: row;
}

@ -0,0 +1,486 @@
"use strict";
import { COMMANDS } from './costants.js'
let FEATURES = {}
let LOGS = {}
class Admin {
constructor () {
return (async () => {
const element = document.createElement('div');
element.classList.add('admin');
const header = document.createElement('h2');
header.innerHTML = "Настройки TemaBot"
element.append(header);
const divider0 = new Divider("Фичи:")
element.append(divider0.element)
FEATURES = await Admin.getFeatures()
console.log("!!! FEATURES", FEATURES)
const features = new Features(FEATURES)
element.append(features.element)
const applyButton = new Button(COMMANDS[2].name, COMMANDS[2].uid, handleApplyButton)
element.append(applyButton.element)
const divider1 = new Divider("Контент:")
element.append(divider1.element)
const downloadButton = new Button("Скачать контент", "download", handleDownloadButton)
element.append(downloadButton.element)
const upload = new Upload("Загрузить контент", "upload", handleSubmit)
element.append(upload.element)
const divider2 = new Divider("Логи:")
element.append(divider2.element)
LOGS = await Admin.getLogs()
console.log("!!! LOGS", LOGS)
const showLogsButton = new Button("Посмотреть логи", "showLogs", handleShowLogsButton)
element.append(showLogsButton.element)
const clearLogsButton = new Button("Очистить логи", "clearLogs", handleClearLogsButton)
element.append(clearLogsButton.element)
const modal = new Modal(JSON.stringify(LOGS, null, '\\n'), handleModalClose)
element.append(modal.element)
const divider3 = new Divider("Рассылка:")
element.append(divider3.element)
const mailing = await Admin.getMailing()
console.log("!!! MAILING", mailing)
const mailingSwitch = new Switch(mailing.status, "Рассылка", "mailingSwitch", handleMailingSwitch)
element.append(mailingSwitch.element)
const divider4 = new Divider("Подписки:")
element.append(divider4.element)
const subscription = await Admin.getSubscriptionMailing()
console.log("!!! MAILING", subscription)
const subscriptionSwitch = new Switch(subscription.status, "Подписки", "subscriptionSwitch", handleSubscriptionSwitch)
const subscriptionTimeInput = new TimeInput(subscription.time ,"Время рассылки", "subscriptionTimeInput", handleSubscriptionTime)
const group = new Group('row', subscriptionTimeInput.element, subscriptionSwitch.element)
element.append(group.element)
this.element = element
return this
})();
}
static async getFeatures() {
const response = await fetch('/settings/', {
method: 'POST',
headers: { 'Content-Type': 'application/json;charset=utf-8' },
body: JSON.stringify({})
})
if (response.ok) {
return await response.json();
} else {
alert("Не получили настройки :( Ошибка HTTP: " + response.status);
return {}
}
}
static async getMailing() {
const response = await fetch('/settings/mailing', {
method: 'POST',
headers: { 'Content-Type': 'application/json;charset=utf-8' },
body: JSON.stringify({})
})
if (response.ok) {
return await response.json();
} else {
alert("Не смогли получить рассылки :( Ошибка HTTP: " + response.status);
return {}
}
}
static async getSubscriptionMailing() {
const response = await fetch('/settings/mailing/subscription', {
method: 'POST',
headers: { 'Content-Type': 'application/json;charset=utf-8' },
body: JSON.stringify({})
})
if (response.ok) {
return await response.json();
} else {
alert("Не смогли получить рассылки :( Ошибка HTTP: " + response.status);
return {}
}
}
static async getLogs() {
const response = await fetch('/settings/logs', {
method: 'POST',
headers: { 'Content-Type': 'application/json;charset=utf-8' },
body: JSON.stringify({})
})
if (response.ok) {
return await response.json();
} else {
alert("Не смогли получить логи :( Ошибка HTTP: " + response.status);
return {}
}
}
static async clearLogs() {
const response = await fetch('/settings/logs/clear', {
method: 'POST',
headers: { 'Content-Type': 'application/json;charset=utf-8' },
body: JSON.stringify({})
})
if (response.ok) {
return await response.json();
} else {
alert("Не смогли очистить логи :( Ошибка HTTP: " + response.status);
return {}
}
}
setStatusName(name) {
this.status.setName(name)
}
}
class Features {
constructor (features) {
const element = document.createElement('div');
Object.entries(features).forEach(([key, value]) => {
const { name, available, readonly } = value
const checkbox = new Checkbox(name, key, available, readonly);
element.append(checkbox.element);
})
this.element = element;
}
}
class Divider {
constructor (title) {
const element = document.createElement('div')
element.classList.add('divider');
const hr = document.createElement('hr')
hr.classList.add('divider_hr');
element.append(hr);
if (title) {
const h3 = document.createElement('h3');
h3.classList.add('divider_h3');
h3.innerHTML = title
element.append(h3);
}
this.element = element;
}
}
class Modal {
constructor (text, onClose) {
const element = document.createElement('div');
element.classList.add('modal');
const content = document.createElement('div');
content.classList.add('modal-content');
element.append(content);
const close = document.createElement('button');
close.classList.add('modal-close');
close.append("X");
close.addEventListener(
'click',
() => {
onClose();
}
)
content.append(close);
const new_text = text.replaceAll("\\n", "")
const p = document.createElement('pre');
p.classList.add('modal-text');
p.append(new_text);
content.append(p);
window.onclick = function(event) {
if (event.target == element) {
element.style.display = "none";
}
}
this.element = element;
}
}
class Button {
constructor (name, uid, onClick) {
const element = document.createElement('button');
element.append(name);
element.classList.add('button');
element.setAttribute('data-name', name)
element.setAttribute('data-uid', uid)
element.addEventListener(
'click',
() => {
onClick(this.element);
}
)
this.element = element;
}
}
class Group {
constructor (direction, ...elements) {
const div = document.createElement('div');
div.classList.add('group');
if (direction === 'column') {
div.classList.add('direction_column');
} else if (direction === 'row') {
div.classList.add('direction_row');
}
for (const element of elements) {
div.append(element)
}
this.element = div;
}
}
class Switch {
constructor (status, name, uid, onClick) {
const element = document.createElement('label');
element.classList.add('switch');
const input = document.createElement('input');
if (status === 'checked') {
input.setAttribute('checked', "true");
}
input.setAttribute('type', "checkbox");
input.setAttribute('data-name', name)
input.setAttribute('data-uid', uid)
input.addEventListener(
'click',
() => {
onClick(input);
}
)
const span = document.createElement('span');
span.classList.add('slider');
span.classList.add('round');
element.append(input);
element.append(span);
this.element = element;
}
}
class TimeInput {
constructor (time, name, uid, onBlur ) {
const element = document.createElement('input');
element.setAttribute('id', uid);
element.setAttribute('type', "time");
element.setAttribute('min', "00.00");
element.setAttribute('max', "23.59");
element.setAttribute('value', time)
element.classList.add('timeInput');
element.setAttribute('data-name', name)
element.setAttribute('data-uid', uid)
element.addEventListener(
'blur',
() => {
onBlur(element);
}
)
this.element = element;
}
}
class Upload {
constructor (name, uid, onSubmit) {
const form = document.createElement('form');
form.setAttribute('action', "/upload/");
form.setAttribute('method', "post");
form.setAttribute('enctype', "multipart/form-data");
form.addEventListener('submit', onSubmit);
const label = document.createElement('label');
label.setAttribute('for', "file");
label.append("Файл: ");
form.append(label);
const input = document.createElement('input');
input.setAttribute('id', "file");
input.setAttribute('name', "file");
input.setAttribute('type', "file");
form.append(input);
const button = document.createElement('button');
button.classList.add('button');
button.setAttribute('data-name', name);
button.setAttribute('data-uid', uid);
button.append(name);
this.button = button
form.append(button);
this.element = form;
}
}
class Checkbox {
constructor (name, uid, available, readonly) {
const element = document.createElement('div');
const label = document.createElement('label');
const checkbox = document.createElement('input');
checkbox.setAttribute('type', 'checkbox');
checkbox.setAttribute('data-name', name);
checkbox.setAttribute('data-uid', uid);
checkbox.setAttribute('data-available', available);
if (available === 'true') {
checkbox.setAttribute('checked','');
}
if (readonly === 'true') {
checkbox.setAttribute('disabled','');
}
checkbox.addEventListener(
'change',
() => {
changeCheckbox(checkbox);
}
)
label.append(checkbox)
label.append(name)
element.append(label)
this.element = element;
}
}
const admin = await new Admin();
document
.getElementById('root')
.append(admin.element)
function changeCheckbox(checkbox) {
console.log(checkbox.dataset.name, checkbox.checked, checkbox.dataset.uid)
const feature = FEATURES[checkbox.dataset.uid]
feature.available = checkbox.checked ? "true" : "false"
}
function specialHandle (data, fetchComponent) {
fetch(fetchComponent, {
method: "POST",
headers: { 'Content-Type': 'application/json;charset=utf-8' },
body: JSON.stringify(data)
})
.then(response => response.json())
.then(result => {
console.log("result", result)
})
.catch((error) => console.log(error));
}
function handleApplyButton (button) {
console.log("click", button.dataset.name, button.dataset.uid);
const data = { command: button.dataset.uid, features: FEATURES}
const fetchComponent = `/tasks/`
specialHandle(data, fetchComponent)
}
function handleMailingSwitch (input) {
console.log("click", input.dataset.name, input.dataset.uid, input.checked)
const data = { status: input.checked}
const fetchComponent = `/settings/mailing/mailing_set`
specialHandle(data, fetchComponent)
}
function handleSubscriptionSwitch (input) {
console.log("click", input.dataset.name, input.dataset.uid, input.checked)
const data = { status: input.checked}
const fetchComponent = `/settings/mailing/subscription/set`
specialHandle(data, fetchComponent)
}
function handleSubscriptionTime (input) {
console.log("click", input.dataset.name, input.dataset.uid, input.checked)
const data = { time: input.value }
const fetchComponent = `/settings/mailing/subscription/set`
specialHandle(data, fetchComponent)
}
function handleDownloadButton (button) {
console.log("click", button.dataset.name, button.dataset.uid);
window.location='./download';
}
function handleShowLogsButton (button) {
console.log("click", button.dataset.name, button.dataset.uid);
const modal = document.getElementsByClassName("modal")[0];
modal.style.display = "block";
}
function handleClearLogsButton (button) {
console.log("click", button.dataset.name, button.dataset.uid);
Admin.clearLogs()
}
function handleModalClose () {
console.log("click", "handleModalClose");
const modal = document.getElementsByClassName("modal")[0];
modal.style.display = "none";
}
/** @param {Event} event */
function handleSubmit(event) {
/** @type {HTMLFormElement} */
const form = event.currentTarget;
const url = new URL(form.action);
const formData = new FormData(form);
const searchParams = new URLSearchParams(formData);
/** @type {Parameters<fetch>[1]} */
const fetchOptions = {
method: form.method,
};
if (form.method.toLowerCase() === 'post') {
if (form.enctype === 'multipart/form-data') {
fetchOptions.body = formData;
} else {
fetchOptions.body = searchParams;
}
} else {
url.search = searchParams;
}
fetch(url, fetchOptions)
.then(response => response.json())
.then(result => {
alert(result.ok)
console.log("result", result)
})
.catch((error) => console.log(error));
event.preventDefault();
}
function ready(callback) {
if (document.readyState !== 'loading') {
callback();
return;
}
document.addEventListener('DOMContentLoaded', callback);
}
ready(function() {
console.log('document ready')
})

@ -0,0 +1,12 @@
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Настройка GameShop</title>
<link href="/static/index.css"rel="stylesheet"/>
<script defer src="/static/index.js" ></script>
</head>
<body>
<div id='root'></div>
</body>
</html>

@ -1,6 +1,5 @@
import unittest
import psycopg2
import json # Используем json.loads вместо eval
from api import (
registration,
add_product_to_basket,
@ -10,8 +9,8 @@ from api import (
unregister,
add_money # Импортируем напрямую
)
from admin_api import add_product_to_shop, delete_product_from_shop
from const import DB_CONFIG
from api import add_product_to_shop, delete_product_from_shop
from consts import DB_CONFIG
class TestFullUserFlow(unittest.TestCase):

@ -0,0 +1 @@
from .type import *

@ -0,0 +1 @@
from .utils import *

@ -1,9 +1,9 @@
from decimal import Decimal
import bcrypt
from datetime import date
from psycopg2 import sql
from const import *
from type import *
import json
def fetch_table_as_json(cursor, table_name) -> list[dict]:
cursor.execute(sql.SQL("SELECT * FROM {}").format(sql.Identifier(table_name)))
@ -49,3 +49,10 @@ def get_user_id(cursor, token: str) -> int:
def get_product_by_id(cursor, product_id):
fetch_row_as_json(cursor, "shop", product_id)
def decimal_to_float(data: list[dict]):
clean_data = [
{k: float(v) if isinstance(v, Decimal) else v for k, v in item.items()}
for item in data
]
return clean_data

@ -1,31 +0,0 @@
from api import *
from utils import *
if __name__ == "__main__":
# products_json = get_products()
# print(json.dumps(products_json, ensure_ascii=False, indent=2))
#
# product1_json = get_product(1)
# print(json.dumps(product1_json, ensure_ascii=False, indent=2))
# try:
# # registration("Vasya1", "Vasya2005", "vasili_pupkin@gmail.com")
# token = login("vasili_pupkin@gmail.com", "Vasya2005")
# logout(token)
# print(check_token(token))
# token = login("stepan.pilipenko@xmail.ru", "sage123")
# registration("Petia", "Petia2005", "petia_pupkin@gmail.com")
# token = login("stepan.pilipenko@xmail.ru", "Vasya2005")
# print(get_basket(token))
# print(get_history(token))
token = login("vasili_pupkin@gmail.com", "Vasya2005")
# add_product_to_basket(token, 3)
# add_product_to_basket(token, 3)
# add_product_to_basket(token, 5)
# add_product_to_basket(token, 5)
add_money()
# buy_products(token)
# print(get_histories(token))
# unregister(token)

@ -1,2 +0,0 @@
psycopg2-binary
bcrypt

@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDDzCCAfegAwIBAgIUf2BJSySSLLnH0Ac5wA7I28/qnPcwDQYJKoZIhvcNAQEL
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI0MDUwMzE5MTkyMFoXDTI0MDYw
MjE5MTkyMFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEA4EUNokJK5XVV1sa6E3iGPqQBwCn5K02X4ktZ5hktg8bo
3dpcgdrmdP6KEB1vPwcEOBmMEasE1bI/ocCiwHGC/yY+C+z+T8YSw5WUBCSQgPEj
xcxNt/AI8r5DZGTcqXt/5M9pEywPy0mjgwCogbe0R8MIL98PLqT6Bt3fIN1v8bjN
XySH04N2WJ/YU4SXdeI8QUy+ET4cTV0U9xC49T7knpYK9I1zGHeYvd+E2AJCYnLU
841qxZAr0O/5Oi0jgiYoltQi+u7ejQaqJJfp2+XYsgHhKpMBdJbyhNuE8KIZLhgU
8kFRejEHBgYJq9X6OyQ6ZRQxsQ18GrUrFq0kf2qZWwIDAQABo1kwVzAUBgNVHREE
DTALgglsb2NhbGhvc3QwCwYDVR0PBAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMB
MB0GA1UdDgQWBBRD48TR9xtQIhaDMHuqbXqH9lsgfzANBgkqhkiG9w0BAQsFAAOC
AQEAAAGRN5NEzE2eKpn1clBbHpFB3sOnxRQ5efVafz0U0XSCqyLlYU9IibGdk3Yk
huaslxUFGtPPlzPbRjHYP3ST5pZpCOuH7Ze/8AOaeNu+DFxILjD04wAGV881JPSB
Sw3wrxk0hWMMwWTCTnsaDIeLOejhf86CB1gH3UdvZzcxGIanXtyb86wcnkj107+B
5Pv6Q8J0Thn2gMjisVqybdqrQi7tAtDURefyoPuRxgK2k6kgTBSMPXxRqWvq6Aem
ozxYrT7/QV0gKXjCNTmhFjEOOIGdN8h/Rmih7/TbcPpaEHcsVwG2w8eRKvJ2xrIk
dHL+Xn35P1jTwxwRq1vfLhEIkg==
-----END CERTIFICATE-----

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDgRQ2iQkrldVXW
xroTeIY+pAHAKfkrTZfiS1nmGS2Dxujd2lyB2uZ0/ooQHW8/BwQ4GYwRqwTVsj+h
wKLAcYL/Jj4L7P5PxhLDlZQEJJCA8SPFzE238AjyvkNkZNype3/kz2kTLA/LSaOD
AKiBt7RHwwgv3w8upPoG3d8g3W/xuM1fJIfTg3ZYn9hThJd14jxBTL4RPhxNXRT3
ELj1PuSelgr0jXMYd5i934TYAkJictTzjWrFkCvQ7/k6LSOCJiiW1CL67t6NBqok
l+nb5diyAeEqkwF0lvKE24TwohkuGBTyQVF6MQcGBgmr1fo7JDplFDGxDXwatSsW
rSR/aplbAgMBAAECggEAAsPXLzDyC3Iu5L7+fE74GL2c5+mckNQcE0YqjZOx4/YH
2PRgP5mbTcX1nc0/Gd/URXzLJUyeeqP/9NaTKxw7KTonea4qVIF0qcSmVoa84VoX
vtCOBL6I3bVKz2oO7mf/Y6rK+NmKOE9oHK1dZnwFVP0qlKyDW2fdTUhe/+C7CJfg
x8KXNQ66cky5VaM+/Oqqaxs866XI9sjUr234YRarwgIwoP1hVrVfOaKJounbqHtS
HCwyWlvk1JkmGNEnPIH8HlkTPfh1Q1ifK3t8qKFfnj6ITarbZildsPJD9P/eN8E5
oujw0eUi1YOkWRI91ecCTVqvFNh35nRFF9fDchRygQKBgQDxCWqaPDfmQIQxRoaX
z2UkXZZfQ2I/CdmVT1TrgpMndyzYI1KC93VdDCDZlWMCEOyfN2qDg/bSbQFCWlUJ
m+fgZd2ovrtyx7AL4P808mVgR0+ZcW6+48ZI5TKs/UI+m/WBy+QMedPcObhx26CA
MqdyLw3nAU5u9M1beQ+F34XGQwKBgQDuMSxQjc+FoT38exDPcLyrmvMxiJDl9F7q
BzWGxp2HtgBxLRvI0E7tb0kJ2ZgZjq3xdbPYgYVUKkaSxDNU7/63nE6vw+ZbW42V
XBnt55c0GixnV9LQLv0Qz9Tp7RfdOZlDQ/azoLHY2XWOt08rac3TgHwD3YNALrkQ
NrDa30tLCQKBgFsCTC7qN80HZSJZ163wT+cYMxPLFIhqxq3ao1y9E6TeGZ+OTrRG
jRjR4IFnJ1f7XeyL9vqrVAGFyOjtxJf5NucCb1wskAg5n54MmS+7qk1c/5AXRVJs
HE0fxS+N/Ho5VsxoWLXhNf48CQlsfMCK37B8Vcp4Ms4wPm2gWx0YFaGTAoGBAN6i
9cHhm0xTV4YMXb0XqjJYZeIxvQZDsQfcbyqnsQztkGI5AJRmKLAD6egcC/AvjeR4
2P6QqdfuoAKFA1nr7VEf9+iQGlvgKmmmKdJWOt2HbWO3EiRnF0HEkUWJyFmOgfP+
rbRein2fXSNlsclpXurHWKOgRBMU2QQPqqUaO91JAoGBAM5EcSu1uZkMs2urRKJY
SDO0N13Dm1rY3t+nInrBk6j4AxEuwdF5k1lAevmEPRNKXSqh0JjInGPQij+Z3Brp
kLCubqZKTRmXkhMkI8p++d0aW5voSo4E1Fr4mWrK/WakMHMVcwVRyqTmmXYyY9v/
FMNjTyw8P7HAaUuhdazp/pzM
-----END PRIVATE KEY-----

@ -0,0 +1,10 @@
services:
web:
platform: linux/arm64/v8
image: firstsoftwarrior/game_shop
build:
context: app
target: builder
container_name: firstsoftwarrior-game_shop
ports:
- '8090:8090'
Loading…
Cancel
Save